[
  {
    "path": ".pylintrc",
    "content": "[MASTER]\n\n# A comma-separated list of package or module names from where C extensions may\n# be loaded. Extensions are loading into the active Python interpreter and may\n# run arbitrary code.\nextension-pkg-whitelist=\n\n# Add files or directories to the blacklist. They should be base names, not\n# paths.\nignore=CVS\n\n# Add files or directories matching the regex patterns to the blacklist. The\n# regex matches against base names, not paths.\nignore-patterns=\n\n# Python code to execute, usually for sys.path manipulation such as\n# pygtk.require().\n#init-hook=\n\n# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the\n# number of processors available to use.\njobs=1\n\n# Control the amount of potential inferred values when inferring a single\n# object. This can help the performance when dealing with large functions or\n# complex, nested conditions.\nlimit-inference-results=100\n\n# List of plugins (as comma separated values of python modules names) to load,\n# usually to register additional checkers.\nload-plugins=\n\n# Pickle collected data for later comparisons.\npersistent=yes\n\n# Specify a configuration file.\n#rcfile=\n\n# When enabled, pylint would attempt to guess common misconfiguration and emit\n# user-friendly hints instead of false-positive error messages.\nsuggestion-mode=yes\n\n# Allow loading of arbitrary C extensions. Extensions are imported into the\n# active Python interpreter and may run arbitrary code.\nunsafe-load-any-extension=no\n\n\n[MESSAGES CONTROL]\n\n# Only show warnings with the listed confidence levels. Leave empty to show\n# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.\nconfidence=\n\n# Disable the message, report, category or checker with the given id(s). You\n# can either give multiple identifiers separated by comma (,) or put this\n# option multiple times (only on the command line, not in the configuration\n# file where it should appear only once). You can also use \"--disable=all\" to\n# disable everything first and then reenable specific checks. For example, if\n# you want to run only the similarities checker, you can use \"--disable=all\n# --enable=similarities\". If you want to run only the classes checker, but have\n# no Warning level messages displayed, use \"--disable=all --enable=classes\n# --disable=W\".\ndisable=invalid-name,\n        missing-docstring,\n        print-statement,\n        parameter-unpacking,\n        unpacking-in-except,\n        old-raise-syntax,\n        backtick,\n        long-suffix,\n        old-ne-operator,\n        old-octal-literal,\n        import-star-module-level,\n        non-ascii-bytes-literal,\n        raw-checker-failed,\n        bad-inline-option,\n        locally-disabled,\n        locally-enabled,\n        file-ignored,\n        suppressed-message,\n        useless-suppression,\n        deprecated-pragma,\n        use-symbolic-message-instead,\n        too-few-public-methods,\n        unused-import,\n        apply-builtin,\n        basestring-builtin,\n        buffer-builtin,\n        cmp-builtin,\n        coerce-builtin,\n        execfile-builtin,\n        file-builtin,\n        long-builtin,\n        raw_input-builtin,\n        reduce-builtin,\n        standarderror-builtin,\n        unicode-builtin,\n        xrange-builtin,\n        coerce-method,\n        delslice-method,\n        getslice-method,\n        setslice-method,\n        no-absolute-import,\n        old-division,\n        dict-iter-method,\n        dict-view-method,\n        next-method-called,\n        metaclass-assignment,\n        indexing-exception,\n        raising-string,\n        reload-builtin,\n        oct-method,\n        hex-method,\n        nonzero-method,\n        cmp-method,\n        input-builtin,\n        round-builtin,\n        intern-builtin,\n        unichr-builtin,\n        map-builtin-not-iterating,\n        zip-builtin-not-iterating,\n        range-builtin-not-iterating,\n        filter-builtin-not-iterating,\n        using-cmp-argument,\n        eq-without-hash,\n        div-method,\n        idiv-method,\n        rdiv-method,\n        exception-message-attribute,\n        invalid-str-codec,\n        sys-max-int,\n        bad-python3-import,\n        deprecated-string-function,\n        deprecated-str-translate-call,\n        deprecated-itertools-function,\n        deprecated-types-field,\n        next-method-defined,\n        dict-items-not-iterating,\n        dict-keys-not-iterating,\n        dict-values-not-iterating,\n        deprecated-operator-function,\n        deprecated-urllib-function,\n        xreadlines-attribute,\n        deprecated-sys-function,\n        exception-escape,\n        comprehension-escape,\n        wrong-import-order\n\n# Enable the message, report, category or checker with the given id(s). You can\n# either give multiple identifier separated by comma (,) or put this option\n# multiple time (only on the command line, not in the configuration file where\n# it should appear only once). See also the \"--disable\" option for examples.\nenable=c-extension-no-member\n\n\n[REPORTS]\n\n# Python expression which should return a note less than 10 (10 is the highest\n# note). You have access to the variables errors warning, statement which\n# respectively contain the number of errors / warnings messages and the total\n# number of statements analyzed. This is used by the global evaluation report\n# (RP0004).\nevaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)\n\n# Template used to display messages. This is a python new-style format string\n# used to format the message information. See doc for all details.\n#msg-template=\n\n# Set the output format. Available formats are text, parseable, colorized, json\n# and msvs (visual studio). You can also give a reporter class, e.g.\n# mypackage.mymodule.MyReporterClass.\noutput-format=text\n\n# Tells whether to display a full report or only the messages.\nreports=no\n\n# Activate the evaluation score.\nscore=yes\n\n\n[REFACTORING]\n\n# Maximum number of nested blocks for function / method body\nmax-nested-blocks=5\n\n# Complete name of functions that never returns. When checking for\n# inconsistent-return-statements if a never returning function is called then\n# it will be considered as an explicit return statement and no message will be\n# printed.\nnever-returning-functions=sys.exit\n\n\n[LOGGING]\n\n# Logging modules to check that the string format arguments are in logging\n# function parameter format.\nlogging-modules=logging\n\n\n[SPELLING]\n\n# Limits count of emitted suggestions for spelling mistakes.\nmax-spelling-suggestions=4\n\n# Spelling dictionary name. Available dictionaries: none. To make it working\n# install python-enchant package..\nspelling-dict=\n\n# List of comma separated words that should not be checked.\nspelling-ignore-words=\n\n# A path to a file that contains private dictionary; one word per line.\nspelling-private-dict-file=\n\n# Tells whether to store unknown words to indicated private dictionary in\n# --spelling-private-dict-file option instead of raising a message.\nspelling-store-unknown-words=no\n\n\n[MISCELLANEOUS]\n\n# List of note tags to take in consideration, separated by a comma.\nnotes=FIXME,\n      XXX,\n      TODO\n\n\n[TYPECHECK]\n\n# List of decorators that produce context managers, such as\n# contextlib.contextmanager. Add to this list to register other decorators that\n# produce valid context managers.\ncontextmanager-decorators=contextlib.contextmanager\n\n# List of members which are set dynamically and missed by pylint inference\n# system, and so shouldn't trigger E1101 when accessed. Python regular\n# expressions are accepted.\ngenerated-members=\n\n# Tells whether missing members accessed in mixin class should be ignored. A\n# mixin class is detected if its name ends with \"mixin\" (case insensitive).\nignore-mixin-members=yes\n\n# Tells whether to warn about missing members when the owner of the attribute\n# is inferred to be None.\nignore-none=yes\n\n# This flag controls whether pylint should warn about no-member and similar\n# checks whenever an opaque object is returned when inferring. The inference\n# can return multiple potential results while evaluating a Python object, but\n# some branches might not be evaluated, which results in partial inference. In\n# that case, it might be useful to still emit no-member and other checks for\n# the rest of the inferred objects.\nignore-on-opaque-inference=yes\n\n# List of class names for which member attributes should not be checked (useful\n# for classes with dynamically set attributes). This supports the use of\n# qualified names.\nignored-classes=optparse.Values,thread._local,_thread._local\n\n# List of module names for which member attributes should not be checked\n# (useful for modules/projects where namespaces are manipulated during runtime\n# and thus existing member attributes cannot be deduced by static analysis. It\n# supports qualified module names, as well as Unix pattern matching.\nignored-modules=\n\n# Show a hint with possible names when a member name was not found. The aspect\n# of finding the hint is based on edit distance.\nmissing-member-hint=yes\n\n# The minimum edit distance a name should have in order to be considered a\n# similar match for a missing member name.\nmissing-member-hint-distance=1\n\n# The total number of similar names that should be taken in consideration when\n# showing a hint for a missing member.\nmissing-member-max-choices=1\n\n\n[VARIABLES]\n\n# List of additional names supposed to be defined in builtins. Remember that\n# you should avoid to define new builtins when possible.\nadditional-builtins=\n\n# Tells whether unused global variables should be treated as a violation.\nallow-global-unused-variables=yes\n\n# List of strings which can identify a callback function by name. A callback\n# name must start or end with one of those strings.\ncallbacks=cb_,\n          _cb\n\n# A regular expression matching the name of dummy variables (i.e. expected to\n# not be used).\ndummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_\n\n# Argument names that match this expression will be ignored. Default to name\n# with leading underscore.\nignored-argument-names=_.*|^ignored_|^unused_\n\n# Tells whether we should check for unused import in __init__ files.\ninit-import=no\n\n# List of qualified module names which can have objects that can redefine\n# builtins.\nredefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io\n\n\n[FORMAT]\n\n# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.\nexpected-line-ending-format=\n\n# Regexp for a line that is allowed to be longer than the limit.\nignore-long-lines=^\\s*(# )?<?https?://\\S+>?$\n\n# Number of spaces of indent required inside a hanging  or continued line.\nindent-after-paren=4\n\n# String used as indentation unit. This is usually \"    \" (4 spaces) or \"\\t\" (1\n# tab).\nindent-string='    '\n\n# Maximum number of characters on a single line.\nmax-line-length=100\n\n# Maximum number of lines in a module.\nmax-module-lines=1000\n\n# List of optional constructs for which whitespace checking is disabled. `dict-\n# separator` is used to allow tabulation in dicts, etc.: {1  : 1,\\n222: 2}.\n# `trailing-comma` allows a space between comma and closing bracket: (a, ).\n# `empty-line` allows space-only lines.\nno-space-check=trailing-comma,\n               dict-separator\n\n# Allow the body of a class to be on the same line as the declaration if body\n# contains single statement.\nsingle-line-class-stmt=no\n\n# Allow the body of an if to be on the same line as the test if there is no\n# else.\nsingle-line-if-stmt=no\n\n\n[SIMILARITIES]\n\n# Ignore comments when computing similarities.\nignore-comments=yes\n\n# Ignore docstrings when computing similarities.\nignore-docstrings=yes\n\n# Ignore imports when computing similarities.\nignore-imports=no\n\n# Minimum lines number of a similarity.\nmin-similarity-lines=4\n\n\n[BASIC]\n\n# Naming style matching correct argument names.\nargument-naming-style=snake_case\n\n# Regular expression matching correct argument names. Overrides argument-\n# naming-style.\n#argument-rgx=\n\n# Naming style matching correct attribute names.\nattr-naming-style=snake_case\n\n# Regular expression matching correct attribute names. Overrides attr-naming-\n# style.\n#attr-rgx=\n\n# Bad variable names which should always be refused, separated by a comma.\nbad-names=foo,\n          bar,\n          baz,\n          toto,\n          tutu,\n          tata\n\n# Naming style matching correct class attribute names.\nclass-attribute-naming-style=any\n\n# Regular expression matching correct class attribute names. Overrides class-\n# attribute-naming-style.\n#class-attribute-rgx=\n\n# Naming style matching correct class names.\nclass-naming-style=PascalCase\n\n# Regular expression matching correct class names. Overrides class-naming-\n# style.\n#class-rgx=\n\n# Naming style matching correct constant names.\nconst-naming-style=UPPER_CASE\n\n# Regular expression matching correct constant names. Overrides const-naming-\n# style.\n#const-rgx=\n\n# Minimum line length for functions/classes that require docstrings, shorter\n# ones are exempt.\ndocstring-min-length=-1\n\n# Naming style matching correct function names.\nfunction-naming-style=snake_case\n\n# Regular expression matching correct function names. Overrides function-\n# naming-style.\n#function-rgx=\n\n# Good variable names which should always be accepted, separated by a comma.\ngood-names=i,\n           j,\n           k,\n           ex,\n           Run,\n           _\n\n# Include a hint for the correct naming format with invalid-name.\ninclude-naming-hint=no\n\n# Naming style matching correct inline iteration names.\ninlinevar-naming-style=any\n\n# Regular expression matching correct inline iteration names. Overrides\n# inlinevar-naming-style.\n#inlinevar-rgx=\n\n# Naming style matching correct method names.\nmethod-naming-style=snake_case\n\n# Regular expression matching correct method names. Overrides method-naming-\n# style.\n#method-rgx=\n\n# Naming style matching correct module names.\nmodule-naming-style=snake_case\n\n# Regular expression matching correct module names. Overrides module-naming-\n# style.\n#module-rgx=\n\n# Colon-delimited sets of names that determine each other's naming style when\n# the name regexes allow several styles.\nname-group=\n\n# Regular expression which should only match function or class names that do\n# not require a docstring.\nno-docstring-rgx=^_\n\n# List of decorators that produce properties, such as abc.abstractproperty. Add\n# to this list to register other decorators that produce valid properties.\n# These decorators are taken in consideration only for invalid-name.\nproperty-classes=abc.abstractproperty\n\n# Naming style matching correct variable names.\nvariable-naming-style=snake_case\n\n# Regular expression matching correct variable names. Overrides variable-\n# naming-style.\n#variable-rgx=\n\n\n[IMPORTS]\n\n# Allow wildcard imports from modules that define __all__.\nallow-wildcard-with-all=no\n\n# Analyse import fallback blocks. This can be used to support both Python 2 and\n# 3 compatible code, which means that the block might have code that exists\n# only in one or another interpreter, leading to false positives when analysed.\nanalyse-fallback-blocks=no\n\n# Deprecated modules which should not be used, separated by a comma.\ndeprecated-modules=optparse,tkinter.tix\n\n# Create a graph of external dependencies in the given file (report RP0402 must\n# not be disabled).\next-import-graph=\n\n# Create a graph of every (i.e. internal and external) dependencies in the\n# given file (report RP0402 must not be disabled).\nimport-graph=\n\n# Create a graph of internal dependencies in the given file (report RP0402 must\n# not be disabled).\nint-import-graph=\n\n# Force import order to recognize a module as part of the standard\n# compatibility libraries.\nknown-standard-library=\n\n# Force import order to recognize a module as part of a third party library.\nknown-third-party=enchant\n\n\n[CLASSES]\n\n# List of method names used to declare (i.e. assign) instance attributes.\ndefining-attr-methods=__init__,\n                      __new__,\n                      setUp\n\n# List of member names, which should be excluded from the protected access\n# warning.\nexclude-protected=_asdict,\n                  _fields,\n                  _replace,\n                  _source,\n                  _make\n\n# List of valid names for the first argument in a class method.\nvalid-classmethod-first-arg=cls\n\n# List of valid names for the first argument in a metaclass class method.\nvalid-metaclass-classmethod-first-arg=cls\n\n\n[DESIGN]\n\n# Maximum number of arguments for function / method.\nmax-args=5\n\n# Maximum number of attributes for a class (see R0902).\nmax-attributes=7\n\n# Maximum number of boolean expressions in an if statement.\nmax-bool-expr=5\n\n# Maximum number of branch for function / method body.\nmax-branches=12\n\n# Maximum number of locals for function / method body.\nmax-locals=15\n\n# Maximum number of parents for a class (see R0901).\nmax-parents=7\n\n# Maximum number of public methods for a class (see R0904).\nmax-public-methods=20\n\n# Maximum number of return / yield for function / method body.\nmax-returns=6\n\n# Maximum number of statements in function / method body.\nmax-statements=50\n\n# Minimum number of public methods for a class (see R0903).\nmin-public-methods=2\n\n\n[EXCEPTIONS]\n\n# Exceptions that will emit a warning when being caught. Defaults to\n# \"Exception\".\novergeneral-exceptions=Exception\n"
  },
  {
    "path": "Chapter_1/ch01_ex1.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 1. Example 1.\n\"\"\"\n\n# Show the basics of timeit.\n\nimport timeit\n\nmethod_time = timeit.timeit(\n    \"obj.method()\",\n    \"\"\"\nclass SomeClass:\n    def method(self):\n        pass\nobj= SomeClass()\n\"\"\",\n)\n\nfunction_time = timeit.timeit(\n    \"f()\",\n    \"\"\"\ndef f():\n    pass\n\"\"\",\n)\n\nif __name__ == \"__main__\":\n    print(f\"Method   {method_time:.4f}\")\n    print(f\"Function {function_time:.4f}\")\n"
  },
  {
    "path": "Chapter_1/ch01_ex2.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 1. Example 2.\n\"\"\"\n\n# pylint: disable=invalid-name\ntest_list = \"\"\"\n    >>> f = [1, 1, 2, 3]\n    >>> f += [f[-1] + f[-2]]\n    >>> f\n    [1, 1, 2, 3, 5]\n    \n    >>> f.__getitem__(-1)\n    5\n    >>> f.__getitem__(-1).__add__(f.__getitem__(-2))\n    8\n    >>> f.__iadd__([8])\n    [1, 1, 2, 3, 5, 8]\n    >>> f\n    [1, 1, 2, 3, 5, 8]\n\"\"\"\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_1/ch01_ex3.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 1. Example 3.\n\"\"\"\n\ndef F(n: int) -> int:\n    if n in (0, 1):\n        return 1\n    else:\n        return F(n-1) + F(n-2)\n\ntest_F_8 = \"\"\"\n    >>> F(8)\n    34\n\"\"\"\n\ndef demo():\n    print(\"Good Use\", F(8))\n    print(\"Bad Use\", F(355/113))\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_1/ch01_ex4.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 1. Example 4.\n\"\"\"\n\n\n# Simple function with docstring.\n\ndef factorial(n: int) -> int:\n    \"\"\"Compute n! recursively.\n\n    :param n: an integer >= 0\n    :returns: n!\n\n    Because of Python's stack limitation, this won't\n    compute a value larger than about 1000!.\n\n    >>> factorial(5)\n    120\n    \"\"\"\n    if n == 0:\n        return 1\n    return n * factorial(n - 1)\n\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod()\n"
  },
  {
    "path": "Chapter_1/ch01_ex5.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 1. Example 5.\n\"\"\"\n\n# Definition of some classes with doctest-based unit tests.\n\nfrom types import SimpleNamespace\n\nclass EmptyClass:\n    pass\n\nEmptyClass2 = SimpleNamespace\n\nEmptyClass3 = type('EmptyClass3', (object,), {})\n\n__test__ = {\n    'EmptyClass': '''\n        >>> ec = EmptyClass()\n        >>> ec.new_attribute = 42\n        >>> ec.new_attribute\n        42\n        >>> ec.undefined  # doctest: +IGNORE_EXCEPTION_DETAIL\n        Traceback (most recent call last):\n        AttributeError: 'EmptyClass' object has no attribute 'undefined'\n        ''',\n    'EmptyClass2': '''\n        >>> ec = EmptyClass2()\n        >>> ec.new_attribute = 42\n        >>> ec.new_attribute\n        42\n        >>> ec.undefined  # doctest: +IGNORE_EXCEPTION_DETAIL\n        Traceback (most recent call last):\n        AttributeError: 'EmptyClass' object has no attribute 'undefined'\n        ''',\n    'EmptyClass3': '''\n        >>> ec = EmptyClass3()\n        >>> ec.new_attribute = 42\n        >>> ec.new_attribute\n        42\n        >>> ec.undefined  # doctest: +IGNORE_EXCEPTION_DETAIL\n        Traceback (most recent call last):\n        AttributeError: 'EmptyClass' object has no attribute 'undefined'\n        ''',\n}\n\nif __name__ == \"__main__\":\n    import doctest\n    doctest.testmod()\n"
  },
  {
    "path": "Chapter_1/getting_started.rst",
    "content": "In order to run the examples mentioned in this book you require the following software:\n\n- Python version 3.7 or higher with the standard suite of libraries.\n\nWe'll look at some additional packages. These include PyYaml, SQLAlchemy, and Jinja2.\nWe'll use pytest for doing unit testing.\n\n- http://pyyaml.org\n\n- http://www.sqlalchemy.org When building this, check the installation guide, http://docs.sqlalchemy.org/en/rel_0_9/intro.html#installation. Using the --without-cextensions option can simplify installation.\n\n- http://jinja.pocoo.org/\n\nWe'll also look at some tools:\n\n- https://docs.pytest.org/en/latest/index.html\n\n- http://sphinx-doc.org\n\n- http://mypy-lang.org\n\n- https://www.pylint.org\n\n- https://github.com/ambv/black\n\n\nThere two alternative approaches to these installations.\n\n-   From Python.org\n\n    1.  Install Python 3.7 from http://www.python.org. This will change your OS-level ``PATH`` settings.\n        This usually means you need to start a new terminal session to make your new Python tools available.\n\n    2.  Use your new pip3 to install the other packages:\n\n\t\t  ``python3 -m pip install pyyaml sqlalchemy jinja2 pytest sphinx mypy pylint black``\n\n    If you install from Python.org, you'll have a single, default Python environment. This isn't optimal.\n    When you need to upgrade or experiment, you'll often want to create additional environments. There\n    are several tools for this. The conda tool seems to be the most versatile.\n\n-   Use Anaconda.com's miniconda to get started.\n\n    1.  Download and install the appropriate miniconda for your platform. https://conda.io/miniconda.html\n\n    2.  Use miniconda to build a Python environment including the required packages.\n\n        ``conda create --name mastering python=3.7 pyyaml sqlalchemy jinja2 pytest sphinx mypy pylint``\n\n    3.  Activate your new environment\n\n        ``conda activate mastering``\n\n        You'll know it's active because your terminal prompt will have ``(mastering)`` as a prefix.\n\n    4.  Add the ``black`` tool using a separate installation after activating the environment.\n\n        ``python3 -m pip install --upgrade pip``\n\n        ``python3 -m pip install black``\n\n"
  },
  {
    "path": "Chapter_10/__init__.py",
    "content": ""
  },
  {
    "path": "Chapter_10/ch10_bonus.py",
    "content": "\"\"\"\nEnumerate all Blackjack outcomes with player mirroring the dealer choice.\nNote that player going bust first is a loss, irrespective of what the dealer holds.\n\nThe question, then, is given two cards, what are the odds of going bust using\ndealer rules of hit 17, stand on 18.\n\nThen bust for player is rule 1.\nBust for dealer is rule 2.\nOtherwise it's a 50/50 proposition.\n\"\"\"\nfrom typing import Optional, Tuple, Dict, Counter\nimport random\nfrom enum import Enum\nimport collections\n\nclass Suit(Enum):\n    Clubs = \"♣\"\n    Diamonds = \"♦\"\n    Hearts = \"♥\"\n    Spades = \"♠\"\n\nclass Card:\n\n    def __init__(self, rank: str, suit: Suit, hard: Optional[int]=None, soft: Optional[int]=None) -> None:\n        self.rank = rank\n        self.suit = suit\n        self.hard = hard or int(rank)\n        self.soft = soft or int(rank)\n\n    def __str__(self) -> str:\n        return f\"{self.rank!s}{self.suit.value!s}\"\n\n\nclass AceCard(Card):\n\n    def __init__(self, rank: str, suit: Suit) -> None:\n        super().__init__(rank, suit, 1, 11)\n\n\nclass FaceCard(Card):\n\n    def __init__(self, rank: str, suit: Suit) -> None:\n        super().__init__(rank, suit, 10, 10)\n\ndef card(rank: int, suit: Suit) -> Card:\n    if rank == 1:\n        return AceCard(\"A\", suit)\n    elif rank in (11, 12, 13):\n        rank_str = {11: \"J\", 12: \"Q\", 13: \"K\"}[rank]\n        return FaceCard(rank_str, suit)\n    else:\n        return Card(str(rank), suit)\n\nclass Deck(list):\n    def __init__(self) -> None:\n        super().__init__(card(r, s) for r in range(1, 14) for s in Suit)\n        random.shuffle(self)\n\nclass Hand(list):\n    @property\n    def hard(self) -> int:\n        return sum(c.hard for c in self)\n\n    @property\n    def soft(self) -> int:\n        return sum(c.soft for c in self)\n\n    def __repr__(self) -> str:\n        cards = [str(c) for c in self]\n        return f\"Hand({cards!r})\"\n\ndef deal_rules(deck: Deck) -> Tuple[Hand, Optional[int]]:\n    hand = Hand([deck.pop(), deck.pop()])\n    while hand.hard < 21:\n        if hand.soft == 21:\n            return hand, 21\n        elif hand.hard == 21:\n            return hand, 21\n        elif hand.soft < 18:\n            hand.append(deck.pop())\n        elif hand.soft > 21 and hand.hard < 18:\n            hand.append(deck.pop())\n        else:\n            return hand, min(hand.hard, hand.soft)\n    return hand, None\n\ndef simulation() -> None:\n    raw_outcomes: Counter[Tuple[Optional[int], Optional[int]]] = collections.Counter()\n    game_payout: Counter[str] = collections.Counter()\n\n    for i in range(20_000):\n        deck = Deck()\n        player_hand, player_result = deal_rules(deck)\n        dealer_hand, dealer_result = deal_rules(deck)\n        raw_outcomes[(player_result, dealer_result)] += 1\n        if player_result is None:\n            game_payout['loss'] += 1\n        elif player_result is 21:\n            game_payout['21'] += 1\n        elif dealer_result is None:\n            game_payout['win'] += 1\n        elif player_result > dealer_result:\n            game_payout['win'] += 1\n        elif player_result == dealer_result:\n            game_payout['push'] += 1\n        else:\n            game_payout['loss'] += 1\n\n    running = 0.0\n    for outcome, count in game_payout.most_common():\n        print(f\"{running:.3f} <= r < {running+count/20_000:.3f}: {outcome}\")\n        running += count/20_000\n\nif __name__ == \"__main__\":\n    import doctest\n    doctest.testmod()\n\n    simulation()\n"
  },
  {
    "path": "Chapter_10/ch10_ex1.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 10. Example 1. JSON\n\"\"\"\n\n# Persistence Classes\n# ========================================\n\n# A detail class for micro-blog posts\nfrom typing import List, Optional, Dict, Any, DefaultDict, Union, Type\nfrom pathlib import Path\nimport datetime\nfrom dataclasses import dataclass\n\n# Technically, this is the type supported by JSON serailization.\n# JSON = Union[Dict[str, 'JSON'], List['JSON'], int, str, float, bool, Type[None]]\nJSON = Union[Dict[str, Any], List[Any], int, str, float, bool, Type[None]]\n\n@dataclass\nclass Post:\n    date: datetime.datetime\n    title: str\n    rst_text: str\n    tags: List[str]\n\n    def as_dict(self) -> Dict[str, Any]:\n        return dict(\n            date=str(self.date),\n            title=self.title,\n            underline=\"-\" * len(self.title),\n            rst_text=self.rst_text,\n            tag_text=\" \".join(self.tags),\n        )\n\n\n# Here's a collection of these posts. This is an extension\n# of list which doesn't work well with JSON.\n\nfrom collections import defaultdict\n\nclass Blog_x(list):\n\n    def __init__(self, title: str, posts: Optional[List[Post]]=None) -> None:\n        self.title = title\n        super().__init__(posts if posts is not None else [])\n\n    def by_tag(self) -> DefaultDict[str, List[Dict[str, Any]]]:\n        tag_index: DefaultDict[str, List[Dict[str, Any]]] = defaultdict(list)\n        for post in self:\n            for tag in post.tags:\n                tag_index[tag].append(post.as_dict())\n        return tag_index\n\n    def as_dict(self) -> Dict[str, Any]:\n        return dict(\n            title=self.title,\n            entries=[p.as_dict() for p in self]\n        )\n\n\n# An example blog\ntravel_x = Blog_x(\"Travel\")\ntravel_x.append(\n    Post(\n        date=datetime.datetime(2013, 11, 14, 17, 25),\n        title=\"Hard Aground\",\n        rst_text=\"\"\"Some embarrassing revelation. Including ☹ and ⚓\"\"\",\n        tags=[\"#RedRanger\", \"#Whitby42\", \"#ICW\"],\n    )\n)\ntravel_x.append(\n    Post(\n        date=datetime.datetime(2013, 11, 18, 15, 30),\n        title=\"Anchor Follies\",\n        rst_text=\"\"\"Some witty epigram. Including < & > characters.\"\"\",\n        tags=[\"#RedRanger\", \"#Whitby42\", \"#Mistakes\"],\n    )\n)\n\n# JSON\n# ================================\n\n# Example 1: Simple\n# ####################\n\n# Simple JSON dump\nimport json\n\ntest_json_1 = \"\"\"\n    >>> print(json.dumps(travel_x.as_dict(), indent=4))\n    {\n        \"title\": \"Travel\",\n        \"entries\": [\n            {\n                \"date\": \"2013-11-14 17:25:00\",\n                \"title\": \"Hard Aground\",\n                \"underline\": \"------------\",\n                \"rst_text\": \"Some embarrassing revelation. Including \\u2639 and \\u2693\",\n                \"tag_text\": \"#RedRanger #Whitby42 #ICW\"\n            },\n            {\n                \"date\": \"2013-11-18 15:30:00\",\n                \"title\": \"Anchor Follies\",\n                \"underline\": \"--------------\",\n                \"rst_text\": \"Some witty epigram. Including < & > characters.\",\n                \"tag_text\": \"#RedRanger #Whitby42 #Mistakes\"\n            }\n        ]\n    }\n    \"\"\"\n\n# Example 2. JSON: Flawed Container Design\n# ########################################\n\n# Flawed Encoder based on flawed design of the class.\ndef blogx_encode(object: Any) -> Dict[str, Any]:\n    if isinstance(object, datetime.datetime):\n        return dict(\n            __class__=\"datetime.datetime\",\n            __args__=[],\n            __kw__=dict(\n                year=object.year,\n                month=object.month,\n                day=object.day,\n                hour=object.hour,\n                minute=object.minute,\n                second=object.second,\n            ),\n        )\n    elif isinstance(object, Post):\n        return dict(\n            __class__=\"Post\",\n            __args__=[],\n            __kw__=dict(\n                date=object.date,\n                title=object.title,\n                rst_text=object.rst_text,\n                tags=object.tags,\n            ),\n        )\n    elif isinstance(object, Blog_x):\n        # Will get ignored...\n        return dict(\n            __class__=\"Blog_x\",\n            __args__=[],\n            __kw__=dict(title=object.title, entries=tuple(object)),\n        )\n    else:\n        return object\n\ndef blogx_decode(some_dict: Dict[str, Any]) -> Dict[str, Any]:\n    if set(some_dict.keys()) == set([\"__class__\", \"__args__\", \"__kw__\"]):\n        class_ = eval(some_dict[\"__class__\"])\n        return class_(*some_dict[\"__args__\"], **some_dict[\"__kw__\"])\n    else:\n        return some_dict\n\ntest_json_2 = \"\"\"\n    >>> text = json.dumps(travel_x, indent=4, default=blogx_encode)\n    >>> print(text)\n    [\n        {\n            \"__class__\": \"Post\",\n            \"__args__\": [],\n            \"__kw__\": {\n                \"date\": {\n                    \"__class__\": \"datetime.datetime\",\n                    \"__args__\": [],\n                    \"__kw__\": {\n                        \"year\": 2013,\n                        \"month\": 11,\n                        \"day\": 14,\n                        \"hour\": 17,\n                        \"minute\": 25,\n                        \"second\": 0\n                    }\n                },\n                \"title\": \"Hard Aground\",\n                \"rst_text\": \"Some embarrassing revelation. Including \\u2639 and \\u2693\",\n                \"tags\": [\n                    \"#RedRanger\",\n                    \"#Whitby42\",\n                    \"#ICW\"\n                ]\n            }\n        },\n        {\n            \"__class__\": \"Post\",\n            \"__args__\": [],\n            \"__kw__\": {\n                \"date\": {\n                    \"__class__\": \"datetime.datetime\",\n                    \"__args__\": [],\n                    \"__kw__\": {\n                        \"year\": 2013,\n                        \"month\": 11,\n                        \"day\": 18,\n                        \"hour\": 15,\n                        \"minute\": 30,\n                        \"second\": 0\n                    }\n                },\n                \"title\": \"Anchor Follies\",\n                \"rst_text\": \"Some witty epigram. Including < & > characters.\",\n                \"tags\": [\n                    \"#RedRanger\",\n                    \"#Whitby42\",\n                    \"#Mistakes\"\n                ]\n            }\n        }\n    ]\n        \n    The Blog structure overall? Vanished. It's only a list\n    >>> from pprint import pprint\n    >>> copy = json.loads(text, object_hook=blogx_decode)\n    >>> pprint(copy)\n    [Post(date=datetime.datetime(2013, 11, 14, 17, 25), title='Hard Aground', rst_text='Some embarrassing revelation. Including ☹ and ⚓', tags=['#RedRanger', '#Whitby42', '#ICW']),\n     Post(date=datetime.datetime(2013, 11, 18, 15, 30), title='Anchor Follies', rst_text='Some witty epigram. Including < & > characters.', tags=['#RedRanger', '#Whitby42', '#Mistakes'])]\n\n\"\"\"\n\n# Example 3 JSON: Better Design\n# ###############################\n\n# Consider this wrap-based design instead of an extension-based version\n\n# Here's another collection of these posts.\n# This wraps a list which works much better with JSON than extending a list.\n\nimport datetime\nfrom collections import defaultdict\n\n\nclass Blog:\n\n    def __init__(self, title: str, posts: Optional[List[Post]]=None) -> None:\n        self.title = title\n        self.entries = posts if posts is not None else []\n\n    @property\n    def underline(self) -> str:\n        return '='*len(self.title)\n\n    def append(self, post: Post) -> None:\n        self.entries.append(post)\n\n    def by_tag(self) -> Dict[str, List[Dict[str, Any]]]:\n        tag_index: Dict[str, List[Dict[str, Any]]] = defaultdict(list)\n        for post in self.entries:\n            for tag in post.tags:\n                tag_index[tag].append(post.as_dict())\n        return tag_index\n\n    def as_dict(self) -> Dict[str, Any]:\n        return dict(\n            title=self.title,\n            underline=self.underline,\n            entries=[p.as_dict() for p in self.entries],\n        )\n\n\n# An example blog\ntravel = Blog(\"Travel\")\ntravel.append(\n    Post(\n        date=datetime.datetime(2013, 11, 14, 17, 25),\n        title=\"Hard Aground\",\n        rst_text=\"\"\"Some embarrassing revelation. Including ☹ and ⚓︎\"\"\",\n        tags=[\"#RedRanger\", \"#Whitby42\", \"#ICW\"],\n    )\n)\ntravel.append(\n    Post(\n        date=datetime.datetime(2013, 11, 18, 15, 30),\n        title=\"Anchor Follies\",\n        rst_text=\"\"\"Some witty epigram. Including < & > characters.\"\"\",\n        tags=[\"#RedRanger\", \"#Whitby42\", \"#Mistakes\"],\n    )\n)\n\n\ndef blog_encode(object: Any) -> Dict[str, Any]:\n    if isinstance(object, datetime.datetime):\n        return dict(\n            __class__=\"datetime.datetime\",\n            __args__=[],\n            __kw__=dict(\n                year=object.year,\n                month=object.month,\n                day=object.day,\n                hour=object.hour,\n                minute=object.minute,\n                second=object.second,\n            ),\n        )\n    elif isinstance(object, Post):\n        return dict(\n            __class__=\"Post\",\n            __args__=[],\n            __kw__=dict(\n                date=object.date,\n                title=object.title,\n                rst_text=object.rst_text,\n                tags=object.tags,\n            ),\n        )\n    elif isinstance(object, Blog):\n        return dict(\n            __class__=\"Blog\", __args__=[object.title, object.entries], __kw__={}\n        )\n    else:\n        return object\n\n\ndef blog_decode(some_dict: Dict[str, Any]) -> Dict[str, Any]:\n    if set(some_dict.keys()) == {\"__class__\", \"__args__\", \"__kw__\"}:\n        class_ = eval(some_dict[\"__class__\"])\n        return class_(*some_dict[\"__args__\"], **some_dict[\"__kw__\"])\n    else:\n        return some_dict\n\n\ntest_json_3 = \"\"\"\n    >>> text = json.dumps(travel, indent=4, default=blog_encode)\n    >>> print(text)\n    {\n        \"__class__\": \"Blog\",\n        \"__args__\": [\n            \"Travel\",\n            [\n                {\n                    \"__class__\": \"Post\",\n                    \"__args__\": [],\n                    \"__kw__\": {\n                        \"date\": {\n                            \"__class__\": \"datetime.datetime\",\n                            \"__args__\": [],\n                            \"__kw__\": {\n                                \"year\": 2013,\n                                \"month\": 11,\n                                \"day\": 14,\n                                \"hour\": 17,\n                                \"minute\": 25,\n                                \"second\": 0\n                            }\n                        },\n                        \"title\": \"Hard Aground\",\n                        \"rst_text\": \"Some embarrassing revelation. Including \\u2639 and \\u2693\\ufe0e\",\n                        \"tags\": [\n                            \"#RedRanger\",\n                            \"#Whitby42\",\n                            \"#ICW\"\n                        ]\n                    }\n                },\n                {\n                    \"__class__\": \"Post\",\n                    \"__args__\": [],\n                    \"__kw__\": {\n                        \"date\": {\n                            \"__class__\": \"datetime.datetime\",\n                            \"__args__\": [],\n                            \"__kw__\": {\n                                \"year\": 2013,\n                                \"month\": 11,\n                                \"day\": 18,\n                                \"hour\": 15,\n                                \"minute\": 30,\n                                \"second\": 0\n                            }\n                        },\n                        \"title\": \"Anchor Follies\",\n                        \"rst_text\": \"Some witty epigram. Including < & > characters.\",\n                        \"tags\": [\n                            \"#RedRanger\",\n                            \"#Whitby42\",\n                            \"#Mistakes\"\n                        ]\n                    }\n                }\n            ]\n        ],\n        \"__kw__\": {}\n    }\n    \n    >>> from pprint import pprint\n    >>> copy = json.loads(text, object_hook=blog_decode)\n    >>> print(copy.title)\n    Travel\n    >>> pprint(copy.entries)\n    [Post(date=datetime.datetime(2013, 11, 14, 17, 25), title='Hard Aground', rst_text='Some embarrassing revelation. Including ☹ and ⚓︎', tags=['#RedRanger', '#Whitby42', '#ICW']),\n     Post(date=datetime.datetime(2013, 11, 18, 15, 30), title='Anchor Follies', rst_text='Some witty epigram. Including < & > characters.', tags=['#RedRanger', '#Whitby42', '#Mistakes'])]\n\"\"\"\n\n# Sidebar: Demo of rendering 1\n# ###############################\n\n# Here's a template for an individual post\nimport string\n\n# Here's a way to render the entire blog in RST\ndef rst_render(blog: Blog) -> None:\n    post = string.Template(\n        \"\"\"\n    $title\n    $underline\n\n    $rst_text\n\n    :date: $date\n\n    :tags: $tag_text\n    \"\"\"\n    )\n\n    # with contextlib.redirect_stdout(\"some_file\"):\n    print(f\"{blog.title}\\n{blog.underline}\\n\")\n    for p in blog.entries:\n        print(post.substitute(**p.as_dict()))\n\n    tag_index = blog.by_tag()\n    print(\"Tag Index\")\n    print(\"=========\")\n    print()\n    for tag in tag_index:\n        print(f\"*   {tag}\")\n        print()\n        for post_dict in tag_index[tag]:\n            print(f\"    -   `{post_dict['title']}`_\")\n        print()\n\ntest_string_template_render = \"\"\"\n    >>> rst_render(travel)\n    Travel\n    ======\n    <BLANKLINE>\n    <BLANKLINE>\n        Hard Aground\n        ------------\n    <BLANKLINE>\n        Some embarrassing revelation. Including ☹ and ⚓︎\n    <BLANKLINE>\n        :date: 2013-11-14 17:25:00\n    <BLANKLINE>\n        :tags: #RedRanger #Whitby42 #ICW\n    <BLANKLINE>\n    <BLANKLINE>\n        Anchor Follies\n        --------------\n    <BLANKLINE>\n        Some witty epigram. Including < & > characters.\n    <BLANKLINE>\n        :date: 2013-11-18 15:30:00\n    <BLANKLINE>\n        :tags: #RedRanger #Whitby42 #Mistakes\n    <BLANKLINE>\n    Tag Index\n    =========\n    <BLANKLINE>\n    *   #RedRanger\n    <BLANKLINE>\n        -   `Hard Aground`_\n        -   `Anchor Follies`_\n    <BLANKLINE>\n    *   #Whitby42\n    <BLANKLINE>\n        -   `Hard Aground`_\n        -   `Anchor Follies`_\n    <BLANKLINE>\n    *   #ICW\n    <BLANKLINE>\n        -   `Hard Aground`_\n    <BLANKLINE>\n    *   #Mistakes\n    <BLANKLINE>\n        -   `Anchor Follies`_\n    <BLANKLINE>\n\n\"\"\"\n\n# Sidebar: Demo of rendering 2 (using Jinja2)\n# ############################################\n\nfrom jinja2 import Template\n\nblog_template = Template(\n\"\"\"{{title}}\n{{underline}}\n\n{% for e in entries %}\n    {{e.title}}\n    {{e.underline}}\n\n    {{e.rst_text}}\n\n    :date: {{e.date}}\n\n    :tags: {{e.tag_text}}\n    \n{% endfor %}\n\nTag Index\n=========\n{% for t in tags %}\n*   {{t}}\n    {% for post in tags[t] %}\n    -   `{{post.title}}`_\n    {%- endfor %}\n{% endfor %}\n\"\"\"\n)\n\ntest_jinja_temple_render = \"\"\"\n    >>> print(blog_template.render(tags=travel.by_tag(), **travel.as_dict()))\n    Travel\n    ======\n    <BLANKLINE>\n    <BLANKLINE>\n        Hard Aground\n        ------------\n    <BLANKLINE>\n        Some embarrassing revelation. Including ☹ and ⚓︎\n    <BLANKLINE>\n        :date: 2013-11-14 17:25:00\n    <BLANKLINE>\n        :tags: #RedRanger #Whitby42 #ICW\n    <BLANKLINE>\n    <BLANKLINE>\n        Anchor Follies\n        --------------\n    <BLANKLINE>\n        Some witty epigram. Including < & > characters.\n    <BLANKLINE>\n        :date: 2013-11-18 15:30:00\n    <BLANKLINE>\n        :tags: #RedRanger #Whitby42 #Mistakes\n    <BLANKLINE>\n    <BLANKLINE>\n    <BLANKLINE>\n    Tag Index\n    =========\n    <BLANKLINE>\n    *   #RedRanger\n    <BLANKLINE>\n        -   `Hard Aground`_\n        -   `Anchor Follies`_\n    <BLANKLINE>\n    *   #Whitby42\n    <BLANKLINE>\n        -   `Hard Aground`_\n        -   `Anchor Follies`_\n    <BLANKLINE>\n    *   #ICW\n    <BLANKLINE>\n        -   `Hard Aground`_\n    <BLANKLINE>\n    *   #Mistakes\n    <BLANKLINE>\n        -   `Anchor Follies`_\n    <BLANKLINE>\n\"\"\"\n\n# Example 4. JSON: Refactoring Encoding\n# ######################################\n\n# Changes to the class definitions to add a ``_json`` method.\n\n\nclass Post_J(Post):\n    \"\"\"Not really essential to inherit from Post, it's simply a dataclass.\"\"\"\n    @property\n    def _json(self) -> Dict[str, Any]:\n        return dict(\n            __class__=self.__class__.__name__,\n            __kw__=dict(\n                date=self.date, title=self.title, rst_text=self.rst_text, tags=self.tags\n            ),\n            __args__=[],\n        )\n\nclass Blog_J(Blog):\n    \"\"\"Note. No explicit reference to Blog_J for entries.\"\"\"\n\n    @property\n    def _json(self) -> Dict[str, Any]:\n        return dict(\n            __class__=self.__class__.__name__,\n            __kw__={},\n            __args__=[self.title, self.entries],\n        )\n\ndef blog_j_encode(object: Union[Blog_J, Post_J, Any]) -> Dict[str, Any]:\n    if isinstance(object, datetime.datetime):\n        return dict(\n            __class__=\"datetime.datetime\",\n            __args__=[],\n            __kw__=dict(\n                year=object.year,\n                month=object.month,\n                day=object.day,\n                hour=object.hour,\n                minute=object.minute,\n                second=object.second,\n            ),\n        )\n    else:\n        try:\n            encoding = object._json\n        except AttributeError:\n            encoding = json.JSONEncoder().default(object)\n        return encoding\n\n\ntravel3 = Blog_J(\"Travel\")\ntravel3.append(\n    Post_J(\n        date=datetime.datetime(2013, 11, 14, 17, 25),\n        title=\"Hard Aground\",\n        rst_text=\"\"\"Some embarrassing revelation. Including ☹ and ⚓\"\"\",\n        tags=[\"#RedRanger\", \"#Whitby42\", \"#ICW\"],\n    )\n)\ntravel3.append(\n    Post_J(\n        date=datetime.datetime(2013, 11, 18, 15, 30),\n        title=\"Anchor Follies\",\n        rst_text=\"\"\"Some witty epigram.\"\"\",\n        tags=[\"#RedRanger\", \"#Whitby42\", \"#Mistakes\"],\n    )\n)\n\ntest_json_4 = \"\"\"\n    >>> text = json.dumps(travel3, indent=4, default=blog_j_encode)\n    >>> print(text)\n    {\n        \"__class__\": \"Blog_J\",\n        \"__kw__\": {},\n        \"__args__\": [\n            \"Travel\",\n            [\n                {\n                    \"__class__\": \"Post_J\",\n                    \"__kw__\": {\n                        \"date\": {\n                            \"__class__\": \"datetime.datetime\",\n                            \"__args__\": [],\n                            \"__kw__\": {\n                                \"year\": 2013,\n                                \"month\": 11,\n                                \"day\": 14,\n                                \"hour\": 17,\n                                \"minute\": 25,\n                                \"second\": 0\n                            }\n                        },\n                        \"title\": \"Hard Aground\",\n                        \"rst_text\": \"Some embarrassing revelation. Including \\u2639 and \\u2693\",\n                        \"tags\": [\n                            \"#RedRanger\",\n                            \"#Whitby42\",\n                            \"#ICW\"\n                        ]\n                    },\n                    \"__args__\": []\n                },\n                {\n                    \"__class__\": \"Post_J\",\n                    \"__kw__\": {\n                        \"date\": {\n                            \"__class__\": \"datetime.datetime\",\n                            \"__args__\": [],\n                            \"__kw__\": {\n                                \"year\": 2013,\n                                \"month\": 11,\n                                \"day\": 18,\n                                \"hour\": 15,\n                                \"minute\": 30,\n                                \"second\": 0\n                            }\n                        },\n                        \"title\": \"Anchor Follies\",\n                        \"rst_text\": \"Some witty epigram.\",\n                        \"tags\": [\n                            \"#RedRanger\",\n                            \"#Whitby42\",\n                            \"#Mistakes\"\n                        ]\n                    },\n                    \"__args__\": []\n                }\n            ]\n        ]\n    }\n\"\"\"\n\n# Example 5: JSON: Super-Flexible Date Encoding\n# #############################################\n\n# Right at the edge of the envelope for dates. This may be too much flexibility.\n# There's an ISO standard for dates, and using it is simpler.\n\n# For other unique data objects, however, this kind of pattern may be helpful\n# for providing a way to parse complex strings.\n\n# Changes to the class definitions\ndef blog_j2_encode(object: Union[Blog_J, Post_J, Any]) -> Dict[str, Any]:\n    if isinstance(object, datetime.datetime):\n        return dict(\n            __class__=\"datetime.datetime.strptime\",\n            __args__=[object.strftime(\"%Y-%m-%dT%H:%M:%S\"), \"%Y-%m-%dT%H:%M:%S\"],\n            __kw__={},\n        )\n    else:\n        try:\n            encoding = object._json\n        except AttributeError:\n            encoding = json.JSONEncoder().default(object)\n        return encoding\n\n\ntest_json_5 = \"\"\"\n    >>> text = json.dumps(travel3, indent=4, default=blog_j2_encode)\n    >>> print(text)\n    {\n        \"__class__\": \"Blog_J\",\n        \"__kw__\": {},\n        \"__args__\": [\n            \"Travel\",\n            [\n                {\n                    \"__class__\": \"Post_J\",\n                    \"__kw__\": {\n                        \"date\": {\n                            \"__class__\": \"datetime.datetime.strptime\",\n                            \"__args__\": [\n                                \"2013-11-14T17:25:00\",\n                                \"%Y-%m-%dT%H:%M:%S\"\n                            ],\n                            \"__kw__\": {}\n                        },\n                        \"title\": \"Hard Aground\",\n                        \"rst_text\": \"Some embarrassing revelation. Including \\u2639 and \\u2693\",\n                        \"tags\": [\n                            \"#RedRanger\",\n                            \"#Whitby42\",\n                            \"#ICW\"\n                        ]\n                    },\n                    \"__args__\": []\n                },\n                {\n                    \"__class__\": \"Post_J\",\n                    \"__kw__\": {\n                        \"date\": {\n                            \"__class__\": \"datetime.datetime.strptime\",\n                            \"__args__\": [\n                                \"2013-11-18T15:30:00\",\n                                \"%Y-%m-%dT%H:%M:%S\"\n                            ],\n                            \"__kw__\": {}\n                        },\n                        \"title\": \"Anchor Follies\",\n                        \"rst_text\": \"Some witty epigram.\",\n                        \"tags\": [\n                            \"#RedRanger\",\n                            \"#Whitby42\",\n                            \"#Mistakes\"\n                        ]\n                    },\n                    \"__args__\": []\n                }\n            ]\n        ]\n    }\n    \n    >>> from pprint import pprint\n    >>> copy = json.loads(text, object_hook=blog_decode)\n    >>> print(copy.title)\n    Travel\n    >>> pprint(copy.entries)\n    [Post_J(date=datetime.datetime(2013, 11, 14, 17, 25), title='Hard Aground', rst_text='Some embarrassing revelation. Including ☹ and ⚓', tags=['#RedRanger', '#Whitby42', '#ICW']),\n     Post_J(date=datetime.datetime(2013, 11, 18, 15, 30), title='Anchor Follies', rst_text='Some witty epigram.', tags=['#RedRanger', '#Whitby42', '#Mistakes'])]\n\"\"\"\n\nwith (Path.cwd()/\"data\"/\"ch10.json\").open(\"w\", encoding=\"UTF-8\") as target:\n    json.dump(travel3, target, separators=(\",\", \":\"), default=blog_j2_encode)\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_10/ch10_ex2.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 10. Example 2. YAML. Base Definitions\n\"\"\"\n\n# Persistence Classes\n# ========================================\n\nfrom typing import List, Optional, Dict, Any\n\n# Example 2: Cards\n# ###################\n\nfrom enum import Enum\nclass Suit(str, Enum):\n    Clubs = \"♣\"\n    Diamonds = \"♦\"\n    Hearts = \"♥\"\n    Spades = \"♠\"\n\nclass Card:\n\n    def __init__(self, rank: str, suit: Suit, hard: Optional[int]=None, soft: Optional[int]=None) -> None:\n        self.rank = rank\n        self.suit = suit\n        self.hard = hard or int(rank)\n        self.soft = soft or int(rank)\n\n    def __str__(self) -> str:\n        return f\"{self.rank!s}{self.suit.value!s}\"\n\n\nclass AceCard(Card):\n\n    def __init__(self, rank: str, suit: Suit) -> None:\n        super().__init__(rank, suit, 1, 11)\n\n\nclass FaceCard(Card):\n\n    def __init__(self, rank: str, suit: Suit) -> None:\n        super().__init__(rank, suit, 10, 10)\n\n\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_10/ch10_ex2a.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 10. Example 2. YAML (part a)\n\"\"\"\n\n# Persistence Classes\n# ========================================\n\n# A detail class for micro-blog posts\nimport datetime\nfrom typing import List, Optional, Dict, Any\nfrom dataclasses import dataclass\nfrom pathlib import Path\n\nfrom Chapter_10.ch10_ex1 import Post, Blog, travel, rst_render\nfrom Chapter_10.ch10_ex2 import Suit, Card, FaceCard, AceCard\n\n# YAML\n# ===================\n\nimport yaml\n\n# Example 1: That's it.\n# ######################\n\n# Start with original definitions\n\ntest_yaml = \"\"\"\n    >>> text = yaml.dump(travel)\n    >>> print(text)\n    !!python/object:Chapter_10.ch10_ex1.Blog\n    entries:\n    - !!python/object:Chapter_10.ch10_ex1.Post\n      date: 2013-11-14 17:25:00\n      rst_text: \"Some embarrassing revelation. Including \\\\u2639 and \\\\u2693\\\\uFE0E\"\n      tags:\n      - '#RedRanger'\n      - '#Whitby42'\n      - '#ICW'\n      title: Hard Aground\n    - !!python/object:Chapter_10.ch10_ex1.Post\n      date: 2013-11-18 15:30:00\n      rst_text: Some witty epigram. Including < & > characters.\n      tags:\n      - '#RedRanger'\n      - '#Whitby42'\n      - '#Mistakes'\n      title: Anchor Follies\n    title: Travel\n    <BLANKLINE>\n    \n    >>> copy = yaml.load(text)\n    >>> print(type(copy), copy.title)\n    <class 'Chapter_10.ch10_ex1.Blog'> Travel\n    >>> for p in copy.entries:\n    ...        print(p.date.year, p.date.month, p.date.day, p.title, p.tags)\n    2013 11 14 Hard Aground ['#RedRanger', '#Whitby42', '#ICW']\n    2013 11 18 Anchor Follies ['#RedRanger', '#Whitby42', '#Mistakes']\n\n    >>> text2 = yaml.dump(travel, allow_unicode=True)\n    >>> print(text2)\n    !!python/object:Chapter_10.ch10_ex1.Blog\n    entries:\n    - !!python/object:Chapter_10.ch10_ex1.Post\n      date: 2013-11-14 17:25:00\n      rst_text: Some embarrassing revelation. Including ☹ and ⚓︎\n      tags:\n      - '#RedRanger'\n      - '#Whitby42'\n      - '#ICW'\n      title: Hard Aground\n    - !!python/object:Chapter_10.ch10_ex1.Post\n      date: 2013-11-18 15:30:00\n      rst_text: Some witty epigram. Including < & > characters.\n      tags:\n      - '#RedRanger'\n      - '#Whitby42'\n      - '#Mistakes'\n      title: Anchor Follies\n    title: Travel\n    <BLANKLINE>\n\n\"\"\"\n\nwith (Path.cwd()/\"data\"/\"ch10.yaml\").open(\"w\", encoding=\"UTF-8\") as target:\n    yaml.dump(travel, target)\n\n# Example 2: Cards\n# ###################\n\n\ndeck = [AceCard(\"A\", Suit.Clubs), Card(\"2\", Suit.Hearts), FaceCard(\"K\", Suit.Diamonds)]\n\ntest_yaml_dump = \"\"\"\n    >>> text = yaml.dump(deck, allow_unicode=True)\n    >>> print(text)\n    - !!python/object:Chapter_10.ch10_ex2.AceCard\n      hard: 1\n      rank: A\n      soft: 11\n      suit: !!python/object/apply:Chapter_10.ch10_ex2.Suit\n      - ♣\n    - !!python/object:Chapter_10.ch10_ex2.Card\n      hard: 2\n      rank: '2'\n      soft: 2\n      suit: !!python/object/apply:Chapter_10.ch10_ex2.Suit\n      - ♥\n    - !!python/object:Chapter_10.ch10_ex2.FaceCard\n      hard: 10\n      rank: K\n      soft: 10\n      suit: !!python/object/apply:Chapter_10.ch10_ex2.Suit\n      - ♦\n    <BLANKLINE>    \n\"\"\"\n\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_10/ch10_ex2b.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 10. Example 2. YAML (part b)\n\"\"\"\n\n# Persistence Classes\n# ========================================\n\n# A detail class for micro-blog posts\nimport datetime\nfrom typing import List, Optional, Dict, Any\nfrom dataclasses import dataclass\nfrom pathlib import Path\nfrom Chapter_10.ch10_ex2 import Suit, Card, AceCard, FaceCard\n\n# YAML -- 2b cards with custom representations\n# =============================================\n\ndeck = [AceCard(\"A\", Suit.Clubs), Card(\"2\", Suit.Hearts), FaceCard(\"K\", Suit.Diamonds)]\n\nimport yaml\n\n\ndef card_representer(dumper: Any, card: Card) -> str:\n    return dumper.represent_scalar(\n        \"!Card\", f\"{card.rank!s}{card.suit.value!s}\")\n\n\ndef acecard_representer(dumper: Any, card: Card) -> str:\n    return dumper.represent_scalar(\n        \"!AceCard\", f\"{card.rank!s}{card.suit.value!s}\")\n\n\ndef facecard_representer(dumper: Any, card: Card) -> str:\n    return dumper.represent_scalar(\n        \"!FaceCard\", f\"{card.rank!s}{card.suit.value!s}\")\n\n\ndef card_constructor(loader: Any, node: Any) -> Card:\n    value = loader.construct_scalar(node)\n    rank, suit = value[:-1], value[-1]\n    return Card(rank, Suit(suit))\n\n\ndef acecard_constructor(loader: Any, node: Any) -> Card:\n    value = loader.construct_scalar(node)\n    rank, suit = value[:-1], value[-1]\n    return AceCard(rank, Suit(suit))\n\n\ndef facecard_constructor(loader: Any, node: Any) -> Card:\n    value = loader.construct_scalar(node)\n    rank, suit = value[:-1], value[-1]\n    return FaceCard(rank, Suit(suit))\n\n# Changes to the yaml module will apply throughout the application.\n# And this test run, also.\n# We can also add this\n\nyaml.add_representer(Card, card_representer)\nyaml.add_representer(AceCard, acecard_representer)\nyaml.add_representer(FaceCard, facecard_representer)\nyaml.add_constructor(\"!Card\", card_constructor)\nyaml.add_constructor(\"!AceCard\", acecard_constructor)\nyaml.add_constructor(\"!FaceCard\", facecard_constructor)\n\ntest_yaml_dump_load = \"\"\"\n    >>> print(*map(str, deck))\n    A♣ 2♥ K♦\n    \n    >>> text = yaml.dump(deck, allow_unicode=True)\n    >>> print(text)\n    - !AceCard 'A♣'\n    - !Card '2♥'\n    - !FaceCard 'K♦'\n    <BLANKLINE>\n    \n    >>> copy = yaml.load(text, Loader=yaml.Loader)\n    >>> print(*map(str, copy))\n    A♣ 2♥ K♦\n\"\"\"\n\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_10/ch10_ex2c.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 10. Example 2. YAML (part c)\n\"\"\"\n\n# Persistence Classes\n# ========================================\n\n# A detail class for micro-blog posts\nimport datetime\nfrom typing import List, Optional, Dict, Any\nfrom dataclasses import dataclass\nfrom pathlib import Path\nfrom Chapter_10.ch10_ex2 import Suit, Card, AceCard, FaceCard\nfrom Chapter_10.ch10_ex2b import facecard_representer, acecard_representer, card_representer\n\n# YAML -- 2c cards with safe custom representations\n# ==================================================\n\nimport yaml\n\n\nclass Card2(yaml.YAMLObject):\n    yaml_tag = \"!Card2\"\n    yaml_loader = yaml.SafeLoader\n\n    def __init__(self, rank, suit, hard=None, soft=None) -> None:\n        self.rank = rank\n        self.suit = suit\n        self.hard = hard or int(rank)\n        self.soft = soft or int(rank)\n\n    def __str__(self) -> str:\n        return \"{0.rank!s}{0.suit!s}\".format(self)\n\n\nclass AceCard2(Card2):\n    yaml_tag = \"!AceCard2\"\n\n    def __init__(self, rank, suit) -> None:\n        super().__init__(rank, suit, 1, 11)\n\n\nclass FaceCard2(Card2):\n    yaml_tag = \"!FaceCard2\"\n\n    def __init__(self, rank, suit) -> None:\n        super().__init__(rank, suit, 10, 10)\n\n\ndeck2 = [AceCard2(\"A\", \"♣\"), Card2(\"2\", \"♥\"), FaceCard2(\"K\", \"♦\")]\n\ntest_yaml_dump_safe_load = \"\"\"\n    # Changes to the yaml module will apply throughout the application.\n    >>> yaml.add_representer(Card, card_representer)\n    >>> yaml.add_representer(AceCard, acecard_representer)\n    >>> yaml.add_representer(FaceCard, facecard_representer)\n\n    >>> text2 = yaml.dump(deck2)\n    >>> print(text2)\n    - !AceCard2\n      hard: 1\n      rank: A\n      soft: 11\n      suit: \"\\\\u2663\"\n    - !Card2\n      hard: 2\n      rank: '2'\n      soft: 2\n      suit: \"\\\\u2665\"\n    - !FaceCard2\n      hard: 10\n      rank: K\n      soft: 10\n      suit: \"\\\\u2666\"\n    <BLANKLINE>\n    \n    >>> copy = yaml.safe_load(text2)\n    >>> print([str(c) for c in copy])\n    ['A♣', '2♥', 'K♦']\n\"\"\"\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_10/ch10_ex3.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 10. Example 3. Pickle\n\"\"\"\n\n# Persistence Classes\n# ========================================\n\nimport datetime\nfrom typing import List, Optional, Dict, Any\nfrom dataclasses import dataclass\nfrom pathlib import Path\n\nfrom Chapter_10.ch10_ex1 import Post, Blog, travel, rst_render\nfrom Chapter_10.ch10_ex2 import FaceCard, AceCard, Card, Suit\n\n# Pickle\n# ===================\n\n# Example 1: Working\n# ####################\n\n# Use pickle to persist our microblog\nimport pickle\nfrom pathlib import Path\n\ntest_pickle = \"\"\"\n    >>> with (Path.cwd()/\"data\"/\"ch10_travel_blog.p\").open(\"wb\") as target:\n    ...     pickle.dump(travel, target)\n\n    >>> with(Path.cwd()/\"data\"/\"ch10_travel_blog.p\").open(\"rb\") as source:\n    ...     copy = pickle.load(source)\n\n    >>> print(copy.title)\n    Travel\n    >>> for post in copy.entries:\n    ...     print(post)\n    Post(date=datetime.datetime(2013, 11, 14, 17, 25), title='Hard Aground', rst_text='Some embarrassing revelation. Including ☹ and ⚓︎', tags=['#RedRanger', '#Whitby42', '#ICW'])\n    Post(date=datetime.datetime(2013, 11, 18, 15, 30), title='Anchor Follies', rst_text='Some witty epigram. Including < & > characters.', tags=['#RedRanger', '#Whitby42', '#Mistakes'])\n\"\"\"\n\n# Example 2: Won't Init\n# ########################\n\nimport logging, sys\n\naudit_log = logging.getLogger(\"audit\")\n\nclass Hand_bad:\n\n    def __init__(self, dealer_card: Card, *cards: Card) -> None:\n        self.dealer_card = dealer_card\n        self.cards = list(cards)\n        for c in self.cards:\n            audit_log.info(\"Initial %s\", c)\n\n    def append(self, card: Card) -> None:\n        self.cards.append(card)\n        audit_log.info(\"Hit %s\", card)\n\n    def __str__(self) -> str:\n        cards = \", \".join(map(str, self.cards))\n        return f\"{self.dealer_card} | {cards}\"\n\n\ntest_audit = \"\"\"\n    >>> logging.basicConfig(stream=sys.stderr, level=logging.INFO)\n\n    >>> logging.info(\"bad create\")\n    >>> h = Hand_bad(FaceCard(\"K\", Suit.Diamonds), AceCard(\"A\", Suit.Clubs), Card(\"9\", Suit.Hearts))\n    >>> print(h)\n    K♦ | A♣, 9♥\n\n    >>> b = pickle.dumps(h)\n\n    >>> logging.info(\"bad load from pickle\")\n    >>> h2 = pickle.loads(b)\n    >>> print(h2)\n    K♦ | A♣, 9♥\n    \n    >>> logging.shutdown()\n\"\"\"\n\n\nclass Hand2:\n\n    def __init__(self, dealer_card: Card, *cards: Card) -> None:\n        self.dealer_card = dealer_card\n        self.cards = list(cards)\n        for c in self.cards:\n            audit_log.info(\"Initial %s\", c)\n\n    def append(self, card: Card) -> None:\n        self.cards.append(card)\n        audit_log.info(\"Hit %s\", card)\n\n    def __str__(self) -> str:\n        cards = \", \".join(map(str, self.cards))\n        return f\"{self.dealer_card} | {cards}\"\n\n    def __getstate__(self) -> Dict[str, Any]:\n        return vars(self)\n\n    def __setstate__(self, state: Dict[str, Any]) -> None:\n        # Not very secure -- hard for mypy to detect what's going on.\n        self.__dict__.update(state)\n        for c in self.cards:\n            audit_log.info(\"Initial (unpickle) %s\", c)\n\ntest_audit_2 = \"\"\"\n    >>> logging.basicConfig(stream=sys.stderr, level=logging.INFO)\n\n    >>> logging.info(\"good create\")\n    >>> hp = Hand2(FaceCard(\"K\", Suit.Diamonds), AceCard(\"A\", Suit.Clubs), Card(\"9\", Suit.Hearts))\n\n    >>> data = pickle.dumps(hp)\n\n    >>> logging.info(\"good load from pickle\")\n    >>> h2p = pickle.loads(data)\n    >>> print(h2p)\n    K♦ | A♣, 9♥\n\n    >>> logging.shutdown()\n\"\"\"\n\n# Example 3: Secure Pickle\n# ########################\n\nimport builtins\n\n\nclass RestrictedUnpickler(pickle.Unpickler):\n\n    def find_class(self, module: str, name: str) -> Any:\n        if module == \"builtins\":\n            if name not in (\"exec\", \"eval\"):\n                return getattr(builtins, name)\n        elif module in (\"__main__\", \"Chapter_10.ch10_ex3\", \"ch10_ex3\"):\n            # Valid module names depends on execution context.\n            return globals()[name]\n        # elif module in any of our application modules...\n        elif module in (\"Chapter_10.ch10_ex2\",):\n            return globals()[name]\n        raise pickle.UnpicklingError(\n            f\"global '{module}.{name}' is forbidden\"\n        )\n\n\ntest_audit_3 = \"\"\"\n    >>> import io\n    >>> logging.basicConfig(stream=sys.stderr, level=logging.INFO)\n\n    >>> hp = Hand2(FaceCard(\"K\", Suit.Diamonds), AceCard(\"A\", Suit.Clubs), Card(\"9\", Suit.Hearts))\n\n    >>> data = pickle.dumps(hp)\n    >>> try:\n    ...     h2s = RestrictedUnpickler(io.BytesIO(data)).load()\n    ... except pickle.UnpicklingError as e:\n    ...     print(e)\n    >>> print(h2s)\n    K♦ | A♣, 9♥\n    \n    Creating an unimportable pickle file requires something not in Chapter_10.ch10_ex2.\n    >>> from Chapter_10.ch10_ex1 import travel\n    >>> bad_data = pickle.dumps(travel)\n    >>> try:\n    ...     travel_copy = RestrictedUnpickler(io.BytesIO(bad_data)).load()\n    ... except pickle.UnpicklingError as e:\n    ...     print(e)\n    global 'Chapter_10.ch10_ex1.Blog' is forbidden\n\"\"\"\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_10/ch10_ex4.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 10. Example 4. CSV\n\"\"\"\n\n# Persistence Classes\n# ========================================\n\n# A detail class for micro-blog posts\nimport datetime\nfrom typing import List, Optional, Dict, Any, Iterator\nfrom dataclasses import dataclass\nfrom pathlib import Path\n\nfrom Chapter_10.ch10_ex1 import Post, Blog, travel, rst_render\nfrom Chapter_10.ch10_ex2 import FaceCard, AceCard, Card\nimport io\n\n# CSV\n# ===================\n\n# Example 1: GameStats\n# ######################\n\n\nclass Table:\n    \"\"\"Abstraction for games played on tables.\"\"\"\n\n    def bet(self, game_state: str, amount: float) -> None:\n        \"\"\"Accepts a bet for a particular future game state.\"\"\"\n        pass\n\n\nclass Player_Strategy:\n    \"\"\"Abstraction for player choices. Varies by game, of course.\"\"\"\n    pass\n\n\nclass Betting:\n\n    def __init__(self, stake: float = 100) -> None:\n        self.stake = stake\n\n    def bet(self, table: Table, game_state: str) -> None:\n        if game_state == \"ante\":\n            table.bet(game_state, 1)\n\n    def win(self, amount: float) -> None:\n        self.stake += amount\n\n    def loss(self, amount: float) -> None:\n        self.stake -= amount\n\n    def push(self) -> None:\n        pass\n\n\nclass Flat_Bet(Betting):\n    pass\n\n\nclass Martingale_Bet(Betting):\n\n    def __init__(self, *args) -> None:\n        super().__init__(*args)\n        self.stage = 1\n\n    def bet(self, table: Table, game_state: str) -> None:\n        if game_state == \"ante\":\n            try:\n                table.bet(game_state, min(self.stage, self.stake))\n            except BadBet as e:\n                limit = e.args[0]\n                table.bet(game_state, min(limit, self.stake))\n\n    def win(self, amount) -> None:\n        self.stage = 1\n        super().win(amount)\n\n    def loss(self, amount) -> None:\n        self.stage = min(self.stage * 2, 512)\n        super().loss(amount)\n\n    def push(self) -> None:\n        super().push()\n\n\nimport random\n\n\nclass BadBet(Exception):\n    pass\n\n\nclass Broke(Exception):\n    pass\n\n\n# A \"Table\" implementation for Blackjack.\nclass Blackjack(Table):\n\n    def __init__(self, play: Player_Strategy, betting: Betting) -> None:\n        self.player = play\n        self.betting = betting\n        self.bets: Dict[str, float] = dict()\n        self.rounds = 0\n\n    @property\n    def stake(self) -> float:\n        return self.betting.stake\n\n    def bet(self, game_state: str, amount: float) -> None:\n        if amount > 50:\n            raise BadBet(50)\n        self.bets[game_state] = amount\n\n    def play_1(self) -> None:\n        if self.betting.stake == 0:\n            raise Broke\n        self.betting.bet(self, \"ante\")\n        bet = sum(self.bets.values())\n        outcome = random.random()\n        if outcome < 0.579:\n            self.betting.loss(bet)\n        elif 0.579 <= outcome < 0.883:\n            self.betting.win(bet)\n        elif 0.883 <= outcome < 0.943:\n            self.betting.push()\n        else:\n            # 0.943 <= outcome\n            self.betting.win(bet * 2)\n\n    def until_broke_or_rounds(self, limit: int) -> None:\n        while self.rounds < limit and self.betting.stake > 0:\n            self.play_1()\n            self.rounds += 1\n\n\n# Example 1 dumping\n# ####################\n\n# An application of the above definitions.\nfrom typing import NamedTuple\n\n\nclass GameStat(NamedTuple):\n    player: str\n    bet: str\n    rounds: int\n    final: float\n\n\nfrom typing import Iterator, Type\n\n\ndef gamestat_iter(\n    player: Type[Player_Strategy], betting: Type[Betting], limit: int = 100\n) -> Iterator[GameStat]:\n    for sample in range(30):\n        random.seed(sample)  # Reproducible\n        b = Blackjack(player(), betting())\n        b.until_broke_or_rounds(limit)\n        yield GameStat(player.__name__, betting.__name__, b.rounds, b.betting.stake)\n\n\nimport csv\nfrom pathlib import Path\n\nwith (Path.cwd() / \"data\" / \"ch10_blackjack_1.csv\").open(\"w\", newline=\"\") as target:\n    writer = csv.DictWriter(target, GameStat._fields)\n    writer.writeheader()\n    for gamestat in gamestat_iter(Player_Strategy, Martingale_Bet):\n        writer.writerow(gamestat._asdict())\n\ndata = gamestat_iter(Player_Strategy, Martingale_Bet)\nwith (Path.cwd() / \"data\" / \"ch10_blackjack_2.csv\").open(\"w\", newline=\"\") as target:\n    writer = csv.DictWriter(target, GameStat._fields)\n    writer.writeheader()\n    writer.writerows(g._asdict() for g in data)\n\n# Example 2 loading\n# ###################\n\n# Loading data from the simulator\n\nwith (Path.cwd() / \"data\" / \"ch10_blackjack_1.csv\").open() as source:\n    reader = csv.DictReader(source)\n    assert set(reader.fieldnames) == set(GameStat._fields)\n    for gs in (GameStat(**r) for r in reader):\n        pass  # print( gs )\n\n\ndef gamestat_rdr_iter(\n        source_data: Iterator[Dict[str, str]]\n    ) -> Iterator[GameStat]:\n    for row in source_data:\n        yield GameStat(row[\"player\"], row[\"bet\"], int(row[\"rounds\"]), int(row[\"final\"]))\n\n\ntest_write_read_1 = \"\"\"\n    >>> with (Path.cwd()/\"data\"/\"ch10_blackjack_1.csv\").open() as source:\n    ...     reader = csv.DictReader(source)\n    ...     assert set(reader.fieldnames) == set(GameStat._fields)\n    ...     for gs in gamestat_rdr_iter(reader):\n    ...         print(gs)\n    GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=100, final=142)\n    GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=27, final=0)\n    GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=25, final=0)\n    GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=100, final=157)\n    GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=100, final=87)\n    GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=18, final=0)\n    GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=100, final=161)\n    GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=10, final=0)\n    GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=22, final=0)\n    GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=53, final=0)\n    GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=37, final=0)\n    GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=27, final=0)\n    GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=100, final=188)\n    GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=58, final=0)\n    GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=100, final=103)\n    GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=28, final=0)\n    GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=60, final=0)\n    GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=100, final=150)\n    GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=9, final=0)\n    GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=13, final=0)\n    GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=97, final=0)\n    GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=100, final=93)\n    GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=72, final=0)\n    GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=12, final=0)\n    GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=36, final=0)\n    GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=35, final=0)\n    GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=78, final=0)\n    GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=68, final=0)\n    GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=39, final=0)\n    GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=47, final=0)\n\"\"\"\n\n# Example 3 blog and post one file\n# ################################\n\n# There are two row types, however -- blogs and posts within a blog.\n\n# Our blog data to be saved positionally.\nblogs = [travel]\n\nwith (Path.cwd() / \"data\" / \"ch10_blog3.csv\").open(\"w\", newline=\"\") as target:\n    wtr = csv.writer(target)\n    wtr.writerow([\"__class__\", \"title\", \"date\", \"title\", \"rst_text\", \"tags\"])\n    for b in blogs:\n        wtr.writerow([\"Blog\", b.title, None, None, None, None])\n        for p in b.entries:\n            wtr.writerow([\"Post\", None, p.date, p.title, p.rst_text, p.tags])\n\n# Super-important: column order must match __init__() param order.\n# Hard to do in general.\n# And impossible to make work with mypy unless your Blog and Post structures\n# are reduced to List[str]\nwith (Path.cwd() / \"data\" / \"ch10_blog3.csv\").open() as source:\n    rdr = csv.reader(source)\n    header = next(rdr)\n    assert header == [\"__class__\", \"title\", \"date\", \"title\", \"rst_text\", \"tags\"]\n    blogs = []\n    for r in rdr:\n        if r[0] == \"Blog\":\n            blog = Blog(*r[1:2])  # type: ignore\n            blogs.append(blog)\n        elif r[0] == \"Post\":\n            post = Post(*r[2:])  # type: ignore\n            blogs[-1].append(post)\n\n# Tags, however, will not be a proper tuple\n# The above doesn't handle Post tags properly!\n\n# Can use the following for safe eval of literals.\nimport ast\n\n\ndef blog_builder(row: List[str]) -> Blog:\n    return Blog(row[1])\n\n\ndef post_builder(row: List[str]) -> Post:\n    return Post(\n        date=datetime.datetime.strptime(row[2], \"%Y-%m-%d %H:%M:%S\"),\n        title=row[3],\n        rst_text=row[4],\n        tags=ast.literal_eval(row[5]),\n    )\n\n\nwith (Path.cwd() / \"data\" / \"ch10_blog3.csv\").open() as source:\n    rdr = csv.reader(source)\n    header = next(rdr)\n    assert header == [\"__class__\", \"title\", \"date\", \"title\", \"rst_text\", \"tags\"]\n    blogs = []\n    for r in rdr:\n        if r[0] == \"Blog\":\n            blog = blog_builder(r)\n            blogs.append(blog)\n        elif r[0] == \"Post\":\n            post = post_builder(r)\n            blogs[-1].append(post)\n\n# Example 4 blog and post with better metadata and filter\n# ########################################################\n\n# Loading the blog with a generator function.\n\nfrom typing import TextIO, cast\n\n\ndef blog_iter(source: TextIO) -> Iterator[Blog]:\n    rdr = csv.reader(source)\n    header = next(rdr)\n    assert header == [\"__class__\", \"title\", \"date\", \"title\", \"rst_text\", \"tags\"]\n    blog: Blog = cast(Blog, None)\n    for r in rdr:\n        if r[0] == \"Blog\":\n            if blog:\n                yield blog\n            blog = blog_builder(r)\n        elif r[0] == \"Post\":\n            post = post_builder(r)\n            blog.append(post)\n    if blog:\n        yield blog\n\n\ntest_blog_3 = \"\"\"\n    >>> with (Path.cwd()/\"data\"/\"ch10_blog3.csv\").open() as source:\n    ...     for b in blog_iter(source):\n    ...         print(b.title, [p.title for p in b.entries])\n    Travel ['Hard Aground', 'Anchor Follies']\n\n    >>> with (Path.cwd()/\"data\"/\"ch10_blog3.csv\").open() as source:\n    ...     blogs = list(blog_iter(source))\n    >>> for b in blogs:\n    ...     print(b.title, [p.title for p in b.entries])\n    Travel ['Hard Aground', 'Anchor Follies']\n\"\"\"\n\n# Example 5 Blog and Post join\n# ################################\n\n# Using a \"join\" between Blog and Post to create a file.\nwith (Path.cwd() / \"data\" / \"ch10_blog5.csv\").open(\"w\", newline=\"\") as target:\n    wtr = csv.writer(target)\n    wtr.writerow(\n        [\"Blog.title\", \"Post.date\", \"Post.title\", \"Post.tags\", \"Post.rst_text\"]\n    )\n    for b in blogs:\n        for p in b.entries:\n            wtr.writerow([b.title, p.date, p.title, p.tags, p.rst_text])\n\nfrom typing import Union, Iterator, Tuple\n\n\nimport ast\n\n\ndef post_builder5(row: Dict[str, str]) -> Post:\n    return Post(\n        date=datetime.datetime.strptime(row[\"Post.date\"], \"%Y-%m-%d %H:%M:%S\"),\n        title=row[\"Post.title\"],\n        rst_text=row[\"Post.rst_text\"],\n        tags=ast.literal_eval(row[\"Post.tags\"]),\n    )\n\n\ndef blog_builder5(row: Dict[str, str]) -> Blog:\n    return Blog(title=row[\"Blog.title\"])\n\n\nfrom typing import TextIO\n\n\ndef blog_iter2(source: TextIO) -> Iterator[Blog]:\n    \"\"\"An iterator which fetches blogs\"\"\"\n\n    rdr = csv.DictReader(source)\n    assert (\n        set(rdr.fieldnames)\n        == {\"Blog.title\", \"Post.date\", \"Post.title\", \"Post.tags\", \"Post.rst_text\"}\n    )\n    # Fetch the first row and build the first Blog and Post from this\n    row = next(rdr)\n    blog = blog_builder5(row)\n    post = post_builder5(row)\n    blog.append(post)\n\n    # Fetch all subsequent rows.\n    # Emit completed Blogs.\n    # Append Posts to the currently open Blog\n    for row in rdr:\n        if row[\"Blog.title\"] != blog.title:\n            yield blog\n            blog = blog_builder5(row)\n        post = post_builder5(row)\n        blog.append(post)\n    yield blog\n\n\ntest_blog_iter_2 = \"\"\"\n    >>> with (Path.cwd()/\"data\"/\"ch10_blog5.csv\").open() as source:\n    ...    for b in blog_iter2(source):\n    ...        print(b.title, b.as_dict())\n    Travel {'title': 'Travel', 'underline': '======', 'entries': [{'date': '2013-11-14 17:25:00', 'title': 'Hard Aground', 'underline': '------------', 'rst_text': 'Some embarrassing revelation. Including ☹ and ⚓︎', 'tag_text': '#RedRanger #Whitby42 #ICW'}, {'date': '2013-11-18 15:30:00', 'title': 'Anchor Follies', 'underline': '--------------', 'rst_text': 'Some witty epigram. Including < & > characters.', 'tag_text': '#RedRanger #Whitby42 #Mistakes'}]}\n        \n\"\"\"\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_10/ch10_ex5.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 10. Example 5. EBCDIC encode/decode\n\"\"\"\n\n# Persistence Classes\n# ========================================\n\n# A detail class for micro-blog posts\nimport datetime\nfrom typing import List, Optional, Dict, Any, Tuple, Callable, Union\nfrom dataclasses import dataclass\nfrom pathlib import Path\n\nfrom Chapter_10.ch10_ex1 import Post, Blog, travel, rst_render\nfrom Chapter_10.ch10_ex2 import FaceCard, AceCard, Card\nfrom Chapter_10.ch10_ex4 import Player_Strategy, Martingale_Bet, gamestat_iter, GameStat\nimport io\n\n# Legacy Files\n# ===================\n\n# We'll look at pure text and mixed text w/ packed decimal.\n\n# Example 1 dumping all text\n# ###########################\n\n# Metadata for Gamestat objects.\n# attribute name, start, size, and an output format specification.\n\nfrom typing import NamedTuple, BinaryIO, TextIO, Iterable, Iterator, cast\nfrom pathlib import Path\n\nclass FixedField(NamedTuple):\n    name: str\n    offset: int\n    length: int\n    format_spec: str\n\nclass Metadata(NamedTuple):\n    fields: List[FixedField]\n    reclen: int\n\nmetadata_txt = Metadata(\n    fields=[\n        FixedField(\"player\", 0, 20, \"{:<{size}s}\"),\n        FixedField(\"bet\", 20, 20, \"{:<{size}s}\"),\n        FixedField(\"rounds\", 40, 5, \"{:>{size}d}\"),\n        FixedField(\"final\", 45, 8, \"{:>{size}d}\"),\n    ],\n    reclen=53,\n)\n\n# A function to transform a namedtuple into a fixed-layout record.\ndef gamestat_record(gamestat:GameStat, metadata: Metadata) -> str:\n    record_fields = [\n        format_spec.format(getattr(gamestat, name), size=size)\n        for name, start, size, format_spec in metadata.fields\n    ]\n    record_text = \"\".join(record_fields)\n    assert len(record_text) == metadata.reclen, f\"Got {len(record_text)} Should Be {metadata.reclen}\"\n    return record_text\n\n\n# An application of the game statistics definitions.\nwith (Path.cwd()/\"data\"/\"ch10_blackjack.file\").open(\"w\", encoding=\"cp037\", newline=\"\") as target:\n    for gamestat in gamestat_iter(Player_Strategy, Martingale_Bet):\n        record = gamestat_record(gamestat, metadata_txt)\n        target.write(record)\n\n# Example 2 loading all text\n# ##########################\n\n# Loading data from the simulator. Part 1 -- Physical decomposition into rows.\ndef line_iter(aFile: TextIO, metadata: Union[Metadata, 'XMetadata']) -> Iterator[str]:\n    recBytes = aFile.read(metadata.reclen)\n    while recBytes:\n        yield recBytes\n        recBytes = aFile.read(metadata.reclen)\n\n\n# Part 2 -- decomposition into named fields.\ndef record_iter(aFile: TextIO, metadata: Metadata) -> Iterator[Dict[str, str]]:\n    for line in line_iter(aFile, metadata):\n        record = {\n            name: line[start:start + size].strip()\n            for name, start, size, format_spec in metadata.fields\n        }\n        yield record\n\n\n# Part 3 -- using the field to dictionary parser.\ntest_reader_1 = \"\"\"\n    >>> with (Path.cwd()/\"data\"/\"ch10_blackjack.file\").open(\"r\", encoding=\"cp037\", newline=\"\") as source:\n    ...     for record_in in record_iter(cast(TextIO, source), metadata_txt):\n    ...         print(record_in)\n    {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '100', 'final': '142'}\n    {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '27', 'final': '0'}\n    {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '25', 'final': '0'}\n    {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '100', 'final': '157'}\n    {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '100', 'final': '87'}\n    {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '18', 'final': '0'}\n    {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '100', 'final': '161'}\n    {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '10', 'final': '0'}\n    {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '22', 'final': '0'}\n    {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '53', 'final': '0'}\n    {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '37', 'final': '0'}\n    {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '27', 'final': '0'}\n    {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '100', 'final': '188'}\n    {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '58', 'final': '0'}\n    {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '100', 'final': '103'}\n    {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '28', 'final': '0'}\n    {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '60', 'final': '0'}\n    {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '100', 'final': '150'}\n    {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '9', 'final': '0'}\n    {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '13', 'final': '0'}\n    {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '97', 'final': '0'}\n    {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '100', 'final': '93'}\n    {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '72', 'final': '0'}\n    {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '12', 'final': '0'}\n    {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '36', 'final': '0'}\n    {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '35', 'final': '0'}\n    {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '78', 'final': '0'}\n    {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '68', 'final': '0'}\n    {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '39', 'final': '0'}\n    {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '47', 'final': '0'}\n\"\"\"\n\n# Example 3 -- USAGE DISPLAY and USAGE COMP3\n# ###########################################\n\n# Using COMP-3 expands the problem into three kinds of data\n#\n# - Alpha and Alphanumeric encoded in EBCDIC or ASCII\n#\n# - Numeric, USAGE DISPLAY, as a string of digits encoded in EBCDIC or ASCII\n#\n# - Numeric, USAGE COMP-3, as string of bytes encoded as packed decimal.\n#\n# All of which require the decimal module's Decimal class definition.\n\nfrom decimal import Decimal\n\n# As a convenience, we map 'ebcdic' to 'cp037' by adding a new lookup function.\n#\nimport codecs\n\n\ndef ebcdic_lookup(name, fallback=codecs.lookup):  # real signature unknown\n    if name == \"ebcdic\":\n        return codecs.lookup(\"cp037\")\n    return fallback(name)\n\n\ncodecs.register(ebcdic_lookup)\n\n# Alphanumeric USAGE DISPLAY conversion.\n# The COBOL program stored text.\ndef alpha_decode(data: bytes, metadata: 'XMetadata', field_metadata: 'XField') -> str:\n    \"\"\"Decode alpha or alphanumeric data.\n    metadata has encoding.\n    field_metadata is (currently) not used.\n\n    Mock metadata objects\n    >>> import types\n    >>> meta = types.SimpleNamespace(reclen=6, encoding='ebcdic')\n    >>> meta.decode = codecs.getdecoder(meta.encoding)\n    >>> field_meta = types.SimpleNamespace()  # Used in other examples...\n\n    >>> data = bytes([0xf9, 0xf8, 0xf7, 0xf6, 0xf5, 0x60])\n    >>> alpha_decode(data, meta, field_meta)\n    '98765-'\n\n    \"\"\"\n    text, _ = metadata.decode(data)\n    return text\n\n\n# Numeric USAGE DISPLAY trailing sign conversion.\n# The COBOL program stored text version of the number.\ndef display_decode(data: bytes, metadata: 'XMetadata', field_metadata: 'XField') -> Decimal:\n    \"\"\"Decode USAGE DISPLAY numeric data.\n    metadata has encoding.\n    field_metadata has attributes name, start, size, format, precision, usage.\n\n    Mock metadata objects\n    >>> import types\n    >>> meta= types.SimpleNamespace(reclen=6, encoding='ebcdic')\n    >>> meta.decode = codecs.getdecoder(meta.encoding)\n    >>> field_meta = types.SimpleNamespace(precision=2)\n\n    >>> data = bytes([0xf9, 0xf8, 0xf7, 0xf6, 0xf5, 0x60])\n    >>> display_decode(data, meta, field_meta)\n    Decimal('-987.65')\n\n    \"\"\"\n    text, _ = metadata.decode(data)\n    precision = field_metadata.precision or 0  # If None, default is 0.\n    text, sign = text[:-1], text[-1]\n    return Decimal(sign + text[:-precision] + \".\" + text[-precision:])\n\n\n# Numeric USAGE COMP-3 conversion.\n# The COBOL program encoded the number into packed decimal representation.\ndef comp3_decode(data: bytes, metadata: 'XMetadata', field_metadata: 'XField') -> Decimal:\n    \"\"\"Decode USAGE COMP-3 data.\n    metadata has encoding, which is not used.\n    field_metadata has attributes name, start, size, format, precision, usage.\n\n    Note that the size is the overall resulting string of bytes.\n    NOT the number of digits involved.\n\n    Mock metadata objects\n    >>> import types\n    >>> meta = types.SimpleNamespace() # Not used\n    >>> field_meta = types.SimpleNamespace(precision=2)\n\n    >>> data = bytes((0x98, 0x76, 0x5d))\n    >>> comp3_decode(data, meta, field_meta)\n    Decimal('-987.65')\n\n    \"\"\"\n    precision = field_metadata.precision or 0  # Default when precision is omitted\n    digits = []\n    for b in data[:-1]:\n        hi, lo = divmod(b, 16)\n        digits.append(str(hi))\n        digits.append(str(lo))\n    digit, sign_byte = divmod(data[-1], 16)\n    digits.append(str(digit))\n    text = \"\".join(digits)\n    sign = \"-\" if sign_byte in (0x0b, 0x0d) else \"+\"\n    return Decimal(sign + text[:-precision] + \".\" + text[-precision:])\n\n\n# Encoder for simple alpha or alphanumeric.\ndef alpha_encode(data: Any, metadata: 'XMetadata', field_metadata: 'XField') -> bytes:\n    \"\"\"Encode alpha or alphanumeric data.\n    metadata has encoding.\n    field_metadata is not used.\n\n    Mock metadata objects\n    >>> import types\n    >>> meta = types.SimpleNamespace(encoding='ebcdic')\n    >>> meta.encode = codecs.getencoder(meta.encoding)\n    >>> field_meta = types.SimpleNamespace(length=6)\n\n    >>> data = '98765-'\n    >>> actual = alpha_encode(data, meta, field_meta)\n    >>> repr(actual)\n    \"b'\\\\\\\\xf9\\\\\\\\xf8\\\\\\\\xf7\\\\\\\\xf6\\\\\\\\xf5`'\"\n    >>> actual == bytes([0xf9, 0xf8, 0xf7, 0xf6, 0xf5, 0x60])\n    True\n\n    \"\"\"\n    bytes, _ = metadata.encode(\"{:<{size}s}\".format(data, size=field_metadata.length))\n    return bytes\n\n\n# Encoder for numeric USAGE DISPLAY, trailing sign.\ndef display_encode(data: Decimal, metadata: 'XMetadata', field_metadata: 'XField') -> bytes:\n    \"\"\"Encode numeric USAGE DISPLAY trailing sign.\n    metadata has encoding.\n    field_metadata has attributes name, start, size, format, precision, usage.\n\n    Mock metadata objects\n    >>> import types, decimal\n    >>> meta = types.SimpleNamespace(encoding='ebcdic')\n    >>> meta.encode = codecs.getencoder(meta.encoding)\n    >>> field_meta = types.SimpleNamespace(length=6, precision=2)\n\n    >>> actual = display_encode(Decimal('-987.65'), meta, field_meta)\n    >>> repr(actual)\n    \"b'\\\\\\\\xf9\\\\\\\\xf8\\\\\\\\xf7\\\\\\\\xf6\\\\\\\\xf5`'\"\n    >>> actual ==  bytes([0xf9, 0xf8, 0xf7, 0xf6, 0xf5, 0x60])\n    True\n\n    \"\"\"\n    precision = field_metadata.precision or 0\n    text = \"{0:0>{size}d}{1}\".format(\n        abs(int(data * Decimal(10) ** precision)),\n        \"-\" if data < 0 else \"+\",\n        size=field_metadata.length - 1,\n    )\n    bytes, _ = metadata.encode(text)\n    return bytes\n\n\n# Encoder for numeric USAGE COMP-3.\ndef comp3_encode(data: Decimal, metadata: 'XMetadata', field_metadata: 'XField') -> bytes:\n    \"\"\"Encode numeric USAGE COMP-3.\n    metadata has encoding which is not used.\n    field_metadata has attributes name, start, size, format, precision, usage.\n\n    Note that the size is the overall resulting string of bytes.\n    NOT the number of digits involved.\n    This has 2 digits per byte + a digit and a sign.\n\n    Mock metadata objects\n    >>> import types\n    >>> meta = types.SimpleNamespace(encoding='ebcdic')\n    >>> field_meta = types.SimpleNamespace(length=3, precision=2)\n\n    >>> actual = comp3_encode(Decimal('-987.65'), meta, field_meta)\n    >>> repr(actual)\n    \"b'\\\\\\\\x98v]'\"\n    >>> actual == bytes((0x98, 0x76, 0x5d))\n    True\n\n    \"\"\"\n    precision = field_metadata.precision or 0\n    value = abs(int(data * Decimal(10) ** precision))\n    digits = [0x0d if data < 0 else 0x00]  # Trailing sign.\n    nDigits = field_metadata.length * 2 - 1\n    for i in range(nDigits):\n        digits = [value % 10] + digits\n        value //= 10\n    b = bytes((hi * 16 + lo for hi, lo in list(zip(digits[::2], digits[1::2]))))\n    return b\n\n\n# Our expanded metadata to include more refined field-level definitions.\n# First, we'll define some encode-decode pairs.\nalphanumeric = (alpha_encode, alpha_decode)\nusage_display = (display_encode, display_decode)\nusage_comp3 = (comp3_encode, comp3_decode)\n\nEncoder = Callable[[Any, Any, Any], bytes]\nDecoder = Callable[[bytes, Any, Any], Any]\nUsage = Tuple[Encoder, Decoder]\n\n# Then we'll define a more sophisticated metadata that includes the\n# precision and a reference to the relevant encode-decode pair.\n#\n# The overall metadata encoding name is transformed into an\n# encode and decode function to save lookups on a field-by-field basis.\nimport collections\n\nclass XField(NamedTuple):\n    name: str\n    offset: int\n    length: int\n    precision: Optional[int]\n    usage: Tuple[Callable, Callable]\n\nclass XMetadata(NamedTuple):\n    fields: List[XField]\n    reclen: int\n    encoding: str\n\n    @property\n    def decode(self) -> Callable[[bytes], Tuple[str, int]]:\n        return codecs.getdecoder(self.encoding)\n\n    @property\n    def encode(self) -> Callable[[str], Tuple[bytes, int]]:\n        return codecs.getencoder(self.encoding)\n\n\nmetadata_comp3 = XMetadata(\n    fields=[\n        XField(\"player\", 0, 20, None, alphanumeric),\n        XField(\"bet\", 20, 20, None, alphanumeric),\n        XField(\"rounds\", 40, 8, 2, usage_display),\n        XField(\"final\", 48, 8, 2, usage_comp3),\n    ],\n    reclen=56,\n    encoding=\"ebcdic\",  # for display fields and alphanumeric fields.\n)\n\n# A function to transform a namedtuple into a fixed-layout record.\ndef gamestat_record_comp3(gamestat: GameStat, metadata: XMetadata) -> bytes:\n    record = [\n        field.usage[0](getattr(gamestat, field.name), metadata, field)\n        for field in metadata.fields\n    ]\n    text = b\"\".join(record)\n    assert len(text) == metadata.reclen, \"Got {0} != Should Be {1}\".format(\n        len(text), metadata.reclen\n    )\n    return text\n\n\n# Example encoding app.\nwith (Path.cwd()/\"data\"/\"ch10_blackjack_comp3.file\").open(\"wb\") as target:\n    for gamestat in gamestat_iter(Player_Strategy, Martingale_Bet):\n        data_bytes = gamestat_record_comp3(gamestat, metadata_comp3)\n        target.write(data_bytes)\n\n# Example decoding iterator using more sophisticated metadata.\ndef record2_iter(aFile: TextIO, metadata: XMetadata) -> Iterator[Dict[str, XField]]:\n    for line in line_iter(aFile, metadata):\n        field_data = (\n            (field, line[field.offset:field.offset + field.length])\n            for field in metadata.fields\n        )\n        record = dict(\n            (field.name, field.usage[1](data, metadata, field))\n            for field, data in field_data\n        )\n        yield record\n\ntest_reader_2 = \"\"\"\n    >>> with (Path.cwd()/\"data\"/\"ch10_blackjack_comp3.file\").open(\"rb\") as source:\n    ...     for record in record2_iter(source, metadata_comp3):\n    ...        print(record)\n    {'player': 'Player_Strategy     ', 'bet': 'Martingale_Bet      ', 'rounds': Decimal('100.00'), 'final': Decimal('142.00')}\n    {'player': 'Player_Strategy     ', 'bet': 'Martingale_Bet      ', 'rounds': Decimal('27.00'), 'final': Decimal('0.00')}\n    {'player': 'Player_Strategy     ', 'bet': 'Martingale_Bet      ', 'rounds': Decimal('25.00'), 'final': Decimal('0.00')}\n    {'player': 'Player_Strategy     ', 'bet': 'Martingale_Bet      ', 'rounds': Decimal('100.00'), 'final': Decimal('157.00')}\n    {'player': 'Player_Strategy     ', 'bet': 'Martingale_Bet      ', 'rounds': Decimal('100.00'), 'final': Decimal('87.00')}\n    {'player': 'Player_Strategy     ', 'bet': 'Martingale_Bet      ', 'rounds': Decimal('18.00'), 'final': Decimal('0.00')}\n    {'player': 'Player_Strategy     ', 'bet': 'Martingale_Bet      ', 'rounds': Decimal('100.00'), 'final': Decimal('161.00')}\n    {'player': 'Player_Strategy     ', 'bet': 'Martingale_Bet      ', 'rounds': Decimal('10.00'), 'final': Decimal('0.00')}\n    {'player': 'Player_Strategy     ', 'bet': 'Martingale_Bet      ', 'rounds': Decimal('22.00'), 'final': Decimal('0.00')}\n    {'player': 'Player_Strategy     ', 'bet': 'Martingale_Bet      ', 'rounds': Decimal('53.00'), 'final': Decimal('0.00')}\n    {'player': 'Player_Strategy     ', 'bet': 'Martingale_Bet      ', 'rounds': Decimal('37.00'), 'final': Decimal('0.00')}\n    {'player': 'Player_Strategy     ', 'bet': 'Martingale_Bet      ', 'rounds': Decimal('27.00'), 'final': Decimal('0.00')}\n    {'player': 'Player_Strategy     ', 'bet': 'Martingale_Bet      ', 'rounds': Decimal('100.00'), 'final': Decimal('188.00')}\n    {'player': 'Player_Strategy     ', 'bet': 'Martingale_Bet      ', 'rounds': Decimal('58.00'), 'final': Decimal('0.00')}\n    {'player': 'Player_Strategy     ', 'bet': 'Martingale_Bet      ', 'rounds': Decimal('100.00'), 'final': Decimal('103.00')}\n    {'player': 'Player_Strategy     ', 'bet': 'Martingale_Bet      ', 'rounds': Decimal('28.00'), 'final': Decimal('0.00')}\n    {'player': 'Player_Strategy     ', 'bet': 'Martingale_Bet      ', 'rounds': Decimal('60.00'), 'final': Decimal('0.00')}\n    {'player': 'Player_Strategy     ', 'bet': 'Martingale_Bet      ', 'rounds': Decimal('100.00'), 'final': Decimal('150.00')}\n    {'player': 'Player_Strategy     ', 'bet': 'Martingale_Bet      ', 'rounds': Decimal('9.00'), 'final': Decimal('0.00')}\n    {'player': 'Player_Strategy     ', 'bet': 'Martingale_Bet      ', 'rounds': Decimal('13.00'), 'final': Decimal('0.00')}\n    {'player': 'Player_Strategy     ', 'bet': 'Martingale_Bet      ', 'rounds': Decimal('97.00'), 'final': Decimal('0.00')}\n    {'player': 'Player_Strategy     ', 'bet': 'Martingale_Bet      ', 'rounds': Decimal('100.00'), 'final': Decimal('93.00')}\n    {'player': 'Player_Strategy     ', 'bet': 'Martingale_Bet      ', 'rounds': Decimal('72.00'), 'final': Decimal('0.00')}\n    {'player': 'Player_Strategy     ', 'bet': 'Martingale_Bet      ', 'rounds': Decimal('12.00'), 'final': Decimal('0.00')}\n    {'player': 'Player_Strategy     ', 'bet': 'Martingale_Bet      ', 'rounds': Decimal('36.00'), 'final': Decimal('0.00')}\n    {'player': 'Player_Strategy     ', 'bet': 'Martingale_Bet      ', 'rounds': Decimal('35.00'), 'final': Decimal('0.00')}\n    {'player': 'Player_Strategy     ', 'bet': 'Martingale_Bet      ', 'rounds': Decimal('78.00'), 'final': Decimal('0.00')}\n    {'player': 'Player_Strategy     ', 'bet': 'Martingale_Bet      ', 'rounds': Decimal('68.00'), 'final': Decimal('0.00')}\n    {'player': 'Player_Strategy     ', 'bet': 'Martingale_Bet      ', 'rounds': Decimal('39.00'), 'final': Decimal('0.00')}\n    {'player': 'Player_Strategy     ', 'bet': 'Martingale_Bet      ', 'rounds': Decimal('47.00'), 'final': Decimal('0.00')}\n\n\"\"\"\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_10/ch10_ex6.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 10. Example 6. XML\n\"\"\"\n\n# Persistence Classes\n# ========================================\n\n# A detail class for micro-blog posts\nimport datetime\nfrom dataclasses import dataclass, field, asdict\nfrom typing import List, DefaultDict, Dict, Any\nfrom collections import defaultdict\nimport io\nfrom Chapter_10.ch10_ex1 import travel, rst_render\n\n\n# XML\n# ===================\n\n# Example 1: XML output\n# ######################\n\nfrom dataclasses import dataclass, field, asdict\n@dataclass\nclass Post_X:\n    date: datetime.datetime\n    title: str\n    rst_text: str\n    tags: List[str]\n    underline: str = field(init=False)\n    tag_text: str = field(init=False)\n\n    def __post_init__(self) -> None:\n        self.underline = \"-\"*len(self.title)\n        self.tag_text = ' '.join(self.tags)\n\n    def as_dict(self) -> Dict[str, Any]:\n        return asdict(self)\n\n    def xml(self) -> str:\n        tags = \"\".join(f\"<tag>{t}</tag>\" for t in self.tags)\n        return f\"\"\"\\\n<entry>\n    <title>{self.title}</title>\n    <date>{self.date}</date>\n    <tags>{tags}</tags>\n    <text>{self.rst_text}</text>\n</entry>\"\"\"\n\n\nfrom dataclasses import dataclass, field, asdict\n\n@dataclass\nclass Blog_X:\n    title: str\n    entries: List[Post_X] = field(default_factory=list)\n    underline: str = field(init=False)\n\n    def __post_init__(self) -> None:\n        self.underline = \"=\"*len(self.title)\n\n    def append(self, post: Post_X) -> None:\n        self.entries.append(post)\n\n    def by_tag(self) -> DefaultDict[str, List[Dict[str, Any]]]:\n        tag_index: DefaultDict[str, List[Dict[str, Any]]] = defaultdict(list)\n        for post in self.entries:\n            for tag in post.tags:\n                tag_index[tag].append(asdict(post))\n        return tag_index\n\n    def as_dict(self) -> Dict[str, Any]:\n        return asdict(self)\n\n    def xml(self) -> str:\n        children = \"\\n\".join(c.xml() for c in self.entries)\n        return f\"\"\"\\\n<blog><title>{self.title}</title>\n<entries>\n{children}\n<entries>\n</blog>\n\"\"\"\n\n\ntravel4 = Blog_X(\"Travel\")\ntravel4.append(\n    Post_X(\n        date=datetime.datetime(2013, 11, 14, 17, 25),\n        title=\"Hard Aground\",\n        rst_text=\"\"\"Some embarrassing revelation. Including ☹ and ⚓\"\"\",\n        tags=[\"#RedRanger\", \"#Whitby42\", \"#ICW\"],\n    )\n)\ntravel4.append(\n    Post_X(\n        date=datetime.datetime(2013, 11, 18, 15, 30),\n        title=\"Anchor Follies\",\n        rst_text=\"\"\"Some witty epigram.\"\"\",\n        tags=[\"#RedRanger\", \"#Whitby42\", \"#Mistakes\"],\n    )\n)\n\ntest_xml_out_1 = \"\"\"\n    >>> print(travel4.xml())  # doctest: +NORMALIZE_WHITESPACE\n    <blog><title>Travel</title>\n    <entries>\n    <entry>\n        <title>Hard Aground</title>\n        <date>2013-11-14 17:25:00</date>\n        <tags><tag>#RedRanger</tag><tag>#Whitby42</tag><tag>#ICW</tag></tags>\n        <text>Some embarrassing revelation. Including ☹ and ⚓</text>\n    </entry>\n    <entry>\n        <title>Anchor Follies</title>\n        <date>2013-11-18 15:30:00</date>\n        <tags><tag>#RedRanger</tag><tag>#Whitby42</tag><tag>#Mistakes</tag></tags>\n        <text>Some witty epigram.</text>\n    </entry>\n    <entries>\n    </blog>\n\"\"\"\n\n# Example 2: element Tree output\n# ##############################\n\nimport xml.etree.ElementTree as XML\nfrom typing import cast\n\nclass Blog_E(Blog_X):\n\n    def xmlelt(self) -> XML.Element:\n        blog = XML.Element(\"blog\")\n        title = XML.SubElement(blog, \"title\")\n        title.text = self.title\n        title.tail = \"\\n\"\n        entities = XML.SubElement(blog, \"entries\")\n        entities.extend(cast('Post_E', c).xmlelt() for c in self.entries)\n        blog.tail = \"\\n\"\n        return blog\n\n\nclass Post_E(Post_X):\n\n    def xmlelt(self) -> XML.Element:\n        post = XML.Element(\"entry\")\n        title = XML.SubElement(post, \"title\")\n        title.text = self.title\n        date = XML.SubElement(post, \"date\")\n        date.text = str(self.date)\n        tags = XML.SubElement(post, \"tags\")\n        for t in self.tags:\n            tag = XML.SubElement(tags, \"tag\")\n            tag.text = t\n        text = XML.SubElement(post, \"rst_text\")\n        text.text = self.rst_text\n        post.tail = \"\\n\"\n        return post\n\n\ntravel5 = Blog_E(\"Travel\")\ntravel5.append(\n    Post_E(\n        date=datetime.datetime(2013, 11, 14, 17, 25),\n        title=\"Hard Aground\",\n        rst_text=\"\"\"Some embarrassing revelation. Including ☹ and ⚓\"\"\",\n        tags=[\"#RedRanger\", \"#Whitby42\", \"#ICW\"],\n    )\n)\ntravel5.append(\n    Post_E(\n        date=datetime.datetime(2013, 11, 18, 15, 30),\n        title=\"Anchor Follies\",\n        rst_text=\"\"\"Some witty epigram. Including < & > characters.\"\"\",\n        tags=[\"#RedRanger\", \"#Whitby42\", \"#Mistakes\"],\n    )\n)\n\ntest_xml_out_2 = \"\"\"\n    >>> tree = XML.ElementTree(travel5.xmlelt())\n    >>> text = XML.tostring(tree.getroot())\n    >>> print(text.decode('utf-8'))  # doctest: +NORMALIZE_WHITESPACE\n    <blog><title>Travel</title>\n    <entries><entry><title>Hard Aground</title><date>2013-11-14 17:25:00</date><tags><tag>#RedRanger</tag><tag>#Whitby42</tag><tag>#ICW</tag></tags><rst_text>Some embarrassing revelation. Including &#9785; and &#9875;</rst_text></entry>\n    <entry><title>Anchor Follies</title><date>2013-11-18 15:30:00</date><tags><tag>#RedRanger</tag><tag>#Whitby42</tag><tag>#Mistakes</tag></tags><rst_text>Some witty epigram. Including &lt; &amp; &gt; characters.</rst_text></entry>\n    </entries></blog>\n\n\"\"\"\n\ndef build_blog(document: XML.ElementTree) -> Blog_X:\n    xml_blog = document.getroot()\n    blog = Blog_X(xml_blog.findtext(\"title\"))\n    for xml_post in xml_blog.findall(\"entries/entry\"):\n        optional_tag_iter = (\n            t.text for t in xml_post.findall(\"tags/tag\")\n        )\n        tags = list(\n            filter(None, optional_tag_iter)\n        )\n        post = Post_X(\n            date=datetime.datetime.strptime(\n                xml_post.findtext(\"date\"), \"%Y-%m-%d %H:%M:%S\"\n            ),\n            title=xml_post.findtext(\"title\"),\n            tags=tags,\n            rst_text=xml_post.findtext(\"rst_text\"),\n        )\n        blog.append(post)\n    return blog\n\ntest_xml_in = \"\"\"\n    >>> tree = XML.ElementTree(travel5.xmlelt())\n    >>> text = XML.tostring(tree.getroot())\n\n    >>> document = XML.parse(io.StringIO(text.decode(\"utf-8\")))\n    >>> blog = build_blog(document)\n    >>> rst_render(blog)  # doctest: +NORMALIZE_WHITESPACE\n    Travel\n    ======\n    <BLANKLINE>\n    <BLANKLINE>\n        Hard Aground\n        ------------\n    <BLANKLINE>\n        Some embarrassing revelation. Including ☹ and ⚓\n    <BLANKLINE>\n        :date: 2013-11-14 17:25:00\n    <BLANKLINE>\n        :tags: #RedRanger #Whitby42 #ICW\n    <BLANKLINE>\n    <BLANKLINE>\n        Anchor Follies\n        --------------\n    <BLANKLINE>\n        Some witty epigram. Including < & > characters.\n    <BLANKLINE>\n        :date: 2013-11-18 15:30:00\n    <BLANKLINE>\n        :tags: #RedRanger #Whitby42 #Mistakes\n    <BLANKLINE>\n    Tag Index\n    =========\n    <BLANKLINE>\n    *   #RedRanger\n    <BLANKLINE>\n        -   `Hard Aground`_\n        -   `Anchor Follies`_\n    <BLANKLINE>\n    *   #Whitby42\n    <BLANKLINE>\n        -   `Hard Aground`_\n        -   `Anchor Follies`_\n    <BLANKLINE>\n    *   #ICW\n    <BLANKLINE>\n        -   `Hard Aground`_\n    <BLANKLINE>\n    *   #Mistakes\n    <BLANKLINE>\n        -   `Anchor Follies`_\n    <BLANKLINE>\n\"\"\"\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_11/__init__.py",
    "content": ""
  },
  {
    "path": "Chapter_11/ch11_ex1.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 11. Example 1.\n\"\"\"\n\n\n# Shelve Basics\n# ========================================\n\nfrom typing import List, Dict, Any, Optional\nfrom collections import defaultdict\nimport datetime\nfrom pathlib import Path\nfrom dataclasses import dataclass, asdict, field\nimport shelve\n\n\n# Some Example Application Classes\n\n\n@dataclass\nclass Post:\n    date: datetime.datetime\n    title: str\n    rst_text: str\n    tags: List[str]\n\n\n@dataclass\nclass Blog:\n\n    title: str\n    entries: List[Post] = field(default_factory=list)\n    underline: str = field(init=False)\n\n    # Part of the persistence, not essential to the class.\n    _id: str = field(default=\"\", init=False, compare=False)\n\n    def __post_init__(self) -> None:\n        self.underline = \"=\" * len(self.title)\n\n    def append(self, post: Post) -> None:\n        self.entries.append(post)\n\n    def by_tag(self) -> Dict[str, List[Dict[str, Any]]]:\n        tag_index: Dict[str, List[Dict[str, Any]]] = defaultdict(list)\n        for post in self.entries:\n            for tag in post.tags:\n                tag_index[tag].append(asdict(post))\n        return tag_index\n\ntest_blog = \"\"\"\n    >>> b1 = Blog(title=\"Travel Blog\")\n    >>> b1\n    Blog(title='Travel Blog', entries=[], underline='===========', _id='')\n\n    >>> import shelve\n    >>> from pathlib import Path\n    >>> path = Path.cwd() / \"data\" / \"ch11_blog1\"\n    >>> shelf = shelve.open(str(path), \"n\")\n    >>> b1._id = 'Blog:1'\n    >>> shelf[b1._id] = b1\n    \n    >>> shelf['Blog:1']\n    Blog(title='Travel Blog', entries=[], underline='===========', _id='Blog:1')\n    >>> shelf['Blog:1'].title \n    'Travel Blog'\n    >>> shelf['Blog:1']._id \n    'Blog:1'\n    >>> list(shelf.keys()) \n    ['Blog:1']\n    >>> shelf.close() \n\"\"\"\n\ntest_query = \"\"\"\n    >>> path = Path.cwd() / \"data\" / \"ch11_blog1\"\n    >>> shelf = shelve.open(str(path))\n    >>> results = (shelf[k] \n    ...     for k in shelf.keys() \n    ...     if k.startswith('Blog:') and shelf[k].title == 'Travel Blog'\n    ... )\n    >>> list(results)\n    [Blog(title='Travel Blog', entries=[], underline='===========', _id='Blog:1')]\n\"\"\"\n\nif __name__ == \"__main__\":\n    # A Blog example\n    b1 = Blog(title=\"Travel Blog\")\n    p1 = Post(\n        date=datetime.datetime(2019, 1, 18),\n        title=\"Some Post\",\n        rst_text=\"Details of the post\",\n        tags=[\"#sample\", \"#data\"],\n    )\n    b1.append(p1)\n\n    # Some Manual access\n    import shelve\n\n    shelf = shelve.open(str(Path.cwd() / \"data\" / \"ch11_blog\"))\n    db_id = 0\n\n    # Typical seqence for saving...\n    db_id += 1\n    b1._id = f\"Blog:{db_id}\"\n    shelf[b1._id] = b1\n    print(f\"Create {shelf[b1._id]._id} {shelf[b1._id].title}\")\n\n    # Seaching through the shelf for a specific title...\n    results = (\n        shelf[k]\n        for k in shelf.keys()\n        if k.startswith(\"Blog:\") and shelf[k].title == \"Travel Blog\"\n    )\n    for r0 in results:\n        print(f\"Retrieve {r0._id} {r0.title}\")\n        for p in r0.entries:\n            print(f\"  {p}\")\n        print(f\"  {r0.by_tag()}\")\n\n    shelf.close()\n\n\n# Some more manual access\nif __name__ == \"__main__\":\n\n    p2 = Post(\n        date=datetime.datetime(2013, 11, 14, 17, 25),\n        title=\"Hard Aground\",\n        rst_text=\"\"\"Some embarrassing revelation. Including ☹ and ⚓︎\"\"\",\n        tags=[\"#RedRanger\", \"#Whitby42\", \"#ICW\"],\n    )\n\n    p3 = Post(\n        date=datetime.datetime(2013, 11, 18, 15, 30),\n        title=\"Anchor Follies\",\n        rst_text=\"\"\"Some witty epigram. Including ☺ and ☀︎︎\"\"\",\n        tags=[\"#RedRanger\", \"#Whitby42\", \"#Mistakes\"],\n    )\n\n    shelf = shelve.open(str(Path.cwd() / \"data\" / \"ch11_blog\"))\n\n    # Retrieve the blog by id\n    blog_id = 1\n    key = f\"Blog:{blog_id}\"\n    the_blog = shelf[key]\n\n    # Update the blog\n    the_blog.append(p2)\n    the_blog.append(p3)\n\n    # Persist the changes to the blog.\n    shelf[key] = the_blog\n\n    # What's in the database?\n    print(\"Database has\", list(shelf.keys()))\n\n    shelf.close()\n\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_11/ch11_ex2.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 11. Example 2.\n\"\"\"\n\nfrom typing import List, Dict, Any, Optional, cast, Iterator, Union, TextIO\nimport datetime\nfrom dataclasses import dataclass, field, asdict\nfrom pathlib import Path\n\n# Application Classes\n# ====================\n\n# Designed to be used separately.\n\n@dataclass\nclass Post:\n    date: datetime.datetime\n    title: str\n    rst_text: str\n    tags: List[str]\n    underline: str = field(init=False)\n    tag_text: str = field(init=False)\n\n    # Will be set as part of saving to the shelf.\n    # Part of the persistence, not essential to the class.\n    _id: str = field(default='', init=False, repr=False, compare=False)\n    _blog_id: str = field(default='', init=False, repr=False, compare=False)\n\n    def __post_init__(self) -> None:\n        self.underline = \"-\" * len(self.title)\n        self.tag_text = \" \".join(self.tags)\n\n@dataclass\nclass Blog:\n\n    title: str\n    underline: str = field(init=False)\n\n    # Will be set as part of saving to the shelf.\n    # Part of the persistence, not essential to the class.\n    _id: str = field(default=\"\", init=False, compare=False)\n\n    def __post_init__(self) -> None:\n        self.underline = \"=\" * len(self.title)\n\n    def by_tag(self, access: 'Access') -> Dict[str, List[Dict[str, Any]]]:\n        tag_index: Dict[str, List[Dict[str, Any]]] = defaultdict(list)\n        for post in access.post_iter(self):\n            if post._blog_id == self._id:\n                for tag in post.tags:\n                    tag_index[tag].append(asdict(post))\n        return tag_index\n\n\ntest_relational_1 = \"\"\"\n    >>> b1 = Blog(title=\"Travel Blog\")\n    \n    >>> p2 = Post(date=datetime.datetime(2013,11,14,17,25), \n    ...        title=\"Hard Aground\", \n    ...        rst_text=\"Some embarrassing revelation. Including ☹ and ⚓\", \n    ...        tags=(\"#RedRanger\", \"#Whitby42\", \"#ICW\"), \n    ...        ) \n \n    >>> p3 = Post(date=datetime.datetime(2013,11,18,15,30), \n    ...        title=\"Anchor Follies\", \n    ...        rst_text=\"Some witty epigram. Including < & > characters.\", \n    ...        tags=(\"#RedRanger\", \"#Whitby42\", \"#Mistakes\"), \n    ...        ) \n\n    >>> import shelve\n    >>> from pathlib import Path\n    >>> path = Path.cwd() / \"data\" / \"ch11_blog2\"\n    >>> shelf = shelve.open(str(path), 'n')\n    \n    >>> b1._id = 'Blog:1'\n    >>> shelf[b1._id] = b1\n    >>> list(shelf.keys())\n    ['Blog:1']\n    \n    >>> owner = shelf['Blog:1'] \n    >>> owner\n    Blog(title='Travel Blog', underline='===========', _id='Blog:1')\n    \n    >>> p2._parent = owner._id \n    >>> p2._id = p2._parent + ':Post:2' \n    >>> shelf[p2._id]= p2 \n     \n    >>> p3._parent = owner._id \n    >>> p3._id = p3._parent + ':Post:3' \n    >>> shelf[p3._id]= p3 \n    \n    >>> shelf.sync()\n\n    >>> sorted(shelf.keys())\n    ['Blog:1', 'Blog:1:Post:2', 'Blog:1:Post:3']\n\"\"\"\n\n# \"Relational\" Access Layer -- Separate Blogs from Posts\n# ======================================================\n\n# We'll use hierarchical keys Post:id and Post:id:Child:id\nimport shelve\n\n\nclass OperationError(Exception):\n    pass\n\n\nclass Access:\n\n    def __init__(self) -> None:\n        self.database: shelve.Shelf = cast(shelve.Shelf, None)\n        self.max: Dict[str, int] = {\"Post\": 0, \"Blog\": 0}\n\n    def new(self, path: Path) -> None:\n        self.database: shelve.Shelf = shelve.open(str(path), \"n\")\n        self.max: Dict[str, int] = {\"Post\": 0, \"Blog\": 0}\n        self.sync()\n\n    def open(self, path: Path) -> None:\n        self.database = shelve.open(str(path), \"w\")\n        self.max = self.database[\"_DB:max\"]\n\n    def close(self) -> None:\n        if self.database:\n            self.database[\"_DB:max\"] = self.max\n            self.database.close()\n        self.database = cast(shelve.Shelf, None)\n\n    def sync(self) -> None:\n        self.database[\"_DB:max\"] = self.max\n        self.database.sync()\n\n    def create_blog(self, blog: Blog) -> Blog:\n        self.max['Blog'] += 1\n        key = f\"Blog:{self.max['Blog']}\"\n        blog._id = key\n        self.database[blog._id] = blog\n        return blog\n\n    def retrieve_blog(self, key: str) -> Blog:\n        return self.database[key]\n\n    def create_post(self, blog: Blog, post: Post) -> Post:\n        self.max['Post'] += 1\n        post_key = f\"Post:{self.max['Post']}\"\n        post._id = post_key\n        post._blog_id = blog._id\n        self.database[post._id] = post\n        return post\n\n    def retrieve_post(self, key: str) -> Post:\n        return self.database[key]\n\n    def update_post(self, post: Post) -> Post:\n        self.database[post._id] = post\n        return post\n\n    def delete_post(self, post: Post) -> None:\n        del self.database[post._id]\n\n    def __iter__(self) -> Iterator[Union[Blog, Post]]:\n        for k in self.database:\n            if k[0] == \"_\":\n                # Skip the administrative objects\n                continue\n            yield self.database[k]\n\n    def blog_iter(self) -> Iterator[Blog]:\n        for k in self.database:\n            if k.startswith('Blog:'):\n                yield self.database[k]\n\n    def post_iter(self, blog: Blog) -> Iterator[Post]:\n        for k in self.database:\n            if k.startswith('Post:'):\n                if self.database[k]._blog_id == blog._id:\n                    yield self.database[k]\n\n    def post_title_iter(self, blog: Blog, title: str) -> Iterator[Post]:\n        return (p for p in self.post_iter(blog) if p.title == title)\n\n    def blog_title_iter(self, title: str) -> Iterator[Blog]:\n        return (b for b in self.blog_iter() if b.title == title)\n\n\n# Demonstration Script\nfrom contextlib import closing\n\ndef database_script(access: Access) -> None:\n    b1 = Blog(title=\"Travel Blog\")\n    p2 = Post(\n        date=datetime.datetime(2013, 11, 14, 17, 25),\n        title=\"Hard Aground\",\n        rst_text=\"\"\"Some embarrassing revelation. Including ☹ and ⚓︎\"\"\",\n        tags=[\"#RedRanger\", \"#Whitby42\", \"#ICW\"],\n    )\n\n    p3 = Post(\n        date=datetime.datetime(2013, 11, 18, 15, 30),\n        title=\"Anchor Follies\",\n        rst_text=\"\"\"Some witty epigram. Including ☺ and ☀︎︎\"\"\",\n        tags=[\"#RedRanger\", \"#Whitby42\", \"#Mistakes\"],\n    )\n    access.create_blog(b1)\n    for post in p2, p3:\n        access.create_post(b1, post)\n\n    b = access.retrieve_blog(b1._id)\n    print(b._id, b)\n    for p in sorted(access.post_iter(b), key=lambda p: p._id):\n        print(p._id, p)\n\ntest_access = \"\"\"\n    >>> with closing(Access()) as access:\n    ...     access.new(Path.cwd() / \"data\" / \"ch11_blog\")\n    ...     database_script(access)\n    Blog:1 Blog(title='Travel Blog', underline='===========', _id='Blog:1')\n    Post:1 Post(date=datetime.datetime(2013, 11, 14, 17, 25), title='Hard Aground', rst_text='Some embarrassing revelation. Including ☹ and ⚓︎', tags=['#RedRanger', '#Whitby42', '#ICW'], underline='------------', tag_text='#RedRanger #Whitby42 #ICW')\n    Post:2 Post(date=datetime.datetime(2013, 11, 18, 15, 30), title='Anchor Follies', rst_text='Some witty epigram. Including ☺ and ☀︎︎', tags=['#RedRanger', '#Whitby42', '#Mistakes'], underline='--------------', tag_text='#RedRanger #Whitby42 #Mistakes')\n\"\"\"\n\n# Another Application\n# ==============================\n\n\nimport string\nfrom collections import defaultdict\nfrom contextlib import redirect_stdout\nimport sys\n\nclass Render:\n\n    def __init__(self, access: Access) -> None:\n        self.access = access\n\n    def emit_all(self, destination: TextIO=sys.stdout) -> None:\n        for blog in self.access.blog_iter():\n            # Compute a filename for each blog.\n            self.emit_blog(blog, destination)\n\n    def emit_blog(self, blog: Blog, output: TextIO) -> None:\n        with redirect_stdout(output):\n            self.tag_index: Dict[str, List[str]] = defaultdict(list)\n            print(\"{title}\\n{underline}\\n\".format(**asdict(blog)))\n            for post in self.access.post_iter(blog):\n                self.emit_post(post)\n                for tag in post.tags:\n                    self.tag_index[tag].append(post._id)\n            self.emit_index()\n\n    def emit_post(self, post: Post) -> None:\n        template = string.Template(\n            \"\"\"\n        $title\n        $underline\n\n        $rst_text\n\n        :date: $date\n\n        :tags: $tag_text\n        \"\"\"\n        )\n        print(template.substitute(asdict(post)))\n\n    def emit_index(self) -> None:\n        print(\"Tag Index\")\n        print(\"=========\")\n        print()\n        for tag in self.tag_index:\n            print(\"*   {0}\".format(tag))\n            print()\n            for b in self.tag_index[tag]:\n                post = self.access.retrieve_post(b)\n                print(\"    -   `{title}`_\".format(**asdict(post)))\n            print()\n\n\n# Demo Script\nimport shelve\nfrom contextlib import closing\n\nif __name__ == \"__main__\":\n    with closing(Access()) as access:\n        access.open(Path.cwd() / \"data\" / \"ch11_blog\")\n        renderer = Render(access)\n        renderer.emit_all()\n\n\n# Better Access Layer\n# ======================================\n\n# Maintain a indexes associated with the Blog\n\n\nclass Access2(Access):\n\n    def create_post(self, blog: Blog, post: Post) -> Post:\n        super().create_post(blog, post)\n        # Update the index; append doesn't work.\n        blog_index = f\"_Index:{blog._id}\"\n        self.database.setdefault(blog_index, [])\n        self.database[blog_index] = self.database[blog_index] + [post._id]\n        return post\n\n    def delete_post(self, post: Post) -> None:\n        super().delete_post(post)\n        # Update the index\n        blog_index = f\"_Index:{post._blog_id}\"\n        index_list = self.database[post._blog_id]\n        index_list.remove(post._id)\n        self.database[post._blog_id] = index_list\n\n    def post_iter(self, blog: Blog) -> Iterator[Post]:\n        blog_index = f\"_Index:{blog._id}\"\n        for k in self.database[blog_index]:\n            yield self.database[k]\n\n\ntest_access_2 = \"\"\"\n    >>> with closing(Access2()) as access:\n    ...     access.new(Path.cwd() / \"data\" / \"ch11_blog2\")\n    ...     database_script(access)\n    Blog:1 Blog(title='Travel Blog', underline='===========', _id='Blog:1')\n    Post:1 Post(date=datetime.datetime(2013, 11, 14, 17, 25), title='Hard Aground', rst_text='Some embarrassing revelation. Including ☹ and ⚓︎', tags=['#RedRanger', '#Whitby42', '#ICW'], underline='------------', tag_text='#RedRanger #Whitby42 #ICW')\n    Post:2 Post(date=datetime.datetime(2013, 11, 18, 15, 30), title='Anchor Follies', rst_text='Some witty epigram. Including ☺ and ☀︎︎', tags=['#RedRanger', '#Whitby42', '#Mistakes'], underline='--------------', tag_text='#RedRanger #Whitby42 #Mistakes')\n\n    >>> with closing(Access2()) as access:\n    ...     access.open(Path.cwd() / \"data\" / \"ch11_blog2\")\n    ...     print(sorted(access.database.keys()))\n    ...     print(access.database['_Index:Blog:1'])\n    ['Blog:1', 'Post:1', 'Post:2', '_DB:max', '_Index:Blog:1']\n    ['Post:1', 'Post:2']\n    \n    \n    >>> with closing(Access2()) as access:\n    ...     access.open(Path.cwd() / \"data\" / \"ch11_blog2\")\n    ...     renderer = Render(access)\n    ...     renderer.emit_all()\n\"\"\"\n\n# Minor Index\n# ==========================\n\n# Another version of Access with slightly different blog add and search.\n# This a tiny help, because the iteration over the cached blog keys\n# is slightly faster.\n\nclass Access3(Access2):\n\n    def new(self, path: Path) -> None:\n        super().new(path)\n        self.database[\"_Index:Blog\"] = list()\n\n    def create_blog(self, blog: Blog) -> Blog:\n        super().create_blog(blog)\n        self.database[\"_Index:Blog\"] += [blog._id]\n        return blog\n\n    def blog_iter(self) -> Iterator[Blog]:\n        return (self.database[k] for k in self.database[\"_Index:Blog\"])\n\n\ntest_access_3 = \"\"\"\n    >>> with closing(Access3()) as access:\n    ...     access.new(Path.cwd() / \"data\" / \"ch11_blog3\")\n    ...     database_script(access)\n    Blog:1 Blog(title='Travel Blog', underline='===========', _id='Blog:1')\n    Post:1 Post(date=datetime.datetime(2013, 11, 14, 17, 25), title='Hard Aground', rst_text='Some embarrassing revelation. Including ☹ and ⚓︎', tags=['#RedRanger', '#Whitby42', '#ICW'], underline='------------', tag_text='#RedRanger #Whitby42 #ICW')\n    Post:2 Post(date=datetime.datetime(2013, 11, 18, 15, 30), title='Anchor Follies', rst_text='Some witty epigram. Including ☺ and ☀︎︎', tags=['#RedRanger', '#Whitby42', '#Mistakes'], underline='--------------', tag_text='#RedRanger #Whitby42 #Mistakes')\n\n    >>> with closing(Access3()) as access:\n    ...     access.open(Path.cwd() / \"data\" / \"ch11_blog3\")\n    ...     print(sorted(access.database.keys()))\n    ...     print(access.database['_Index:Blog:1'])\n    ['Blog:1', 'Post:1', 'Post:2', '_DB:max', '_Index:Blog', '_Index:Blog:1']\n    ['Post:1', 'Post:2']\n\n\n    >>> with closing(Access3()) as access:\n    ...     access.open(Path.cwd() / \"data\" / \"ch11_blog3\")\n    ...     renderer = Render(access)\n    ...     renderer.emit_all()\n\"\"\"\n\n\n# Additional Indices\n# ================================\n\n# A class with multiple indices.\n# Is this really worth the extra complexity?\nclass Access4(Access3):\n\n    def new(self, path: Path) -> None:\n        super().new(path)\n        self.database[\"_Index:Blog_Title\"] = dict()\n\n    def create_blog(self, blog):\n        super().create_blog(blog)\n        blog_title_dict = self.database[\"_Index:Blog_Title\"]\n        blog_title_dict.setdefault(blog.title, [])\n        blog_title_dict[blog.title].append(blog._id)\n        self.database[\"_Index:Blog_Title\"] = blog_title_dict\n        return blog\n\n    def update_blog(self, blog: Blog) -> Blog:\n        \"\"\"Replace this Blog; update index.\"\"\"\n        self.database[blog._id] = blog\n        blog_title = self.database[\"_Index:Blog_Title\"]\n        # Remove key from index in old spot.\n        empties = []\n        for k in blog_title:\n            if blog._id in blog_title[k]:\n                blog_title[k].remove(blog._id)\n                if len(blog_title[k]) == 0:\n                    empties.append(k)\n        # Cleanup zero-length lists from defaultdict.\n        for k in empties:\n            del blog_title[k]\n        # Put key into index in new spot.\n        blog_title[blog.title].append(blog._id)\n        self.database[\"_Index:Blog_Title\"] = blog_title\n        return blog\n\n    def blog_iter(self) -> Iterator[Blog]:\n        return (self.database[k] for k in self.database[\"_Index:Blog\"])\n\n    def blog_title_iter(self, title: str) -> Iterator[Blog]:\n        blog_title = self.database[\"_Index:Blog_Title\"]\n        return (self.database[k] for k in blog_title[title])\n\n\ntest_access_4 = \"\"\"\n    >>> with closing(Access4()) as access:\n    ...     access.new(Path.cwd() / \"data\" / \"ch11_blog4\")\n    ...     database_script(access)\n    Blog:1 Blog(title='Travel Blog', underline='===========', _id='Blog:1')\n    Post:1 Post(date=datetime.datetime(2013, 11, 14, 17, 25), title='Hard Aground', rst_text='Some embarrassing revelation. Including ☹ and ⚓︎', tags=['#RedRanger', '#Whitby42', '#ICW'], underline='------------', tag_text='#RedRanger #Whitby42 #ICW')\n    Post:2 Post(date=datetime.datetime(2013, 11, 18, 15, 30), title='Anchor Follies', rst_text='Some witty epigram. Including ☺ and ☀︎︎', tags=['#RedRanger', '#Whitby42', '#Mistakes'], underline='--------------', tag_text='#RedRanger #Whitby42 #Mistakes')\n\n    >>> with closing(Access4()) as access:\n    ...     access.open(Path.cwd() / \"data\" / \"ch11_blog4\")\n    ...     print(sorted(access.database.keys()))\n    ...     print(access.database['_Index:Blog:1'])\n    ['Blog:1', 'Post:1', 'Post:2', '_DB:max', '_Index:Blog', '_Index:Blog:1', '_Index:Blog_Title']\n    ['Post:1', 'Post:2']\n\n\n    >>> with closing(Access4()) as access:\n    ...     access.open(Path.cwd() / \"data\" / \"ch11_blog4\")\n    ...     renderer = Render(access)\n    ...     renderer.emit_all()\n\"\"\"\n\n\n# Timing Comparison\n# ================================\n\n# Larger Database Required\nimport time\nimport io\n\n\ndef create(access, blogs=100, posts_per_blog=100) -> None:\n    for b_n in range(blogs):\n        b = Blog(\"Blog {0}\".format(b_n))\n        access.create_blog(b)\n        for p_n in range(posts_per_blog):\n            p = Post(\n                date=datetime.datetime.now(),\n                title=\"Blog {0}; Post {1}\".format(b_n, p_n),\n                rst_text=\"Blog {0}; Post {1}\\nText\\n\".format(b_n, p_n),\n                tags=list(\"#tag{0}\".format(p_n + i) for i in range(3)),\n            )\n            access.create_post(b, p)\n\n\ndef performance(cycles=3):\n    import random\n\n    result: Dict[str, float] = defaultdict(int)\n    base_path = Path.cwd() / \"data\"\n\n    for filename, class_ in (\n        (base_path / \"ch11_blog_t\", Access),\n        (base_path / \"ch11_blog_t2\", Access2),\n        (base_path / \"ch11_blog_t3\", Access3),\n        (base_path / \"ch11_blog_t4\", Access4),\n    ):\n\n        buffer = io.StringIO()\n        start = time.perf_counter()\n        for _ in range(cycles):\n            with closing(class_()) as access:\n                access.new(filename)\n                create(access, blogs=100, posts_per_blog=100)\n            with closing(class_()) as access:\n                access.open(filename)\n                renderer = Render(access)\n                renderer.emit_all(buffer)\n            with closing(class_()) as access:\n                access.open(filename)\n                renderer = Render(access)\n                titles = []\n                for i in range(10):\n                    choice = random.randint(1, 100)\n                    blog_by_id = access.retrieve_blog(f\"Blog:{choice}\")\n                    renderer.emit_blog(blog_by_id, buffer)\n                    titles.append(blog_by_id.title)\n            with closing(class_()) as access:\n                access.open(filename)\n                renderer = Render(access)\n                for t in titles:\n                    blogs = access.blog_title_iter(t)\n                    for b in blogs:\n                        renderer.emit_blog(b, buffer)\n        finish = time.perf_counter()\n        result[class_.__name__] = finish - start\n\n    print(\"Time to create and render 10,000 posts\")\n    for r in sorted(result):\n        print(\n            f\"Access Layer {r}: {result[r]/cycles:.1f} seconds \"\n        )\n\n    for path in base_path.glob(\"ch11_blog_t*.*\"):\n        path.unlink()\n\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n    # performance()   # Takes 45 seconds\n\n\"\"\"\nTime to create and render 10,000 posts\nAccess Layer Access: 33.5 seconds \nAccess Layer Access2: 4.0 seconds \nAccess Layer Access3: 3.9 seconds \nAccess Layer Access4: 4.0 seconds \n\"\"\""
  },
  {
    "path": "Chapter_12/__init__.py",
    "content": ""
  },
  {
    "path": "Chapter_12/ch12_ex1.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 12. Example 1.\n\"\"\"\n\nfrom typing import Dict, List, Tuple\n\n# One issue here is that the microblog has no processing.\n# The classes tend to be rather anemic.\n\n# The upside is that it has all of the relevant relationships\n# So it shows SQL key handling nicely.\n\n# SQL Basics\n# ========================================\n\n# Some Example Table Declarations for a simple microblog.\nsql_cleanup = \"\"\"\nDROP TABLE IF EXISTS blog;\nDROP TABLE IF EXISTS post;\nDROP TABLE IF EXISTS tag;\nDROP TABLE IF EXISTS assoc_post_tag;\n\"\"\"\n\nsql_ddl = \"\"\"\nCREATE TABLE blog(\n    ID INTEGER PRIMARY KEY AUTOINCREMENT,\n    TITLE TEXT \n);\nCREATE TABLE post(\n    id INTEGER PRIMARY KEY AUTOINCREMENT,\n    date TIMESTAMP,\n    title TEXT,\n    rst_text TEXT,\n    blog_id INTEGER REFERENCES blog(id)\n);\nCREATE TABLE tag(\n    id INTEGER PRIMARY KEY AUTOINCREMENT,\n    phrase TEXT UNIQUE ON CONFLICT FAIL\n);\nCREATE TABLE assoc_post_tag(\n    post_id INTEGER REFERENCES post(id),\n    tag_id INTEGER REFERENCES tag(id)\n);\n\"\"\"\n\nimport sqlite3\nfrom pathlib import Path\nfrom contextlib import closing\n\ndatabase = sqlite3.connect(Path.cwd() / \"data\" / \"ch12_blog.db\")  # type: ignore\n# Note that sqlite3 really does use a Path. The declaration doesn't include it.\n# We have two choices.\n# 1. Use a ``# typing: ignore`` comment\n# 2. use ``str(Path.cwd()/\"data\"/\"ch12_blog.db\")``\n\n# reveal_type(sqlite3.connect)\n\ndatabase.executescript(sql_cleanup)\n\nwith closing(database.cursor()) as cursor:\n    for stmt in (stmt.rstrip() for stmt in sql_ddl.split(\";\")):\n        print(stmt)\n        cursor.execute(stmt)\n        print(cursor)\ndatabase.commit()\ndatabase.close()\n\n# ACID\n# ===============\n\ndatabase = sqlite3.connect(\n    Path.cwd() / \"data\" / \"ch12_blog.db\", isolation_level=\"DEFERRED\"\n)  # type: ignore\ntry:\n    with closing(database.cursor()) as cursor:\n        cursor.execute(\"BEGIN\")\n        # cursor.execute(\"some statement\")\n        # cursor.execute(\"another statement\")\n    database.commit()\nexcept Exception as e:\n    database.rollback()\n\n# Simple SQL\n# ======================\n\n# Import\nimport datetime\n\n# Connection\ndatabase = sqlite3.connect(Path.cwd() / \"data\" / \"ch12_blog.db\")  # type: ignore\n\n# Useful query to figuring out what PK was automatically assigned.\nget_last_id = \"\"\"\nSELECT last_insert_rowid()\n\"\"\"\n\nwith closing(database.cursor()) as cursor:\n    cursor.execute(\"BEGIN\")\n\n    # Build BLOG\n    create_blog = \"\"\"\n        INSERT INTO blog(title) VALUES(?)\n    \"\"\"\n    cursor.execute(create_blog, (\"Travel Blog\",))\n    row = cursor.execute(get_last_id).fetchone()\n    blog_id = row[0]\n\n    # Build POST\n    create_post = \"\"\"\n        INSERT INTO post(date, title, rst_text, blog_id) VALUES(?, ?, ?, ?)\n    \"\"\"\n    cursor.execute(\n        create_post,\n        (\n            datetime.datetime(2013, 11, 14, 17, 25),\n            \"Hard Aground\",\n            \"\"\"Some embarrassing revelation. Including ☹ and ⚓︎\"\"\",\n            blog_id,\n        ),\n    )\n    row = cursor.execute(get_last_id).fetchone()\n    post_id = row[0]\n\n    # Build TAGs for a Post\n    create_tag = \"\"\"\n        INSERT INTO tag(phrase) VALUES(?)\n    \"\"\"\n    retrieve_tag = \"\"\"\n        SELECT id, phrase FROM tag WHERE phrase = ?\n    \"\"\"\n    create_tag_post_association = \"\"\"\n        INSERT INTO assoc_post_tag(post_id, tag_id) VALUES (?, ?)\n    \"\"\"\n    for tag in (\"#RedRanger\", \"#Whitby42\", \"#ICW\"):\n        row = cursor.execute(retrieve_tag, (tag,)).fetchone()\n        if row:\n            tag_id = row[0]\n        else:\n            cursor.execute(create_tag, (tag,))\n            row = cursor.execute(get_last_id).fetchone()\n            tag_id = row[0]\n        cursor.execute(create_tag_post_association, (post_id, tag_id))\n\ndatabase.commit()\n\nupdate_blog = \"\"\"\n    UPDATE blog SET title=:new_title WHERE title=:old_title\n\"\"\"\nwith closing(database.cursor()) as cursor:\n    # Sample Update\n    cursor.execute(\"BEGIN\")\n    cursor.execute(\n        update_blog,\n        dict(\n            new_title=\"2013-2014 Travel\",\n            old_title=\"Travel Blog\")\n    )\ndatabase.commit()\n\n# Sample Delete\ndelete_post_tag_by_blog_title = \"\"\"\n    DELETE FROM assoc_post_tag\n    WHERE post_id IN (\n        SELECT DISTINCT post_id\n        FROM blog JOIN post ON blog.id = post.blog_id\n        WHERE blog.title=:old_title)\n\"\"\"\ndelete_post_by_blog_title = \"\"\"\n    DELETE FROM post WHERE blog_id IN (\n        SELECT id FROM blog WHERE title=:old_title)\n\"\"\"\ndelete_blog_by_title = \"\"\"\n    DELETE FROM blog WHERE title=:old_title\n\"\"\"\ntry:\n    with closing(database.cursor()) as cursor:\n        title = dict(old_title=\"2013-2014 Travel\")\n        cursor.execute(\"BEGIN\")\n        cursor.execute(delete_post_tag_by_blog_title, title)\n        cursor.execute(delete_post_by_blog_title, title)\n        cursor.execute(delete_blog_by_title, title)\n        print(\"Post Delete, Pre Commit; should be no '2013-2014 Travel'\")\n        cursor.execute(\"SELECT * FROM blog\")\n        for row in cursor.fetchall():\n            print(row)\n        cursor.execute(\"SELECT * FROM post\")\n        for row in cursor.fetchall():\n            print(row)\n        cursor.execute(\"SELECT * FROM assoc_post_tag\")\n        for row in cursor.fetchall():\n            print(row)\n        raise Exception(\"Demonstrating an Error\")\n    print(\"Should not get here to commit.\")\n    database.commit()\nexcept Exception as ex:\n    print(f\"Rollback due to {ex!r}\")\n    database.rollback()\n\n# Bulk examination of database to show simple queries\nwith closing(database.cursor()) as cursor:\n    print(\"Dumping whole database.\")\n    for row in cursor.execute(\"SELECT * FROM blog\"):\n        print(\"BLOG\", row)\n    for row in cursor.execute(\"SELECT * FROM post\"):\n        print(\"POST\", row)\n    for row in cursor.execute(\"SELECT * FROM tag\"):\n        print(\"TAG\", row)\n    for row in cursor.execute(\n        \"\"\"\n        SELECT assoc_post_tag.* \n        FROM post \n            JOIN assoc_post_tag ON post.id=assoc_post_tag.post_id \n            JOIN tag ON tag.id=assoc_post_tag.tag_id\n        \"\"\"\n    ):\n        print(\"ASSOC_POST_TAG\", row)\n\n# Naked SQL Query\n# ==========================\n\nprint(\"Dump a single blog by title.\")\n\n# Three-step nested queries\nquery_blog_by_title = \"\"\"\nSELECT * FROM blog WHERE title=?\n\"\"\"\nquery_post_by_blog_id = \"\"\"\nSELECT * FROM post WHERE blog_id=?\n\"\"\"\nquery_tag_by_post_id = \"\"\"\nSELECT tag.*\nFROM tag JOIN assoc_post_tag ON tag.id = assoc_post_tag.tag_id\nWHERE assoc_post_tag.post_id=?\n\"\"\"\nwith closing(database.cursor()) as blog_cursor:\n    blog_cursor.execute(query_blog_by_title, (\"2013-2014 Travel\",))\n    for blog in blog_cursor.fetchall():\n        print(\"Blog\", blog)\n        with closing(database.cursor()) as post_cursor:\n            post_cursor.execute(query_post_by_blog_id, (blog[0],))\n            for post in post_cursor:\n                print(\"Post\", post)\n                with closing(database.cursor()) as tag_cursor:\n                    tag_cursor.execute(query_tag_by_post_id, (post[0],))\n                    for tag in tag_cursor.fetchall():\n                        print(\"Tag\", tag)\n\n# Tag index\nfrom collections import defaultdict\n\nquery_by_tag = \"\"\"\n    SELECT tag.phrase, post.title, post.id\n    FROM tag JOIN assoc_post_tag ON tag.id = assoc_post_tag.tag_id\n    JOIN post ON post.id = assoc_post_tag.post_id\n    JOIN blog ON post.blog_id = blog.id\n    WHERE blog.title=?\n\"\"\"\ntag_index: Dict[str, List[Tuple[str, int]]] = defaultdict(list)\nwith closing(database.cursor()) as cursor:\n    cursor.execute(query_by_tag, (\"2013-2014 Travel\",))\n    for tag, post_title, post_id in cursor.fetchall():\n        tag_index[tag].append((post_title, post_id))\n    print(tag_index)\n\ndatabase.close()\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_12/ch12_ex2.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 12. Example 2.\n\"\"\"\n\n# BLOB Mapping\n# =========================\n\n# Adding Decimal data to a SQLite database.\nimport sqlite3\nimport decimal\n\n\ndef adapt_currency(value):\n    return str(value)\n\n\nsqlite3.register_adapter(decimal.Decimal, adapt_currency)\n\n\ndef convert_currency(bytes):\n    return decimal.Decimal(bytes.decode())\n\n\nsqlite3.register_converter(\"DECIMAL\", convert_currency)\n\n# When we define a table, we must use the type \"decimal\"\n# to get two-digit decimal values.\n\ndecimal_cleanup = \"\"\"\nDROP TABLE IF EXISTS budget\n\"\"\"\n\ndecimal_ddl = \"\"\"\nCREATE TABLE budget(\n    year INTEGER,\n    month INTEGER,\n    category TEXT,\n    amount DECIMAL\n)\n\"\"\"\ninsert_budget = \"\"\"\nINSERT INTO budget(year, month, category, amount) VALUES(:year, :month, :category, :amount)\n\"\"\"\nquery_budget = \"\"\"\nSELECT * FROM budget\n\"\"\"\n\ntest_decimal = \"\"\"\n>>> from pathlib import Path\n>>> database = sqlite3.connect(\n...    Path.cwd() / \"data\" / \"ch12_blog.db\", \n...    detect_types=sqlite3.PARSE_DECLTYPES  # Required to include additional types\n... )  # type: ignore\n\n>>> _ = database.execute(decimal_cleanup)\n>>> _ = database.execute(decimal_ddl)\n\n>>> _ = database.execute(\n...    insert_budget,\n...    dict(year=2013, month=1, category=\"fuel\", amount=decimal.Decimal(\"256.78\")),\n... )\n>>> _ = database.execute(\n...    insert_budget,\n...    dict(year=2013, month=2, category=\"fuel\", amount=decimal.Decimal(\"287.65\")),\n... )\n\n>>> for row in database.execute(query_budget):\n...    print(row)\n(2013, 1, 'fuel', Decimal('256.78'))\n(2013, 2, 'fuel', Decimal('287.65'))\n>>> database.close()\n\"\"\"\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_12/ch12_ex3.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 12. Example 3.\n\"\"\"\n\n# Manual ORM\n# =========================\n\nfrom pathlib import Path\nfrom dataclasses import dataclass, asdict, field\nfrom typing import List, Dict, Any, DefaultDict, Optional, Iterator, cast\nimport sqlite3\nimport datetime\nfrom contextlib import closing\nfrom collections import defaultdict\nfrom weakref import ref\n\n\n@dataclass\nclass Blog:\n\n    title: str\n    underline: str = field(init=False)\n\n    # Part of the persistence, not essential to the class.\n    _id: str = field(default=\"\", init=False, compare=False)\n    _access: Optional[ref] = field(init=False, repr=False, default=None, compare=False)\n\n    def __post_init__(self) -> None:\n        self.underline = \"=\" * len(self.title)\n\n    @property\n    def entries(self) -> List['Post']:\n        if self._access and self._access():\n            posts = cast('Access', self._access()).post_iter(self)\n            return list(posts)\n        raise RuntimeError(\"Can't work with Blog: no associated Access instance\")\n\n    def by_tag(self) -> Dict[str, List[Dict[str, Any]]]:\n        if self._access and self._access():\n            return cast('Access', self._access()).post_by_tag(self)\n        raise RuntimeError(\"Can't work with Blog: no associated Access instance\")\n\n@dataclass\nclass Post:\n\n    date: datetime.datetime\n    title: str\n    rst_text: str\n    tags: List[str] = field(default_factory=list)\n    _id: str = field(default=\"\", init=False, compare=False)\n\n    def append(self, tag):\n        self.tags.append(tag)\n\n\n# An access layer to map back and forth between Python objects and SQL rows.\nclass Access:\n    get_last_id = \"\"\"\n        SELECT last_insert_rowid()\n    \"\"\"\n\n    def open(self, path: Path) -> None:\n        self.database = sqlite3.connect(path)\n        self.database.row_factory = sqlite3.Row\n\n    def get_blog(self, id: str) -> Blog:\n        query_blog = \"\"\"\n            SELECT * FROM blog WHERE id=?\n        \"\"\"\n        row = self.database.execute(query_blog, (id,)).fetchone()\n        blog = Blog(title=row[\"TITLE\"])\n        blog._id = row[\"ID\"]\n        blog._access = ref(self)\n        return blog\n\n    def add_blog(self, blog: Blog) -> Blog:\n        insert_blog = \"\"\"\n            INSERT INTO blog(title) VALUES(:title)\n        \"\"\"\n        self.database.execute(insert_blog, dict(title=blog.title))\n        row = self.database.execute(self.get_last_id).fetchone()\n        blog._id = str(row[0])\n        blog._access = ref(self)\n        return blog\n\n    def get_post(self, id: str) -> Post:\n        query_post = \"\"\"\n            SELECT * FROM post WHERE id=?\n        \"\"\"\n        row = self.database.execute(query_post, (id,)).fetchone()\n        post = Post(\n            title=row[\"TITLE\"], date=row[\"DATE\"], rst_text=row[\"RST_TEXT\"]\n        )\n        post._id = row[\"ID\"]\n        # Get tag text, too\n        query_tags = \"\"\"\n            SELECT tag.*\n            FROM tag JOIN assoc_post_tag ON tag.id = assoc_post_tag.tag_id\n            WHERE assoc_post_tag.post_id=?\n        \"\"\"\n        results = self.database.execute(query_tags, (id,))\n        for tag_id, phrase in results:\n            post.append(phrase)\n        return post\n\n    def add_post(self, blog: Blog, post: Post) -> Post:\n        insert_post = \"\"\"\n            INSERT INTO post(title, date, rst_text, blog_id) VALUES(:title, :date, :rst_text, :blog_id)\n        \"\"\"\n        query_tag = \"\"\"\n            SELECT * FROM tag WHERE phrase=?\n        \"\"\"\n        insert_tag = \"\"\"\n            INSERT INTO tag(phrase) VALUES(?)\n        \"\"\"\n        insert_association = \"\"\"\n            INSERT INTO assoc_post_tag(post_id, tag_id) VALUES(:post_id, :tag_id)\n        \"\"\"\n        try:\n            with closing(self.database.cursor()) as cursor:\n                cursor.execute(\n                    insert_post,\n                    dict(\n                        title=post.title,\n                        date=post.date,\n                        rst_text=post.rst_text,\n                        blog_id=blog._id,\n                    ),\n                )\n                row = cursor.execute(self.get_last_id).fetchone()\n                post._id = str(row[0])\n                for tag in post.tags:\n                    tag_row = cursor.execute(query_tag, (tag,)).fetchone()\n                    if tag_row is not None:\n                        tag_id = tag_row[\"ID\"]\n                    else:\n                        cursor.execute(insert_tag, (tag,))\n                        row = cursor.execute(self.get_last_id).fetchone()\n                        tag_id = str(row[0])\n                    cursor.execute(\n                        insert_association,\n                        dict(tag_id=tag_id, post_id=post._id)\n                    )\n            self.database.commit()\n        except Exception as ex:\n            self.database.rollback()\n            raise\n        return post\n\n    def blog_iter(self) -> Iterator[Blog]:\n        query = \"\"\"\n            SELECT * FROM blog\n        \"\"\"\n        results = self.database.execute(query)\n        for row in results:\n            blog = Blog(title=row[\"TITLE\"])\n            blog._id = row[\"ID\"]\n            blog._access = ref(self)\n            yield blog\n\n    def post_iter(self, blog: Blog) -> Iterator[Post]:\n        query = \"\"\"\n            SELECT id FROM post WHERE blog_id=?\n        \"\"\"\n        results = self.database.execute(query, (blog._id,))\n        for row in results:\n            yield self.get_post(row[\"ID\"])\n\n    def post_by_tag(self, blog: Blog) -> Dict[str, List[Dict[str, Any]]]:\n        \"\"\"All posts of a blog, organized by tag, represented as dictionaries.\"\"\"\n        query_by_tag = \"\"\" \n            SELECT tag.phrase, post.id\n            FROM tag JOIN assoc_post_tag ON tag.id = assoc_post_tag.tag_id \n            JOIN post ON post.id = assoc_post_tag.post_id \n            JOIN blog ON post.blog_id = blog.id \n            WHERE blog.title=? \n        \"\"\"\n        results = self.database.execute(query_by_tag, (blog.title,))\n        tags: DefaultDict[str, List[Dict[str, Any]]] = defaultdict(list)\n        for phrase, post_id in results.fetchall():\n            tags[phrase].append(asdict(self.get_post(post_id)))\n        return tags\n\n\nif __name__ == \"__main__\":\n    database_access = Access()\n    database_access.open(Path.cwd() / \"data\" / \"ch12_blog.db\")\n    b = Blog(title=\"2012 Travel\")\n    database_access.add_blog(b)\n    print(b._id)\n    p = Post(\n        title=\"Some History\",\n        date=datetime.datetime(2012, 9, 16, 10, 00),\n        rst_text=\"Some historyical notes.\",\n        tags=[\"#History\", \"#RedRanger\"],\n    )\n    database_access.add_post(b, p)\n\n    d = b.by_tag()\n    print(d)\n\n    for b in database_access.blog_iter():\n        # print(f\"b = {iter(b)}\")\n        print(asdict(b))\n        for p in database_access.post_iter(b):\n            print(asdict(p))\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_12/ch12_ex4.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 12. Example 4.\n\n..  important::\n\n    SQLAlchemy doesn't include any stubs or type hints.\n\n    There's no point in running mypy on this module. You'll see the following\n    errors (plus a few others)::\n\n        Chapter_12/ch12_ex4.py:18: error: No library stub file for module 'sqlalchemy.ext.declarative'\n        Chapter_12/ch12_ex4.py:18: note: (Stub files are from https://github.com/python/typeshed)\n        Chapter_12/ch12_ex4.py:23: error: No library stub file for module 'sqlalchemy'\n        Chapter_12/ch12_ex4.py:44: error: No library stub file for module 'sqlalchemy.orm'\n        Chapter_12/ch12_ex4.py:117: error: No library stub file for module 'sqlalchemy.exc'\n\n\"\"\"\n\nimport datetime\n\n# SQLAlchemy Mapping\n# ==============================\n\n# Some Classes that reflect our SQL data.\nfrom sqlalchemy.ext.declarative import declarative_base\n\n# Section 3.2.5 lists the column types\nfrom sqlalchemy import Column, Table\nfrom sqlalchemy import (\n    BigInteger,\n    Boolean,\n    Date,\n    DateTime,\n    Enum,\n    Float,\n    Integer,\n    Interval,\n    LargeBinary,\n    Numeric,\n    PickleType,\n    SmallInteger,\n    String,\n    Text,\n    Time,\n    Unicode,\n    UnicodeText,\n    ForeignKey,\n)\nfrom sqlalchemy.orm import relationship, backref\n\n# There are standard types and vendor types, also.\n# We'll stick with generic types.\n\n# The metaclass\nBase = declarative_base()\n\n# The application class/table declarations\nclass Blog(Base):\n    __tablename__ = \"BLOG\"\n    id = Column(Integer, primary_key=True)\n    title = Column(String)\n\n    def as_dict(self):\n        return dict(\n            title=self.title,\n            underline=\"=\" * len(self.title),\n            entries=[e.as_dict() for e in self.entries],\n        )\n\n\nassoc_post_tag = Table(\n    \"ASSOC_POST_TAG\",\n    Base.metadata,\n    Column(\"POST_ID\", Integer, ForeignKey(\"POST.id\")),\n    Column(\"TAG_ID\", Integer, ForeignKey(\"TAG.id\")),\n)\n\n\nclass Post(Base):\n    __tablename__ = \"POST\"\n    id = Column(Integer, primary_key=True)\n    title = Column(String)\n    date = Column(DateTime)\n    rst_text = Column(UnicodeText)\n    blog_id = Column(Integer, ForeignKey(\"BLOG.id\"))\n    blog = relationship(\"Blog\", backref=\"entries\")\n    tags = relationship(\"Tag\", secondary=assoc_post_tag, backref=\"posts\")\n\n    def as_dict(self):\n        return dict(\n            title=self.title,\n            underline=\"-\" * len(self.title),\n            date=self.date,\n            rst_text=self.rst_text,\n            tags=[t.phrase for t in self.tags],\n        )\n\n\nclass Tag(Base):\n    __tablename__ = \"TAG\"\n    id = Column(Integer, primary_key=True)\n    phrase = Column(String, unique=True)\n\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n\n    # Building a schema\n    from sqlalchemy import create_engine\n\n    engine = create_engine(\"sqlite:///./data/ch12_blog2.db\", echo=True)\n    Base.metadata.drop_all(engine)\n    Base.metadata.create_all(engine)\n\n    # Loading some data\n    import sqlalchemy.exc\n    from sqlalchemy.orm import sessionmaker\n\n    Session = sessionmaker(bind=engine)\n\n    session = Session()\n\n    blog = Blog(title=\"Travel 2013\")\n    session.add(blog)\n\n    tags = []\n    for phrase in \"#RedRanger\", \"#Whitby42\", \"#ICW\":\n        try:\n            tag = session.query(Tag).filter(Tag.phrase == phrase).one()\n        except sqlalchemy.orm.exc.NoResultFound:\n            tag = Tag(phrase=phrase)\n            session.add(tag)\n        tags.append(tag)\n\n    p2 = Post(\n        date=datetime.datetime(2013, 11, 14, 17, 25),\n        title=\"Hard Aground\",\n        rst_text=\"\"\"Some embarrassing revelation. Including ☹ and ⚓︎\"\"\",\n        blog=blog,\n        tags=tags,\n    )\n    session.add(p2)\n\n    tags = []\n    for phrase in \"#RedRanger\", \"#Whitby42\", \"#Mistakes\":\n        try:\n            tag = session.query(Tag).filter(Tag.phrase == phrase).one()\n        except sqlalchemy.orm.exc.NoResultFound:\n            tag = Tag(phrase=phrase)\n            session.add(tag)\n        tags.append(tag)\n\n    p3 = Post(\n        date=datetime.datetime(2013, 11, 18, 15, 30),\n        title=\"Anchor Follies\",\n        rst_text=\"\"\"Some witty epigram. Including ☺ and ☀︎︎\"\"\",\n        blog=blog,\n        tags=tags,\n    )\n    session.add(p3)\n    blog.posts = [p2, p3]\n\n    session.commit()\n\n    session = Session()\n\n    for blog in session.query(Blog):\n        print(\"{title}\\n{underline}\\n\".format(**blog.as_dict()))\n        for p in blog.entries:\n            print(p.as_dict())\n\n    session2 = Session()\n    results = (\n        session2.query(Post).join(assoc_post_tag).join(Tag).filter(\n            Tag.phrase == \"#Whitby42\"\n        )\n    )\n    for post in results:\n        print(post.blog.title, post.date, post.title, [t.phrase for t in post.tags])\n"
  },
  {
    "path": "Chapter_13/__init__.py",
    "content": ""
  },
  {
    "path": "Chapter_13/cards_openapi.json",
    "content": "{\n  \"openapi\": \"3.0.0\",\n  \"info\": {\n    \"description\": \"Deals simple hands of cards\",\n    \"version\": \"2019.02\",\n    \"title\": \"Chapter 13. Example 2\"\n  },\n  \"components\": {\n    \"schemas\": {\n      \"cards\": {\n        \"type\": \"array\",\n        \"items\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"rank\": {\n              \"type\": \"number\"\n            },\n            \"suit\": {\n              \"type\": \"string\"\n            }\n          }\n        }\n      }\n    }\n  },\n  \"paths\": {\n    \"/cards/{n}\": {\n      \"get\": {\n        \"summary\": \"Get a hand of cards\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"n\",\n            \"description\": \"Number of Cards\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"number\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Hand of cards\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"status\": {\n                      \"type\": \"string\"\n                    },\n                    \"cards\": {\n                      \"$ref\": \"#/components/schemas/cards\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Invalid input\"\n          }\n        }\n      }\n    },\n    \"/hands/{h}/cards/{c}\": {\n      \"get\": {\n        \"summary\": \"Get several hands of cards\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"h\",\n            \"description\": \"Number of Hands\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"number\"\n            }\n          },\n          {\n            \"in\": \"path\",\n            \"name\": \"c\",\n            \"description\": \"Number of Cards\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"number\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"List of hands of cards\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"status\": {\n                      \"type\": \"string\"\n                    },\n                    \"hands\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"$ref\": \"#/components/schemas/cards\"\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Invalid input\"\n          }\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "Chapter_13/ch13_e1_ex2.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 13. Example 2.\n\"\"\"\n\n\n# REST basics\n# ========================================\n\n# Stateless. Roulette.  Base class definitions.\n\nfrom typing import Optional, Iterable, TYPE_CHECKING\n\nif TYPE_CHECKING:\n    from wsgiref.types import WSGIApplication, WSGIEnvironment, StartResponse\n\nimport random\nfrom Chapter_13.ch13_ex1 import (\n    Wheel,\n    Zero,\n    DoubleZero,\n    American,\n    European,\n    Response,\n    json_get,\n)\n\n\nimport sys\nimport wsgiref.util\nimport json\n\n\n# REST Revised: Callable WSGI Applications\n# =========================================\n\n\nclass Wheel2(Wheel):\n\n    def __call__(\n        self, environ: \"WSGIEnvironment\", start_response: \"StartResponse\"\n    ) -> Iterable[bytes]:\n        winner = self.spin()\n        status = \"200 OK\"\n        headers = [(\"Content-type\", \"application/json; charset=utf-8\")]\n        start_response(status, headers)\n        return [json.dumps(winner).encode(\"UTF-8\")]\n\n\nclass American2(DoubleZero, Wheel2):\n    pass\n\n\nclass European2(Zero, Wheel2):\n    pass\n\n\ntest_wheel = \"\"\"\n    >>> am = American2(seed=2)\n    >>> def mock_start(status, headers):\n    ...     print(status, headers)\n    >>> am({}, mock_start)\n    200 OK [('Content-type', 'application/json; charset=utf-8')]\n    [b'{\"4\": [35, 1], \"Black\": [1, 1], \"Lo\": [1, 1], \"Even\": [1, 1]}']\n\"\"\"\n\n\n# A WSGI wrapper application.\nimport sys\n\n\nclass Wheel3:\n\n    def __init__(self, seed: Optional[int] = None) -> None:\n        self.am = American2(seed)\n        self.eu = European2(seed)\n\n    def __call__(\n        self, environ: \"WSGIEnvironment\", start_response: \"StartResponse\"\n    ) -> Iterable[bytes]:\n        request = wsgiref.util.shift_path_info(environ)  # 1. Parse.\n        print(\"Wheel3\", request, file=sys.stderr)  # 2. Logging.\n        if request and request.lower().startswith(\"eu\"):  # 3. Evaluate.\n            response = self.eu(environ, start_response)\n        else:\n            response = self.am(environ, start_response)\n        return response  # 4. Respond.\n\n\ntest_wheel3 = \"\"\"\n    >>> wheel = Wheel3(seed=2)\n    >>> def mock_start(status, headers):\n    ...     print(status, headers)\n    >>> wheel({\"PATH_INFO\": \"/am\"}, mock_start)\n    200 OK [('Content-type', 'application/json; charset=utf-8')]\n    [b'{\"4\": [35, 1], \"Black\": [1, 1], \"Lo\": [1, 1], \"Even\": [1, 1]}']\n\"\"\"\n\n# Revised Server\ndef roulette_server_3(count: int = 1) -> None:\n    from wsgiref.simple_server import make_server\n\n    httpd = make_server(\"localhost\", 8080, Wheel3(2))\n    if count is None:\n        httpd.serve_forever()\n    else:\n        for c in range(count):\n            httpd.handle_request()\n\n\n# Wheel3 Demo\n# ---------------\n\n# When run as the main script, start a server and interact with it.\ndef server_3() -> None:\n\n    import concurrent.futures\n    import time\n\n    with concurrent.futures.ProcessPoolExecutor() as executor:\n        srvr = executor.submit(roulette_server_3, 2)\n        time.sleep(0.1)  # Wait for the server to start\n        r1 = json_get(\"/am\")\n        r2 = json_get(\"/eu\")\n        assert (\n            str(r1)\n            == \"200: {'4': [35, 1], 'Black': [1, 1], 'Lo': [1, 1], 'Even': [1, 1]}\"\n        )\n        assert (\n            str(r2)\n            == \"200: {'4': [35, 1], 'Black': [1, 1], 'Lo': [1, 1], 'Even': [1, 1]}\"\n        )\n\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n    server_3()\n"
  },
  {
    "path": "Chapter_13/ch13_e1_ex3.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 13. Example 3.\n\"\"\"\n\n\n# REST basics\n# ========================================\n\n# Stateless. Roulette.  Base class definitions.\n\nfrom typing import Dict, Tuple, List, Any, Optional, Iterable, TYPE_CHECKING\n\nif TYPE_CHECKING:\n    from wsgiref.types import WSGIApplication, WSGIEnvironment, StartResponse\n\nimport random\nfrom Chapter_13.ch13_ex1 import (\n    Wheel,\n    Zero,\n    DoubleZero,\n    American,\n    European,\n    Response,\n    json_get,\n)\n\n\nimport sys\nimport wsgiref.util\nimport json\n\n\n# REST Revised: Callable WSGI Applications\n# =========================================\n\n\nclass Wheel2(Wheel):\n\n    def __call__(\n        self, environ: \"WSGIEnvironment\", start_response: \"StartResponse\"\n    ) -> Iterable[bytes]:\n        winner = self.spin()\n        status = \"200 OK\"\n        headers = [(\"Content-type\", \"application/json; charset=utf-8\")]\n        start_response(status, headers)\n        return [json.dumps(winner).encode(\"UTF-8\")]\n\n\nclass American2(DoubleZero, Wheel2):\n    pass\n\n\nclass European2(Zero, Wheel2):\n    pass\n\n\n# A WSGI wrapper application.\nimport sys\n\n\nclass Wheel3:\n\n    def __init__(self) -> None:\n        self.am = American2()\n        self.eu = European2()\n\n    def __call__(\n        self, environ: \"WSGIEnvironment\", start_response: \"StartResponse\"\n    ) -> Iterable[bytes]:\n        request = wsgiref.util.shift_path_info(environ)  # 1. Parse.\n        print(\"Wheel3\", request, file=sys.stderr)  # 2. Logging.\n        if request and request.lower().startswith(\"eu\"):  # 3. Evaluate.\n            response = self.eu(environ, start_response)\n        else:\n            response = self.am(environ, start_response)\n        return response  # 4. Respond.\n\n\n# REST with sessions and state\n# ========================================\n\n# Player and Bet for Roulette.\n\n# CRUD design issues.\n# Player:\n# - GET to see stake and rounds played.\n# Bet:\n# - POST to create a series of bets or decline to bet.\n# - GET to see bets.\n# Wheel:\n# - GET to get spin and payout.\n\n# Stateful object\nfrom collections import defaultdict\n\n\nclass Table:\n\n    def __init__(self, stake: int = 100) -> None:\n        self.bets: Dict[str, int] = defaultdict(int)\n        self.stake = stake\n\n    def place_bet(self, name: str, amount: int) -> None:\n        self.bets[name] += amount\n\n    def clear_bets(self, name: str) -> None:\n        self.bets: Dict[str, int] = defaultdict(int)\n\n    def resolve(self, spin: Dict[str, Tuple[int, int]]) -> List[Tuple[str, int, str]]:\n        \"\"\"spin is a dict with bet:(x:y).\"\"\"\n        details = []\n        while self.bets:\n            bet, amount = self.bets.popitem()\n            if bet in spin:\n                x, y = spin[bet]\n                self.stake += int(amount * x / y)\n                details.append((bet, amount, \"win\"))\n            else:\n                self.stake -= amount\n                details.append((bet, amount, \"lose\"))\n        return details\n\n\n# WSGI Applications\nclass WSGI:\n\n    def __call__(\n        self, environ: \"WSGIEnvironment\", start_response: \"StartResponse\"\n    ) -> Iterable[bytes]:\n        raise NotImplementedError\n\n\nclass RESTException(Exception):\n    pass\n\n\nclass Roulette(WSGI):\n\n    def __init__(self, wheel: Wheel) -> None:\n        self.table = Table(100)\n        self.rounds = 0\n        self.wheel = wheel\n\n    def __call__(\n        self, environ: \"WSGIEnvironment\", start_response: \"StartResponse\"\n    ) -> Iterable[bytes]:\n        # print( environ, file=sys.stderr )\n        app = wsgiref.util.shift_path_info(environ)\n        try:\n            if app and app.lower() == \"player\":\n                return self.player_app(environ, start_response)\n            elif app and app.lower() == \"bet\":\n                return self.bet_app(environ, start_response)\n            elif app and app.lower() == \"wheel\":\n                return self.wheel_app(environ, start_response)\n            else:\n                raise RESTException(\n                    \"404 NOT_FOUND\",\n                    \"Unknown app in {SCRIPT_NAME}/{PATH_INFO}\".format_map(environ),\n                )\n        except RESTException as e:\n            status = e.args[0]\n            headers = [(\"Content-type\", \"text/plain; charset=utf-8\")]\n            start_response(status, headers, sys.exc_info())\n            return [repr(e.args).encode(\"UTF-8\")]\n\n    def player_app(\n        self, environ: \"WSGIEnvironment\", start_response: \"StartResponse\"\n    ) -> Iterable[bytes]:\n        if environ[\"REQUEST_METHOD\"] == \"GET\":\n            details = dict(stake=self.table.stake, rounds=self.rounds)\n            status = \"200 OK\"\n            headers = [(\"Content-type\", \"application/json; charset=utf-8\")]\n            start_response(status, headers)\n            return [json.dumps(details).encode(\"UTF-8\")]\n        else:\n            raise RESTException(\n                \"405 METHOD_NOT_ALLOWED\",\n                \"Method '{REQUEST_METHOD}' not allowed\".format_map(environ),\n            )\n\n    def bet_app(\n        self, environ: \"WSGIEnvironment\", start_response: \"StartResponse\"\n    ) -> Iterable[bytes]:\n        if environ[\"REQUEST_METHOD\"] == \"GET\":\n            details = dict(self.table.bets)\n        elif environ[\"REQUEST_METHOD\"] == \"POST\":\n            size = int(environ[\"CONTENT_LENGTH\"])\n            raw = environ[\"wsgi.input\"].read(size).decode(\"UTF-8\")\n            try:\n                data = json.loads(raw)\n                if isinstance(data, dict):\n                    data = [data]\n                for detail in data:\n                    self.table.place_bet(detail[\"bet\"], int(detail[\"amount\"]))\n            except Exception as e:\n                # Must undo all bets.\n                raise RESTException(f\"403 FORBIDDEN\", \"Bet {raw!r}\")\n            details = dict(self.table.bets)\n        else:\n            raise RESTException(\n                \"405 METHOD_NOT_ALLOWED\",\n                \"Method '{REQUEST_METHOD}' not allowed\".format_map(environ),\n            )\n        status = \"200 OK\"\n        headers = [(\"Content-type\", \"application/json; charset=utf-8\")]\n        start_response(status, headers)\n        return [json.dumps(details).encode(\"UTF-8\")]\n\n    def wheel_app(\n        self, environ: \"WSGIEnvironment\", start_response: \"StartResponse\"\n    ) -> Iterable[bytes]:\n        if environ[\"REQUEST_METHOD\"] == \"POST\":\n            size = environ[\"CONTENT_LENGTH\"]\n            if size != \"0\":\n                raw = environ[\"wsgi.input\"].read(int(size))\n                raise RESTException(\n                    \"403 FORBIDDEN\", f\"Data {raw!r} not allowed\"\n                )\n            spin = self.wheel.spin()\n            payout = self.table.resolve(spin)\n            self.rounds += 1\n            details = dict(\n                spin=spin, payout=payout, stake=self.table.stake, rounds=self.rounds\n            )\n            status = \"200 OK\"\n            headers = [(\"Content-type\", \"application/json; charset=utf-8\")]\n            start_response(status, headers)\n            return [json.dumps(details).encode(\"UTF-8\")]\n        else:\n            raise RESTException(\n                \"405 METHOD_NOT_ALLOWED\",\n                \"Method '{REQUEST_METHOD}' not allowed\".format_map(environ),\n            )\n\n\ntest_table = \"\"\"\n    Spike to show that the essential features work.\n    \n    >>> wheel = American(seed=2)\n    >>> roulette = Roulette(wheel)\n    >>> data = {\"bet\": \"Black\", \"amount\": 2}\n    >>> roulette.table.place_bet(data[\"bet\"], int(data[\"amount\"]))\n    >>> print(roulette.table.bets)\n    defaultdict(<class 'int'>, {'Black': 2})\n    >>> spin = wheel.spin()\n    >>> payout = roulette.table.resolve(spin)\n    >>> print(spin, payout)\n    {'4': (35, 1), 'Black': (1, 1), 'Lo': (1, 1), 'Even': (1, 1)} [('Black', 2, 'win')]\n\"\"\"\n\n# Server\ndef roulette_server_3(count: int = 1) -> None:\n    from wsgiref.simple_server import make_server\n    from wsgiref.validate import validator\n\n    wheel = American(seed=1)\n    roulette = Roulette(wheel)\n    debug = validator(roulette)\n    httpd = make_server(\"\", 8080, debug)\n    if count is None:\n        httpd.serve_forever()\n    else:\n        for c in range(count):\n            httpd.handle_request()\n\n\n# Client\nimport http.client\nimport json\n\n\ndef roulette_client(\n    method: str = \"GET\",\n    path: str = \"/\",\n    data: Optional[Dict[str, str]] = None\n) -> Response:\n    rest = http.client.HTTPConnection(\"localhost\", 8080)\n    if data:\n        header = {\"Content-type\": \"application/json; charset=utf-8'\"}\n        params = json.dumps(data).encode(\"UTF-8\")\n        rest.request(method, path, params, header)\n    else:\n        rest.request(method, path)\n    response = rest.getresponse()\n    raw = response.read().decode(\"utf-8\")\n    try:\n        document = json.loads(raw)\n    except json.decoder.JSONDecodeError as ex:\n        document = raw\n    return Response(response.status, dict(response.getheaders()), document)\n\n\ndef server_3() -> None:\n    import concurrent.futures\n    import time\n\n    with concurrent.futures.ProcessPoolExecutor() as executor:\n        executor.submit(roulette_server_3, 4)\n        time.sleep(0.1)  # Wait for the server to start\n        r1 = roulette_client(\"GET\", \"/player/\")\n        print(r1)\n        r2 = roulette_client(\"POST\", \"/bet/\", {\"bet\": \"Black\", \"amount\": \"2\"})\n        print(r2)\n        r3 = roulette_client(\"GET\", \"/bet/\")\n        print(r3)\n        r4 = roulette_client(\"POST\", \"/wheel/\")\n        print(r4)\n        assert r1.status == 200 and r1.content == {\"stake\": 100, \"rounds\": 0}\n        assert r2.status == 200 and r2.content == {\"Black\": 2}\n        assert r3.status == 200 and r3.content == {\"Black\": 2}\n        assert (\n            r4.status == 200\n            and r4.content == {'spin': {'9': [35, 1], 'Red': [1, 1], 'Lo': [1, 1], 'Odd': [1, 1]}, 'payout': [['Black', 2, 'lose']], 'stake': 98, 'rounds': 1}\n        ), f\"{r4!r}\"\n\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n    server_3()\n"
  },
  {
    "path": "Chapter_13/ch13_e1_ex4.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 13. Example 4.\n\"\"\"\n\n\n# REST basics\n# ========================================\n\n# Stateless. Roulette.  Base class definitions.\n\nfrom typing import Dict, Tuple, Optional, List, Iterable, Any, TYPE_CHECKING, cast\n\nif TYPE_CHECKING:\n    from wsgiref.types import WSGIApplication, WSGIEnvironment, StartResponse\n\nimport random\nfrom Chapter_13.ch13_ex1 import (\n    Wheel,\n    Zero,\n    DoubleZero,\n    American,\n    European,\n    Response,\n    json_get,\n)\n\n\nimport sys\nimport wsgiref.util\nimport json\n\n\n# REST Revised: Callable WSGI Applications\n# =========================================\n\n\nclass Wheel2(Wheel):\n\n    def __call__(\n        self, environ: \"WSGIEnvironment\", start_response: \"StartResponse\"\n    ) -> Iterable[bytes]:\n        winner = self.spin()\n        status = \"200 OK\"\n        headers = [(\"Content-type\", \"application/json; charset=utf-8\")]\n        start_response(status, headers)\n        return [json.dumps(winner).encode(\"UTF-8\")]\n\n\nclass American2(DoubleZero, Wheel2):\n    pass\n\n\nclass European2(Zero, Wheel2):\n    pass\n\n\n# A WSGI wrapper application.\nimport sys\n\n\nclass Wheel3:\n\n    def __init__(self):\n        self.am = American2()\n        self.eu = European2()\n\n    def __call__(\n        self, environ: \"WSGIEnvironment\", start_response: \"StartResponse\"\n    ) -> Iterable[bytes]:\n        request = wsgiref.util.shift_path_info(environ)  # 1. Parse.\n        print(\"Wheel3\", request, file=sys.stderr)  # 2. Logging.\n        if request and request.lower().startswith(\"eu\"):  # 3. Evaluate.\n            response = self.eu(environ, start_response)\n        else:\n            response = self.am(environ, start_response)\n        return response  # 4. Respond.\n\n\n# REST with sessions and state\n# ========================================\n\n# Player and Bet for Roulette.\n\n# CRUD design issues.\n# Player:\n# - GET to see stake and rounds played.\n# Bet:\n# - POST to create a series of bets or decline to bet.\n# - GET to see bets.\n# Wheel:\n# - GET to get spin and payout.\n\n# Stateful object\nfrom collections import defaultdict\n\n\nclass Table:\n\n    def __init__(self, stake: int = 100) -> None:\n        self.bets: Dict[str, int] = defaultdict(int)\n        self.stake = stake\n\n    def place_bet(self, name: str, amount: int) -> None:\n        self.bets[name] += amount\n\n    def clear_bets(self, name: str) -> None:\n        self.bets: Dict[str, int] = defaultdict(int)\n\n    def resolve(self, spin: Dict[str, Tuple[int, int]]) -> List[Tuple[str, int, str]]:\n        \"\"\"spin is a dict with bet:(x:y).\"\"\"\n        details = []\n        while self.bets:\n            bet, amount = self.bets.popitem()\n            if bet in spin:\n                x, y = spin[bet]\n                self.stake += int(amount * x / y)\n                details.append((bet, amount, \"win\"))\n            else:\n                self.stake -= amount\n                details.append((bet, amount, \"lose\"))\n        return details\n\n\n# WSGI Applications\nclass WSGI:\n\n    def __call__(\n        self, environ: \"WSGIEnvironment\", start_response: \"StartResponse\"\n    ) -> Iterable[bytes]:\n        raise NotImplementedError\n\n\nclass RESTException(Exception):\n    pass\n\n\nclass Roulette(WSGI):\n\n    def __init__(self, wheel):\n        self.table = Table(100)\n        self.rounds = 0\n        self.wheel = wheel\n\n    def __call__(\n        self, environ: \"WSGIEnvironment\", start_response: \"StartResponse\"\n    ) -> Iterable[bytes]:\n        # print(environ, file=sys.stderr)\n        app = wsgiref.util.shift_path_info(environ)\n        try:\n            if app and app.lower() == \"player\":\n                return self.player_app(environ, start_response)\n            elif app and app.lower() == \"bet\":\n                return self.bet_app(environ, start_response)\n            elif app and app.lower() == \"wheel\":\n                return self.wheel_app(environ, start_response)\n            else:\n                raise RESTException(\n                    \"404 NOT_FOUND\",\n                    \"Unknown app in {SCRIPT_NAME}/{PATH_INFO}\".format_map(environ),\n                )\n        except RESTException as e:\n            status = e.args[0]\n            headers = [(\"Content-type\", \"text/plain; charset=utf-8\")]\n            start_response(status, headers, sys.exc_info())\n            return [repr(e.args).encode(\"UTF-8\")]\n\n    def player_app(\n        self, environ: \"WSGIEnvironment\", start_response: \"StartResponse\"\n    ) -> Iterable[bytes]:\n        if environ[\"REQUEST_METHOD\"] == \"GET\":\n            details = dict(stake=self.table.stake, rounds=self.rounds)\n            status = \"200 OK\"\n            headers = [(\"Content-type\", \"application/json; charset=utf-8\")]\n            start_response(status, headers)\n            return [json.dumps(details).encode(\"UTF-8\")]\n        else:\n            raise RESTException(\n                \"405 METHOD_NOT_ALLOWED\",\n                \"Method '{REQUEST_METHOD}' not allowed\".format_map(environ),\n            )\n\n    def bet_app(\n        self, environ: \"WSGIEnvironment\", start_response: \"StartResponse\"\n    ) -> Iterable[bytes]:\n        if environ[\"REQUEST_METHOD\"] == \"GET\":\n            details = dict(self.table.bets)\n        elif environ[\"REQUEST_METHOD\"] == \"POST\":\n            size = int(environ[\"CONTENT_LENGTH\"])\n            raw = environ[\"wsgi.input\"].read(size).decode(\"UTF-8\")\n            try:\n                data = json.loads(raw)\n                if isinstance(data, dict):\n                    data = [data]\n                for detail in data:\n                    self.table.place_bet(detail[\"bet\"], int(detail[\"amount\"]))\n            except Exception as e:\n                # TODO: Must undo all bets.\n                raise RESTException(f\"403 FORBIDDEN\", \"Bet {raw!r}\")\n            details = dict(self.table.bets)\n        else:\n            raise RESTException(\n                \"405 METHOD_NOT_ALLOWED\",\n                \"Method '{REQUEST_METHOD}' not allowed\".format_map(environ),\n            )\n        status = \"200 OK\"\n        headers = [(\"Content-type\", \"application/json; charset=utf-8\")]\n        start_response(status, headers)\n        return [json.dumps(details).encode(\"UTF-8\")]\n\n    def wheel_app(\n        self, environ: \"WSGIEnvironment\", start_response: \"StartResponse\"\n    ) -> Iterable[bytes]:\n        if environ[\"REQUEST_METHOD\"] == \"POST\":\n            size = environ[\"CONTENT_LENGTH\"]\n            if size != \"\":\n                raw = environ[\"wsgi.input\"].read(int(size))\n                raise RESTException(\n                    \"403 FORBIDDEN\", f\"Data '{raw!r}' not allowed\"\n                )\n            spin = self.wheel.spin()\n            payout = self.table.resolve(spin)\n            self.rounds += 1\n            details = dict(\n                spin=spin, payout=payout, stake=self.table.stake, rounds=self.rounds\n            )\n            status = \"200 OK\"\n            headers = [(\"Content-type\", \"application/json; charset=utf-8\")]\n            start_response(status, headers)\n            return [json.dumps(details).encode(\"UTF-8\")]\n        else:\n            raise RESTException(\n                \"405 METHOD_NOT_ALLOWED\",\n                \"Method '{REQUEST_METHOD}' not allowed\".format_map(environ),\n            )\n\n\ntest_table = \"\"\"\n    Spike to show that the essential features work.\n    \n    >>> wheel = American(seed=2)\n    >>> roulette = Roulette(wheel)\n    >>> data = {\"bet\": \"Black\", \"amount\": 2}\n    >>> roulette.table.place_bet(data[\"bet\"], int(data[\"amount\"]))\n    >>> print(roulette.table.bets)\n    defaultdict(<class 'int'>, {'Black': 2})\n    >>> spin = wheel.spin()\n    >>> payout = roulette.table.resolve(spin)\n    >>> print(spin, payout)\n    {'4': (35, 1), 'Black': (1, 1), 'Lo': (1, 1), 'Even': (1, 1)} [('Black', 2, 'win')]\n\"\"\"\n\n# Server\ndef roulette_server_4(count: int = 1):\n    from wsgiref.simple_server import make_server\n    from wsgiref.validate import validator\n\n    wheel = American()\n    roulette = Roulette(wheel)\n    debug = validator(roulette)\n    httpd = make_server(\"\", 8080, debug)\n    if count is None:\n        httpd.serve_forever()\n    else:\n        for c in range(count):\n            httpd.handle_request()\n\n\n# Client\nimport http.client\nimport json\n\n\ndef roulette_client(method=\"GET\", path=\"/\", data=None):\n    rest = http.client.HTTPConnection(\"localhost\", 8080)\n    if data:\n        header = {\"Content-type\": \"application/json; charset=utf-8'\"}\n        params = json.dumps(data).encode(\"UTF-8\")\n        rest.request(method, path, params, header)\n    else:\n        rest.request(method, path)\n    response = rest.getresponse()\n    raw = response.read().decode(\"utf-8\")\n    if 200 <= response.status < 300:\n        document = json.loads(raw)\n        return document\n    else:\n        print(response.status, response.reason)\n        print(response.getheaders())\n        print(raw)\n\n\n# REST with authentication\n# ========================================\n\n# Authentication class definition with password hashing.\nfrom hashlib import sha256\nimport os\n\n\nclass Authentication:\n    iterations = 1000\n\n    def __init__(self, username: bytes, password: bytes, salt: Optional[bytes]=None) -> None:\n        \"\"\"Works with bytes. Not Unicode strings.\"\"\"\n        self.username = username\n        self.salt = salt or os.urandom(24)\n        self.hash = self._iter_hash(self.iterations, self.salt, username, password)\n\n    @staticmethod\n    def _iter_hash(iterations: int, salt: bytes, username: bytes, password: bytes):\n        seed = salt + b\":\" + username + b\":\" + password\n        for i in range(iterations):\n            seed = sha256(seed).digest()\n        return seed\n\n    def __eq__(self, other: Any) -> bool:\n        other = cast(\"Authentication\", other)\n        return self.username == other.username and self.hash == other.hash\n\n    def __hash__(self) -> int:\n        return hash(self.hash)\n\n    def __repr__(self) -> str:\n        salt_x = \"\".join(\"{0:x}\".format(b) for b in self.salt)\n        hash_x = \"\".join(\"{0:x}\".format(b) for b in self.hash)\n        return f\"{self.username} {self.iterations:d}:{salt_x}:{hash_x}\"\n\n    def match(self, password: bytes) -> bool:\n        test = self._iter_hash(self.iterations, self.salt, self.username, password)\n        return self.hash == test  # Constant Time is Best\n\n\n# Collection of users.\nclass Users(dict):\n\n    def __init__(self, *args, **kw) -> None:\n        super().__init__(*args, **kw)\n        # Can never be found -- dict key is invalid and isn't the username.\n        self[\"\"] = Authentication(b\"__dummy__\", b\"Doesn't Matter\")\n\n    def add(self, authentication: Authentication) -> None:\n        if authentication.username == \"\":\n            raise KeyError(\"Invalid Authentication\")\n        self[authentication.username] = authentication\n\n    def match(self, username: bytes, password: bytes) -> bool:\n        if username in self and username != \"\":\n            return self[username].match(password)\n        else:\n            # Time-wasting comparison\n            return self[\"\"].match(b\"Something which doesn't match\")\n\n\n# Global Objects\nusers = Users()\nusers.add(Authentication(b\"Aladdin\", b\"open sesame\"))\n\ntest_matching = \"\"\"\n    Spike to show user matching rule.\n    \n    >>> test_salt = bytes(range(24))\n    >>> al = Authentication(b\"Aladdin\", b\"open sesame\", test_salt)\n    >>> al\n    b'Aladdin' 1000:0123456789abcdef1011121314151617:a53bdcd6d16acc8fd33fc982c973147f15f6ce43cff4fb83a5f6b267de1\n    \n    >>> users = Users()\n    >>> users.add(Authentication(b\"Aladdin\", b\"open sesame\"))\n\n    >>> users.match(\"\", b\"Doesn't Matter\")\n    False\n    >>> users.match(b\"__dummy__\", b\"Doesn't Matter\")\n    False\n\"\"\"\n\n# Authentication app\nimport base64\n\n\nclass Authenticate(WSGI):\n\n    def __init__(self, users, target_app):\n        self.users = users\n        self.target_app = target_app\n\n    def __call__(\n        self, environ: \"WSGIEnvironment\", start_response: \"StartResponse\"\n    ) -> Iterable[bytes]:\n        if \"HTTP_AUTHORIZATION\" in environ:\n            scheme, credentials = environ[\"HTTP_AUTHORIZATION\"].split()\n            if scheme == \"Basic\":\n                username, password = base64.b64decode(credentials).split(b\":\")\n                if self.users.match(username, password):\n                    environ[\"Authenticate.username\"] = username\n                    return self.target_app(environ, start_response)\n        status = \"401 UNAUTHORIZED\"\n        headers = [\n            (\"Content-Type\", \"text/plain; charset=utf-8\"),\n            (\"WWW-Authenticate\", 'Basic realm=\"roulette@localhost\"'),\n        ]\n        start_response(status, headers)\n        return [\"Not authorized\".encode(\"utf-8\")]\n\n\n# Some app which requires authentication\nclass Some_App(WSGI):\n\n    def __call__(\n        self, environ: \"WSGIEnvironment\", start_response: \"StartResponse\"\n    ) -> Iterable[bytes]:\n        status = \"200 OK\"\n        headers = [(\"Content-type\", \"text/plain; charset=utf-8\")]\n        start_response(status, headers)\n        return [\"Welcome\".encode(\"UTF-8\")]\n\n\n# Demo client\nimport base64\n\n\ndef authenticated_client(\n    method: str = \"GET\",\n    path: str = \"/\",\n    data: Optional[str] = None,\n    username: str = \"\",\n    password: str = \"\",\n) -> Tuple[int, str, str]:\n    rest = http.client.HTTPConnection(\"localhost\", 8080)\n    headers = {}\n    if username and password:\n        enc = base64.b64encode(\n            username.encode(\"ascii\") + b\":\" + password.encode(\"ascii\")\n        )\n        headers[\"Authorization\"] = f\"Basic {enc.decode('ascii')}\"\n    if data:\n        headers[\"Content-type\"] = \"application/json; charset=utf-8\"\n        params = json.dumps(data).encode(\"utf-8\")\n        rest.request(method, path, params, headers=headers)\n    else:\n        rest.request(method, path, headers=headers)\n    # print(f\"*** CLIENT: {headers}\")\n    response = rest.getresponse()\n    raw = response.read().decode(\"utf-8\")\n    if response.status == 401:\n        print(response.getheaders())\n    return response.status, response.reason, raw\n\n\n# Server\ndef auth_server(count: int = 1) -> None:\n    from wsgiref.simple_server import make_server\n    from wsgiref.validate import validator\n\n    secure_app = Some_App()\n    authenticated_app = Authenticate(users, secure_app)\n    debug = validator(authenticated_app)\n    httpd = make_server(\"\", 8080, debug)\n    if count is None:\n        httpd.serve_forever()\n    else:\n        for c in range(count):\n            httpd.handle_request()\n\n\n# Demo\ndef server_5() -> None:\n    import concurrent.futures\n    import time\n\n    with concurrent.futures.ProcessPoolExecutor() as executor:\n        executor.submit(auth_server, 3)\n        time.sleep(0.1)  # Wait for the server to start\n        print(authenticated_client(\"GET\", \"/player/\"))\n        print(\n            authenticated_client(\n                \"GET\", \"/player/\", username=\"Aladdin\", password=\"open sesame\"\n            )\n        )\n        print(\n            authenticated_client(\n                \"GET\", \"/player/\", username=\"Aladdin\", password=\"not right\"\n            )\n        )\n\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n    server_5()\n"
  },
  {
    "path": "Chapter_13/ch13_ex1.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 13. Example 1.\n\"\"\"\n\n\n# REST basics\n# ========================================\n\n# Object and state\n\ntest_example_1 = \"\"\"\n>>> from dataclasses import dataclass, asdict\n>>> import json\n\n>>> @dataclass\n... class Greeting:\n...     message: str\n    \n>>> g = Greeting(\"Hello World\")\n>>> text = json.dumps(asdict(g))\n>>> text\n'{\"message\": \"Hello World\"}'\n>>> text.encode('utf-8')\nb'{\"message\": \"Hello World\"}'\n\"\"\"\n\n# Stateless Roulette Server\n# ==========================\n\n# Base class definitions.\n\nfrom typing import (\n    Dict,\n    Tuple,\n    Optional,\n    Callable,\n    List,\n    Union,\n    Iterator,\n    NamedTuple,\n    Any,\n    Type,\n    Iterable,\n    TYPE_CHECKING,\n)\nfrom abc import abstractmethod\nimport random\n\n\nclass Wheel:\n    \"\"\"Abstract, zero bins omitted.\"\"\"\n\n    def __init__(self, seed: Optional[int] = None) -> None:\n        self.rng = random.Random()\n        self.rng.seed(seed)\n        self.bins = [\n            {\n                str(n): (35, 1),\n                self.redblack(n): (1, 1),\n                self.hilo(n): (1, 1),\n                self.evenodd(n): (1, 1),\n            }\n            for n in range(1, 37)\n        ]\n        self.bins.extend(self.zero())\n\n    @abstractmethod\n    def zero(self) -> List[Dict[str, Tuple[int, int]]]:\n        pass\n\n    @staticmethod\n    def redblack(n: int) -> str:\n        return \"Red\" if n in (\n            1, 3, 5, 7, 9, 12, 14, 16, 18, 19, 21, 23, 25, 27, 30, 32, 34, 36\n        ) else \"Black\"\n\n    @staticmethod\n    def hilo(n: int) -> str:\n        return \"Hi\" if n >= 19 else \"Lo\"\n\n    @staticmethod\n    def evenodd(n: int) -> str:\n        return \"Even\" if n % 2 == 0 else \"Odd\"\n\n    def spin(self) -> Dict[str, Tuple[int, int]]:\n        return self.rng.choice(self.bins)\n\n\nclass Zero:\n\n    def zero(self) -> List[Dict[str, Tuple[int, int]]]:\n        return [{\"0\": (35, 1)}]\n\n\nclass DoubleZero(Zero):\n\n    def zero(self) -> List[Dict[str, Tuple[int, int]]]:\n        z_bins = super().zero()\n        z_bins += [{\"00\": (35, 1)}]\n        return z_bins\n\n\nclass American(DoubleZero, Wheel):\n    pass\n\n\nclass European(Zero, Wheel):\n    pass\n\n\n# Some global objects used by a WSGI application function\namerican = American(9973)\neuropean = European(9973)\n\ntest_demonstrate_wheel = \"\"\"\n    >>> american.bins[-2:]\n    [{'0': (35, 1)}, {'00': (35, 1)}]\n    >>> european.bins[-1]\n    {'0': (35, 1)}\n\n    >>> for i in range(7):\n    ...     print(american.spin())\n    {'25': (35, 1), 'Red': (1, 1), 'Hi': (1, 1), 'Odd': (1, 1)}\n    {'18': (35, 1), 'Red': (1, 1), 'Lo': (1, 1), 'Even': (1, 1)}\n    {'20': (35, 1), 'Black': (1, 1), 'Hi': (1, 1), 'Even': (1, 1)}\n    {'21': (35, 1), 'Red': (1, 1), 'Hi': (1, 1), 'Odd': (1, 1)}\n    {'32': (35, 1), 'Red': (1, 1), 'Hi': (1, 1), 'Even': (1, 1)}\n    {'34': (35, 1), 'Red': (1, 1), 'Hi': (1, 1), 'Even': (1, 1)}\n    {'21': (35, 1), 'Red': (1, 1), 'Hi': (1, 1), 'Odd': (1, 1)}\n\n    >>> for i in range(7):\n    ...     print(european.spin())\n    {'25': (35, 1), 'Red': (1, 1), 'Hi': (1, 1), 'Odd': (1, 1)}\n    {'18': (35, 1), 'Red': (1, 1), 'Lo': (1, 1), 'Even': (1, 1)}\n    {'20': (35, 1), 'Black': (1, 1), 'Hi': (1, 1), 'Even': (1, 1)}\n    {'21': (35, 1), 'Red': (1, 1), 'Hi': (1, 1), 'Odd': (1, 1)}\n    {'32': (35, 1), 'Red': (1, 1), 'Hi': (1, 1), 'Even': (1, 1)}\n    {'34': (35, 1), 'Red': (1, 1), 'Hi': (1, 1), 'Even': (1, 1)}\n    {'21': (35, 1), 'Red': (1, 1), 'Hi': (1, 1), 'Odd': (1, 1)}\n\"\"\"\n\nimport sys\nimport wsgiref.util\nimport json\n\nif TYPE_CHECKING:\n    from wsgiref.types import WSGIApplication, WSGIEnvironment, StartResponse\n\n\ndef wsgi_wheel(\n    environ: \"WSGIEnvironment\", start_response: \"StartResponse\"\n) -> Iterable[bytes]:\n    request = wsgiref.util.shift_path_info(environ)  # 1. Parse.\n    print(\"wheel\", repr(request), file=sys.stderr)  # 2. Logging.\n    if request and request.lower().startswith(\"eu\"):  # 3. Evaluate.\n        winner = european.spin()\n    else:\n        winner = american.spin()\n    status = \"200 OK\"  # 4. Respond.\n    headers = [(\"Content-Type\", \"text/plain; charset=utf-8\")]\n    start_response(status, headers)\n    return [json.dumps(winner).encode(\"UTF-8\")]\n\n\n# A function we can call to start a server\n# which handles a finite number of requests.\n# Handy for testing.\nfrom wsgiref.simple_server import make_server\n\n\ndef roulette_server(count: int = 1) -> None:\n    httpd = make_server(\"\", 8080, wsgi_wheel)\n    if count is None:\n        httpd.serve_forever()\n    else:\n        for c in range(count):\n            httpd.handle_request()\n\n\n# REST Client\n# -------------\n\n# A REST client that simply loads a JSON document.\nimport http.client\nimport json\nfrom typing import NamedTuple\n\n\nclass Response(NamedTuple):\n    status: int\n    headers: Dict[str, str]\n    content: Optional[Any]\n\n    def __str__(self) -> str:\n        return f\"{self.status}: {self.content}\"\n\n\ndef json_get(path: str = \"/\") -> Response:\n    rest = http.client.HTTPConnection(\"localhost\", 8080, timeout=5)\n    rest.request(\"GET\", path)\n    response = rest.getresponse()\n    # print(f\"client: {response.status} {response.reason}\")\n    # print(f\"  {response.getheaders()}\")\n    raw = response.read().decode(\"utf-8\")\n    # print(f\"  {raw}\")\n    try:\n        document = json.loads(raw)\n    except json.decoder.JSONDecodeError as ex:\n        document = None\n    return Response(response.status, dict(response.getheaders()), document)\n\n\n# Roulette Demo\n# --------------\n\n# When run as the main script, start a server and interact with it.\n# Note that the subprocess will inherit the state of the wheel from the parent\n# process; the results are therefore based on the seed, and deterministic.\n\n\ndef server() -> None:\n    import concurrent.futures\n    import time\n\n    with concurrent.futures.ProcessPoolExecutor() as executor:\n        # We'll make four requests. This allows for a very clean termination of a test server.\n        srvr = executor.submit(roulette_server, 4)\n        time.sleep(0.1)  # Wait for the server to start\n        r1 = json_get()\n        r2 = json_get()\n        r3 = json_get(\"/european/\")\n        r4 = json_get(\"/european/\")\n        assert (\n            str(r1)\n            == \"200: {'22': [35, 1], 'Black': [1, 1], 'Hi': [1, 1], 'Even': [1, 1]}\"\n        )\n        assert (\n            str(r2)\n            == \"200: {'15': [35, 1], 'Black': [1, 1], 'Lo': [1, 1], 'Odd': [1, 1]}\"\n        )\n        assert (\n            str(r3)\n            == \"200: {'22': [35, 1], 'Black': [1, 1], 'Hi': [1, 1], 'Even': [1, 1]}\"\n        )\n        assert (\n            str(r4)\n            == \"200: {'15': [35, 1], 'Black': [1, 1], 'Lo': [1, 1], 'Odd': [1, 1]}\"\n        )\n\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=True)\n    server()\n"
  },
  {
    "path": "Chapter_13/ch13_ex2.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 13. Example 2.\n\"\"\"\n\n\n# Problem Domain\n# ==============\n\n\nfrom dataclasses import dataclass, asdict, astuple\nfrom typing import List, Dict, Any, Tuple, NamedTuple\nimport random\n\n\n@dataclass(frozen=True)\nclass Domino:\n    v_0: int\n    v_1: int\n\n    @property\n    def double(self):\n        return self.v_0 == self.v_1\n\n    def __repr__(self):\n        if self.double:\n            return f\"Double({self.v_0})\"\n        else:\n            return f\"Domino({self.v_0}, {self.v_1})\"\n\n\nclass Boneyard:\n    \"\"\"\n    >>> random.seed(2)\n    >>> b = Boneyard(limit=6)\n    >>> len(b._dominoes)\n    28\n    >>> b.deal(tiles=7, hands=2)\n    [[Domino(2, 0), Double(5), Domino(5, 2), Domino(5, 0), Double(0), Domino(6, 3), Domino(2, 1)], [Domino(3, 1), Double(4), Domino(5, 1), Domino(5, 4), Domino(6, 2), Domino(4, 2), Domino(5, 3)]]\n\n    \"\"\"\n\n    def __init__(self, limit=6):\n        self._dominoes = [\n            Domino(x, y) for x in range(0, limit + 1) for y in range(0, x + 1)\n        ]\n        random.shuffle(self._dominoes)\n\n    def deal(self, tiles: int = 7, hands: int = 4) -> List[List[Tuple[int, int]]]:\n        if tiles * hands > len(self._dominoes):\n            raise ValueError(f\"Invalid tiles={tiles}, hands={hands}\")\n        return [self._dominoes[h:h + tiles] for h in range(0, tiles * hands, tiles)]\n\n\n# FLASK Restful Web Service\n# =========================\n\nfrom typing import Dict, Any, Tuple\n\nfrom flask import Flask, jsonify, abort\nfrom http import HTTPStatus\n\n# Application Server\n\napp = Flask(__name__)\n\n\n@app.route(\"/dominoes/<n>\")\ndef dominoes(n: str) -> Tuple[Dict[str, Any], int]:\n    try:\n        hand_size = int(n)\n    except ValueError:\n        abort(HTTPStatus.BAD_REQUEST)\n\n    if app.env == \"development\":\n        random.seed(2)\n    b = Boneyard(limit=6)\n    hand_0 = b.deal(hand_size)[0]\n    app.logger.info(\"Send %r\", hand_0)\n\n    return jsonify(status=\"OK\", dominoes=[astuple(d) for d in hand_0]), HTTPStatus.OK\n\n\n@app.route(\"/hands/<int:h>/dominoes/<int:c>\")\ndef hands(h: int, c: int) -> Tuple[Dict[str, Any], int]:\n    if h == 0 or c == 0:\n        return jsonify(\n            status=\"Bad Request\", error=[f\"hands={h!r}, dominoes={c!r} is invalid\"]\n        ), HTTPStatus.BAD_REQUEST\n\n    if app.env == \"development\":\n        random.seed(2)\n    b = Boneyard(limit=6)\n    try:\n        hand_list = b.deal(c, h)\n    except ValueError as ex:\n        return jsonify(status=\"Bad Request\", error=ex.args), HTTPStatus.BAD_REQUEST\n    app.logger.info(\"Send %r\", hand_list)\n\n    return jsonify(\n        status=\"OK\", dominoes=[[astuple(d) for d in hand] for hand in hand_list]\n    ), HTTPStatus.OK\n\n\nOPENAPI_SPEC = {\n    \"openapi\": \"3.0.0\",\n    \"info\": {\n        \"description\": \"Deals simple hands of dominoes\",\n        \"version\": \"2019.02\",\n        \"title\": \"Chapter 13. Example 2\",\n    },\n    \"paths\": {},\n}\n\n\n@app.route(\"/openapi.json\")\ndef openapi() -> Dict[str, Any]:\n    \"\"\"\n    >>> client = app.test_client()\n    >>> response = client.get(\"/openapi.json\")\n    >>> response.get_json()['openapi']\n    '3.0.0'\n    >>> response.get_json()['info']['title']\n    'Chapter 13. Example 2'\n    \"\"\"\n    # See dominoes_openapi.json for full specification\n    return jsonify(OPENAPI_SPEC)\n\n\ntest_openapi_spec = \"\"\"\n    >>> random.seed(2)\n    >>> client = app.test_client()\n    >>> response = client.get(\"/openapi.json\")\n    >>> response.get_json()['openapi']\n    '3.0.0'\n    >>> response.get_json()['info']['title']\n    'Chapter 13. Example 2'\n    \n    >>> response = client.get(\"/dominoes/5\")\n    >>> response.status\n    '200 OK'\n    >>> response.status_code\n    200\n    >>> response.get_json()\n    {'dominoes': [[2, 0], [5, 5], [5, 2], [5, 0], [0, 0]], 'status': 'OK'}\n    \n    >>> document = response.get_json()\n    >>> hand = list(Domino(*d) for d in document['dominoes'])\n    >>> hand\n    [Domino(2, 0), Double(5), Domino(5, 2), Domino(5, 0), Double(0)]\n    \n    >>> response = client.get(\"hands/2/dominoes/7\")\n    >>> response.status\n    '200 OK'\n    >>> response.status_code\n    200\n    >>> document = response.get_json()\n    >>> document\n    {'dominoes': [[[5, 3], [1, 0], [4, 1], [3, 3], [2, 1], [2, 0], [3, 0]], [[5, 4], [4, 4], [6, 3], [6, 5], [5, 0], [6, 4], [3, 2]]], 'status': 'OK'}\n\n    >>> hands = list(list(Domino(*d) for d in h) for h in document['dominoes'])\n    >>> for player, h in enumerate(hands):\n    ...     for d in h:\n    ...         if d.double:\n    ...             print(player, d)\n    0 Double(3)\n    1 Double(4)\n    \n    >>> response = client.get(\"hands/nope/dominoes/7\")\n    >>> response.status\n    '404 NOT FOUND'\n    >>> response = client.get(\"hands/0/dominoes/nope\")\n    >>> response.status\n    '404 NOT FOUND'\n\n    >>> response = client.get(\"hands/0/dominoes/7\")\n    >>> response.status\n    '400 BAD REQUEST'\n    >>> response.get_json()\n    {'error': ['hands=0, dominoes=7 is invalid'], 'status': 'Bad Request'}\n    >>> response = client.get(\"hands/4/dominoes/0\")\n    >>> response.status\n    '400 BAD REQUEST'\n    >>> response.get_json()\n    {'error': ['hands=4, dominoes=0 is invalid'], 'status': 'Bad Request'}\n\n    >>> response = client.get(\"hands/7/dominoes/7\")\n    >>> response.status\n    '400 BAD REQUEST'\n    >>> response.get_json()\n    {'error': ['Invalid tiles=7, hands=7'], 'status': 'Bad Request'}\n\"\"\"\n\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_13/ch13_ex3.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 13. Example 3.\n\"\"\"\n\n\n# Problem Domain\n# ==============\n\nfrom typing import Dict, Any, Tuple, List\nfrom dataclasses import dataclass, asdict\nimport random\nimport secrets\nfrom enum import Enum\n\n\nclass Status(str, Enum):\n    UPDATED = \"Updated\"\n    CREATED = \"Created\"\n\n\n@dataclass\nclass Dice:\n    roll: List[int]\n    identifier: str\n    status: str\n\n    def reroll(self, keep_positions: List[int]) -> None:\n        for i in range(len(self.roll)):\n            if i not in keep_positions:\n                self.roll[i] = random.randint(1, 6)\n        self.status = Status.UPDATED\n\n\ndef make_dice(n_dice: int) -> Dice:\n    # Could also be a @classmethod\n    return Dice(\n        roll=[random.randint(1, 6) for _ in range(n_dice)],\n        identifier=secrets.token_urlsafe(8),\n        status=Status.CREATED,\n    )\n\n\n# FLASK Restful Web Service\n# =========================\n\n\nfrom flask import Flask, jsonify, request, url_for, Blueprint, current_app, abort\nfrom typing import Dict, Any, Tuple, List\nfrom http import HTTPStatus\n\nOPENAPI_SPEC = {\n    \"openapi\": \"3.0.0\",\n    \"info\": {\n        \"title\": \"Chapter 13. Example 3\",\n        \"version\": \"2019.02\",\n        \"description\": \"Rolls dice\",\n    },\n    \"paths\": {\n        \"/rolls\": {\n            \"post\": {\n                \"description\": \"first roll\",\n                \"responses\": {201: {\"description\": \"Success\"}},\n            },\n            \"get\": {\n                \"description\": \"current state\",\n                \"responses\": {200: {\"description\": \"Current state\"}},\n            },\n            \"patch\": {\n                \"description\": \"subsequent roll\",\n                \"responses\": {200: {\"description\": \"Modified\"}},\n            }\n        }\n    }\n}\n\n\nSESSIONS: Dict[str, Dice] = {}\n\n\nrolls = Blueprint(\"rolls\", __name__)\n\n\n@rolls.route(\"/openapi.json\")\ndef openapi() -> Dict[str, Any]:\n    # See dice_openapi.json for full specification\n    return jsonify(OPENAPI_SPEC)\n\n\n@rolls.route(\"/rolls\", methods=[\"POST\"])\ndef make_roll() -> Tuple[Dict[str, Any], HTTPStatus, Dict[str, str]]:\n    body = request.get_json(force=True)\n    if set(body.keys()) != {\"dice\"}:\n        raise BadRequest(f\"Extra fields in {body!r}\")\n    try:\n        n_dice = int(body[\"dice\"])\n    except ValueError as ex:\n        raise BadRequest(f\"Bad 'dice' value in {body!r}\")\n\n    dice = make_dice(n_dice)\n    SESSIONS[dice.identifier] = dice\n    current_app.logger.info(f\"Rolled roll={dice!r}\")\n\n    headers = {\"Location\": url_for(\"rolls.get_roll\", identifier=dice.identifier)}\n    return jsonify(asdict(dice)), HTTPStatus.CREATED, headers\n\n\n@rolls.route(\"/rolls/<identifier>\", methods=[\"GET\"])\ndef get_roll(identifier) -> Tuple[Dict[str, Any], HTTPStatus]:\n    if identifier not in SESSIONS:\n        abort(HTTPStatus.NOT_FOUND)\n\n    return jsonify(asdict(SESSIONS[identifier])), HTTPStatus.OK\n\n\n@rolls.route(\"/rolls/<identifier>\", methods=[\"PATCH\"])\ndef patch_roll(identifier) -> Tuple[Dict[str, Any], HTTPStatus]:\n    if identifier not in SESSIONS:\n        abort(HTTPStatus.NOT_FOUND)\n    body = request.get_json(force=True)\n    if set(body.keys()) != {\"keep\"}:\n        raise BadRequest(f\"Extra fields in {body!r}\")\n    try:\n        keep_positions = [int(d) for d in body[\"keep\"]]\n    except ValueError as ex:\n        raise BadRequest(f\"Bad 'keep' value in {body!r}\")\n\n    dice = SESSIONS[identifier]\n    dice.reroll(keep_positions)\n\n    return jsonify(asdict(dice)), HTTPStatus.OK\n\n\nclass BadRequest(Exception):\n    pass\n\n\ndef make_app() -> Flask:\n\n    app = Flask(__name__)\n    # Only used for HTML-based sessions...\n    # app.secret_key = 'lt0oypOUT9Vu7cbyivfv9hdEzWLlEf_w'\n\n    @app.errorhandler(BadRequest)\n    def error_message(ex) -> Tuple[Dict[str, Any], HTTPStatus]:\n        current_app.logger.error(f\"{ex.args}\")\n        return jsonify(status=\"Bad Request\", message=ex.args), HTTPStatus.BAD_REQUEST\n\n    app.register_blueprint(rolls)\n\n    return app\n\n\ntest_not_found = \"\"\"\n    >>> random.seed(2)\n    >>> app = make_app()\n    >>> client = app.test_client()\n    >>> response = client.get(\"/rando_path\")\n    >>> response.status\n    '404 NOT FOUND'\n    >>> response.status_code\n    404\n    >>> response.data[:55]\n    b'<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">'\n\"\"\"\n\ntest_post_get_patch = \"\"\"\n    >>> random.seed(2)\n    >>> app = make_app()\n    >>> client = app.test_client()\n    >>> response1 = client.post(\"/rolls\", json={\"dice\": 5})\n    >>> response1.status\n    '201 CREATED'\n    >>> response1.status_code\n    201\n    >>> document1 = response1.get_json()\n    >>> document1['roll']\n    [1, 1, 1, 3, 2]\n    >>> document1['status']\n    'Created'\n    >>> document1['identifier'] in SESSIONS\n    True\n    >>> response1.headers['Location'].endswith(document1['identifier'])\n    True\n    >>> response1.headers['Location'].startswith('http://localhost/rolls/')\n    True\n    \n    >>> response2 = client.get(f\"/rolls/{document1['identifier']}\")\n    >>> response2.status\n    '200 OK'\n    >>> document2 = response2.get_json()\n    >>> document2['roll']\n    [1, 1, 1, 3, 2]\n    >>> document2['status']\n    'Created'\n    >>> document2['identifier'] == document1['identifier']\n    True\n\n    >>> response3 = client.patch(f\"/rolls/{document1['identifier']}\", json={\"keep\": [0, 1, 2]})\n    >>> response3.status\n    '200 OK'\n    >>> document3 = response3.get_json()\n    >>> document3['roll']\n    [1, 1, 1, 6, 6]\n    >>> document3['status']\n    'Updated'\n    >>> document3['identifier'] == document1['identifier']\n    True\n\"\"\"\n\ntest_post_bad_get = \"\"\"\n    >>> random.seed(2)\n    >>> app = make_app()\n    >>> client = app.test_client()\n    >>> response1 = client.post(\"/rolls\", json={\"dice\": 5})\n    >>> response1.status\n    '201 CREATED'\n    >>> document1 = response1.get_json()\n    >>> document1['roll']\n    [1, 1, 1, 3, 2]\n    >>> document1['status']\n    'Created'\n\n    # Definitely NOT the identifier.\n    >>> response2 = client.get(f\"/rolls/xyzzy_{document1['identifier']}\")\n    >>> response2.status\n    '404 NOT FOUND'\n    >>> document2 = response2.data\n    >>> document2[:55]\n    b'<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">'\n\"\"\"\n\ntest_bad_post = \"\"\"\n    >>> random.seed(2)\n    >>> app = make_app()\n    >>> client = app.test_client()\n    >>> response1 = client.post(\"/rolls\", json={\"not_the_document\": \"you were looking for\"})\n    >>> response1.status\n    '400 BAD REQUEST'\n    >>> document1 = response1.get_json()\n    >>> document1['status']\n    'Bad Request'\n    >>> document1['message']\n    [\"Extra fields in {'not_the_document': 'you were looking for'}\"]\n\"\"\"\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_13/ch13_ex4.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 13. Example 4.\n\n..  note::\n\n    This example can't easily be tested by the automated test scripts.\n    It requires a server to be started.\n\"\"\"\nimport requests\n\n\ndef demo(headers=None):\n    headers = headers or {}\n\n    get_openapi = requests.get(\"http://127.0.0.1:5000/openapi.json\")\n    if get_openapi.status_code == 200:\n        document = get_openapi.json()\n        if not document['openapi'].startswith('3.0'):\n            raise Exception(\"Not a valid OpenAPI Version\")\n        if document['info']['version'] != '2019.02':\n            raise Exception(\"Not a useful release\")\n\n    roll = requests.post(\"http://127.0.0.1:5000/roll\", json={\"dice\": 5}, headers=headers)\n    print(roll.status_code, roll.reason, roll.headers)\n    body = roll.json()\n    print(body)\n\n    identifier = body[\"identifier\"]\n    roll_url = roll.headers[\"Location\"]\n\n    response = requests.get(f\"http://127.0.0.1:5000/roll/{identifier}\", headers=headers)\n    print(response.status_code, response.reason)\n    print(response.json())\n\n    response2 = requests.get(roll_url, headers=headers)\n    print(response2.status_code, response2.reason)\n    print(response2.json())\n\n    reroll = requests.patch(roll_url, json={\"keep\": [0, 1]}, headers=headers)\n    print(reroll.status_code, reroll.reason)\n    print(reroll.json())\n\n\nimport pytest\nfrom unittest.mock import Mock, call\n\n\n@pytest.fixture\ndef mock_requests(monkeypatch):\n    r0 = requests.Response()\n    r0.status_code = 200\n    r0._content = b'{\"openapi\": \"3.0.0\", \"info\": {\"version\": \"2019.02\"}}'\n    r1 = requests.Response()\n    r1.status_code = 200\n    r1._content = b'{\"valid\": \"json\"}'\n    r2 = requests.Response()\n    r2.status_code = 201\n    r2.headers = {\"Location\": \"http://mocked/roll/mockity-mock-mock\"}\n    r2._content = b'{\"status\": \"OK\", \"roll\": [1, 2, 3, 4, 5, 6], \"identifier\": \"mockity-mock-mock\"}'\n\n    mock_module = Mock(\n        get=Mock(side_effect=[r0, r1, r1]),\n        post=Mock(return_value=r2),\n        patch=Mock(return_value=r1),\n    )\n    monkeypatch.setitem(globals(), \"requests\", mock_module)\n    return mock_module\n\n\ndef test_demo(mock_requests):\n    demo()\n    assert (\n        mock_requests.mock_calls\n        == [\n            call.get(\"http://127.0.0.1:5000/openapi.json\"),\n            call.post(\"http://127.0.0.1:5000/roll\", json={\"dice\": 5}, headers={}),\n            call.get(\"http://127.0.0.1:5000/roll/mockity-mock-mock\", headers={}),\n            call.get(\"http://mocked/roll/mockity-mock-mock\", headers={}),\n            call.patch(\"http://mocked/roll/mockity-mock-mock\", json={\"keep\": [0, 1]}, headers={}),\n        ]\n    )\n\n\nif __name__ == \"__main__\":\n    pytest.main([__file__])\n    demo()\n    # demo({\"Api-Key\": \"some_key\"})\n    # demo({\"Api-Key\": \"nope\"})"
  },
  {
    "path": "Chapter_13/ch13_ex5.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 13. Example 5.\n\"\"\"\n\n\n# FLASK Restful Web Service with State & Open SSL Cert\n# ====================================================\n\n# Creating a certificate for common name will be 127.0.0.1 (since we're running locally)\n#\n# $ openssl req -newkey rsa:2048 -nodes -keyout key.pem -x509 -days 365 -out certificate.pem\n# Generating a RSA private key\n# ......................................+++++\n# ....................+++++\n# writing new private key to 'key.pem'\n# -----\n# You are about to be asked to enter information that will be incorporated\n# into your certificate request.\n# What you are about to enter is what is called a Distinguished Name or a DN.\n# There are quite a few fields but you can leave some blank\n# For some fields there will be a default value,\n# If you enter '.', the field will be left blank.\n# -----\n# Country Name (2 letter code) [AU]:US\n# State or Province Name (full name) [Some-State]:VA\n# Locality Name (eg, city) []:McLean\n# Organization Name (eg, company) [Internet Widgits Pty Ltd]:Mastering OO Python 2e\n# Organizational Unit Name (eg, section) []:\n# Common Name (e.g. server FQDN or YOUR name) []:127.0.0.1\n# Email Address []:\n\n# Using the certificate\n#\n# $ FLASK_APP=ch13_ex5.py FLASK_ENV=development python -m flask run --cert certificate.pem --key key.pem\n#\n# Your browser will have questions about the authority, since this is self-signed.\n\nfrom flask import Flask, jsonify, request, url_for, Blueprint, current_app, json, abort\nfrom http import HTTPStatus\nfrom typing import Dict, Any, Tuple, Callable, Set\nfrom pathlib import Path\nimport secrets\nimport random\n\nfrom functools import wraps\nfrom typing import Callable, Set\n\nVALID_API_KEYS: Set[str] = set()\n\n\ndef init_app(app):\n    global VALID_API_KEYS\n    if app.env == \"development\":\n        VALID_API_KEYS = {\"read-only\", \"admin\", \"write\"}\n    else:\n        app.logger.info(\"Loading from {app.config['VALID_KEYS']}\")\n        raw_lines = (Path(app.config[\"VALID_KEYS\"]).read_text().splitlines())\n        VALID_API_KEYS = set(filter(None, raw_lines))\n\n\ndef valid_api_key(view_function: Callable) -> Callable:\n\n    @wraps(view_function)\n    def confirming_view_function(*args, **kw):\n        api_key = request.headers.get(\"Api-Key\")\n        if api_key not in VALID_API_KEYS:\n            current_app.logger.error(f\"Rejecting Api-Key:{api_key!r}\")\n            abort(HTTPStatus.UNAUTHORIZED)\n        return view_function(*args, **kw)\n\n    return confirming_view_function\n\n\ndef api_key_in(valid_values: Set[str]):\n\n    def concrete_decorator(view_function: Callable) -> Callable:\n\n        @wraps(view_function)\n        def confirming_view_function(*args, **kw):\n            api_key = request.headers.get(\"Api-Key\")\n            if api_key not in valid_values:\n                current_app.logger.error(f\"Rejecting Api-Key:{api_key!r}\")\n                abort(HTTPStatus.UNAUTHORIZED)\n            return view_function(*args, **kw)\n\n        return confirming_view_function\n\n    return concrete_decorator\n\n\nSESSIONS: Dict[str, Any] = {}\n\nroll = Blueprint(\"roll\", __name__)\n\n\n@roll.route(\"/openapi.json\")\ndef openapi() -> Dict[str, Any]:\n    source_path = next(Path.cwd().glob(\"**/dice_openapi.json\"))\n    return jsonify(json.loads(source_path.read_text()))\n\n\n@roll.route(\"/roll\", methods=[\"POST\"])\n@valid_api_key\ndef create_roll() -> Tuple[Any, HTTPStatus, Dict[str, Any]]:\n    body = request.get_json(force=True)\n    if set(body.keys()) != {\"dice\"}:\n        raise BadRequest(f\"Extra fields in {body!r}\")\n    try:\n        n_dice = int(body[\"dice\"])\n    except ValueError as ex:\n        raise BadRequest(f\"Bad 'dice' value in {body!r}\")\n\n    roll = [random.randint(1, 6) for _ in range(n_dice)]\n    identifier = secrets.token_urlsafe(8)\n    SESSIONS[identifier] = roll\n    current_app.logger.info(f\"Rolled roll={roll!r}, id={identifier!r}\")\n\n    headers = {\"Location\": url_for(\"roll.get_roll\", identifier=identifier)}\n    return jsonify(\n        roll=roll, identifier=identifier, status=\"Created\"\n    ), HTTPStatus.CREATED, headers\n\n\n@roll.route(\"/roll/<identifier>\", methods=[\"GET\"])\n@valid_api_key\ndef get_roll(identifier) -> Tuple[Dict[str, Any], HTTPStatus]:\n    if identifier not in SESSIONS:\n        abort(HTTPStatus.NOT_FOUND)\n\n    return jsonify(\n        roll=SESSIONS[identifier], identifier=identifier, status=\"OK\"\n    ), HTTPStatus.OK\n\n\n@roll.route(\"/roll/<identifier>\", methods=[\"PATCH\"])\n@valid_api_key\ndef patch_roll(identifier) -> Tuple[Dict[str, Any], HTTPStatus]:\n    if identifier not in SESSIONS:\n        raise BadRequest(f\"Unknown {identifier!r}\")\n    body = request.get_json(force=True)\n    if set(body.keys()) != {\"keep\"}:\n        raise BadRequest(f\"Extra fields in {body!r}\")\n    try:\n        keep_positions = [int(d) for d in body[\"keep\"]]\n    except ValueError as ex:\n        raise BadRequest(f\"Bad 'keep' value in {body!r}\")\n\n    roll = SESSIONS[identifier]\n    for i in range(len(roll)):\n        if i not in keep_positions:\n            roll[i] = random.randint(1, 6)\n    SESSIONS[identifier] = roll\n\n    return jsonify(\n        roll=SESSIONS[identifier], identifier=identifier, status=\"OK\"\n    ), HTTPStatus.OK\n\n\nclass BadRequest(Exception):\n    pass\n\n\ndef make_app() -> Flask:\n    app = Flask(__name__)\n    app.config[\"VALID_KEYS\"] = \"valid_keys_file.txt\"\n    app.config[\"ENV\"] = \"development\"\n\n    @app.errorhandler(BadRequest)\n    def error_message(ex) -> Tuple[Dict[str, Any], HTTPStatus]:\n        current_app.logger.error(f\"{ex.args}\")\n        return jsonify(status=\"Bad Request\", message=ex.args), HTTPStatus.BAD_REQUEST\n\n    init_app(app)\n    app.register_blueprint(roll)\n\n    return app\n\n\ntest_get_openapi_spec = \"\"\"\n    >>> random.seed(2)\n    >>> app = make_app()\n    >>> client = app.test_client()\n    >>> response = client.get(\"/openapi.json\")\n    >>> response.status\n    '200 OK'\n    >>> response.status_code\n    200\n    >>> spec = response.get_json()\n    >>> spec['info']['version']\n    '2019.02'\n    >>> spec['info']['title']\n    'Chapter 13. Examples 3 and 5'\n\"\"\"\n\ntest_get_bad_post_roll = \"\"\"\n    >>> random.seed(2)\n    >>> app = make_app()\n    >>> client = app.test_client()\n    >>> response = client.post(\"/roll\")\n    >>> response.status\n    '401 UNAUTHORIZED'\n\"\"\"\n\ntest_get_good_post_roll = \"\"\"\n    >>> random.seed(2)\n    >>> app = make_app()\n    >>> client = app.test_client()\n    >>> response = client.post(\"/roll\", json={\"dice\": 5}, headers=[(\"Api-Key\", \"admin\")])\n    >>> response.status\n    '201 CREATED'\n    >>> document = response.get_json()\n    >>> document['roll']\n    [1, 1, 1, 3, 2]\n\"\"\"\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_13/ch13_ex6.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 13. Example 6.\n\"\"\"\n\n# Multiprocessing Example\n# =========================\n\n# We want a Simulation process to cough up some statistics\n\n# Import the simulation model...\nfrom Chapter_13.simulation_model import *\n\nimport multiprocessing\n\n\nclass Simulation(multiprocessing.Process):\n\n    def __init__(\n        self,\n        setup_queue: multiprocessing.SimpleQueue,\n        result_queue: multiprocessing.SimpleQueue,\n    ) -> None:\n        self.setup_queue = setup_queue\n        self.result_queue = result_queue\n        super().__init__()\n\n    def run(self) -> None:\n        \"\"\"Waits for a termination\"\"\"\n        print(f\"{self.__class__.__name__} start\")\n        item = self.setup_queue.get()\n        while item != (None, None):\n            table, player = item\n            self.sim = Simulate(table, player, samples=1)\n            results = list(self.sim)\n            self.result_queue.put((table, player, results[0]))\n            item = self.setup_queue.get()\n        print(f\"{self.__class__.__name__} finish\")\n\n\n# We want a Summarization process to gather and summarize all those stats.\nclass Summarize(multiprocessing.Process):\n\n    def __init__(self, queue: multiprocessing.SimpleQueue) -> None:\n        self.queue = queue\n        super().__init__()\n\n    def run(self) -> None:\n        \"\"\"Waits for a termination\"\"\"\n        print(f\"{self.__class__.__name__} start\")\n        count = 0\n        item = self.queue.get()\n        while item != (None, None, None):\n            print(item)\n            count += 1\n            item = self.queue.get()\n        print(f\"{self.__class__.__name__} finish {count}\")\n\n\n# Create and run the simulation\n# -----------------------------\n\n\ndef server_6() -> None:\n\n    # Two queues\n    setup_q: multiprocessing.SimpleQueue = multiprocessing.SimpleQueue()\n    results_q: multiprocessing.SimpleQueue = multiprocessing.SimpleQueue()\n\n    # The summarization process: waiting for work\n    result = Summarize(results_q)\n    result.start()\n\n    # The simulation process: also waiting for work.\n    # We might want to create a Pool of these so that\n    # we can get even more done at one time.\n    simulators = []\n    for i in range(4):\n        sim = Simulation(setup_q, results_q)\n        sim.start()\n        simulators.append(sim)\n\n    # Queue up some objects to work on.\n    table = Table(decks=6, limit=50, dealer=Hit17(), split=ReSplit(), payout=(3, 2))\n    for bet in Flat, Martingale, OneThreeTwoSix:\n        player = Player(SomeStrategy(), bet(), 100, 25)\n        for sample in range(5):\n            setup_q.put((table, player))\n\n    # Queue a terminator for each simulator.\n    for sim in simulators:\n        setup_q.put((None, None))\n\n    # Wait for the simulations to all finish.\n    for sim in simulators:\n        sim.join()\n\n    # Queue up a results terminator.\n    # Results processing done?\n    results_q.put((None, None, None))\n    result.join()\n    del results_q\n    del setup_q\n\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n    server_6()\n"
  },
  {
    "path": "Chapter_13/dice_openapi.json",
    "content": "{\n  \"openapi\": \"3.0.0\",\n  \"info\": {\n    \"description\": \"Rolls dice\",\n    \"version\": \"2019.02\",\n    \"title\": \"Chapter 13. Examples 3 and 5\"\n  },\n  \"components\": {\n    \"schemas\": {\n      \"roll\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"status\": {\n            \"type\": \"string\"\n          },\n          \"roll\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"integer\"\n            }\n          },\n          \"identifier\": {\n            \"type\": \"string\"\n          }\n        }\n      }\n    }\n  },\n  \"paths\": {\n    \"/roll\": {\n      \"post\": {\n        \"summary\": \"Creates a roll\",\n        \"responses\": {\n          \"201\": {\n            \"description\": \"The new roll\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/roll\"\n                }\n              }\n            },\n            \"headers\": {\n              \"Location\": {\n                \"description\": \"URL to use\",\n                \"schema\": {\n                  \"type\": \"string\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/roll/{id}\": {\n      \"get\": {\n        \"summary\": \"Repeats a roll's details\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"id\",\n            \"description\": \"identifier\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"An existing roll\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/roll\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Invalid input\"\n          },\n          \"404\": {\n            \"description\": \"Unknown\"\n          }\n        }\n      },\n      \"patch\": {\n        \"summary\": \"Modifies a roll\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"id\",\n            \"description\": \"identifier\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"The revised roll\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/roll\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Invalid input\"\n          },\n          \"404\": {\n            \"description\": \"Unknown\"\n          }\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "Chapter_13/dominoes_openapi.json",
    "content": "{\n  \"openapi\": \"3.0.0\",\n  \"info\": {\n    \"description\": \"Deals simple hands of dominoes\",\n    \"version\": \"2019.02\",\n    \"title\": \"Chapter 13. Example 2\"\n  },\n  \"components\": {\n    \"schemas\": {\n      \"dominoes\": {\n        \"type\": \"array\",\n        \"items\": {\n          \"type\": \"array\",\n          \"minLength\": 2,\n          \"maxLength\": 2,\n          \"items\": {\n            \"type\": \"number\"\n          }\n        }\n      }\n    }\n  },\n  \"paths\": {\n    \"/cards/{n}\": {\n      \"get\": {\n        \"summary\": \"Get a hand of dominoes\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"n\",\n            \"description\": \"Number of dominoes\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"number\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Hand of dominoes\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"status\": {\n                      \"type\": \"string\"\n                    },\n                    \"cards\": {\n                      \"$ref\": \"#/components/schemas/dominoes\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Invalid input\"\n          }\n        }\n      }\n    },\n    \"/hands/{h}/cards/{c}\": {\n      \"get\": {\n        \"summary\": \"Get several hands of dominoes\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"h\",\n            \"description\": \"Number of Hands\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"number\"\n            }\n          },\n          {\n            \"in\": \"path\",\n            \"name\": \"c\",\n            \"description\": \"Number of dominoes\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"number\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"List of hands of dominoes\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"status\": {\n                      \"type\": \"string\"\n                    },\n                    \"hands\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"$ref\": \"#/components/schemas/dominoes\"\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Invalid input\"\n          }\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "Chapter_13/simulation_model.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 13. Example 5 -- simulation model.\n\"\"\"\n\nfrom typing import Tuple, Iterator\n\n# Mock Object Model\n# =====================\n\n# A set of class hierarchies that we'll use for several examples.\n# The content is mostly mocks.\nclass DealerRule:\n    pass\n\n\nclass Hit17(DealerRule):\n    \"\"\"Hits soft 17\"\"\"\n    pass\n\n\nclass Stand17(DealerRule):\n    \"\"\"Stands on soft 17\"\"\"\n    pass\n\n\nclass SplitRule:\n    pass\n\n\nclass ReSplit(SplitRule):\n    \"\"\"Simplistic resplit anything.\"\"\"\n    pass\n\n\nclass NoReSplit(SplitRule):\n    \"\"\"Simplistic no resplit.\"\"\"\n    pass\n\n\nclass NoReSplitAces(SplitRule):\n    \"\"\"One card only to aces; no resplit.\"\"\"\n    pass\n\n\nclass Table:\n\n    def __init__(self, decks: int, limit: int, dealer: DealerRule, split: SplitRule, payout: Tuple[int, int]) -> None:\n        self.decks = decks\n        self.limit = limit\n        self.dealer = dealer\n        self.split = split\n        self.payout = payout\n\n    def as_tuple(self):\n        return (\n            self.decks,\n            self.limit,\n            self.dealer.__class__.__name__,\n            self.split.__class__.__name__,\n            self.payout,\n        )\n\n\nclass PlayerStrategy:\n    pass\n\n\nclass SomeStrategy(PlayerStrategy):\n    pass\n\n\nclass AnotherStrategy(PlayerStrategy):\n    pass\n\n\nclass BettingStrategy:\n\n    def bet(self) -> int:\n        raise NotImplementedError(\"No bet method\")\n\n    def record_win(self) -> None:\n        pass\n\n    def record_loss(self) -> None:\n        pass\n\n\nclass Flat(BettingStrategy):\n    pass\n\n\nclass Martingale(BettingStrategy):\n    pass\n\n\nclass OneThreeTwoSix(BettingStrategy):\n    pass\n\n\nclass Player:\n\n    def __init__(self, play: PlayerStrategy, betting: BettingStrategy, rounds: int, stake: int) -> None:\n        self.play = play\n        self.betting = betting\n        self.max_rounds = rounds\n        self.init_stake = float(stake)\n\n    def reset(self) -> None:\n        self.rounds = self.max_rounds\n        self.stake = self.init_stake\n\n    def as_tuple(self) -> Tuple:\n        return (\n            self.play.__class__.__name__,\n            self.betting.__class__.__name__,\n            self.max_rounds,\n            self.init_stake,\n            self.rounds,\n            self.stake,\n        )\n\n\n# A mock simulation which is built from the above mock objects.\nimport random\n\n\nclass Simulate:\n\n    def __init__(\n            self,\n            table: Table,\n            player: Player,\n            samples: int\n    ) -> None:\n        \"\"\"Define table, player and number of samples.\"\"\"\n        self.table = table\n        self.player = player\n        self.samples = samples\n\n    def __iter__(self) -> Iterator[Tuple]:\n        \"\"\"Yield statistical samples.\"\"\"\n        x, y = self.table.payout\n        blackjack_payout = x / y\n        for count in range(self.samples):\n            self.player.reset()\n            while self.player.stake > 0 and self.player.rounds > 0:\n                self.player.rounds -= 1\n\n                outcome = random.random()\n                if outcome < 0.579:\n                    self.player.stake -= 1\n                elif 0.579 <= outcome < 0.883:\n                    self.player.stake += 1\n                elif 0.883 <= outcome < 0.943:\n                    # a \"push\"\n                    pass\n                else:\n                    # 0.943 <= outcome\n                    self.player.stake += blackjack_payout\n\n            yield self.table.as_tuple() + self.player.as_tuple()\n"
  },
  {
    "path": "Chapter_14/__init__.py",
    "content": ""
  },
  {
    "path": "Chapter_14/ch14_ex1.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 14. Example 1.\n\"\"\"\n\nfrom Chapter_14.simulation_model import *\n\n# A typical main program using the above class definitions\n\nfrom pathlib import Path\nfrom typing import List, Any, TextIO, Iterator, Union\nimport csv\n\n\ndef simulate_blackjack() -> None:\n    dealer_rule = Hit17()\n    split_rule = NoReSplitAces()\n    table = Table(\n        decks=6, limit=50, dealer=dealer_rule,\n        split=split_rule, payout=(3, 2)\n    )\n    player_rule = SomeStrategy()\n    betting_rule = Flat()\n    player = Player(\n        play=player_rule, betting=betting_rule, max_rounds=100, init_stake=50\n    )\n\n    simulator = Simulate(\n        table, player, samples=100\n    )\n    result_path = Path.cwd() / \"data\" / \"ch14_simulation.dat\"\n    with result_path.open(\"w\", newline=\"\") as results:\n        wtr = csv.writer(results)\n        wtr.writerows(simulator)\n\n\nif __name__ == \"__main__\":\n    simulate_blackjack()\n    check(Path.cwd() / \"data\" / \"ch14_simulation.dat\")\n\n# Locations\n# ============\n\n# Tyical list of locations for config\n\ndef location_list(config_name: str = \"someapp.config\") -> List[Path]:\n    config_locations = (\n        Path(__file__),\n        # Path(\"~someapp\").expanduser(), if a special username\n        Path(\"/opt\") / \"someapp\",\n        Path(\"/etc\") / \"someapp\",\n        Path.home(),\n        Path.cwd(),\n    )\n    candidates = (dir / config_name for dir in config_locations)\n    config_paths = [path for path in candidates if path.exists()]\n    return config_paths\n\ntest_location_list = \"\"\"\n>>> import os\n>>> previous = os.getcwd()\n>>> os.chdir(\"Chapter_14\")\n>>> location_list()  # doctest: +ELLIPSIS\n[PosixPath('.../Chapter_14/someapp.config')]\n>>> os.chdir(previous)\n\"\"\"\n\n# INI files\n# =========\n\n# Sample INI files\nimport io\n\nini_file = io.StringIO(\n    \"\"\"\n; Default casino rules\n[table]\n    dealer= Hit17\n    split= NoResplitAces\n    decks= 6\n    limit= 50\n    payout= (3,2)\n\n; Player with SomeStrategy\n[player]\n    play= SomeStrategy\n    betting= Flat\n    max_rounds= 100\n    init_stake= 50\n\n[simulator]\n    samples= 100\n    outputfile= data/ch14_simulation1.dat\n\"\"\"\n)\n\nini2_file = io.StringIO(\n    \"\"\"\n; Need to compare with OtherStrategy\n[player]\n    play= OtherStrategy\n    betting= Flat\n    max_rounds= 100\n    init_stake= 50\n\n[simulator]\n    samples= 100\n    outputfile= data/ch14_simulation1a.dat\n\"\"\"\n)\n\nimport configparser\n\n# Using the config to build objects\ndef main_ini(config: configparser.ConfigParser) -> None:\n    dealer_nm = config.get(\"table\", \"dealer\", fallback=\"Hit17\")\n    dealer_rule = {\n        \"Hit17\": Hit17(),\n        \"Stand17\": Stand17(),\n    }.get(dealer_nm, Hit17())\n    split_nm = config.get(\"table\", \"split\", fallback=\"ReSplit\")\n    split_rule = {\n        \"ReSplit\": ReSplit(),\n        \"NoReSplit\": NoReSplit(),\n        \"NoReSplitAces\": NoReSplitAces(),\n    }.get(split_nm, ReSplit())\n    decks = config.getint(\"table\", \"decks\", fallback=6)\n    limit = config.getint(\"table\", \"limit\", fallback=100)\n    payout = eval(\n        config.get(\"table\", \"payout\", fallback=\"(3,2)\")\n    )\n    table = Table(\n        decks=decks, limit=limit, dealer=dealer_rule,\n        split=split_rule, payout=payout\n    )\n\n    player_nm = config.get(\n        \"player\", \"play\", fallback=\"SomeStrategy\")\n    player_rule = {\n        \"SomeStrategy\": SomeStrategy(),\n        \"AnotherStrategy\": AnotherStrategy()\n    }.get(player_nm, SomeStrategy())\n    bet_nm = config.get(\"player\", \"betting\", fallback=\"Flat\")\n    betting_rule = {\n        \"Flat\": Flat(),\n        \"Martingale\": Martingale(),\n        \"OneThreeTwoSix\": OneThreeTwoSix()\n    }.get(bet_nm, Flat())\n    max_rounds = config.getint(\"player\", \"max_rounds\", fallback=100)\n    init_stake = config.getint(\"player\", \"init_stake\", fallback=50)\n    player = Player(\n        play=player_rule, betting=betting_rule,\n        max_rounds=max_rounds, init_stake=init_stake\n    )\n\n    outputfile = config.get(\n        \"simulator\", \"outputfile\", fallback=\"blackjack.csv\")\n    samples = config.getint(\"simulator\", \"samples\", fallback=100)\n    simulator = Simulate(table, player, samples=samples)\n    with Path(outputfile).open(\"w\", newline=\"\") as results:\n        wtr = csv.writer(results)\n        wtr.writerows(simulator)\n\n\n# Sample Main Script to parse and start the application.\nif __name__ == \"__main__\":\n    config = configparser.ConfigParser()\n    config.read_file(ini_file)\n    config.read_file(ini2_file)\n\n    # Could use config.read_string(text), also\n    # When there are multiple candidate locations, config.read(location_list(\"blackjack.ini\"))\n\n    for name, section in config.items():\n        print(name)\n        for p in config.items(name):\n            print(\" \", p)\n\n    main_ini(config)\n    check(Path(config.get(\"simulator\", \"outputfile\")))\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_14/ch14_ex2.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 14. Example 2.\n\"\"\"\n\nfrom Chapter_14.simulation_model import *\n\n# A typical main program using the above class definitions\n\nfrom pathlib import Path\nfrom typing import List, Any, TextIO, Iterator, Union, Type\nimport csv\nfrom dataclasses import dataclass\nfrom types import SimpleNamespace\n\n# PY files\n# ========\n\n# Top-level -- v1\n# ################\n\n\ndef simulate(table: Table, player: Player, outputpath: Path, samples: int) -> None:\n    simulator = Simulate(table, player, samples=samples)\n    with outputpath.open(\"w\", newline=\"\") as results:\n        wtr = csv.writer(results)\n        wtr.writerows(simulator)\n\n\n# Configuration in the main script\n#\n# ``from simulator import *``\n#\ndef simulate_SomeStrategy_Flat() -> None:\n    dealer_rule = Hit17()\n    split_rule = NoReSplitAces()\n    table = Table(\n        decks=6, limit=50, dealer=dealer_rule, split=split_rule, payout=(3, 2)\n    )\n    player_rule = SomeStrategy()\n    betting_rule = Flat()\n    player = Player(\n        play=player_rule, betting=betting_rule, max_rounds=100, init_stake=50\n    )\n    simulate(table, player, Path.cwd() / \"data\" / \"ch14_simulation2a.dat\", 100)\n\n\nif __name__ == \"__main__\":\n    simulate_SomeStrategy_Flat()\n    check(Path.cwd() / \"data\" / \"ch14_simulation2a.dat\")\n\n# Top-level -- v2b\n# ################\n\n# Stuff imported from some application module\n#\n# ``from simulator import *``\n#\nclass AppConfig:\n    \"\"\"\n    These really are class-level (\"static\") values.\n    This is *not* a dataclass-style definition of instance variables.\n    \"\"\"\n    table: Table\n    player: Player\n    samples: int\n    outputfile: Path\n\n\ndef simulate_c(config: Union[Type[AppConfig], SimpleNamespace]) -> None:\n    simulator = Simulate(config.table, config.player, config.samples)\n    with Path(config.outputfile).open(\"w\", newline=\"\") as results:\n        wtr = csv.writer(results)\n        wtr.writerows(simulator)\n\n\n# Configuration in the main script using a Python class definition\nclass Example2(AppConfig):\n    dealer_rule = Hit17()\n    split_rule = NoReSplitAces()\n    table = Table(\n        decks=6, limit=50, dealer=dealer_rule, split=split_rule, payout=(3, 2)\n    )\n    player_rule = SomeStrategy()\n    betting_rule = Flat()\n    player = Player(\n        play=player_rule, betting=betting_rule, max_rounds=100, init_stake=50\n    )\n    outputfile = Path.cwd() / \"data\" / \"ch14_simulation2b.dat\"\n    samples = 100\n\n\nif __name__ == \"__main__\":\n    simulate_c(Example2)\n    check(Path.cwd() / \"data\" / \"ch14_simulation2b.dat\")\n\n# Top-level -- v2c\n# ################\n\n# SimpleNamespace version c\nfrom types import SimpleNamespace\n\nconfig2c = SimpleNamespace(\n    dealer_rule=Hit17(),\n    split_rule=NoReSplitAces(),\n    player_rule=SomeStrategy(),\n    betting_rule=Flat(),\n    outputfile=Path.cwd() / \"data\" / \"ch14_simulation2c.dat\",\n    samples=100,\n)\n\nconfig2c.table = Table(\n    decks=6,\n    limit=50,\n    dealer=config2c.dealer_rule,\n    split=config2c.split_rule,\n    payout=(3, 2),\n)\nconfig2c.player = Player(\n    play=config2c.player_rule,\n    betting=config2c.betting_rule,\n    max_rounds=100,\n    init_stake=50,\n)\n\nif __name__ == \"__main__\":\n    simulate_c(config2c)\n    check(Path.cwd() / \"data\" / \"ch14_simulation2c.dat\")\n\n# SimpleNamespace version 2d\n# ##########################\n\nconfig2d = SimpleNamespace()\nconfig2d.dealer_rule = Hit17()\nconfig2d.split_rule = NoReSplitAces()\nconfig2d.table = Table(\n    decks=6,\n    limit=50,\n    dealer=config2d.dealer_rule,\n    split=config2d.split_rule,\n    payout=(3, 2),\n)\nconfig2d.player_rule = SomeStrategy()\nconfig2d.betting_rule = Flat()\nconfig2d.player = Player(\n    play=config2d.player_rule,\n    betting=config2d.betting_rule,\n    max_rounds=100,\n    init_stake=50,\n)\nconfig2d.outputfile = Path.cwd() / \"data\" / \"ch14_simulation2d.dat\"\nconfig2d.samples = 100\n\nif __name__ == \"__main__\":\n    simulate_c(config2d)\n    check(Path.cwd() / \"data\" / \"ch14_simulation2d.dat\")\n\n# SimpleNamespace version 2e\n# ##########################\n\n\ndef make_config(\n    dealer_rule: DealerRule = Hit17(),\n    split_rule: SplitRule = NoReSplitAces(),\n    decks: int = 6,\n    limit: int = 50,\n    payout: Tuple[int, int] = (3, 2),\n    player_rule: PlayerStrategy = SomeStrategy(),\n    betting_rule: BettingStrategy = Flat(),\n    base_name: str = \"ch14_simulation2e.dat\",\n    samples: int = 100,\n) -> SimpleNamespace:\n    return SimpleNamespace(\n        dealer_rule=dealer_rule,\n        split_rule=split_rule,\n        table=Table(\n            decks=decks,\n            limit=limit,\n            dealer=dealer_rule,\n            split=split_rule,\n            payout=payout,\n        ),\n        payer_rule=player_rule,\n        betting_rule=betting_rule,\n        player=Player(\n            play=player_rule, betting=betting_rule, max_rounds=100, init_stake=50\n        ),\n        outputfile=Path.cwd() / \"data\" / base_name,\n        samples=samples,\n    )\n\n\nif __name__ == \"__main__\":\n    simulate_c(make_config(base_name=\"ch14_simulation2e_1.dat\"))\n    check(Path.cwd() / \"data\" / \"ch14_simulation2e_1.dat\")\n    simulate_c(make_config(dealer_rule=Stand17(), base_name=\"ch14_simulation2e_2.dat\"))\n    check(Path.cwd() / \"data\" / \"ch14_simulation2e_2.dat\")\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_14/ch14_ex3.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 14. Example 3.\n\"\"\"\n\nfrom Chapter_14.simulation_model import *\nfrom pprint import pprint\n\n# A typical main program using the above class definitions\n\nfrom pathlib import Path\nfrom typing import List, Any, TextIO, Iterator, Union, Dict\nimport typing\nimport csv\nfrom types import SimpleNamespace\n\ndef simulate(table: Table, player: Player, outputpath: Path, samples: int) -> None:\n    simulator = Simulate(table, player, samples=samples)\n    with outputpath.open(\"w\", newline=\"\") as results:\n        wtr = csv.writer(results)\n        for gamestats in simulator:\n            wtr.writerow(gamestats)\n\n# Exec Import\n# ################\n\nimport io\n\npy_file = io.StringIO(\n\"\"\"\n# SomeStrategy setup\n\n# Table\ndealer_rule = Hit17()\nsplit_rule = NoReSplitAces()\ntable = Table(decks=6, limit=50, dealer=dealer_rule,\n        split=split_rule, payout=(3,2))\n\n# Player\nplayer_rule = SomeStrategy()\nbetting_rule = Flat()\nplayer = Player(play=player_rule, betting=betting_rule,\n        max_rounds=100, init_stake=50)\n\n# Simulation\noutputfile = Path.cwd()/\"data\"/\"ch14_simulation3a.dat\"\nsamples = 100\n\"\"\"\n)\n\nif __name__ == \"__main__\":\n    code = compile(py_file.read(), \"stringio\", \"exec\")\n    assignments: Dict[str, Any] = dict()\n    exec(code, globals(), assignments)\n    config = SimpleNamespace(**assignments)\n\n    print(\"Exec Import...\")\n    pprint(assignments)\n    print(\"Table...\")\n    print(config.table)\n\n    simulate(config.table, config.player, config.outputfile, config.samples)\n    check(config.outputfile)\n\n# ChainMap and Import\n# =====================\n\n# Essential Example\nfrom collections import ChainMap\n\nconfig_name = \"config.py\"\nconfig_locations = (\n    Path.cwd(),\n    Path.home(),\n    Path(\"/etc/thisapp\"),\n    # Optionally Path(\"~thisapp\").expanduser(), when an app has a \"home\" directory\n    Path(__file__),\n)\n\ncandidates = (dir / config_name for dir in config_locations)\nconfig_paths = (path for path in candidates if path.exists())\ncm_config_1: typing.ChainMap[str, Any] = ChainMap()\nfor path in config_paths:\n    config_layer_1: Dict[str, Any] = {}\n    source_code = path.read_text()\n    exec(source_code, globals(), config_layer_1)\n    cm_config_1.maps.append(config_layer_1)\n\n# Demo with Mock files\nimport io\n\npy_text = \"\"\"\n# Default casino rules\n# Table\ndealer_rule = Hit17()\nsplit_rule = NoReSplitAces()\ntable = Table(decks=6, limit=50, dealer=dealer_rule,\n        split=split_rule, payout=(3,2))\n\n# Player\nplayer_rule = SomeStrategy()\nbetting_rule = Flat()\nplayer = Player(play=player_rule, betting=betting_rule,\n        max_rounds=100, init_stake=50)\n\n# Simulation\noutputfile = Path.cwd()/\"data\"/\"ch14_simulation3b.dat\"\nsamples = 100\n\"\"\"\n\npy2_text = \"\"\"\n# Override values\n# Player\nplayer_rule = AnotherStrategy()\nbetting_rule = Martingale()\nplayer = Player(play=player_rule, betting=betting_rule,\n        max_rounds=100, init_stake=50)\n\n# Simulation\noutputfile = Path.cwd()/\"data\"/\"ch14_simulation3b.dat\"\n\"\"\"\n\ntest_cm_config = \"\"\"\n>>> default_file = io.StringIO(py_text)\n>>> override_file = io.StringIO(py2_text)\n>>> cm_config: typing.ChainMap[str, Any] = ChainMap()\n>>> for path in override_file, default_file:\n...     config_layer: Dict[str, Any] = {}\n...     source_code = path.read()\n...     exec(source_code, globals(), config_layer)\n...     cm_config.maps.append(config_layer)\n>>> cm_config['player_rule']  # doctest: +ELLIPSIS\nAnotherStrategy()\n>>> cm_config['betting_rule']  # doctest: +ELLIPSIS\nMartingale()\n>>> cm_config['betting_rule'] = \"final override\"\n>>> pprint(cm_config)  # doctest: +ELLIPSIS\nChainMap({'betting_rule': 'final override'},\n         {'betting_rule': Martingale(),\n          'outputfile': PosixPath('.../data/ch14_simulation3b.dat'),\n          'player': Player(play=AnotherStrategy(), betting=Martingale(), max_rounds=100, init_stake=50, rounds=100, stake=50),\n          'player_rule': AnotherStrategy()},\n         {'betting_rule': Flat(),\n          'dealer_rule': Hit17(),\n          'outputfile': PosixPath('.../data/ch14_simulation3b.dat'),\n          'player': Player(play=SomeStrategy(), betting=Flat(), max_rounds=100, init_stake=50, rounds=100, stake=50),\n          'player_rule': SomeStrategy(),\n          'samples': 100,\n          'split_rule': NoReSplitAces(),\n          'table': Table(decks=6, limit=50, dealer=Hit17(), split=NoReSplitAces(), payout=(3, 2))})\n\"\"\"\n\nif __name__ == \"__main__\":\n    default_file = io.StringIO(py_text)\n    override_file = io.StringIO(py2_text)\n    cm_config: typing.ChainMap[str, Any] = ChainMap()\n    for config_file in override_file, default_file:\n        config_layer: Dict[str, Any] = {}\n        source_code = config_file.read()\n        exec(source_code, globals(), config_layer)\n        cm_config.maps.append(config_layer)\n\n    print()\n    print(\"ChainMap\")\n    pprint(cm_config)\n\n\nclass AttrChainMap(ChainMap):\n\n    def __getattr__(self, name: str) -> Any:\n        if name == \"maps\":\n            return self.__dict__[\"maps\"]\n        return super().get(name, None)\n\n    def __setattr__(self, name: str, value: Any) -> None:\n        if name == \"maps\":\n            self.__dict__[\"maps\"] = value\n            return\n        self[name] = value\n\ntest_acm_config = \"\"\"\n>>> default_file = io.StringIO(py_text)\n>>> override_file = io.StringIO(py2_text)\n>>> acm_config = AttrChainMap()\n>>> for path in override_file, default_file:\n...     config_layer: Dict[str, Any] = {}\n...     source_code = path.read()\n...     exec(source_code, globals(), config_layer)\n...     acm_config.maps.append(config_layer)\n>>> acm_config['player_rule']  # doctest: +ELLIPSIS\nAnotherStrategy()\n>>> acm_config.player_rule  # doctest: +ELLIPSIS\nAnotherStrategy()\n>>> acm_config.betting_rule  # doctest: +ELLIPSIS\nMartingale()\n\"\"\"\n\nif __name__ == \"__main__\":\n\n    default_file = io.StringIO(py_text)\n    override_file = io.StringIO(py2_text)\n    config_acm = AttrChainMap()\n    for file in override_file, default_file:\n        config_layer = {}\n        config_source = file.read()\n        exec(config_source, globals(), config_layer)\n        config_acm.maps.append(config_layer)\n\n    print()\n    print(\"AttrChainMap\")\n    pprint(config_acm)\n    print(config_acm.table)\n    print(config_acm[\"table\"])\n\n    simulate(config_acm.table, config_acm.player, config_acm.outputfile, config_acm.samples)\n    check(config_acm.outputfile)\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_14/ch14_ex4.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 14. Example 4.\n\"\"\"\n\nfrom Chapter_14.simulation_model import *\n\n# A typical main program using the above class definitions\n\nfrom pathlib import Path\nfrom typing import List, Any, TextIO, Iterator, Union, Dict\nimport csv\nfrom collections import ChainMap\n\n\nimport io\n\n# JSON or YAML files\n# ===================\n\n# JSON using dictionary-of-dictionaries nested structures.\n# This is inconvenient to handle multiple configuration files.\nimport io\n\njson_file = io.StringIO(\n    \"\"\"\n{\n    \"table\":{\n        \"dealer\":\"Hit17\",\n        \"split\":\"NoResplitAces\",\n        \"decks\":6,\n        \"limit\":50,\n        \"payout\":[3,2]\n    },\n    \"player\":{\n        \"play\":\"SomeStrategy\",\n        \"betting\":\"Flat\",\n        \"rounds\":100,\n        \"stake\":50\n    },\n    \"simulator\":{\n        \"samples\":100,\n        \"outputfile\":\"data/ch14_simulation4a.dat\"\n    }\n}\n\"\"\"\n)\n\n\ndef main_nested_dict(config: Dict[str, Any]) -> None:\n    dealer_nm = config.get(\"table\", {}).get(\"dealer\", \"Hit17\")\n    dealer_rule = {\n        \"Hit17\": Hit17(),\n        \"Stand17\": Stand17()\n    }.get(dealer_nm, Hit17())\n    split_nm = config.get(\"table\", {}).get(\"split\", \"ReSplit\")\n    split_rule = {\n        \"ReSplit\": ReSplit(),\n        \"NoReSplit\": NoReSplit(),\n        \"NoReSplitAces\": NoReSplitAces()\n    }.get(split_nm, ReSplit())\n    decks = config.get(\"table\", {}).get(\"decks\", 6)\n    limit = config.get(\"table\", {}).get(\"limit\", 100)\n    payout = config.get(\"table\", {}).get(\"payout\", (3, 2))\n    table = Table(\n        decks=decks, limit=limit, dealer=dealer_rule, split=split_rule, payout=payout\n    )\n\n    player_nm = config.get(\"player\", {}).get(\"play\", \"SomeStrategy\")\n    player_rule = {\n        \"SomeStrategy\": SomeStrategy(), \"AnotherStrategy\": AnotherStrategy()\n    }.get(\n        player_nm, SomeStrategy()\n    )\n    bet_nm = config.get(\"player\", {}).get(\"betting\", \"Flat\")\n    betting_rule = {\n        \"Flat\": Flat(), \"Martingale\": Martingale(), \"OneThreeTwoSix\": OneThreeTwoSix()\n    }.get(\n        bet_nm, Flat()\n    )\n    rounds = config.get(\"player\", {}).get(\"rounds\", 100)\n    stake = config.get(\"player\", {}).get(\"stake\", 50)\n    player = Player(play=player_rule, betting=betting_rule, max_rounds=rounds, init_stake=stake)\n\n    outputfile = config.get(\"simulator\", {}).get(\"outputfile\", \"blackjack.csv\")\n    samples = config.get(\"simulator\", {}).get(\"samples\", 100)\n    simulator = Simulate(table, player, samples)\n    with Path(outputfile).open(\"w\", newline=\"\") as results:\n        wtr = csv.writer(results)\n        for gamestats in simulator:\n            wtr.writerow(gamestats)\n\n\nif __name__ == \"__main__\":\n    import json\n\n    config = json.load(json_file)\n    main_nested_dict(config)\n    check(Path(config[\"simulator\"][\"outputfile\"]))\n\n# Flat Version, allows multiple configuration files.\njson2_file = io.StringIO(\n    \"\"\"\n{\n\"player.betting\": \"Flat\",\n\"player.play\": \"SomeStrategy\",\n\"player.rounds\": 100,\n\"player.stake\": 50,\n\"table.dealer\": \"Hit17\",\n\"table.decks\": 6,\n\"table.limit\": 50,\n\"table.payout\": [3, 2],\n\"table.split\": \"NoResplitAces\",\n\"simulator.outputfile\": \"data/ch14_simulation4b.dat\",\n\"simulator.samples\": 100\n}\n\"\"\"\n)\n\njson3_file = io.StringIO(\n    \"\"\"\n{\n\"player.betting\": \"Flat\",\n\"simulator.outputfile\": \"data/ch14_simulation4b.dat\"\n}\n\"\"\"\n)\n\ndef simulate(table: Table, player: Player, outputpath: Path, samples: int) -> None:\n    simulator = Simulate(table, player, samples=samples)\n    with outputpath.open(\"w\", newline=\"\") as results:\n        wtr = csv.writer(results)\n        for gamestats in simulator:\n            wtr.writerow(gamestats)\n\n# Using the config to build objects\ndef main_cm(config: Dict[str, Any]) -> None:\n    dealer_nm = config.get(\"table.dealer\", \"Hit17\")\n    dealer_rule = {\"Hit17\": Hit17(), \"Stand17\": Stand17()}.get(dealer_nm, Hit17())\n    split_nm = config.get(\"table.split\", \"ReSplit\")\n    split_rule = {\n        \"ReSplit\": ReSplit(), \"NoReSplit\": NoReSplit(), \"NoReSplitAces\": NoReSplitAces()\n    }.get(\n        split_nm, ReSplit()\n    )\n    decks = int(config.get(\"table.decks\", 6))\n    limit = int(config.get(\"table.limit\", 100))\n    payout = config.get(\"table.payout\", (3, 2))\n    table = Table(\n        decks=decks, limit=limit, dealer=dealer_rule, split=split_rule, payout=payout\n    )\n\n    player_nm = config.get(\"player.play\", \"SomeStrategy\")\n    player_rule = {\n        \"SomeStrategy\": SomeStrategy(), \"AnotherStrategy\": AnotherStrategy()\n    }.get(\n        player_nm, SomeStrategy()\n    )\n    bet_nm = config.get(\"player.betting\", \"Flat\")\n    betting_rule = {\n        \"Flat\": Flat(), \"Martingale\": Martingale(), \"OneThreeTwoSix\": OneThreeTwoSix()\n    }.get(\n        bet_nm, Flat()\n    )\n    rounds = int(config.get(\"player.rounds\", 100))\n    stake = int(config.get(\"player.stake\", 50))\n    player = Player(play=player_rule, betting=betting_rule, max_rounds=rounds, init_stake=stake)\n\n    # import yaml\n    # print(yaml.dump(vars()))\n\n    outputfile = Path(config.get(\"simulator.outputfile\", \"blackjack.csv\"))\n    samples = int(config.get(\"simulator.samples\", 100))\n    simulate(table, player, outputfile, samples)\n\n\n# Sample Main Script to parse and start the application.\nif __name__ == \"__main__\":\n    config_files = json2_file, json3_file,\n    config = ChainMap(*[json.load(file) for file in reversed(config_files)])\n    print(config)\n\n    main_cm(config)\n\n    check(Path(config.get(\"simulator.outputfile\")))\n\n# YAML\n# #######\n\n# Simple YAML\nyaml1_file = io.StringIO(\n    \"\"\"\nplayer:\n  betting: Flat\n  play: SomeStrategy\n  rounds: 100\n  stake: 50\ntable:\n  dealer: Hit17\n  decks: 6\n  limit: 50\n  payout: [3, 2]\n  split: NoResplitAces\nsimulator: {outputfile: \"data/ch14_simulation.dat\", samples: 100}\n\"\"\"\n)\n\nimport yaml\n\nconfig = yaml.load(yaml1_file)\nif __name__ == \"__main__\":\n    from pprint import pprint\n\n    pprint(config)\n\n\nyaml1_file = io.StringIO(\n    \"\"\"\n# Complete Simulation Settings\ntable: !!python/object:Chapter_14.simulation_model.Table\n  dealer: !!python/object:Chapter_14.simulation_model.Hit17 {}\n  decks: 6\n  limit: 50\n  payout: !!python/tuple [3, 2]\n  split: !!python/object:Chapter_14.simulation_model.NoReSplitAces {}\nplayer: !!python/object:Chapter_14.simulation_model.Player\n  betting:  !!python/object:Chapter_14.simulation_model.Flat {}\n  init_stake: 50\n  max_rounds: 100\n  play: !!python/object:Chapter_14.simulation_model.SomeStrategy {}\n  rounds: 0\n  stake: 63.0\nsamples: 100\noutputfile: data/ch14_simulation4c.dat\n\"\"\"\n)\n\nimport yaml\nif __name__ == \"__main__\":\n\n    config = yaml.load(yaml1_file)\n    print(config)\n\n    simulate(config[\"table\"], config[\"player\"], Path(config[\"outputfile\"]), config[\"samples\"])\n    check(Path(config[\"outputfile\"]))\n\n\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_14/ch14_ex5.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 14. Example 5.\n\"\"\"\n\nfrom Chapter_14.simulation_model import *\n\n# A typical main program using the above class definitions\n\nfrom pathlib import Path\nfrom typing import List, Any, TextIO, Iterator, Union, IO, cast\nimport csv\nfrom collections import ChainMap\n\nimport io\n\n# Property files\n# ===============\n\n# - Lines have keys and values.\n# - Key ends with the first unescaped '=', ':', or white space character.\n# - Value is optional and defaults to \"\".\n# - Number sign (#) or the exclamation mark (!) as\n#   the first non blank character in a line is a comment.\n# - The backwards slash is used to escape a character.\n# - Since  #, !, =, and : have meaning,\n#   when involved in a piece of key or element, use a preceding backslash\n# - Key with spaces is tolerated using '\\ '.\n# - Key or value with newline is tolerated using '\\\\n'.\n# - Unicode escapes may be used:  \\uxxxx is the format.\n# - Everything is text, explicit conversions required\n\n# Example 1\n# From http://en.wikipedia.org/wiki/.properties\nprop1 = \"\"\"\n# You are reading the \".properties\" entry.\n! The exclamation mark can also mark text as comments.\n# The key and element characters #, !, =, and : are written with a preceding backslash to ensure that they are properly loaded.\nwebsite = http\\://en.wikipedia.org/\nlanguage = English\n# The backslash below tells the application to continue reading\n# the value onto the next line.\nmessage = Welcome to \\\\\n          Wikipedia\\!\n# Add spaces to the key\nkey\\ with\\ spaces = This is the value that could be looked up with the key \"key with spaces\".\n# Unicode\ntab : \\\\u0009\n\"\"\"\n\n# Example 2\n# From http://docs.oracle.com/javase/7/docs/api/java/util/Properties.html\nprop2 = \"\"\"\n\\:\\=\nTruth = Beauty\n Truth:Beauty\nTruth                    :Beauty\n\nfruits                          apple, banana, pear, \\\\\n                                cantaloupe, watermelon, \\\\\n                                kiwi, mango\n\ncheeses\n\"\"\"\n\n# Property File Parsing Class\nimport re\n\n\nclass PropertyParser:\n\n    def read_string(self, data: str) -> Iterator[Tuple[str, str]]:\n        return self._parse(data)\n\n    def read_file(self, file: IO[str]) -> Iterator[Tuple[str, str]]:\n        data = file.read()\n        return self.read_string(data)\n\n    def read(self, path: Path) -> Iterator[Tuple[str, str]]:\n        with path.open(\"r\") as file:\n            return self.read_file(file)\n\n    key_element_pat = re.compile(r\"(.*?)\\s*(?<!\\\\)[:=\\s]\\s*(.*)\")\n\n    def _parse(self, data: str) -> Iterator[Tuple[str, str]]:\n        logical_lines = (\n            line.strip() for line in re.sub(r\"\\\\\\n\\s*\", \"\", data).splitlines()\n        )\n        non_empty = (line for line in logical_lines if len(line) != 0)\n        non_comment = (\n            line\n            for line in non_empty\n            if not (line.startswith(\"#\") or line.startswith(\"!\"))\n        )\n        for line in non_comment:\n            ke_match = self.key_element_pat.match(line)\n            if ke_match:\n                key, element = ke_match.group(1), ke_match.group(2)\n            else:\n                key, element = line, \"\"\n            key = self._escape(key)\n            element = self._escape(element)\n            yield key, element\n\n    def load(\n        self, file_name_or_path: Union[TextIO, str, Path]\n    ) -> Iterator[Tuple[str, str]]:\n        if isinstance(file_name_or_path, io.TextIOBase):\n            return self.loads(file_name_or_path.read())\n        else:\n            name_or_path = cast(Union[str, Path], file_name_or_path)\n            with Path(name_or_path).open(\"r\") as file:\n                return self.loads(file.read())\n\n    def loads(self, data: str) -> Iterator[Tuple[str, str]]:\n        return self._parse(data)\n\n    def _escape(self, data: str) -> str:\n        d1 = re.sub(r\"\\\\([:#!=\\s])\", lambda x: x.group(1), data)\n        d2 = re.sub(r\"\\\\u([0-9A-Fa-f]+)\", lambda x: chr(int(x.group(1), 16)), d1)\n        return d2\n\n    def _escape2(self, data: str) -> str:\n        d2 = re.sub(\n            r\"\\\\([:#!=\\s])|\\\\u([0-9A-Fa-f]+)\",\n            lambda x: x.group(1) if x.group(1) else chr(int(x.group(2), 16)),\n            data,\n        )\n        return d2\n\n\ntest_should_parse_prop1 = \"\"\"\nA test for the prop1 example. We can create a dict since each key is unique.\n\n>>> parser = PropertyParser()\n>>> actual = dict(parser.read_string(prop1))\n>>> expected = {\n...    \"key with spaces\": 'This is the value that could be looked up with the key \"key with spaces\".',\n...    \"language\": \"English\",\n...    \"message\": \"Welcome to Wikipedia!\",\n...    \"tab\": \"\\\\t\",\n...    \"website\": \"http://en.wikipedia.org/\",\n... }\n>>> expected == actual\nTrue\n\"\"\"\n\ntest_should_parse_prop2 = \"\"\"\nA test for the prop2 example. We create a list since each key is not unique.\n\n>>> parser = PropertyParser()\n>>> actual = list(parser.read_string(prop2))\n>>> expected = [\n...    (\":=\", \"\"),\n...    (\"Truth\", \"Beauty\"),\n...    (\"Truth\", \"Beauty\"),\n...    (\"Truth\", \"Beauty\"),\n...    (\"fruits\", \"apple, banana, pear, cantaloupe, watermelon, kiwi, mango\"),\n...    (\"cheeses\", \"\"),\n... ]\n>>> expected == actual\nTrue\n>>> actual\n[(':=', ''), ('Truth', 'Beauty'), ('Truth', 'Beauty'), ('Truth', 'Beauty'), ('fruits', 'apple, banana, pear, cantaloupe, watermelon, kiwi, mango'), ('cheeses', '')]\n\"\"\"\n\ntest_edge_case = \"\"\"\n>>> parser = PropertyParser()\n>>> prop = \"a\\:b: value\"\n>>> actual = list(parser.read_string(prop))\n>>> expected = [(\"a:b\", \"value\")]\n>>> actual == expected\nTrue\n>>> actual\n[('a:b', 'value')]\n\"\"\"\n\n# Main Program to use property file input\nimport ast\n\n\ndef main_cm_prop(config):\n    dealer_nm = config.get(\"table.dealer\", \"Hit17\")\n    dealer_rule = {\"Hit17\": Hit17(), \"Stand17\": Stand17()}.get(dealer_nm, Hit17())\n    split_nm = config.get(\"table.split\", \"ReSplit\")\n    split_rule = {\n        \"ReSplit\": ReSplit(), \"NoReSplit\": NoReSplit(), \"NoReSplitAces\": NoReSplitAces()\n    }.get(\n        split_nm, ReSplit()\n    )\n    decks = int(config.get(\"table.decks\", 6))\n    limit = int(config.get(\"table.limit\", 100))\n    payout = ast.literal_eval(config.get(\"table.payout\", \"(3,2)\"))\n    table = Table(\n        decks=decks, limit=limit, dealer=dealer_rule, split=split_rule, payout=payout\n    )\n\n    player_nm = config.get(\"player.play\", \"SomeStrategy\")\n    player_rule = {\n        \"SomeStrategy\": SomeStrategy(), \"AnotherStrategy\": AnotherStrategy()\n    }.get(\n        player_nm, SomeStrategy()\n    )\n    bet_nm = config.get(\"player.betting\", \"Flat\")\n    betting_rule = {\n        \"Flat\": Flat(), \"Martingale\": Martingale(), \"OneThreeTwoSix\": OneThreeTwoSix()\n    }.get(\n        bet_nm, Flat()\n    )\n    rounds = int(config.get(\"player.rounds\", 100))\n    stake = int(config.get(\"player.stake\", 50))\n    player = Player(\n        play=player_rule, betting=betting_rule, max_rounds=rounds, init_stake=stake\n    )\n\n    outputfile = config.get(\"simulator.outputfile\", \"blackjack.csv\")\n    samples = int(config.get(\"simulator.samples\", 100))\n    simulator = Simulate(table, player, samples)\n    with open(outputfile, \"w\", newline=\"\") as results:\n        wtr = csv.writer(results)\n        for gamestats in simulator:\n            wtr.writerow(gamestats)\n\n\n# Example property file.\nprop_file = io.StringIO(\n\"\"\"\n# Example Simulation Setup\n\nplayer.betting: Flat\nplayer.play: SomeStrategy\nplayer.rounds: 100\nplayer.stake: 50\n\ntable.dealer: Hit17\ntable.decks: 6\ntable.limit: 50\ntable.payout: (3,2)\ntable.split: NoResplitAces\n\nsimulator.outputfile = data/ch14_simulation5.dat\nsimulator.samples = 100\n\"\"\"\n)\n\n\nif __name__ == \"__main__\":\n    from pprint import pprint\n\n    pp = PropertyParser()\n\n    candidate_list = [prop_file]\n    properties = [dict(pp.read_file(file)) for file in reversed(candidate_list)]\n    pprint(properties)\n    config = ChainMap(*properties)\n\n    main_cm_prop(config)\n    check(Path(config[\"simulator.outputfile\"]))\n\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_14/ch14_ex6.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 14. Example 6.\n\"\"\"\n\nfrom Chapter_14.simulation_model import *\n\n# A typical main program using the above class definitions\n\nfrom pathlib import Path\nfrom typing import List, Any, TextIO, Iterator, Union\nimport csv\nfrom collections import ChainMap\nimport ast\n\n# Exec Import\n# ################\n\nimport io\n\n# JSON or YAML files\n# ===================\n\n# JSON using dictionary-of-dictionaries nested structures.\n# This is inconvenient to handle multiple configuration files.\nimport io\n\n\n# XML files\n# ==========\n\n# Plist\n# #######\n\n# Sample PLIST Document. As bytes.\nimport io\n\nplist_file = io.BytesIO(\n    b\"\"\"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n    <key>player</key>\n    <dict>\n        <key>betting</key>\n        <string>Flat</string>\n        <key>play</key>\n        <string>SomeStrategy</string>\n        <key>rounds</key>\n        <integer>100</integer>\n        <key>stake</key>\n        <integer>50</integer>\n    </dict>\n    <key>simulator</key>\n    <dict>\n        <key>outputfile</key>\n        <string>ch14_simulation6a.dat</string>\n        <key>samples</key>\n        <integer>100</integer>\n    </dict>\n    <key>table</key>\n    <dict>\n        <key>dealer</key>\n        <string>Hit17</string>\n        <key>decks</key>\n        <integer>6</integer>\n        <key>limit</key>\n        <integer>50</integer>\n        <key>payout</key>\n        <array>\n            <integer>3</integer>\n            <integer>2</integer>\n        </array>\n        <key>split</key>\n        <string>NoResplitAces</string>\n    </dict>\n</dict>\n</plist>\n\"\"\"\n)\n\nimport plistlib\nprint(plistlib.load(plist_file))\n\n# Non-Plist\n# ##########\n\n# A completely customized XML document\nimport io\n\nxml_file = io.BytesIO(\n    b\"\"\"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<simulation>\n    <table>\n        <dealer>Hit17</dealer>\n        <split>NoResplitAces</split>\n        <decks>6</decks>\n        <limit>50</limit>\n        <payout>(3,2)</payout>\n    </table>\n    <player>\n        <betting>Flat</betting>\n        <play>SomeStrategy</play>\n        <rounds>100</rounds>\n        <stake>50</stake>\n    </player>\n    <simulator>\n        <outputfile>data/ch14_simulation6b.dat</outputfile>\n        <samples>100</samples>\n    </simulator>\n</simulation>\n\"\"\"\n)\n\nimport xml.etree.ElementTree as XML\n\n\nclass Configuration:\n\n    def read_file(self, file):\n        self.config = XML.parse(file)\n\n    def read(self, filename):\n        self.config = XML.parse(filename)\n\n    def read_string(self, text):\n        self.config = XML.fromstring(text)\n\n    def get(self, qual_name, default):\n        section, _, item = qual_name.partition(\".\")\n        query = \"./{0}/{1}\".format(section, item)\n        node = self.config.find(query)\n        if node is None:\n            return default\n        return node.text\n\n    def __getitem__(self, section):\n        query = \"./{0}\".format(section)\n        parent = self.config.find(query)\n        return dict((item.tag, item.text) for item in parent)\n\ndef main_cm_prop(config):\n    dealer_nm = config.get(\"table.dealer\", \"Hit17\")\n    dealer_rule = {\"Hit17\": Hit17(), \"Stand17\": Stand17()}.get(dealer_nm, Hit17())\n    split_nm = config.get(\"table.split\", \"ReSplit\")\n    split_rule = {\n        \"ReSplit\": ReSplit(), \"NoReSplit\": NoReSplit(), \"NoReSplitAces\": NoReSplitAces()\n    }.get(\n        split_nm, ReSplit()\n    )\n    decks = int(config.get(\"table.decks\", 6))\n    limit = int(config.get(\"table.limit\", 100))\n    payout = ast.literal_eval(config.get(\"table.payout\", \"(3,2)\"))\n    table = Table(\n        decks=decks, limit=limit, dealer=dealer_rule, split=split_rule, payout=payout\n    )\n\n    player_nm = config.get(\"player.play\", \"SomeStrategy\")\n    player_rule = {\n        \"SomeStrategy\": SomeStrategy(), \"AnotherStrategy\": AnotherStrategy()\n    }.get(\n        player_nm, SomeStrategy()\n    )\n    bet_nm = config.get(\"player.betting\", \"Flat\")\n    betting_rule = {\n        \"Flat\": Flat(), \"Martingale\": Martingale(), \"OneThreeTwoSix\": OneThreeTwoSix()\n    }.get(\n        bet_nm, Flat()\n    )\n    rounds = int(config.get(\"player.rounds\", 100))\n    stake = int(config.get(\"player.stake\", 50))\n    player = Player(play=player_rule, betting=betting_rule, max_rounds=rounds, init_stake=stake)\n\n    outputfile = config.get(\"simulator.outputfile\", \"blackjack.csv\")\n    samples = int(config.get(\"simulator.samples\", 100))\n    simulator = Simulate(table, player, samples)\n    with open(outputfile, \"w\", newline=\"\") as results:\n        wtr = csv.writer(results)\n        for gamestats in simulator:\n            wtr.writerow(gamestats)\n\n\nif __name__ == \"__main__\":\n\n    config = Configuration()\n    config.read_file(xml_file)\n    main_cm_prop(config)\n    check(Path(config[\"simulator\"][\"outputfile\"]))\n\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_14/simulation_model.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 14. Example 1 -- simulation model.\n\"\"\"\n\nfrom dataclasses import dataclass, astuple, asdict, field\nfrom typing import Tuple, Iterator\nfrom pathlib import Path\nimport csv\n\n# Mock Object Model\n# =====================\n\n# A set of class hierarchies that we'll use for several examples.\n# The content is mostly mocks.\nclass DealerRule:\n\n    def __repr__(self) -> str:\n        return f\"{self.__class__.__name__}()\"\n\nclass Hit17(DealerRule):\n    \"\"\"Hits soft 17\"\"\"\n    pass\n\n\nclass Stand17(DealerRule):\n    \"\"\"Stands on soft 17\"\"\"\n    pass\n\n\nclass SplitRule:\n\n    def __repr__(self) -> str:\n        return f\"{self.__class__.__name__}()\"\n\n\nclass ReSplit(SplitRule):\n    \"\"\"Simplistic resplit anything.\"\"\"\n    pass\n\n\nclass NoReSplit(SplitRule):\n    \"\"\"Simplistic no resplit.\"\"\"\n    pass\n\n\nclass NoReSplitAces(SplitRule):\n    \"\"\"One card only to aces; no resplit.\"\"\"\n    pass\n\n\n@dataclass\nclass Table:\n\n    decks: int\n    limit: int\n    dealer: DealerRule\n    split: SplitRule\n    payout: Tuple[int, int]\n\n\nclass PlayerStrategy:\n\n    def __repr__(self) -> str:\n        return f\"{self.__class__.__name__}()\"\n\n\nclass SomeStrategy(PlayerStrategy):\n    pass\n\n\nclass AnotherStrategy(PlayerStrategy):\n    pass\n\n\nclass BettingStrategy:\n\n    def __repr__(self) -> str:\n        return f\"{self.__class__.__name__}()\"\n\n    def bet(self) -> int:\n        raise NotImplementedError(\"No bet method\")\n\n    def record_win(self) -> None:\n        pass\n\n    def record_loss(self) -> None:\n        pass\n\n\nclass Flat(BettingStrategy):\n    pass\n\n\nclass Martingale(BettingStrategy):\n    pass\n\n\nclass OneThreeTwoSix(BettingStrategy):\n    pass\n\n\n@dataclass\nclass Player:\n\n    play: PlayerStrategy\n    betting: BettingStrategy\n    max_rounds: int\n    init_stake: int\n\n    rounds: int = field(init=False)\n    stake: float = field(init=False)\n\n    def __post_init__(self):\n        self.reset()\n\n    def reset(self) -> None:\n        self.rounds = self.max_rounds\n        self.stake = self.init_stake\n\n\n# A mock simulation which is built from the above mock objects.\nimport random\n\n\n@dataclass\nclass Simulate:\n    \"\"\"Mock simulation.\"\"\"\n\n    table: Table\n    player: Player\n    samples: int\n\n    def __iter__(self) -> Iterator[Tuple]:\n        \"\"\"Yield statistical samples.\"\"\"\n        x, y = self.table.payout\n        blackjack_payout = x / y\n        for count in range(self.samples):\n            self.player.reset()\n            while self.player.stake > 0 and self.player.rounds > 0:\n                self.player.rounds -= 1\n\n                outcome = random.random()\n                if outcome < 0.579:\n                    self.player.stake -= 1\n                elif 0.579 <= outcome < 0.883:\n                    self.player.stake += 1\n                elif 0.883 <= outcome < 0.943:\n                    # a \"push\"\n                    pass\n                else:\n                    # 0.943 <= outcome\n                    self.player.stake += blackjack_payout\n\n            yield astuple(self.table) + astuple(self.player)\n\n\ndef check(path: Path) -> None:\n    \"\"\"\n    Validate unit test result file can be read.\n\n    :param path: Path to the example output\n    \"\"\"\n    with path.open(\"r\") as results:\n        rdr = csv.reader(results)\n        outcomes = (float(row[10]) for row in rdr)\n        first = next(outcomes)\n        sum_0, sum_1 = 1, first\n        value_min = value_max = first\n        for value in outcomes:\n            sum_0 += 1  # value**0\n            sum_1 += value  # value**1\n            value_min = min(value_min, value)\n            value_max = max(value_max, value)\n        mean = sum_1 / sum_0\n        print(\n            f\"{path}\\nMean = {mean:.1f}\\n\"\n            f\"House Edge = { 1 - mean / 50:.1%}\\n\"\n            f\"Range = {value_min:.1f} {value_max:.1f}\"\n        )\n"
  },
  {
    "path": "Chapter_14/someapp.config",
    "content": ""
  },
  {
    "path": "Chapter_15/__init__.py",
    "content": ""
  },
  {
    "path": "Chapter_15/ch15_ex1.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 15. Example 1.\n\"\"\"\nimport random\nfrom typing import Tuple, List, Iterator, Optional, Type\n\n# A poor design\n\n\nclass DominoBoneYard:\n    \"\"\"\n    A relatively poor design. A number of unrelated things all jumbled\n    together\n\n    >>> random.seed(42)\n    >>> dby = DominoBoneYard()\n    >>> len(dby._dominoes)\n    28\n    >>> hands = list(dby.hand_iter(4))\n    >>> hands[0]\n    [(5, 3), (5, 1), (4, 0), (6, 0), (6, 6), (3, 0), (2, 2)]\n    >>> dby.score_hand(hands[0])\n    43\n    >>> hands[1]\n    [(4, 1), (4, 4), (3, 3), (6, 3), (4, 2), (5, 4), (5, 0)]\n    >>> dby.rank_hand(hands[1])\n    >>> dby.score_hand(hands[1][-2:])\n    10\n    >>> hands[2]\n    [(6, 4), (1, 0), (4, 3), (1, 1), (5, 2), (6, 5), (2, 1)]\n    >>> dby.doubles_indices(hands[0])\n    [4, 6]\n    >>> for d in  dby.doubles_indices(hands[0]):\n    ...     print(hands[0][d])\n    (6, 6)\n    (2, 2)\n    >>> dby.can_play_first(hands[0])\n    True\n    \"\"\"\n\n    def __init__(self, limit: int = 6) -> None:\n        self._dominoes = [(x, y) for x in range(limit + 1) for y in range(x + 1)]\n        random.shuffle(self._dominoes)\n\n    def double(self, domino: Tuple[int, int]) -> bool:\n        x, y = domino\n        return x == y\n\n    def score(self, domino: Tuple[int, int]) -> int:\n        return domino[0] + domino[1]\n\n    def hand_iter(self, players: int = 4) -> Iterator[List[Tuple[int, int]]]:\n        for p in range(players):\n            yield self._dominoes[p * 7:p * 7 + 7]\n\n    def can_play_first(self, hand: List[Tuple[int, int]]) -> bool:\n        for d in hand:\n            if self.double(d) and d[0] == 6:\n                return True\n        return False\n\n    def score_hand(self, hand: List[Tuple[int, int]]) -> int:\n        return sum(d[0] + d[1] for d in hand)\n\n    def rank_hand(self, hand: List[Tuple[int, int]]) -> None:\n        hand.sort(key=self.score, reverse=True)\n\n    def doubles_indices(self, hand: List[Tuple[int, int]]) -> List[int]:\n        return [i for i in range(len(hand)) if self.double(hand[i])]\n\n\n# Revised and Decomposed based on ISP\nfrom typing import NamedTuple\n\n\nclass Domino(NamedTuple):\n    v1: int\n    v2: int\n\n    def double(self) -> bool:\n        return self.v1 == self.v2\n\n    def score(self) -> int:\n        return self.v1 + self.v2\n\n\nclass Hand(list):\n\n    def score(self) -> int:\n        return sum(d.score() for d in self)\n\n    def rank(self) -> None:\n        self.sort(key=lambda d: d.score(), reverse=True)\n\n    def doubles_indices(self) -> List[int]:\n        return [i for i in range(len(self)) if self[i].double()]\n\n\nclass DominoBoneYard2:\n\n    def __init__(self, limit: int = 6) -> None:\n        self._dominoes = [Domino(x, y) for x in range(limit + 1) for y in range(x + 1)]\n        random.shuffle(self._dominoes)\n\n    def hand_iter(self, players: int = 4) -> Iterator[Hand]:\n        for p in range(players):\n            hand, self._dominoes = Hand(self._dominoes[:7]), self._dominoes[7:]\n            yield hand\n\n\ntest_dby2 = \"\"\"\n    >>> random.seed(42)\n    >>> dby = DominoBoneYard2()\n    >>> len(dby._dominoes)\n    28\n    >>> hands = list(dby.hand_iter(4))\n    >>> hands[0]\n    [Domino(v1=5, v2=3), Domino(v1=5, v2=1), Domino(v1=4, v2=0), Domino(v1=6, v2=0), Domino(v1=6, v2=6), Domino(v1=3, v2=0), Domino(v1=2, v2=2)]\n    >>> hands[0].score()\n    43\n    >>> hands[1]\n    [Domino(v1=4, v2=1), Domino(v1=4, v2=4), Domino(v1=3, v2=3), Domino(v1=6, v2=3), Domino(v1=4, v2=2), Domino(v1=5, v2=4), Domino(v1=5, v2=0)]\n    >>> hands[1].rank()\n    >>> hands[1].pop(0)\n    Domino(v1=6, v2=3)\n    >>> hands[1].pop(0)\n    Domino(v1=5, v2=4)\n    >>> hands[1].pop(0)\n    Domino(v1=4, v2=4)\n    >>> hands[1].pop(0)\n    Domino(v1=3, v2=3)\n    >>> hands[1].pop(0)\n    Domino(v1=4, v2=2)\n    >>> hands[1].score()\n    10\n    >>> hands[2]\n    [Domino(v1=6, v2=4), Domino(v1=1, v2=0), Domino(v1=4, v2=3), Domino(v1=1, v2=1), Domino(v1=5, v2=2), Domino(v1=6, v2=5), Domino(v1=2, v2=1)]\n    >>> hands[0].doubles_indices()\n    [4, 6]\n    >>> for d in hands[0].doubles_indices():\n    ...     print(hands[0][d])\n    Domino(v1=6, v2=6)\n    Domino(v1=2, v2=2)\n\"\"\"\n\n\nclass Hand3(Hand):\n\n    def highest_double_index(self) -> Optional[int]:\n        descending = sorted(\n            self.doubles_indices(),\n            key=lambda double_index: self[double_index].v1,\n            reverse=True,\n        )\n        if descending:\n            return descending[0]\n        return None\n\n\nclass DominoBoneYard3(DominoBoneYard2):\n\n    def hand_iter(self, players: int = 4) -> Iterator[Hand3]:\n        for p in range(players):\n            hand, self._dominoes = Hand3(self._dominoes[:7]), self._dominoes[7:]\n            yield hand\n\n\ntest_dby3 = \"\"\"\n    >>> random.seed(42)\n    >>> dby = DominoBoneYard3()\n    >>> len(dby._dominoes)\n    28\n    >>> hands = list(dby.hand_iter(4))\n    >>> hands[0]\n    [Domino(v1=5, v2=3), Domino(v1=5, v2=1), Domino(v1=4, v2=0), Domino(v1=6, v2=0), Domino(v1=6, v2=6), Domino(v1=3, v2=0), Domino(v1=2, v2=2)]\n    >>> hands[0].score()\n    43\n    >>> hdi = hands[0].highest_double_index()\n    >>> hdi\n    4\n    >>> hands[0][hdi]\n    Domino(v1=6, v2=6)\n    >>> hands[1]\n    [Domino(v1=4, v2=1), Domino(v1=4, v2=4), Domino(v1=3, v2=3), Domino(v1=6, v2=3), Domino(v1=4, v2=2), Domino(v1=5, v2=4), Domino(v1=5, v2=0)]\n\"\"\"\n\n\nclass FancyDealer4:\n\n    def __init__(self):\n        self.boneyard = DominoBoneYard3()\n\n    def hand_iter(\n        self, players: int = 4, tiles: int = 7\n    ) -> Iterator[Hand3]:\n        if players * tiles > len(self.boneyard._dominoes):\n            raise ValueError(f\"Can't deal players={players} tiles={tiles}\")\n        for p in range(players):\n            hand = Hand3(self.boneyard._dominoes[:tiles])\n            self.boneyard._dominoes = self.boneyard._dominoes[tiles:]\n            yield hand\n\n\ntest_fancy4 = \"\"\"\n    >>> random.seed(42)\n    >>> dby1 = FancyDealer4()\n    >>> hands = list(dby1.hand_iter(4))\n    >>> hands[0]\n    [Domino(v1=5, v2=3), Domino(v1=5, v2=1), Domino(v1=4, v2=0), Domino(v1=6, v2=0), Domino(v1=6, v2=6), Domino(v1=3, v2=0), Domino(v1=2, v2=2)]\n\n    >>> random.seed(42)\n    >>> dby2 = FancyDealer4()\n    >>> hands5 = list(dby2.hand_iter(players=2, tiles=5))\n    >>> hands5[0]\n    [Domino(v1=5, v2=3), Domino(v1=5, v2=1), Domino(v1=4, v2=0), Domino(v1=6, v2=0), Domino(v1=6, v2=6)]\n\"\"\"\n\n\nclass DominoBoneYard3b:\n\n    hand_size: int = 7\n\n    def __init__(self, limit: int = 6) -> None:\n        self._dominoes = [Domino(x, y) for x in range(limit + 1) for y in range(x + 1)]\n        random.shuffle(self._dominoes)\n\n    def hand_iter(self, players: int = 4) -> Iterator[Hand3]:\n        for p in range(players):\n            hand = Hand3(self._dominoes[:self.hand_size])\n            self._dominoes = self._dominoes[self.hand_size:]\n            yield hand\n\n\ntest_dby5 = \"\"\"\n    >>> random.seed(42)\n    >>> dby = DominoBoneYard3b()\n    >>> len(dby._dominoes)\n    28\n    >>> hands = list(dby.hand_iter(4))\n    >>> hands[0]\n    [Domino(v1=5, v2=3), Domino(v1=5, v2=1), Domino(v1=4, v2=0), Domino(v1=6, v2=0), Domino(v1=6, v2=6), Domino(v1=3, v2=0), Domino(v1=2, v2=2)]\n    >>> hands[0].score()\n    43\n    >>> hdi = hands[0].highest_double_index()\n    >>> hdi\n    4\n    >>> hands[0][hdi]\n    Domino(v1=6, v2=6)\n    >>> hands[1]\n    [Domino(v1=4, v2=1), Domino(v1=4, v2=4), Domino(v1=3, v2=3), Domino(v1=6, v2=3), Domino(v1=4, v2=2), Domino(v1=5, v2=4), Domino(v1=5, v2=0)]\n\"\"\"\n\n\nclass DominoBoneYard3c:\n\n    domino_class: Type[Domino] = Domino\n\n    hand_class: Type[Hand] = Hand3\n\n    hand_size: int = 7\n\n    def __init__(self, limit: int = 6) -> None:\n        self._dominoes = [\n            self.domino_class(x, y) for x in range(limit + 1) for y in range(x + 1)\n        ]\n        random.shuffle(self._dominoes)\n\n    def hand_iter(self, players: int = 4) -> Iterator[Hand]:\n        for p in range(players):\n            hand = self.hand_class(self._dominoes[:self.hand_size])\n            self._dominoes = self._dominoes[self.hand_size:]\n            yield hand\n\n\nclass Hand4(Hand3):\n\n    def __init__(self, *args) -> None:\n        super().__init__(*args)\n        self.doubles = [d for d in self if d.double()]\n        self.doubles.sort(key=lambda d: d.score())\n\n    def doubles_indices(self) -> List[int]:\n        return [self.index(d) for d in self.doubles]\n\n\ntest_dby6 = \"\"\"\n    >>> random.seed(42)\n    >>> dby = DominoBoneYard3c()\n    >>> len(dby._dominoes)\n    28\n    >>> hands = list(dby.hand_iter(4))\n    >>> hands[0]\n    [Domino(v1=5, v2=3), Domino(v1=5, v2=1), Domino(v1=4, v2=0), Domino(v1=6, v2=0), Domino(v1=6, v2=6), Domino(v1=3, v2=0), Domino(v1=2, v2=2)]\n    >>> hands[0].score()\n    43\n    >>> hdi = hands[0].highest_double_index()\n    >>> hdi\n    4\n    >>> hands[0][hdi]\n    Domino(v1=6, v2=6)\n    >>> hands[1]\n    [Domino(v1=4, v2=1), Domino(v1=4, v2=4), Domino(v1=3, v2=3), Domino(v1=6, v2=3), Domino(v1=4, v2=2), Domino(v1=5, v2=4), Domino(v1=5, v2=0)]\n\n    >>> random.seed(42)\n    >>> DominoBoneYard3c.hand_class = Hand4\n    >>> dby = DominoBoneYard3c()\n    >>> len(dby._dominoes)\n    28\n    >>> hands = list(dby.hand_iter(4))\n    >>> hands[0]\n    [Domino(v1=5, v2=3), Domino(v1=5, v2=1), Domino(v1=4, v2=0), Domino(v1=6, v2=0), Domino(v1=6, v2=6), Domino(v1=3, v2=0), Domino(v1=2, v2=2)]\n    >>> hands[0].score()\n    43\n    >>> hdi = hands[0].highest_double_index()\n    >>> hdi\n    4\n\n\"\"\"\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_15/ch15_ex2.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 15. Example 2.\n\"\"\"\nfrom typing import (\n    NamedTuple,\n    List,\n    Type,\n    Optional,\n    Iterator,\n    Tuple,\n    DefaultDict,\n    Union,\n    cast,\n    Any,\n)\nimport random\nfrom collections import defaultdict\n\n# Duck Typing\n\nfrom typing import NamedTuple\n\n\nclass Domino_1(NamedTuple):\n    v1: int\n    v2: int\n\n    @property\n    def double(self) -> bool:\n        return self.v1 == self.v2\n\n    @property\n    def score(self) -> int:\n        return self.v1 + self.v2\n\n\nfrom dataclasses import dataclass\n\n\n@dataclass(frozen=True, eq=True, order=True)\nclass Domino_2:\n    v1: int\n    v2: int\n\n    @property\n    def double(self) -> bool:\n        return self.v1 == self.v2\n\n    @property\n    def score(self) -> int:\n        return self.v1 + self.v2\n\n\nDomino = Union[Domino_1, Domino_2]\n\n\ndef builder(v1: int, v2: int) -> Domino:\n    return Domino_2(v1, v2)\n\n\ntest_dominoe_classes = \"\"\"\n    >>> d_1a = Domino_1(6, 5)\n    >>> d_1b = Domino_1(6, 5)\n    >>> d_1a == d_1b\n    True\n    >>> d_1a.double\n    False\n    >>> d_1a.score\n    11\n\n    >>> d_2a = Domino_2(5, 3)\n    >>> d_2b = Domino_2(5, 3)\n    >>> d_2a == d_2b\n    True\n    >>> d_2a.double\n    False\n    >>> d_2a.score\n    8\n\"\"\"\n\n# More Complex Example\n\n\nclass Hand(list):\n\n    def __init__(self, *args: Domino) -> None:\n        super().__init__(cast(Tuple[Any], args))\n\n    def score(self) -> int:\n        return sum(d.score for d in self)\n\n    def rank(self) -> None:\n        self.sort(key=lambda d: d.score, reverse=True)\n\n    def doubles(self) -> List[Domino_1]:\n        return [d for d in self if d.double]\n\n    def highest_double(self) -> Optional[Domino_1]:\n        descending = sorted(self.doubles(), key=lambda d: d.v1, reverse=True)\n        if descending:\n            return descending[0]\n        return None\n\n\nclass DominoBoneYard:\n\n    domino_class: Type[Domino] = Domino_1\n\n    hand_class: Type[Hand] = Hand\n\n    hand_size: int = 7\n\n    def __init__(self, limit: int = 6) -> None:\n        self._dominoes: List[Domino] = [\n            self.domino_class(x, y) for x in range(limit + 1) for y in range(x + 1)\n        ]\n        random.shuffle(self._dominoes)\n\n    def draw(self, n: int = 1) -> Optional[List[Domino]]:\n        deal, remainder = self._dominoes[:n], self._dominoes[n:]\n        if len(deal) != n:\n            return None\n        self._dominoes = remainder\n        return deal\n\n    def hand_iter(self, players: int = 4) -> Iterator[Hand]:\n        hands: List[Optional[List[Domino]]] = [\n            self.draw(self.hand_size) for _ in range(players)\n        ]\n        if not all(hands):\n            raise ValueError(f\"Can't deal {self.hand_size} tiles to {players} players\")\n        yield from (self.hand_class(*h) for h in hands if h is not None)\n\n\ntest_dby = \"\"\"\n    >>> random.seed(42)\n    >>> DominoBoneYard.hand_class = Hand\n    >>> dby = DominoBoneYard()\n    >>> len(dby._dominoes)\n    28\n    >>> hands = list(dby.hand_iter(4))\n    >>> hands[0]\n    [Domino_1(v1=5, v2=3), Domino_1(v1=5, v2=1), Domino_1(v1=4, v2=0), Domino_1(v1=6, v2=0), Domino_1(v1=6, v2=6), Domino_1(v1=3, v2=0), Domino_1(v1=2, v2=2)]\n    >>> hands[0].score()\n    43\n    >>> hd = hands[0].highest_double()\n    >>> hd\n    Domino_1(v1=6, v2=6)\n\n    >>> hands[1]\n    [Domino_1(v1=4, v2=1), Domino_1(v1=4, v2=4), Domino_1(v1=3, v2=3), Domino_1(v1=6, v2=3), Domino_1(v1=4, v2=2), Domino_1(v1=5, v2=4), Domino_1(v1=5, v2=0)]\n\"\"\"\n\ntest_dby_exception = \"\"\"\n    >>> random.seed(42)\n    >>> DominoBoneYard.hand_class = Hand\n    >>> dby = DominoBoneYard()\n    >>> hands = list(dby.hand_iter(5))  # doctest: +IGNORE_EXCEPTION_DETAIL\n    Traceback (most recent call last):\n      File \"/Users/slott/miniconda3/envs/mastering/lib/python3.7/doctest.py\", line 1329, in __run\n        compileflags, 1), test.globs)\n      File \"<doctest __main__.__test__.test_dby_2[3]>\", line 1, in <module>\n        hands = list(dby.hand_iter(5))\n      File \"/Users/slott/Documents/.../mastering-oo-python-2e/Chapter_15/ch15_ex2.py\", line 119, in hand_iter\n        raise ValueError(f\"Can't deal {self.hand_size} tiles to {players} players\")\n    ValueError: Can't deal 7 tiles to 5 players\n\"\"\"\n\n\nclass Hand_X1(Hand):\n\n    def __init__(self, *args) -> None:\n        super().__init__(*args)\n        self.end: DefaultDict[int, List[Domino_1]] = defaultdict(list)\n        for d in self:\n            self.end[d.v1].append(d)\n            self.end[d.v2].append(d)\n\n    def matches(self, spots: int) -> List[Domino_1]:\n        return self.end.get(spots, [])\n\n\ntest_dby_3 = \"\"\"\n    >>> random.seed(42)\n    >>> DominoBoneYard.hand_class = Hand_X1\n    >>> DominoBoneYard.domino_class = Domino_2\n    >>> dby = DominoBoneYard()\n    >>> len(dby._dominoes)\n    28\n    >>> hands = list(dby.hand_iter(4))\n    >>> h_0 = hands[0]\n    >>> h_0\n    [Domino_2(v1=5, v2=3), Domino_2(v1=5, v2=1), Domino_2(v1=4, v2=0), Domino_2(v1=6, v2=0), Domino_2(v1=6, v2=6), Domino_2(v1=3, v2=0), Domino_2(v1=2, v2=2)]\n    >>> h_0.score()\n    43\n    >>> h_0.matches(3)\n    [Domino_2(v1=5, v2=3), Domino_2(v1=3, v2=0)]\n\"\"\"\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_16/__init__.py",
    "content": ""
  },
  {
    "path": "Chapter_16/ch16_ex1.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 16. Example 1.\n\"\"\"\n\nfrom typing import Type\n\n# Simple Logging\n# ==============\n\n\nclass Player:\n\n    def __init__(self, bet: str, strategy: str, stake: int) -> None:\n        self.logger = logging.getLogger(self.__class__.__qualname__)\n        self.logger.debug(\"init bet %r, strategy %r, stake %r\", bet, strategy, stake)\n\n\n# Decorator for Logging\n# ========================\n\n# Define a decorator for a class.\n# This is confusing to mypy because it's not clear the decorator adds an attribute\n# It's not optimal\n\n\ndef logged(cls: Type) -> Type:\n    cls.logger = logging.getLogger(cls.__qualname__)\n    return cls\n\n\nimport logging\nimport sys\n\n# Add a level\n# ============\n\nlogging.addLevelName(15, \"VERBOSE\")\nVERBOSE = 15\n\n# Manual Logging\n# ===============\n\n# Mypy is happier. But. We're repeated the class name.\nclass Player_2:\n    logger = logging.getLogger(\"Player_2\")\n\n    def __init__(self, bet: str, strategy: str, stake: int) -> None:\n        self.logger.debug(\"init bet %s, strategy %s, stake %d\", bet, strategy, stake)\n\n\n# Using a metaclass for consistent logger definition\n# ==================================================\n\n\nclass LoggedClassMeta(type):\n\n    def __new__(cls, name, bases, namespace, **kwds):\n        result = type.__new__(cls, name, bases, dict(namespace))\n        result.logger = logging.getLogger(result.__qualname__)\n        return result\n\n\nclass LoggedClass(metaclass=LoggedClassMeta):\n    logger: logging.Logger\n\n\n# Sample Class\n\n\nclass Player_3(LoggedClass):\n\n    def __init__(self, bet: str, strategy: str, stake: int) -> None:\n        self.logger.debug(\"init bet %s, strategy %s, stake %d\", bet, strategy, stake)\n\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n\n    # No configuration -- no output\n\n    logger = logging.getLogger(\"no_config\")\n    logger.info(\"Create Player 2\")\n    p2 = Player_2(\"Bet1\", \"Strategy1\", 1)\n    logger.info(\"Create Player 3\")\n    p3 = Player_3(\"Bet1\", \"Strategy1\", 1)\n\n    # Configuration changed -- now there's output\n    logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)\n\n    loggerc = logging.getLogger(\"config\")\n    loggerc.info(\"Create Player\")\n    pc = Player(\"Bet\", \"Strategy\", 10)\n    loggerc.info(\"Create Player 2\")\n    pc2 = Player_2(\"Bet2\", \"Strategy2\", 2)\n    loggerc.info(\"Create Player 3\")\n    pc3 = Player_3(\"Bet3\", \"Strategy3\", 3)\n\n    logging.shutdown()\n"
  },
  {
    "path": "Chapter_16/ch16_ex10.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 16. Example 9.\n\"\"\"\n\nfrom Chapter_16.ch16_ex9 import Log_Producer, Log_Consumer_1\nimport logging\nimport logging.handlers\nimport yaml\nimport queue\n\n# Modified Queue Handler\n# ==================================\n\n# Extended QueueHandler class\n\n\nclass WaitQueueHandler(logging.handlers.QueueHandler):\n\n    def enqueue(self, record):\n        self.queue.put(record)\n\n\n# Revised Producer\n\n\nclass Log_Producer_2(Log_Producer):\n    handler_class = WaitQueueHandler\n\n\n# The Queue\n\nimport multiprocessing\n\nqueue2: multiprocessing.Queue = multiprocessing.Queue(100)  # Waaayyyy too small\n\n# The consumer process\n\nconsumer2 = Log_Consumer_1(queue2)\nconsumer2.start()\n\n# The producers\n\nproducers = []\nfor i in range(10):\n    proc = Log_Producer_2(i, queue2)\n    proc.start()\n    producers.append(proc)\n\n# Normal termination\n\nfor p in producers:\n    p.join()\n\nqueue2.put(None)\n\nconsumer2.join()\n\nlogging.shutdown()\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_16/ch16_ex2.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 16. Example 2.\n\"\"\"\n\nfrom typing import Type\nimport logging\nimport sys\n\n\n# Multiple Loggers\n# ===========================\n\n\n# This is confusing to mypy because it's not clear the decorator adds attributes.\n\ndef log_to(*names: str):\n    if len(names) == 0:\n        names = ('logger',)\n\n    def concrete_log_to(cls: Type) -> Type:\n        for log_name in names:\n            setattr(cls, log_name, logging.getLogger(\n                f\"{log_name}.{cls.__qualname__}\"))\n        return cls\n\n    return concrete_log_to\n\n# Sample Class\n\n# Chapter_16/ch16_ex2.py:41: error: \"Player\" has no attribute \"audit\"\n# Chapter_16/ch16_ex2.py:42: error: \"Player\" has no attribute \"verbose\"\n\n@log_to(\"audit\", \"verbose\")\nclass Player:\n    def __init__(self, bet: str, strategy: str, stake: int) -> None:\n        self.audit.info(f\"Initial {stake:d}\")\n        self.verbose.info(f\"Init bet={bet:s} strategy={strategy:s} stake={stake:d}\")\n\n# Chapter_16/ch16_ex2.py:50: error: \"Table\" has no attribute \"security\"\n\n@log_to(\"security\")\nclass Table:\n    def add_player(self, player: Player) -> None:\n        self.security.info(f\"Adding {player}\")\n\n# Demo Output\n\nlogging.basicConfig(stream=sys.stderr, level=logging.DEBUG, style=\"{\")\n\nprint(\"Create Player 2\")\np3 = Player(\"Bet3\", \"Strategy3\", 3)\nt = Table()\nt.add_player(p3)\n\nlogging.shutdown()\n\n\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_16/ch16_ex3.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 16. Example 3.\n\"\"\"\n\n\n# Multiple Loggers with YAML Config\n# =============================================\n\n# Sample configuration file\n\nconfig3 = \"\"\"\nversion: 1\nhandlers:\n  console:\n    class: logging.StreamHandler\n    stream: ext://sys.stderr\n    formatter: basic\n  audit_file:\n    class: logging.FileHandler\n    filename: data/ch16_audit.log\n    encoding: utf-8\n    formatter: basic\nformatters:\n  basic:\n    style: \"{\"\n    format: \"{levelname:s}:{name:s}:{message:s}\"\nloggers:\n  verbose:\n    handlers: [console]\n    level: INFO\n    propagate: False # Added\n  audit:\n    handlers: [console,audit_file]\n    level: INFO\n    propagate: False # Added\nroot: # Added\n  handlers: [console]\n  level: INFO\n\"\"\"\n\n\nimport logging.config\nimport yaml\n\nconfig_dict = yaml.load(config3)\nprint(config_dict)\n\nlogging.config.dictConfig(config_dict)\n\n# Logging\n\nverbose = logging.getLogger(\"verbose.example.SomeClass\")\naudit = logging.getLogger(\"audit.example.SomeClass\")\nverbose.info(\"Verbose information\")\naudit.info(\"Audit record with before and after\")\n\nprint(\"Root Handlers:\", logging.getLogger().handlers)\nprint(\"Verbose Handlers:\", logging.getLogger('verbose').handlers)\nprint(\"Audit Handlers:\", logging.getLogger('audit').handlers)\n\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_16/ch16_ex4.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 16. Example 4.\n\"\"\"\n\n\n# Startup/Shutdown\n# =============================================\n\n# Some main function\n\nfrom typing import Dict, Counter\nimport logging\n\nimport collections\nfrom Chapter_16.ch16_ex1 import LoggedClass\n\n\nclass Main(LoggedClass):\n\n    def __init__(self) -> None:\n        self.counts: Counter[str] = collections.Counter()\n\n    def run(self) -> int:\n        self.logger.info(\"Start\")\n\n        # Some processing in and around the counter increments\n        self.counts[\"input\"] += 2000\n        self.counts[\"reject\"] += 500\n        self.counts[\"output\"] += 1500\n\n        self.logger.info(\"Counts %s\", self.counts)\n\n        for k in self.counts:\n            self.logger.info(f\"{k:.<16s} {self.counts[k]:>6,d}\")\n\n        return 0\n\n\nconfig3 = \"\"\"\nversion: 1\nhandlers:\n  console:\n    class: logging.StreamHandler\n    stream: ext://sys.stderr\n    formatter: control\n  audit_file:\n    class: logging.FileHandler\n    filename: data/ch16_audit.log\n    encoding: utf-8\n    formatter: basic\nformatters:\n  control:\n    style: \"{\"\n    format: \"{levelname:s}:{message:s}\"\n  basic:\n    style: \"{\"\n    format: \"{levelname:s}:{name:s}:{message:s}\"\nloggers:\n  verbose:\n    handlers: [console]\n    level: INFO\n    propagate: False # Added\n  audit:\n    handlers: [console,audit_file]\n    level: INFO\n    propagate: False # Added\nroot: # Added\n  handlers: [console]\n  level: INFO\ndisable_existing_loggers: False\n\"\"\"\n\n# Main program\n\n\ndef demo4a() -> None:\n    import sys\n    import logging\n    import logging.config\n    import yaml\n\n    logging.config.dictConfig(yaml.load(config3))\n    try:\n        application = Main()\n        status = application.run()\n    except Exception as e:\n        logging.exception(e)\n        status = 2\n    finally:\n        logging.shutdown()\n    # sys.exit(status)\n\n\n# Atexit\n\n\ndef demo4b() -> None:\n    import atexit\n    import logging\n    import logging.config\n    import yaml\n    import sys\n\n    logging.config.dictConfig(yaml.load(config3))\n    atexit.register(logging.shutdown)\n    try:\n        application = Main()\n        status = application.run()\n    except Exception as e:\n        logging.exception(e)\n        status = 2\n    # sys.exit(status)\n\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n\n    demo4a()\n    demo4b()\n"
  },
  {
    "path": "Chapter_16/ch16_ex5.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 16. Example 5.\n\"\"\"\nfrom typing import Type, Dict\nimport logging\nimport logging.config\nimport yaml\n\n# A context manager can be used, also.\n# Note that there are profound limitations when using dictConfig.\n# Any loggers created prior to running dictConfig wind up disconnected.\n# Be sure to include ``disable_existing_loggers: False`` in the dictionary.\n\n\n# Debugging\n# ==================\n\n# New Config\n\nconfig5 = \"\"\"\nversion: 1\ndisable_existing_loggers: False\nhandlers:\n  console:\n    class: logging.StreamHandler\n    stream: ext://sys.stderr\n    formatter: basic\n  audit_file:\n    class: logging.FileHandler\n    filename: data/ch16_audit.log\n    encoding: utf-8\n    formatter: detailed\nformatters:\n  basic:\n    style: \"{\"\n    format: \"{levelname:s}:{name:s}:{message:s}\"\n  detailed:\n    style: \"{\"\n    format: \"{levelname:s}:{name:s}:{asctime:s}:{message:s}\"\n    datefmt: \"%Y-%m-%d %H:%M:%S\"\nloggers:\n  audit:\n    handlers: [console,audit_file]\n    level: INFO\n    propagate: False\nroot:\n  handlers: [console]\n  level: INFO\ndisable_existing_loggers: False\n\"\"\"\n\n\n# Some classes\nfrom Chapter_16.ch16_ex1 import LoggedClass\n\n\nclass BettingStrategy(LoggedClass):\n\n    def bet(self) -> int:\n        raise NotImplementedError(\"No bet method\")\n\n    def record_win(self) -> None:\n        pass\n\n    def record_loss(self) -> None:\n        pass\n\n\nclass OneThreeTwoSix(BettingStrategy):\n\n    def __init__(self) -> None:\n        self.wins = 0\n\n    def _state(self) -> Dict[str, int]:\n        return dict(wins=self.wins)\n\n    def bet(self) -> int:\n        bet = {0: 1, 1: 3, 2: 2, 3: 6}[self.wins % 4]\n        self.logger.debug(f\"Bet {self._state()}; based on {bet}\")\n        return bet\n\n    def record_win(self) -> None:\n        self.wins += 1\n        self.logger.debug(f\"Win: {self._state()}\")\n\n    def record_loss(self) -> None:\n        self.wins = 0\n        self.logger.debug(f\"Loss: {self._state()}\")\n\n\n# A Decorator -- This confuses mypy\n\n\ndef audited(cls: Type) -> Type:\n    cls.logger = logging.getLogger(cls.__qualname__)\n    cls.audit = logging.getLogger(f\"audit.{cls.__qualname__}\")\n    return cls\n\n\n# A metaclass -- Much easier on mypy\n# Extending the basic logged class meta to add yet more features\n\nfrom Chapter_16.ch16_ex1 import LoggedClassMeta\n\nclass AuditedClassMeta(LoggedClassMeta):\n\n    def __new__(cls, name, bases, namespace, **kwds):\n        result = LoggedClassMeta.__new__(cls, name, bases, dict(namespace))\n        for item, type_ref in result.__annotations__.items():\n            if issubclass(type_ref, logging.Logger):\n                prefix = \"\" if item == \"logger\" else f\"{item}.\"\n                logger = logging.getLogger(f\"{prefix}{result.__qualname__}\")\n                setattr(result, item, logger)\n        return result\n\n\nclass AuditedClass(LoggedClass, metaclass=AuditedClassMeta):\n    audit: logging.Logger\n    pass\n\n\nclass Table(AuditedClass):\n\n    def bet(self, bet: str, amount: int) -> None:\n        self.logger.info(\"Betting %d on %s\", amount, bet)\n        self.audit.info(\"Bet:%r, Amount:%r\", bet, amount)\n\n\n# A Main Program demo\n\nimport atexit\n\nlogging.config.dictConfig(yaml.load(config5))\natexit.register(logging.shutdown)\nlog = logging.getLogger(\"main\")\nlog.info(\"Starting\")\nstrategy = OneThreeTwoSix()\napplication = Table()\napplication.bet(\"Black\", strategy.bet())\nstrategy.record_win()\napplication.bet(\"Black\", strategy.bet())\nstrategy.record_win()\napplication.bet(\"Black\", strategy.bet())\nstrategy.record_loss()\napplication.bet(\"Black\", strategy.bet())\nlog.info(\"Finish\")\nlogging.shutdown()\n\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_16/ch16_ex6.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 16. Example 6.\n\"\"\"\n\nfrom typing import Optional\nimport logging\nimport logging.config\nimport yaml\nimport getpass\n\n\nconfig5 = \"\"\"\nversion: 1\ndisable_existing_loggers: False\nhandlers:\n  console:\n    class: logging.StreamHandler\n    stream: ext://sys.stderr\n    formatter: basic\n  audit_file:\n    class: logging.FileHandler\n    filename: data/ch16_audit.log\n    encoding: utf-8\n    formatter: detailed\nformatters:\n  basic:\n    style: \"{\"\n    format: \"{levelname:s}:{name:s}:{message:s}\"\n  detailed:\n    style: \"{\"\n    format: \"{levelname:s}:{name:s}:{asctime:s}:{message:s}\"\n    datefmt: \"%Y-%m-%d %H:%M:%S\"\nloggers:\n  audit:\n    handlers: [console,audit_file]\n    level: INFO\n    propagate: False\nroot:\n  handlers: [console]\n  level: INFO\n\"\"\"\n\n# Extending\n# ====================\n\n# Doesn't seem to work as expected.\n\n# Note that the factory is somehow bypassed by a LoggerAdapter\n# Also. Thus mystifies mypy because we're adding attributes to the base class.\n\nclass UserLogRecordFactory:\n    def __init__(self) -> None:\n        self.user: Optional[str] = None\n        self.previous = logging.getLogRecordFactory()\n\n    def __call__(self, *args, **kwargs) -> logging.LogRecord:\n        print(\"Building log with \", args, kwargs)\n        user = getpass.getuser()\n        record = self.previous(*args, **kwargs)\n        record.user = user  # type: ignore\n        return record\n\n# Adapter. This kind of extension may not be needed.\n# The \"extra\" is set as the default behavior.\n# However, the processing is obscure. It behaves as if it bypassed the factory.\n# Yet. The code looks like it won't bypass the factory.\n\nclass UserLogAdapter(logging.LoggerAdapter):\n    def process(self, msg, kwargs):\n        kwargs['user'] = self.extra.get('user', None)\n        return msg, kwargs\n\n# Installation\n\nlogging.config.dictConfig(yaml.load(config5))\nlogging.setLogRecordFactory(UserLogRecordFactory())\n\n# Use\n\nlog = logging.getLogger(\"test.demo6\")\nfor h in logging.getLogger().handlers:\n    h.setFormatter(logging.Formatter(fmt=\"{user}:{name}:{levelname}:{message}\", style=\"{\"))\n\nimport threading\ndata = threading.local()\ndata.user = \"Some User\"\ndata.ip_address = \"127.0.0.1\"\n\nlog.info(\"message without User\")\nlog.info(\"message with user\")\nlog.info(\"message with extra\", extra={\"more\": \"More Data\"})\n\n# auth_log = logging.LoggerAdapter( log, data.__dict__ )  # \"Attempt to overwrite 'user' in LogRecord\"\n# auth_log = UserLogAdapter( log, data.__dict__ )  # _log() got an unexpected keyword argument 'user'\n# auth_log.info( \"message with User\" )\n\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_16/ch16_ex7.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 16. Example 7.\n\"\"\"\n\n\n# Warnings\n# ====================\n\n# Deprecation\n\nimport warnings\n\n\nclass Player:\n    \"\"\"version 2.1\"\"\"\n\n    def bet(self) -> None:\n        warnings.warn(\n            \"bet is deprecated, use place_bet\",\n            DeprecationWarning, stacklevel=2)\n        pass\n\n\nwarnings.simplefilter(\"always\", category=DeprecationWarning)\np2 = Player()\np2.bet()\n\n# Configuration\n\nimport warnings\n\ntry:\n    import simulation_model_1 as model\nexcept ImportError as e:\n    warnings.warn(repr(e))\nif 'model' not in globals():\n    try:\n        import simulation_model_2 as model\n    except ImportError as e:\n        warnings.warn(repr(e))\nif 'model' not in globals():\n    # raise ImportError(\"Missing simulation_model_1 and simulation_model_2\")\n    pass\n\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_16/ch16_ex8.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 16. Example 8.\n\"\"\"\n\n\n# Tail Buffer\n# ========================\n\n# Class Definition\n\n# Note. Logging has no type hints. So. mypy fails here.\n\nimport logging\nimport logging.config\nimport logging.handlers\nimport yaml\n\n\nclass TailHandler(logging.handlers.MemoryHandler):\n    def shouldFlush(self, record: logging.LogRecord) -> bool:\n        \"\"\"\n        Check for buffer full or a record at the flushLevel or higher.\n        \"\"\"\n        if record.levelno >= self.flushLevel:\n            return True\n        while len(self.buffer) > self.capacity:\n            self.acquire()\n            try:\n                del self.buffer[0]\n            finally:\n                self.release()\n        return False\n\n# Configuration\n\nconfig8 = \"\"\"\nversion: 1\ndisable_existing_loggers: False\nhandlers:\n  console:\n    class: logging.StreamHandler\n    stream: ext://sys.stderr\n    formatter: basic\n  tail:\n    (): __main__.TailHandler\n    target: cfg://handlers.console\n    capacity: 5\nformatters:\n  basic:\n    style: \"{\"\n    format: \"{levelname:s}:{name:s}:{message:s}\"\nloggers:\n  test:\n    handlers: [tail]\n    level: DEBUG\n    propagate: False\nroot:\n  handlers: [console]\n  level: INFO\n\"\"\"\n\n\n# Installation\n\nif __name__ == \"__main__\":\n    logging.config.dictConfig(yaml.load(config8))\n    log = logging.getLogger(\"test.demo8\")\n\n    # Use Case 1 -- last 5 before ERROR.\n\n    log.info(\"Last 5 before error\")\n\n    for i in range(20):\n        log.debug(f\"Message {i:d}\")\n\n    log.error(\"Error causes dump of last 5\")\n\n    # Use Case 2 -- last 5 before shutdown.\n\n    log.info(\"Last 5 before shutdown\")\n\n    for i in range(20, 40):\n        log.debug(f\"Message {i:d}\")\n\n    log.info(\"Shutdown causes dump of last 5\")\n\n    logging.shutdown()\n\n\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_16/ch16_ex9.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 16. Example 9.\n\"\"\"\n\nimport logging\nimport logging.config\nimport logging.handlers\nimport yaml\nimport time\n\n# Producer/Consumer\n# ==========================\n\n# The Consumer\n\nconsumer_config = \"\"\"\nversion: 1\ndisable_existing_loggers: False\nhandlers:\n  console:\n    class: logging.StreamHandler\n    stream: ext://sys.stderr\n    formatter: basic\nformatters:\n  basic:\n    style: \"{\"\n    format: \"{levelname:s}:{name:s}:{message:s}\"\nloggers:\n  combined:\n    handlers: [console]\n    formatter: detail\n    level: INFO\n    propagate: False\nroot:\n  handlers: [console]\n  level: INFO\n\"\"\"\n\nimport collections\nimport logging\nimport multiprocessing\n\n\nclass Log_Consumer_1(multiprocessing.Process):\n    \"\"\"In effect, an instance of QueueListener.\"\"\"\n\n    def __init__(self, queue):\n        self.source = queue\n        super().__init__()\n        logging.config.dictConfig(yaml.load(consumer_config))\n        self.combined = logging.getLogger(f\"combined.{self.__class__.__qualname__}\")\n        self.log = logging.getLogger(self.__class__.__qualname__)\n        self.counts = collections.Counter()\n\n    def run(self):\n        self.log.info(\"Consumer Started\")\n        while True:\n            log_record = self.source.get()\n            if log_record == None: break\n            self.combined.handle(log_record)\n            self.counts[log_record.getMessage()] += 1\n        self.log.info(\"Consumer Finished\")\n        self.log.info(self.counts)\n\n\n# The Producers\n\nclass Log_Producer(multiprocessing.Process):\n    handler_class = logging.handlers.QueueHandler\n\n    def __init__(self, proc_id, queue):\n        self.proc_id = proc_id\n        self.destination = queue\n        super().__init__()\n        self.log = logging.getLogger(\n            f\"{self.__class__.__qualname__}.{self.proc_id}\")\n        self.log.handlers = [self.handler_class(self.destination)]\n        self.log.setLevel(logging.INFO)\n\n    def run(self):\n        self.log.info(f\"Started\")\n        for i in range(100):\n            self.log.info(f\"Message {i:d}\")\n            time.sleep(0.001)\n        self.log.info(f\"Finished\")\n\ndef demo():\n\n    # The Queue\n\n    import multiprocessing\n    # size = 10  # Too small.\n    size = 30  # Better\n    queue1: multiprocessing.Queue = multiprocessing.Queue(size)\n\n    # The consumer process\n\n    consumer = Log_Consumer_1(queue1)\n    consumer.start()\n\n    # The producers\n\n    producers = []\n    for i in range(10):\n        proc = Log_Producer(i, queue1)\n        proc.start()\n        producers.append(proc)\n\n    # Normal termination\n\n    for p in producers:\n        p.join()\n\n    queue1.put(None)\n\n    consumer.join()\n\n    logging.shutdown()\n\n\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n    demo()\n"
  },
  {
    "path": "Chapter_17/__init__.py",
    "content": ""
  },
  {
    "path": "Chapter_17/ch17_data.csv",
    "content": "rate_in,time_in,distance_in,rate_out,time_out,distance_out\n2,3,,2,3,6\n5,,7,5,1.4,7\n,11,13,1.18,11,13\n"
  },
  {
    "path": "Chapter_17/ch17_ex1.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 17. Example 1.\n\"\"\"\n\n# Card and Deck\n# ========================\n\nfrom typing import Type, cast, Callable\nimport enum\n\n\nclass Suit(enum.Enum):\n    CLUB = \"♣\"\n    DIAMOND = \"♦\"\n    HEART = \"♥\"\n    SPADE = \"♠\"\n\n\nclass Card:\n\n    def __init__(\n        self, rank: int, suit: Suit, hard: int = None, soft: int = None\n    ) -> None:\n        self.rank = rank\n        self.suit = suit\n        self.hard = hard or int(rank)\n        self.soft = soft or int(rank)\n\n    def __str__(self) -> str:\n        return f\"{self.rank!s}{self.suit.value!s}\"\n\n\nclass AceCard(Card):\n\n    def __init__(self, rank: int, suit: Suit) -> None:\n        super().__init__(rank, suit, 1, 11)\n\n\nclass FaceCard(Card):\n\n    def __init__(self, rank: int, suit: Suit) -> None:\n        super().__init__(rank, suit, 10, 10)\n\n\nclass LogicError(Exception):\n    pass\n\n\ndef card(rank: int, suit: Suit) -> Card:\n    if rank == 1:\n        return AceCard(rank, suit)\n    elif 2 <= rank < 11:\n        return Card(rank, suit)\n    elif 11 <= rank < 14:\n        return FaceCard(rank, suit)\n    else:\n        raise LogicError(f\"Rank {rank} invalid\")\n\n\nimport random\n\n\nclass Deck1(list):\n\n    def __init__(self, size: int = 1) -> None:\n        super().__init__()\n        self.rng = random.Random()\n        for d in range(size):\n            for s in iter(Suit):\n                cards: List[Card] = (\n                    [cast(Card, AceCard(1, s))]\n                    + [Card(r, s) for r in range(2, 12)]\n                    + [FaceCard(r, s) for r in range(12, 14)]\n                )\n                super().extend(cards)\n        self.rng.shuffle(self)\n\n\nclass Deck2(list):\n\n    def __init__(\n        self,\n        size: int = 1,\n        random: random.Random = random.Random(),\n        ace_class: Type[Card] = AceCard,\n        card_class: Type[Card] = Card,\n        face_class: Type[Card] = FaceCard,\n    ) -> None:\n        super().__init__()\n        self.rng = random\n        for d in range(size):\n            for s in iter(Suit):\n                cards = (\n                    [ace_class(1, s)]\n                    + [card_class(r, s) for r in range(2, 12)]\n                    + [face_class(r, s) for r in range(12, 14)]\n                )\n                super().extend(cards)\n        self.rng.shuffle(self)\n\n\n# Card Test\n# ========================\n\n# Some Test Cases\n\nimport unittest\n\n\nclass TestCard(unittest.TestCase):\n\n    def setUp(self) -> None:\n        self.three_clubs = Card(3, Suit.CLUB)\n\n    def test_should_returnStr(self) -> None:\n        self.assertEqual(\"3♣\", str(self.three_clubs))\n\n    def test_should_getAttrValues(self) -> None:\n        self.assertEqual(3, self.three_clubs.rank)\n        self.assertEqual(Suit.CLUB, self.three_clubs.suit)\n        self.assertEqual(3, self.three_clubs.hard)\n        self.assertEqual(3, self.three_clubs.soft)\n\n\nclass TestAceCard(unittest.TestCase):\n\n    def setUp(self) -> None:\n        self.ace_spades = AceCard(1, Suit.SPADE)\n\n    @unittest.expectedFailure\n    def test_should_returnStr(self) -> None:\n        self.assertEqual(\"A♠\", str(self.ace_spades))\n\n    def test_should_getAttrValues(self) -> None:\n        self.assertEqual(1, self.ace_spades.rank)\n        self.assertEqual(Suit.SPADE, self.ace_spades.suit)\n        self.assertEqual(1, self.ace_spades.hard)\n        self.assertEqual(11, self.ace_spades.soft)\n\n\nclass TestFaceCard(unittest.TestCase):\n\n    def setUp(self) -> None:\n        self.queen_hearts = FaceCard(12, Suit.HEART)\n\n    @unittest.expectedFailure\n    def test_should_returnStr(self) -> None:\n        self.assertEqual(\"Q♥\", str(self.queen_hearts))\n\n    def test_should_getAttrValues(self) -> None:\n        self.assertEqual(12, self.queen_hearts.rank)\n        self.assertEqual(Suit.HEART, self.queen_hearts.suit)\n        self.assertEqual(10, self.queen_hearts.hard)\n        self.assertEqual(10, self.queen_hearts.soft)\n\n\n# Suite\n\n\ndef suite2() -> unittest.TestSuite:\n    s = unittest.TestSuite()\n    load_from = unittest.defaultTestLoader.loadTestsFromTestCase\n    s.addTests(load_from(TestCard))\n    s.addTests(load_from(TestAceCard))\n    s.addTests(load_from(TestFaceCard))\n    return s\n\n\nif __name__ == \"__main__\":\n    t = unittest.TextTestRunner()\n    t.run(suite2())\n\n\n# Card Factory Test\n# =============================\n\n# Another Test Case\n\n\nclass TestCardFactory(unittest.TestCase):\n\n    def test_rank1_should_createAceCard(self) -> None:\n        c = card(1, Suit.CLUB)\n        self.assertIsInstance(c, AceCard)\n\n    def test_rank2_should_createCard(self) -> None:\n        c = card(2, Suit.DIAMOND)\n        self.assertIsInstance(c, Card)\n\n    def test_rank10_should_createCard(self) -> None:\n        c = card(10, Suit.HEART)\n        self.assertIsInstance(c, Card)\n\n    def test_rank10_should_createFaceCard(self) -> None:\n        c = card(11, Suit.SPADE)\n        self.assertIsInstance(c, Card)\n\n    def test_rank13_should_createFaceCard(self) -> None:\n        c = card(13, Suit.CLUB)\n        self.assertIsInstance(c, Card)\n\n    def test_otherRank_should_exception(self) -> None:\n        with self.assertRaises(LogicError):\n            c = card(14, Suit.DIAMOND)\n        with self.assertRaises(LogicError):\n            c = card(0, Suit.DIAMOND)\n\n\n# Another Suite\n\n\ndef suite3() -> unittest.TestSuite:\n    s = unittest.TestSuite()\n    s.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestCardFactory))\n    return s\n\n\nif __name__ == \"__main__\":\n    t = unittest.TextTestRunner()\n    t.run(suite3())\n\n# Deck with Mock Card\n# ==============================\n\n# Class Definitions\n\n\nclass DeckEmpty(Exception):\n    pass\n\n\nclass Deck3(list):\n\n    def __init__(\n        self,\n        size: int = 1,\n        random: random.Random = random.Random(),\n        card_factory: Callable[[int, Suit], Card] = card,\n    ) -> None:\n        super().__init__()\n        self.rng = random\n        for d in range(size):\n            super().extend(\n                [card_factory(r, s) for r in range(1, 14) for s in iter(Suit)]\n            )\n        self.rng.shuffle(self)\n\n    def deal(self) -> Card:\n        try:\n            return self.pop(0)\n        except IndexError:\n            raise DeckEmpty()\n\n\n# Test Cases\n\nimport unittest\nimport unittest.mock\n\n\nclass TestDeckBuild(unittest.TestCase):\n\n    def setUp(self) -> None:\n        self.mock_card = unittest.mock.Mock(return_value=unittest.mock.sentinel.card)\n        self.mock_rng = unittest.mock.Mock(wraps=random.Random())\n        self.mock_rng.shuffle = unittest.mock.Mock()\n\n    def test_Deck3_should_build(self) -> None:\n        d = Deck3(size=1, random=self.mock_rng, card_factory=self.mock_card)\n        self.assertEqual(52 * [unittest.mock.sentinel.card], d)\n        self.mock_rng.shuffle.assert_called_with(d)\n        self.assertEqual(52, len(self.mock_card.mock_calls))\n        expected = [\n            unittest.mock.call(r, s)\n            for r in range(1, 14)\n            for s in (Suit.CLUB, Suit.DIAMOND, Suit.HEART, Suit.SPADE)\n        ]\n        self.assertEqual(expected, self.mock_card.mock_calls)\n\n\nclass TestDeckDeal(unittest.TestCase):\n\n    def setUp(self) -> None:\n        self.mock_deck = [getattr(unittest.mock.sentinel, str(x)) for x in range(52)]\n        self.mock_card = unittest.mock.Mock(side_effect=self.mock_deck)\n        self.mock_rng = unittest.mock.Mock(wraps=random.Random())\n        self.mock_rng.shuffle = unittest.mock.Mock()\n\n    def test_Deck3_should_deal(self) -> None:\n        d = Deck3(size=1, random=self.mock_rng, card_factory=self.mock_card)\n        dealt = []\n        for i in range(52):\n            card = d.deal()\n            dealt.append(card)\n        self.assertEqual(dealt, self.mock_deck)\n\n    def test_empty_deck_should_exception(self) -> None:\n        d = Deck3(size=1, random=self.mock_rng, card_factory=self.mock_card)\n        for i in range(52):\n            card = d.deal()\n        self.assertRaises(DeckEmpty, d.deal)\n\n\n# Suite\n\n\ndef suite4():\n    s = unittest.TestSuite()\n    s.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestDeckBuild))\n    s.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestDeckDeal))\n    return s\n\n\nif __name__ == \"__main__\":\n    t = unittest.TextTestRunner()\n    t.run(suite4())\n\n# Doctest\n# ===============\n\n# Sample Function with doctest string\n\n\ndef ackermann(m: int, n: int) -> int:\n    \"\"\"Ackermann's Function\n    ackermann(m, n) = $2 \\\\uparrow^{m-2} (n+3)-3$\n\n    See http://en.wikipedia.org/wiki/Ackermann_function and\n    http://en.wikipedia.org/wiki/Knuth%27s_up-arrow_notation.\n\n    >>> from Chapter_17.ch17_ex1 import ackermann\n    >>> ackermann(2,4)\n    11\n    >>> ackermann(0,4)\n    5\n    >>> ackermann(1,0)\n    2\n    >>> ackermann(1,1)\n    3\n\n    \"\"\"\n    if m == 0:\n        return n + 1\n    elif m > 0 and n == 0:\n        return ackermann(m - 1, 1)\n    elif m > 0 and n > 0:\n        return ackermann(m - 1, ackermann(m, n - 1))\n    else:\n        raise LogicError()\n\n\nif __name__ == \"__main__\":\n    import doctest\n\n    suite5 = doctest.DocTestSuite()\n    t = unittest.TextTestRunner(verbosity=2)\n    t.run(suite5)\n\n\n# Combined Testing\n# =========================\n\n# Main Program to combine suites\n\nif __name__ == \"__main__\":\n    all_tests = unittest.TestSuite()\n    all_tests.addTests(suite2())\n    all_tests.addTests(suite3())\n    all_tests.addTests(suite4())\n    all_tests.addTests(suite5)\n    t = unittest.TextTestRunner()\n    t.run(all_tests)\n\n# OS testing\n# ======================\n\n# Functions to test\n\nfrom collections import defaultdict\nfrom typing import NamedTuple, Dict, List\n\n\nclass GameStat(NamedTuple):\n    player: str\n    bet: str\n    rounds: int\n    final: int\n\n\nimport csv\nfrom pathlib import Path\nfrom typing import Iterable, Iterator, Dict, DefaultDict, List\n\n\ndef gamestat_iter(source: Iterable[Dict[str, str]]) -> Iterator[GameStat]:\n    for row in source:\n        yield GameStat(row[\"player\"], row[\"bet\"], int(row[\"rounds\"]), int(row[\"final\"]))\n\n\ndef rounds_final(path: Path) -> DefaultDict[int, List[int]]:\n    stats: DefaultDict[int, List[int]] = defaultdict(list)\n    with path.open() as source:\n        reader = csv.DictReader(source)\n        assert set(reader.fieldnames) == set(GameStat._fields)\n        for gs in gamestat_iter(reader):\n            stats[gs.rounds].append(gs.final)\n    return stats\n\n\n# Two approaches:\n#\n# - io.StringIO()\n#\n# - create a file\n\n# We might want to test missing or damaged file features, in which\n# case StringIO doesn't work as well as creating a file.\n\n# Test Cases\n\nimport os\n\n\nclass Test_Missing(unittest.TestCase):\n\n    def setUp(self) -> None:\n        try:\n            (Path.cwd() / \"data\" / \"ch17_sample.csv\").unlink()\n            # print(f\"setUp removed {(Path.cwd()/\"data\"/\"ch17_sample.csv\")}\")\n        except OSError as e:\n            pass\n            # print(\"setUp expected\", e)\n\n    def test_missingFile_should_returnDefault(self) -> None:\n        self.assertRaises(\n            FileNotFoundError, rounds_final, (Path.cwd() / \"data\" / \"ch17_sample.csv\")\n        )\n\n\nclass Test_Damaged(unittest.TestCase):\n\n    def setUp(self) -> None:\n        with (Path.cwd() / \"data\" / \"ch17_sample.csv\").open(\"w\") as target:\n            print(\"not_player,bet,rounds,final\", file=target)\n            print(\"data,1,1,1\", file=target)\n\n    def test_damagedFile_should_raiseException(self) -> None:\n        self.assertRaises(\n            AssertionError, rounds_final, (Path.cwd() / \"data\" / \"ch17_sample.csv\")\n        )\n\n\ndef suite7():\n    s = unittest.TestSuite()\n    s.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(Test_Missing))\n    s.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(Test_Damaged))\n    return s\n\n\nif __name__ == \"__main__\":\n    t = unittest.TextTestRunner()\n    t.run(suite7())\n\n# External CSV Examples\n# ======================\n\n# Unit Under Test\n\nfrom Chapter_4.ch04_ex3 import RateTimeDistance, RTD_Dynamic\n\n# Sample data\n\nsample_data = \"\"\"\\\nrate_in,time_in,distance_in,rate_out,time_out,distance_out\n2,3,,2,3,6\n5,,7,5,1.4,7\n,11,13,1.18,11,13\n\"\"\"\n\n# Parse the sample data\n\nfrom typing import Optional\nimport csv\n\n\ndef float_or_none(text: str) -> Optional[float]:\n    if len(text) == 0:\n        return None\n    return float(text)\n\n\n# TestCase with only one test method\n\n\nclass Test_RTD(unittest.TestCase):\n\n    def runTest(self) -> None:\n        with (Path.cwd() / \"data\" / \"ch17_data.csv\").open() as source:\n            rdr = csv.DictReader(source)\n            for row in rdr:\n                self.example(**row)\n\n    def example(\n        self,\n        rate_in: str,\n        time_in: str,\n        distance_in: str,\n        rate_out: str,\n        time_out: str,\n        distance_out: str,\n    ) -> None:\n        args = dict(\n            rate=float_or_none(rate_in),\n            time=float_or_none(time_in),\n            distance=float_or_none(distance_in),\n        )\n        expected = dict(\n            rate=float(rate_out), time=float(time_out), distance=float(distance_out)\n        )\n        rtd = RateTimeDistance(**args)\n        assert rtd.distance and rtd.rate and rtd.time\n        self.assertAlmostEqual(rtd.distance, rtd.rate * rtd.time, places=2)\n        self.assertAlmostEqual(rtd.rate, expected[\"rate\"], places=2)\n        self.assertAlmostEqual(rtd.time, expected[\"time\"], places=2)\n        self.assertAlmostEqual(rtd.distance, expected[\"distance\"], places=2)\n\n\n# Build Suite from user-supplied sample data\n\nwith (Path.cwd() / \"data\" / \"ch17_data.csv\").open(\"w\", newline=\"\") as target:\n    target.write(sample_data)\n\n\ndef suite9():\n    suite = unittest.TestSuite()\n    suite.addTest(Test_RTD())\n    return suite\n\n\nif __name__ == \"__main__\":\n    t = unittest.TextTestRunner()\n    t.run(suite9())\n\n# Performance Testing\n# ======================\n\n# Using unittest for this is a bit \"forced.\" We don't really need unittest framework\n# for this.\n\nimport unittest\nimport timeit\n\n\nclass Test_Performance(unittest.TestCase):\n\n    def test_simpleCalc_shouldbe_fastEnough(self):\n        t = timeit.timeit(\n            stmt=\"\"\"RateTimeDistance(rate=1, time=2)\"\"\",\n            setup=\"\"\"from Chapter_4.ch04_ex3 import RateTimeDistance\"\"\",\n        )\n        print(\"Run time\", t)\n        self.assertLess(t, 10, f\"run time {t} >= 10\")\n\n\n# Make a suite of the testcases\n\n\ndef suite10():\n    s = unittest.TestSuite()\n    s.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(Test_Performance))\n    return s\n\n\nif __name__ == \"__main__\":\n    t = unittest.TextTestRunner()\n    t.run(suite10())\n"
  },
  {
    "path": "Chapter_17/ch17_ex2.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 17. Example 2.\n\"\"\"\nimport unittest\n\n\n# SQLite testing\n# =========================\n\n# This is integration testing, not unit testing.\n# Integration means we use the database\n# instead of isolating our code from the database.\n# A more formal unit test would mock the database layer.\n\n\n# SQLAlchemy ORM classes\n\nfrom typing import Any\nfrom Chapter_12.ch12_ex4 import Base, Blog, Post, Tag, assoc_post_tag\nimport datetime\n\n\nimport sqlalchemy.exc\nfrom sqlalchemy import create_engine\n\n\ndef build_test_db(name=\"sqlite:///./data/ch17_blog.db\"):\n    \"\"\"\n    Create Test Database and Schema\n    \"\"\"\n    engine = create_engine(name, echo=True)\n    Base.metadata.drop_all(engine)\n    Base.metadata.create_all(engine)\n    return engine\n\n\n# Unittest Case\n\nfrom sqlalchemy.orm import sessionmaker, Session\n\n\nclass Test_Blog_Queries(unittest.TestCase):\n\n    Session: Any\n    session: Session\n\n    @staticmethod\n    def setUpClass() -> None:\n        engine = build_test_db()\n        Test_Blog_Queries.Session = sessionmaker(bind=engine)\n        session = Test_Blog_Queries.Session()\n\n        tag_rr = Tag(phrase=\"#RedRanger\")\n        session.add(tag_rr)\n        tag_w42 = Tag(phrase=\"#Whitby42\")\n        session.add(tag_w42)\n        tag_icw = Tag(phrase=\"#ICW\")\n        session.add(tag_icw)\n        tag_mis = Tag(phrase=\"#Mistakes\")\n        session.add(tag_mis)\n\n        blog1 = Blog(title=\"Travel 2013\")\n        session.add(blog1)\n        b1p1 = Post(\n            date=datetime.datetime(2013, 11, 14, 17, 25),\n            title=\"Hard Aground\",\n            rst_text=\"\"\"Some embarrassing revelation. Including ☹ and ⚓︎\"\"\",\n            blog=blog1,\n            tags=[tag_rr, tag_w42, tag_icw],\n        )\n        session.add(b1p1)\n        b1p2 = Post(\n            date=datetime.datetime(2013, 11, 18, 15, 30),\n            title=\"Anchor Follies\",\n            rst_text=\"\"\"Some witty epigram. Including ☺ and ☀︎︎\"\"\",\n            blog=blog1,\n            tags=[tag_rr, tag_w42, tag_mis],\n        )\n        session.add(b1p2)\n\n        blog2 = Blog(title=\"Travel 2014\")\n        session.add(blog2)\n        session.commit()\n\n    def setUp(self) -> None:\n        self.session = Test_Blog_Queries.Session()\n\n    def test_query_eqTitle_should_return1Blog(self) -> None:\n        \"\"\"Tests schema definition\"\"\"\n        results = self.session.query(Blog).filter(Blog.title == \"Travel 2013\").all()\n        self.assertEqual(1, len(results))\n        self.assertEqual(2, len(results[0].entries))\n\n    def test_query_likeTitle_should_return2Blog(self) -> None:\n        \"\"\"Tests SQLAlchemy, and test data\"\"\"\n        results = self.session.query(Blog).filter(Blog.title.like(\"Travel %\")).all()\n        self.assertEqual(2, len(results))\n\n    def test_query_eqW42_tag_should_return2Post(self) -> None:\n        results = self.session.query(Post).join(assoc_post_tag).join(Tag).filter(\n            Tag.phrase == \"#Whitby42\"\n        ).all()\n        self.assertEqual(2, len(results))\n\n    def test_query_eqICW_tag_should_return1Post(self) -> None:\n        results = self.session.query(Post).join(assoc_post_tag).join(Tag).filter(\n            Tag.phrase == \"#ICW\"\n        ).all()\n        # print( [r.title for r in results] )\n        self.assertEqual(1, len(results))\n        self.assertEqual(\"Hard Aground\", results[0].title)\n        self.assertEqual(\"Travel 2013\", results[0].blog.title)\n        self.assertEqual(\n            set([\"#RedRanger\", \"#Whitby42\", \"#ICW\"]),\n            set(t.phrase for t in results[0].tags),\n        )\n\n\n# Make a suite of the testcases\n\n\ndef suite8() -> unittest.TestSuite:\n    s = unittest.TestSuite()\n    s.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(Test_Blog_Queries))\n    return s\n\n\nif __name__ == \"__main__\":\n    t = unittest.TextTestRunner()\n    t.run(suite8())\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_17/test_ch17.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 17. Example 2.\n\n..  note::\n\n    This example's name, ``test_ch17.py`` is chosen to help pytest\n    do test discovery.\n\"\"\"\n\n# Card and Deck\n# ========================\n\nimport enum\nfrom typing import cast, Type\nimport random\n\nclass Suit(enum.Enum):\n    CLUB = \"♣\"\n    DIAMOND = \"♦\"\n    HEART = \"♥\"\n    SPADE = \"♠\"\n\n\nclass Card:\n\n    def __init__(\n        self, rank: int, suit: Suit, hard: int = None, soft: int = None\n    ) -> None:\n        self.rank = rank\n        self.suit = suit\n        self.hard = hard or int(rank)\n        self.soft = soft or int(rank)\n\n    def __str__(self) -> str:\n        return f\"{self.rank!s}{self.suit.value!s}\"\n\n\nclass AceCard(Card):\n\n    def __init__(self, rank: int, suit: Suit) -> None:\n        super().__init__(rank, suit, 1, 11)\n\n\nclass FaceCard(Card):\n\n    def __init__(self, rank: int, suit: Suit) -> None:\n        super().__init__(rank, suit, 10, 10)\n\n\nclass LogicError(Exception):\n    pass\n\n\ndef card(rank: int, suit: Suit) -> Card:\n    if rank == 1:\n        return AceCard(rank, suit)\n    elif 2 <= rank < 11:\n        return Card(rank, suit)\n    elif 11 <= rank < 14:\n        return FaceCard(rank, suit)\n    else:\n        raise LogicError(f\"Rank {rank} invalid\")\n\n\nclass Deck1(list):\n\n    def __init__(self, size: int = 1) -> None:\n        super().__init__()\n        self.rng = random.Random()\n        for d in range(size):\n            for s in iter(Suit):\n                cards: List[Card] = (\n                    [cast(Card, AceCard(1, s))]\n                    + [Card(r, s) for r in range(2, 12)]\n                    + [FaceCard(r, s) for r in range(12, 14)]\n                )\n                super().extend(cards)\n        self.rng.shuffle(self)\n\n\nclass Deck2(list):\n\n    def __init__(\n        self,\n        size: int = 1,\n        random: random.Random = random.Random(),\n        ace_class: Type[Card] = AceCard,\n        card_class: Type[Card] = Card,\n        face_class: Type[Card] = FaceCard,\n    ) -> None:\n        super().__init__()\n        self.rng = random\n        for d in range(size):\n            for s in iter(Suit):\n                cards = (\n                    [ace_class(1, s)]\n                    + [card_class(r, s) for r in range(2, 12)]\n                    + [face_class(r, s) for r in range(12, 14)]\n                )\n                super().extend(cards)\n        self.rng.shuffle(self)\n\n\n# Card Test\n# ========================\n\n# Some Test Cases\nfrom pytest import mark\n\n\ndef test_card():\n    three_clubs = Card(3, Suit.CLUB)\n    assert \"3♣\" == str(three_clubs)\n    assert 3 == three_clubs.rank\n    assert Suit.CLUB == three_clubs.suit\n    assert 3 == three_clubs.hard\n    assert 3 == three_clubs.soft\n\n@mark.xfail\ndef test_ace_card():\n    ace_spades = AceCard(1, Suit.SPADE)\n    assert \"A♠\" == str(ace_spades), \"This is expected to fail\"\n    assert 1 == ace_spades.rank\n    assert Suit.SPADE == ace_spades.suit\n    assert 1 == ace_spades.hard\n    assert 11 == ace_spades.soft\n\n@mark.xfail\ndef test_face_card():\n    queen_hearts = FaceCard(12, Suit.HEART), \"This is expected to fail\"\n    assert \"Q♥\" == str(queen_hearts)\n    assert 12 == queen_hearts.rank\n    assert Suit.HEART == queen_hearts.suit\n    assert 10 == queen_hearts.hard\n    assert 10 == queen_hearts.soft\n\n\n# Suites -- not relevant for pytest -- the test discovery handles this.\n\n\n# Card Factory Test\n# =============================\n\n# Another Test Case\n\nfrom pytest import raises\n\ndef test_card_factory():\n\n    c1 = card(1, Suit.CLUB)\n    assert isinstance(c1, AceCard)\n\n    c2 = card(2, Suit.DIAMOND)\n    assert isinstance(c1, Card)\n\n    c10 = card(10, Suit.HEART)\n    assert isinstance(c10, Card)\n\n    cj = card(11, Suit.SPADE)\n    assert isinstance(cj, FaceCard)\n\n    ck = card(13, Suit.CLUB)\n    assert isinstance(ck, FaceCard)\n\n    with raises(LogicError):\n        c14 = card(14, Suit.DIAMOND)\n\n    with raises(LogicError):\n        c0 = card(0, Suit.DIAMOND)\n\n\n# Deck with Mock Card\n# ==============================\n\n# Class Definitios\n\n\nclass DeckEmpty(Exception):\n    pass\n\n\nclass Deck3(list):\n\n    def __init__(self, size=1, random=random.Random(), card_factory=card):\n        super().__init__()\n        self.rng = random\n        for d in range(size):\n            super().extend([card_factory(r, s) for r in range(1, 14) for s in iter(Suit)])\n        self.rng.shuffle(self)\n\n    def deal(self):\n        try:\n            return self.pop(0)\n        except IndexError:\n            raise DeckEmpty()\n\n\n# Test Cases\n\nimport unittest.mock\nfrom types import SimpleNamespace\nfrom pytest import fixture\n\n@fixture\ndef deck_context():\n    mock_deck = [\n        getattr(unittest.mock.sentinel, str(x))\n        for x in range(52)\n    ]\n    mock_card = unittest.mock.Mock(side_effect=mock_deck)\n    mock_rng = unittest.mock.Mock(\n        wraps=random.Random,\n        shuffle=unittest.mock.Mock(return_value=None)\n    )\n    return SimpleNamespace(**locals())\n\n\ndef test_deck_build(deck_context):\n    d = Deck3(\n        size=1,\n        random=deck_context.mock_rng,\n        card_factory=deck_context.mock_card\n    )\n    deck_context.mock_rng.shuffle.assert_called_once_with(d)\n    assert 52 == len(deck_context.mock_card.mock_calls)\n    expected = [\n        unittest.mock.call(r, s) for r in range(1, 14) for s in iter(Suit)\n    ]\n    assert expected == deck_context.mock_card.mock_calls\n\n\ndef test_deck_deal(deck_context):\n    d = Deck3(\n        size=1,\n        random=deck_context.mock_rng,\n        card_factory=deck_context.mock_card\n    )\n    dealt = []\n    for c in range(52):\n        c = d.deal()\n        dealt.append(c)\n    assert deck_context.mock_deck == dealt\n\n    with raises(DeckEmpty):\n        extra = d.deal()\n\n# Doctest\n# ===============\n\n# Sample Function with doctest string. pytest finds these, too.\n\n\ndef ackermann(m, n):\n    \"\"\"Ackermann's Function\n    ackermann(m, n) = $2 \\\\uparrow^{m-2} (n+3)-3$\n\n    See http://en.wikipedia.org/wiki/Ackermann_function and\n    http://en.wikipedia.org/wiki/Knuth%27s_up-arrow_notation.\n\n    >>> ackermann(2,4)\n    11\n    >>> ackermann(0,4)\n    5\n    >>> ackermann(1,0)\n    2\n    >>> ackermann(1,1)\n    3\n\n    \"\"\"\n    if m == 0:\n        return n + 1\n    elif m > 0 and n == 0:\n        return ackermann(m - 1, 1)\n    elif m > 0 and n > 0:\n        return ackermann(m - 1, ackermann(m, n - 1))\n\n\n# OS testing\n# ======================\n\n# Functions to test\n\nfrom collections import defaultdict\n\nfrom typing import NamedTuple, Dict, List\n\nclass GameStat(NamedTuple):\n    player: str\n    bet: str\n    rounds: int\n    final: int\n\nimport csv\nfrom pathlib import Path\n\n\ndef gamestat_iter(iterator):\n    for row in iterator:\n        yield GameStat(row[\"player\"], row[\"bet\"], int(row[\"rounds\"]), int(row[\"final\"]))\n\n\ndef rounds_final(path: Path):\n    stats: Dict[int, List[int]] = defaultdict(list)\n    with path.open() as source:\n        reader = csv.DictReader(source)\n        assert set(reader.fieldnames) == set(GameStat._fields)\n        for gs in gamestat_iter(reader):\n            stats[gs.rounds].append(gs.final)\n    return stats\n\n\n# Two approaches:\n#\n# - io.StringIO()\n#\n# - create a file\n\n# We might want to test missing or damaged file features, in which\n# case StringIO doesn't work as well as creating a file.\n\n# Test Cases\n\nfrom pytest import fixture, mark\n\n@fixture\ndef no_file_path():\n    file_path = Path.cwd() / \"data\" / \"ch17_sample.csv\"\n    try:\n        file_path.unlink()\n        # print(f\"no_file_path fixture removed {file_path}\")\n    except FileNotFoundError as e:\n        pass\n    yield file_path\n    # Cleanup can go here\n\ndef test_missing(no_file_path):\n    with raises(FileNotFoundError):\n        rounds_final(no_file_path)\n\n\n@fixture\ndef damaged_file_path():\n    file_path = Path.cwd() / \"data\" / \"ch17_sample.csv\"\n    with file_path.open(\"w\", newline=\"\") as target:\n        print(\"not_player,bet,rounds,final\", file=target)\n        print(\"data,1,1,1\", file=target)\n    yield file_path\n    file_path.unlink()\n\ndef test_damaged(damaged_file_path):\n    with raises(AssertionError):\n        stats = rounds_final(Path.cwd()/\"data\"/\"ch17_sample.csv\")\n\n\n# SQLite testing\n# =========================\n\n# This is integration testing, not unit testing.\n# Integration means we use the database\n# instead of isolating our code from the database.\n# A more formal unit test would mock the database layer.\n\n# SQLAlchemy ORM classes\n\nfrom Chapter_12.ch12_ex4 import Base, Blog, Post, Tag, assoc_post_tag\nimport datetime\n\n# Create Test Database and Schema\n\nimport sqlalchemy.exc\nfrom sqlalchemy import create_engine\n\n\ndef built_test_db(name=\"sqlite:///./data/ch17_blog.db\"):\n    engine = create_engine(name, echo=True)\n    Base.metadata.drop_all(engine)\n    Base.metadata.create_all(engine)\n    return engine\n\n# Unittest Case\n\nfrom sqlalchemy.orm import sessionmaker\n\n@fixture(scope=\"module\")\ndef db_session_maker():\n    engine = built_test_db()\n    session_maker = sessionmaker(bind=engine)\n\n    session = session_maker()\n    tag_rr = Tag(phrase=\"#RedRanger\")\n    session.add(tag_rr)\n    tag_w42 = Tag(phrase=\"#Whitby42\")\n    session.add(tag_w42)\n    tag_icw = Tag(phrase=\"#ICW\")\n    session.add(tag_icw)\n    tag_mis = Tag(phrase=\"#Mistakes\")\n    session.add(tag_mis)\n\n    blog1 = Blog(title=\"Travel 2013\")\n    session.add(blog1)\n    b1p1 = Post(\n        date=datetime.datetime(2013, 11, 14, 17, 25),\n        title=\"Hard Aground\",\n        rst_text=\"\"\"Some embarrassing revelation. Including ☹ and ⚓︎\"\"\",\n        blog=blog1,\n        tags=[tag_rr, tag_w42, tag_icw],\n    )\n    session.add(b1p1)\n    b1p2 = Post(\n        date=datetime.datetime(2013, 11, 18, 15, 30),\n        title=\"Anchor Follies\",\n        rst_text=\"\"\"Some witty epigram. Including ☺ and ☀︎︎\"\"\",\n        blog=blog1,\n        tags=[tag_rr, tag_w42, tag_mis],\n    )\n    session.add(b1p2)\n\n    blog2 = Blog(title=\"Travel 2014\")\n    session.add(blog2)\n    session.commit()\n    return session_maker\n\ndef test_database(db_session_maker):\n\n    db_session = db_session_maker()\n\n    # Tests schema definition\n    results = db_session.query(Blog).filter(Blog.title == \"Travel 2013\").all()\n    assert 1 == len(results)\n    assert 2 == len(results[0].entries)\n\n    # Tests SQLAlchemy, and test data\n    results = db_session.query(Blog).filter(Blog.title.like(\"Travel %\")).all()\n    assert 2 == len(results)\n\n    results = db_session.query(Post).join(assoc_post_tag).join(Tag).filter(\n        Tag.phrase == \"#Whitby42\"\n    ).all()\n    assert 2 == len(results)\n\n    results = db_session.query(Post).join(assoc_post_tag).join(Tag).filter(\n        Tag.phrase == \"#ICW\"\n    ).all()\n    # print( [r.title for r in results] )\n    assert 1 == len(results)\n    assert \"Hard Aground\" == results[0].title\n    assert \"Travel 2013\" == results[0].blog.title\n    assert set([\"#RedRanger\", \"#Whitby42\", \"#ICW\"]) == set(t.phrase for t in results[0].tags)\n\n# External CSV Examples\n# ======================\n\n# Unit Under Test\n\nfrom Chapter_4.ch04_ex3 import RateTimeDistance\nfrom pytest import approx\n\n# Parsing the sample data\n\ndef float_or_none(text):\n    if len(text) == 0:\n        return None\n    return float(text)\n\n# Build Suite from user-supplied sample data\n\nimport csv\n\nwith (Path.cwd() / \"Chapter_17\" / \"ch17_data.csv\").open() as source:\n    rdr = csv.DictReader(source)\n    rtd_cases = list(rdr)\n\n@fixture(params=rtd_cases)\ndef rtd_example(request):\n    \"\"\"Each request will include a param. This will be a row from the source cases.\"\"\"\n    yield request.param\n\ndef test_rtd(rtd_example):\n    args = dict(\n        rate=float_or_none(rtd_example['rate_in']),\n        time=float_or_none(rtd_example['time_in']),\n        distance=float_or_none(rtd_example['distance_in']),\n    )\n    result = dict(\n        rate=float_or_none(rtd_example['rate_out']),\n        time=float_or_none(rtd_example['time_out']),\n        distance=float_or_none(rtd_example['distance_out']),\n    )\n    # print(f\"***{args}***\")\n\n    rtd = RateTimeDistance(**args)\n\n    assert rtd.distance == approx(rtd.rate * rtd.time)\n    assert rtd.rate == approx(result[\"rate\"], abs=1E-2)\n    assert rtd.time == approx(result[\"time\"])\n    assert rtd.distance == approx(result[\"distance\"])\n\n\n\n\n# Performance Testing\n# ======================\n\n# This can be used stand-alone, or with pytest.\n\nimport timeit\n\ndef test_performance():\n\n    t = timeit.timeit(\n        stmt=\"\"\"RateTimeDistance( rate=1, time=2 )\"\"\",\n        setup=\"\"\"from Chapter_4.ch04_ex3 import RateTimeDistance\"\"\",\n    )\n    print(\"Run time\", t)\n    assert t < 10, f\"run time {t} >= 10\"\n"
  },
  {
    "path": "Chapter_18/__init__.py",
    "content": ""
  },
  {
    "path": "Chapter_18/ch18_demo.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 18. demo.\n\n\"\"\"\nimport sys\nprint(sys.argv)\n\n"
  },
  {
    "path": "Chapter_18/ch18_ex1.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 18. Example 1.\n\"\"\"\n\nimport os\n\n# Command-line parsing\n# =======================\n\nimport argparse\nimport sys\nimport logging\nfrom typing import List, Optional\n\n__version__ = \"2e\"\n\n\ndef get_options_1(\n    argv: List[str] = sys.argv[1:], defaults: Optional[argparse.Namespace] = None\n) -> argparse.Namespace:\n    \"\"\"\n    Parse command-line arguments and options\n\n    :param argv: Command line, default ``sys.argv[1:]``\n    :param defaults: an ``argparse.Namespace`` with defaults\n    :return: argparse.Namespace with parameters\n    \"\"\"\n    # Step 1: Parser.\n\n    parser = argparse.ArgumentParser(\n        prog=\"ch18_ex1.py\",\n        description=\"Simulate Blackjack\",\n        add_help=False,\n        formatter_class=argparse.ArgumentDefaultsHelpFormatter,\n    )\n\n    # Step 2: Arguments.\n\n    # Simple on-off options\n\n    parser.add_argument(\"-v\", \"--verbose\", action=\"store_true\", default=False)\n    parser.add_argument(\n        \"--debug\",\n        action=\"store_const\",\n        const=logging.DEBUG,\n        default=logging.INFO,\n        dest=\"logging_level\",\n    )\n\n    # Options with values\n\n    parser.add_argument(\n        \"--dealerhit\",\n        action=\"store\",\n        default=\"Hit17\",\n        choices=[\"Hit17\", \"Stand17\"],\n        dest=\"dealer_rule\",\n    )\n    parser.add_argument(\n        \"--resplit\",\n        action=\"store\",\n        default=\"ReSplit\",\n        choices=[\"ReSplit\", \"NoReSplit\", \"NoReSplitAces\"],\n        dest=\"split_rule\",\n    )\n\n    parser.add_argument(\n        \"--decks\", action=\"store\", default=6, type=int, help=\"Decks to deal\"\n    )\n    parser.add_argument(\"--limit\", action=\"store\", default=50, type=int)\n    parser.add_argument(\"--payout\", action=\"store\", default=\"(3,2)\")\n\n    parser.add_argument(\n        \"-p\",\n        \"--playerstrategy\",\n        action=\"store\",\n        default=\"SomeStrategy\",\n        choices=[\"SomeStrategy\", \"AnotherStrategy\"],\n        dest=\"player_rule\",\n    )\n    parser.add_argument(\n        \"-b\",\n        \"--bet\",\n        action=\"store\",\n        default=\"Flat\",\n        choices=[\"Flat\", \"Martingale\", \"OneThreeTwoSix\"],\n        dest=\"betting_rule\",\n    )\n    parser.add_argument(\"-r\", \"--rounds\", action=\"store\", default=100, type=int)\n    parser.add_argument(\"-s\", \"--stake\", action=\"store\", default=50, type=int)\n\n    parser.add_argument(\n        \"--samples\",\n        action=\"store\",\n        default=int(os.environ.get(\"SIM_SAMPLES\", 100)),\n        type=int,\n        help=\"Samples to generate\",\n    )\n\n    # Arguments\n\n    parser.add_argument(\"outputfile\", action=\"store\", metavar=\"output\")  # required\n\n    # Version and help\n\n    parser.add_argument(\"-V\", \"--version\", action=\"version\", version=__version__)\n    parser.add_argument(\"-?\", \"--help\", action=\"help\")\n\n    # Step 3: Parse\n\n    return parser.parse_args(argv, namespace=defaults)\n\n\n# Examples\n\ntest_parsing_1 = \"\"\"\n    >>> config1 = get_options_1(\n    ...     [\"data/ch18_simulation1.dat\"]\n    ... )\n    >>> config1\n    Namespace(betting_rule='Flat', dealer_rule='Hit17', decks=6, limit=50, logging_level=20, outputfile='data/ch18_simulation1.dat', payout='(3,2)', player_rule='SomeStrategy', rounds=100, samples=100, split_rule='ReSplit', stake=50, verbose=False)\n    \n    >>> config2 = get_options_1(\n    ...     [\"-v\", \"--samples\", \"2\", \"data/ch18_simulation2.dat\"]\n    ... )\n    >>> config2\n    Namespace(betting_rule='Flat', dealer_rule='Hit17', decks=6, limit=50, logging_level=20, outputfile='data/ch18_simulation2.dat', payout='(3,2)', player_rule='SomeStrategy', rounds=100, samples=2, split_rule='ReSplit', stake=50, verbose=True)\n    \n    >>> config3 = get_options_1(\n    ...     [\"-b\", \"Martingale\", \"--samples\", \"3\", \"data/ch18_simulation3.dat\"]\n    ... )\n    >>> config3\n    Namespace(betting_rule='Martingale', dealer_rule='Hit17', decks=6, limit=50, logging_level=20, outputfile='data/ch18_simulation3.dat', payout='(3,2)', player_rule='SomeStrategy', rounds=100, samples=3, split_rule='ReSplit', stake=50, verbose=False)\n    \n    >>> import shlex\n    >>> config4 = get_options_1(\n    ...     shlex.split(\"-b Martingale --samples 3 data/ch18_simulation3.dat\")\n    ... )\n    >>> config4\n    Namespace(betting_rule='Martingale', dealer_rule='Hit17', decks=6, limit=50, logging_level=20, outputfile='data/ch18_simulation3.dat', payout='(3,2)', player_rule='SomeStrategy', rounds=100, samples=3, split_rule='ReSplit', stake=50, verbose=False)\n\"\"\"\n\nimport pytest\n\n\ndef test_get_config_1(capsys):\n\n    with pytest.raises(SystemExit) as exception:\n        get_options_1(\n            [\"-b\", \"Doesn't Work\", \"--samples\", \"x\", \"data/ch18_simulation3.dat\"]\n        )\n\n    assert exception.value.args == (2,)\n    out, err = capsys.readouterr()\n    expected_err = \"\"\"\\\nusage: ch18_ex1.py [-v] [--debug] [--dealerhit {Hit17,Stand17}]\n                   [--resplit {ReSplit,NoReSplit,NoReSplitAces}]\n                   [--decks DECKS] [--limit LIMIT] [--payout PAYOUT]\n                   [-p {SomeStrategy,AnotherStrategy}]\n                   [-b {Flat,Martingale,OneThreeTwoSix}] [-r ROUNDS]\n                   [-s STAKE] [--samples SAMPLES] [-V] [-?]\n                   output\nch18_ex1.py: error: argument -b/--bet: invalid choice: \"Doesn't Work\" (choose from 'Flat', 'Martingale', 'OneThreeTwoSix')\n\"\"\"\n    assert expected_err == err\n\n\n# Supplying Defaults\n# =====================\n\n# Simple\n\nconfig4 = argparse.Namespace()\nconfig4.dealer_rule = \"Hit17\"\nconfig4.split_rule = \"NoReSplitAces\"\nconfig4.limit = 50\nconfig4.decks = 6\nconfig4.payout = \"(3,2)\"\nconfig4.player_rule = \"SomeStrategy\"\nconfig4.betting_rule = \"Flat\"\nconfig4.rounds = 100\nconfig4.stake = 50\nconfig4.outputfile = \"data/ch18_simulation4.dat\"\nconfig4.samples = int(os.environ.get(\"SIM_SAMPLES\", 200))\n\ntest_parsing_2 = \"\"\"\n    >>> config5 = get_options_1(\n    ...    [\"-b\", \"OneThreeTwoSix\", \"data/ch18_simulation4.dat\"], defaults=config4\n    ... )\n    >>> config5\n    Namespace(betting_rule='OneThreeTwoSix', dealer_rule='Hit17', decks=6, limit=50, logging_level=20, outputfile='data/ch18_simulation4.dat', payout='(3,2)', player_rule='SomeStrategy', rounds=100, samples=200, split_rule='NoReSplitAces', stake=50, verbose=False)\n\"\"\"\n\n# Path manipulations\n# ===================\n\nfrom pathlib import Path\n\ntest_path_examples = \"\"\"\n    >>> p = Path.cwd() / \"data\" / \"simulation.csv\"\n    >>> p.name\n    'simulation.csv'\n    >>> p.suffix\n    '.csv'\n    >>> p.exists()\n    False\n\"\"\"\n\ntest_path_example2 = \"\"\"\n    >>> source_path = Path(\"data\")/\"ch14_simulation.dat\"\n    >>> with source_path.open() as source_file:\n    ...     count = 0\n    ...     for line in source_file:\n    ...         if len(line) > 0:\n    ...              count += 1\n    >>> count\n    100\n\"\"\"\n\ntest_path_example3 = \"\"\"\n    >>> if (Path(\"data\")/\"ch18_directory\").exists():\n    ...     (Path(\"data\")/\"ch18_directory\").rmdir()\n    \n    >>> target = Path(\"data\")/\"ch18_directory\"\n    >>> target.mkdir(exist_ok=True, parents=True)\n    >>> (Path(\"data\")/\"ch18_directory\").exists()\n    True\n    \n    >>> import datetime\n    >>> today = datetime.datetime.today()\n    >>> today = datetime.datetime(2019, 3, 18)\n    >>> target = Path(\"data\")/today.strftime(\"%Y%m%d\")\n    >>> target.mkdir(exist_ok=True, parents=True)\n    >>> target.exists()\n    True\n\"\"\"\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n\n    import pytest\n\n    pytest.main([\"Chapter_18/ch18_ex1.py\"])\n\n    get_options_1(['--help'])\n"
  },
  {
    "path": "Chapter_18/ch18_ex2.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 18. Example 2.\n\"\"\"\n\n# Command-line parsing\n# =======================\n\n\n# Supply Defaults Via ChainMap built from configuration files and environment variables.\n\n\nfrom collections import ChainMap\nfrom typing import Optional, cast, Dict, Any, List, Type\nfrom pathlib import Path\nimport yaml\nimport sys\nimport os\nimport argparse\n\nfrom Chapter_18.ch18_ex1 import get_options_1\n\n\ndef nint(x: Optional[str]) -> Optional[int]:\n    if x is None:\n        return x\n    return int(x)\n\n\ndef get_options_2(argv: List[str] = sys.argv[1:]) -> argparse.Namespace:\n    \"\"\"\n    Get arguments and options\n\n    :param argv: default sys.argv[1:]\n    :return: argparse.Namespace\n    \"\"\"\n    # 1. Get files\n    config_locations = (\n        Path.cwd(),\n        Path.home(),\n        Path.cwd() / \"opt\",  # A stand-in for Path(\"/etc\") or Path(\"/opt\")\n        Path(__file__) / \"config\",\n        # Other common places...\n        # Path(\"~someapp\").expanduser(),\n    )\n\n    candidate_paths = (dir / \"ch18app.yaml\" for dir in config_locations)\n    config_paths = (path for path in candidate_paths if path.exists())\n    files_values = [yaml.load(str(path)) for path in config_paths]\n\n    # 2. Get potential overrides from the run-time environment\n    env_settings = [\n        (\"samples\", nint(os.environ.get(\"SIM_SAMPLES\", None))),\n        (\"stake\", nint(os.environ.get(\"SIM_STAKE\", None))),\n        (\"rounds\", nint(os.environ.get(\"SIM_ROUNDS\", None))),\n    ]\n    env_values = {k: v for k, v in env_settings if v is not None}\n\n    # 3. Build defaults\n    defaults = argparse.Namespace(\n        **ChainMap(\n            env_values,  # check here first\n            *files_values  # All of the files, in order\n        )\n    )\n\n    # 4. Use the previously-defined argument parser.\n    return get_options_1(argv, defaults)\n\n\ntest_env_override = \"\"\"\n    >>> config5a = get_options_2(\n    ...        [\"-b\", \"OneThreeTwoSix\", \"data/ch18_simulation5.dat\"],\n    ... )\n    >>> config5a\n    Namespace(betting_rule='OneThreeTwoSix', dealer_rule='Hit17', decks=6, limit=50, logging_level=20, outputfile='data/ch18_simulation5.dat', payout='(3,2)', player_rule='SomeStrategy', rounds=100, samples=100, split_rule='ReSplit', stake=50, verbose=False)\n\n    \n    >>> os.environ[\"SIM_STAKE\"] = \"100\"  # Mock the user's environment\n    >>> config5b = get_options_2(\n    ...        [\"-b\", \"OneThreeTwoSix\", \"data/ch18_simulation5.dat\"],\n    ... )\n    >>> config5b\n    Namespace(betting_rule='OneThreeTwoSix', dealer_rule='Hit17', decks=6, limit=50, logging_level=20, outputfile='data/ch18_simulation5.dat', payout='(3,2)', player_rule='SomeStrategy', rounds=100, samples=100, split_rule='ReSplit', stake=100, verbose=False)\n\n\"\"\"\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_18/ch18_ex3.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 18. Example 3.\n\"\"\"\n\n# Preliminary Definitions\n# ========================\n\n# Import the simulation model we've been using.\n# Plus a handy validation function that assures the output is sensible.\n# And logging, since we'll use it for some examples.\n\nfrom Chapter_14.simulation_model import *\n\nimport logging\nfrom pprint import pprint\nfrom pathlib import Path\nimport os\n\n\n# Top-level Function\n# ===================\n\n# Here's the main feature of a program as a top-level function.\n\n\nimport ast\nimport csv\nimport argparse\n\n\ndef simulate_blackjack(config: argparse.Namespace) -> None:\n    dealer_classes = {\"Hit17\": Hit17, \"Stand17\": Stand17}\n    dealer_rule = dealer_classes[config.dealer_rule]()\n    split_classes = {\n        \"ReSplit\": ReSplit, \"NoReSplit\": NoReSplit, \"NoReSplitAces\": NoReSplitAces\n    }\n    split_rule = split_classes[config.split_rule]()\n    try:\n        payout = ast.literal_eval(config.payout)\n        assert len(payout) == 2\n    except Exception as ex:\n        raise ValueError(f\"Invalid payout {config.payout}\") from ex\n    table = Table(\n        decks=config.decks,\n        limit=config.limit,\n        dealer=dealer_rule,\n        split=split_rule,\n        payout=payout,\n    )\n    player_classes = {\"SomeStrategy\": SomeStrategy, \"AnotherStrategy\": AnotherStrategy}\n    player_rule = player_classes[config.player_rule]()\n    betting_classes = {\n        \"Flat\": Flat, \"Martingale\": Martingale, \"OneThreeTwoSix\": OneThreeTwoSix\n    }\n    betting_rule = betting_classes[config.betting_rule]()\n    player = Player(\n        play=player_rule,\n        betting=betting_rule,\n        max_rounds=config.rounds,\n        init_stake=config.stake,\n    )\n    simulate = Simulate(table, player, config.samples)\n    with Path(config.outputfile).open(\"w\", newline=\"\") as target:\n        wtr = csv.writer(target)\n        wtr.writerows(simulate)\n\n\n# Using the top-level function\n\nfrom Chapter_18.ch18_ex2 import get_options_2\n\nif __name__ == \"__main__\":\n    arguments = [\"-b\", \"OneThreeTwoSix\", \"data/ch18_simulation5.dat\"]\n    config_1 = get_options_2(arguments)\n    simulate_blackjack(config_1)\n    check(Path.cwd() / \"data\" / \"ch18_simulation5.dat\")\n\n\n# Here's how we can build the configuration as a context manager.\n# It's consistent with using a context manager for logging setup.\n\nfrom typing import List\n\n\nclass Build_Config:\n\n    def __init__(self, argv: List[str]) -> None:\n        self.options = get_options_2(argv)\n\n    def __enter__(self) -> argparse.Namespace:\n        return self.options\n\n    def __exit__(self, *exc) -> None:\n        return\n\n\n# Using the top-level function with a context manager that collects\n# the configuration.\n\nif __name__ == \"__main__\":\n    arguments = [\"-b\", \"OneThreeTwoSix\", \"data/ch18_simulation5.dat\"]\n    with Build_Config(arguments) as config_2:\n        simulate_blackjack(config_2)\n        check(Path.cwd() / \"data\" / \"ch18_simulation5.dat\")\n\n# Logging and config as context\n# ===============================\n\n\nimport logging.config\nimport sys\n\n# Here's logging setup as a context manager.\n\n\nclass Setup_Logging:\n\n    def __init__(self, stream=sys.stderr, disable_existing_loggers=False) -> None:\n        \"\"\"\n        Preserves existing loggers.\n        \"\"\"\n        self.config = dict(\n            version=1,\n            handlers={\n                \"console\": {\n                    \"class\": \"logging.StreamHandler\",\n                    \"stream\": stream,\n                    \"formatter\": \"basic\",\n                }\n            },\n            formatters={\n                \"basic\": {\"format\": \"{name} ({levelname}) {message}\", \"style\": \"{\"}\n            },\n            root={\"handlers\": [\"console\"], \"level\": logging.INFO},\n            disable_existing_loggers=disable_existing_loggers,\n        )\n\n    def __enter__(self) -> \"Setup_Logging\":\n        logging.config.dictConfig(self.config)\n        return self\n\n    def __exit__(self, *exc) -> None:\n        logging.shutdown()\n        return\n\n\n# The downside of using a dictConfig as a context manager is that\n# logging objects created before logging is configured don't connect.\n# properly to the root logger with usable handlers.\n\n\nclass ClassLogger:\n    log = logging.getLogger(\"ClassLogger\")\n\n    def work(self) -> None:\n        self.log.info(\"Some Info\")\n        self.log.warning(\"A Warning\")\n\n\nclass InstanceLogger:\n\n    def __init__(self, name: str) -> None:\n        self.log = logging.getLogger(f\"InstanceLogger.{name}\")\n\n    def work(self) -> None:\n        self.log.info(\"Some Info\")\n        self.log.warning(\"A Warning\")\n\n\n# Here's a main function that uses two nested contexts.\n\ntest_nested_contexts_1 = \"\"\"\n    Loggers created outside the context\n    *will* be ignored because disable_existing_loggers is True.\n    This includes loggers in class definitions.\n    \n    >>> il_early = InstanceLogger(\"ignored\")\n    >>> with Setup_Logging(disable_existing_loggers=True, stream=sys.stdout):\n    ...     cl = ClassLogger()\n    ...     il = InstanceLogger(\"good\")\n    ...     cl.work()\n    ...     il.work()\n    ...     il_early.work()\n    InstanceLogger.good (INFO) Some Info\n    InstanceLogger.good (WARNING) A Warning\n\"\"\"\n\ntest_nested_contexts_2 = \"\"\"\n    All loggers *will* be used because disable_existing_loggers is False\n    \n    >>> il_early = InstanceLogger(\"retained\")\n    >>> with Setup_Logging(disable_existing_loggers=False, stream=sys.stdout):\n    ...     cl = ClassLogger()\n    ...     il = InstanceLogger(\"good\")\n    ...     cl.work()\n    ...     il.work()\n    ...     il_early.work()\n    ClassLogger (INFO) Some Info\n    ClassLogger (WARNING) A Warning\n    InstanceLogger.good (INFO) Some Info\n    InstanceLogger.good (WARNING) A Warning\n    InstanceLogger.retained (INFO) Some Info\n    InstanceLogger.retained (WARNING) A Warning\n\"\"\"\n\n\n# PITL via Function Composition\n# =======================================\n\n# Example of adding features through more\n# top-level functions.\n\n\ndef simulate_blackjack_betting(config: argparse.Namespace) -> None:\n    for bet_class in \"Flat\", \"Martingale\", \"OneThreeTwoSix\":\n        config.betting_rule = bet_class\n        config.outputfile = Path(\"data\")/f\"ch18_simulation6_{bet_class}.dat\"\n        simulate_blackjack(config)\n\n\n# This works reasonably well. We can do a bit more with object\n# composition.\n\n# Top script\n\nif __name__ == \"__main__\":\n    arguments = [\"-b\", \"OneThreeTwoSix\", \"data/ch18_simulation5.dat\"]\n    with Setup_Logging():\n        with Build_Config(arguments) as config_3:\n            simulate_blackjack_betting(config_3)\n    check(Path.cwd() / \"data\" / \"ch18_simulation6_Flat.dat\")\n    check(Path.cwd() / \"data\" / \"ch18_simulation6_Martingale.dat\")\n    check(Path.cwd() / \"data\" / \"ch18_simulation6_OneThreeTwoSix.dat\")\n\n# PITL via Object Composition\n# =======================================\n\n# Proper **Command** design pattern\n\nfrom typing import Dict, Any, Type\n\n\nclass Command:\n    \"\"\"\n    Typical use\n\n    >>> c = Command()\n    >>> c.configure(argparse.Namespace(item=\"value\"))\n    >>> c.run()\n    \"\"\"\n\n    def __init__(self) -> None:\n        self.config: Dict[str, Any] = {}\n\n    def configure(self, namespace: argparse.Namespace) -> None:\n        self.config.update(vars(namespace))\n\n    def run(self) -> None:\n        \"\"\"Overridden by a subclass\"\"\"\n        pass\n\n\nclass Simulate_Command(Command):\n    dealer_rule_map = {\"Hit17\": Hit17, \"Stand17\": Stand17}\n    split_rule_map = {\n        \"ReSplit\": ReSplit, \"NoReSplit\": NoReSplit, \"NoReSplitAces\": NoReSplitAces\n    }\n    player_rule_map = {\"SomeStrategy\": SomeStrategy, \"AnotherStrategy\": AnotherStrategy}\n    betting_rule_map = {\n        \"Flat\": Flat, \"Martingale\": Martingale, \"OneThreeTwoSix\": OneThreeTwoSix\n    }\n\n    def run(self) -> None:\n        dealer_rule = self.dealer_rule_map[self.config[\"dealer_rule\"]]()\n        split_rule = self.split_rule_map[self.config[\"split_rule\"]]()\n        payout: Tuple[int, int]\n        try:\n            payout = ast.literal_eval(self.config[\"payout\"])\n            assert len(payout) == 2\n        except Exception as e:\n            raise Exception(f\"Invalid payout {self.config['payout']!r}\") from e\n        table = Table(\n            decks=self.config[\"decks\"],\n            limit=self.config[\"limit\"],\n            dealer=dealer_rule,\n            split=split_rule,\n            payout=payout,\n        )\n        player_rule = self.player_rule_map[self.config[\"player_rule\"]]()\n        betting_rule = self.betting_rule_map[self.config[\"betting_rule\"]]()\n        player = Player(\n            play=player_rule,\n            betting=betting_rule,\n            max_rounds=self.config[\"rounds\"],\n            init_stake=self.config[\"stake\"],\n        )\n        simulate = Simulate(table, player, self.config[\"samples\"])\n        with Path(self.config[\"outputfile\"]).open(\"w\", newline=\"\") as target:\n            wtr = csv.writer(target)\n            wtr.writerows(simulate)\n\n\nif __name__ == \"__main__\":\n    arguments = [\"-b\", \"OneThreeTwoSix\", \"data/ch18_simulation5.dat\"]\n    with Setup_Logging():\n        with Build_Config(arguments) as config_4:\n            main = Simulate_Command()\n            main.configure(config_4)\n            main.run()\n\n\n# Composition\n\n\nclass Analyze_Command(Command):\n\n    def run(self) -> None:\n        with Path(self.config[\"outputfile\"]).open() as target:\n            rdr = csv.reader(target)\n            outcomes = (float(row[10]) for row in rdr)\n            first = next(outcomes)\n            sum_0, sum_1 = 1, first\n            value_min = value_max = first\n            for value in outcomes:\n                sum_0 += 1  # value**0\n                sum_1 += value  # value**1\n                value_min = min(value_min, value)\n                value_max = max(value_max, value)\n            mean = sum_1 / sum_0\n            print(\n                f\"{self.config['outputfile']}\\n\"\n                f\"Mean = {mean:.1f}\\n\"\n                f\"House Edge = {1 - mean / 50:.1%}\\n\"\n                f\"Range = {value_min:.1f} {value_max:.1f}\"\n            )\n\n\nclass Command_Sequence(Command):\n    \"\"\"\n    Subclass provides a sequence of classes. These will be expanded into instance and configured.\n    \"\"\"\n    steps: List[Type[Command]] = []\n\n    def __init__(self) -> None:\n        self._sequence = [class_() for class_ in self.steps]\n\n    def configure(self, config: argparse.Namespace) -> None:\n        for step in self._sequence:\n            step.configure(config)\n\n    def run(self) -> None:\n        for step in self._sequence:\n            step.run()\n\n\nclass Simulate_and_Analyze(Command_Sequence):\n    steps = [Simulate_Command, Analyze_Command]\n\n\nif __name__ == \"__main__\":\n    with Build_Config(arguments) as config_5:\n        both = Simulate_and_Analyze()\n        both.configure(config_5)\n        both.run()\n    check(Path.cwd() / \"data\" / \"ch18_simulation6_Flat.dat\")\n    check(Path.cwd() / \"data\" / \"ch18_simulation6_Martingale.dat\")\n    check(Path.cwd() / \"data\" / \"ch18_simulation6_OneThreeTwoSix.dat\")\n\n\n# Wrapping another class in a For-All loop.\n\n\nclass ForAllBets_Simulate(Command):\n\n    def run(self) -> None:\n        for bet_class in \"Flat\", \"Martingale\", \"OneThreeTwoSix\":\n            self.config[\"betting_rule\"] = bet_class\n            self.config[\"outputfile\"] = Path(\"data\")/f\"ch18_simulation7_{bet_class}.dat\"\n            sim = Simulate_Command()\n            # sim.config = self.config  # Push the configuration directly.\n            sim.configure(argparse.Namespace(**self.config))\n            sim.run()\n\n\nif __name__ == \"__main__\":\n    with Build_Config(arguments) as config_6:\n        msc = ForAllBets_Simulate()\n        msc.configure(config_6)\n        msc.run()\n    check(Path.cwd() / \"data\" / \"ch18_simulation7_Flat.dat\")\n    check(Path.cwd() / \"data\" / \"ch18_simulation7_Martingale.dat\")\n    check(Path.cwd() / \"data\" / \"ch18_simulation7_OneThreeTwoSix.dat\")\n\n\n# Overall doctest\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_18/ch18app.yaml",
    "content": "# User configuration file: Chapter_18/ch18app.yaml\ndealer_rule: Hit17\nsplit_rule: NoReSplitAces\n"
  },
  {
    "path": "Chapter_18/opt/ch18app.yaml",
    "content": "# Installation-wide configuration file: Chapter_18/opt/ch18app.yaml\n\nrounds: 100\nsamples: 100\nstake: 50\n"
  },
  {
    "path": "Chapter_19/__init__.py",
    "content": ""
  },
  {
    "path": "Chapter_19/ch19_ex1.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 19. Example 1.\n\"\"\"\n\nimport unittest\n\n# Import the test suite.\nfrom Chapter_19.tests import test_all\n\nif __name__ == \"__main__\":\n    # Execute all tests which can be discovered in the suite.\n    unittest.main(test_all, verbosity=2)\n"
  },
  {
    "path": "Chapter_19/ch19_ex2.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 19. Example 2.\n\"\"\"\n\nimport pytest\n\nif __name__ == \"__main__\":\n    # Execute all tests which can be discovered in the suite.\n    pytest.main([\"-v\", \"tests\"])\n"
  },
  {
    "path": "Chapter_19/some_algorithm/__init__.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 19. Example 3.\n\"\"\"\n\n# Package with variant implementations\n# -------------------------------------\n\n# A complex import\n\nimport sys\n\nfrom typing import Type\n\nfrom Chapter_19.some_algorithm.abstraction import AbstractSomeAlgorithm\n\nSomeAlgorithm: Type[AbstractSomeAlgorithm]\n\nif sys.platform.endswith(\"32\"):\n    # print(f\"{sys.platform}: SHORT\")\n    from Chapter_19.some_algorithm.short_version import *\n\n    SomeAlgorithm = Implementation_Short\nelse:\n    # print(f\"{sys.platform}: LONG\")\n    from Chapter_19.some_algorithm.long_version import *\n\n    SomeAlgorithm = Implementation_Long\n\n# Some additional debugging to display the import behavior\n\nprint(f\"{__name__}: {SomeAlgorithm.__module__}\\n{SomeAlgorithm.__doc__}\")\n"
  },
  {
    "path": "Chapter_19/some_algorithm/abstraction.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 19. Example 3.\n\"\"\"\n\n\nclass AbstractSomeAlgorithm:\n    pass\n"
  },
  {
    "path": "Chapter_19/some_algorithm/long_version.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 19. Example 3.\n\"\"\"\n\n# Long-version Implementation\n# -------------------------------------\n\nfrom .abstraction import AbstractSomeAlgorithm\n\n\nclass Implementation_Long(AbstractSomeAlgorithm):\n    \"\"\"\n    The Long Version\n    \"\"\"\n\n    def value(self) -> int:\n        return 2 ** 42\n"
  },
  {
    "path": "Chapter_19/some_algorithm/short_version.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 19. Example 3.\n\"\"\"\n\n# Short-version Implementation\n# -------------------------------------\n\nfrom .abstraction import AbstractSomeAlgorithm\n\n\nclass Implementation_Short(AbstractSomeAlgorithm):\n    \"\"\"\n    The Short Version\n    \"\"\"\n\n    def value(self) -> int:\n        return 42\n"
  },
  {
    "path": "Chapter_19/tests/__init__.py",
    "content": ""
  },
  {
    "path": "Chapter_19/tests/test_all.py",
    "content": "# A Test Module\n# ----------------\n\n\"\"\"Test all features of some_algorithm.\n\nRequires some_algorithm package be on the PYTHONPATH.\n\"\"\"\n\n# Imports\n\nimport unittest\nfrom Chapter_19 import some_algorithm\n\n# Test Case\n\n\nclass TestSomeAlgorithm(unittest.TestCase):\n\n    def test_import_should_see_value(self):\n        x = some_algorithm.SomeAlgorithm()\n        assert 2 ** 42 == x.value()\n\n\n# Run the implicit test suite if this is used as a main program.\n\nif __name__ == \"__main__\":\n    unittest.main(exit=False)\n"
  },
  {
    "path": "Chapter_2/__init__.py",
    "content": ""
  },
  {
    "path": "Chapter_2/ch02_ex1.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 2. Example 1.\n\"\"\"\nfrom typing import Tuple\n\n\nclass Card:\n\n    def __init__(self, rank: str, suit: str) -> None:\n        self.suit = suit\n        self.rank = rank\n        self.hard, self.soft = self._points()\n\n    def _points(self) -> Tuple[int, int]:\n        return int(self.rank), int(self.rank)\n\n\nclass AceCard(Card):\n\n    def _points(self) -> Tuple[int, int]:\n        return 1, 11\n\n\nclass FaceCard(Card):\n\n    def _points(self) -> Tuple[int, int]:\n        return 10, 10\n\n\ntest_card = \"\"\"\n    >>> x = Card('2','♠')\n    >>> str(x)  # doctest: +ELLIPSIS\n    '<....Card object at ...>'\n    >>> repr(x)  # doctest: +ELLIPSIS\n    '<....Card object at ...>'\n    >>> print(x)  # doctest: +ELLIPSIS\n    <....Card object at ...>\n    >>> cards = [AceCard('A', '♠'), Card('2','♠'), FaceCard('J','♠'),]\n    >>> cards  # doctest: +ELLIPSIS\n    [<...AceCard ...>, <...Card ...>, <...FaceCard ...>]\n    \n\"\"\"\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_2/ch02_ex2.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 2. Example 2.\n\"\"\"\n\nfrom typing import Tuple, Any, Union, cast\n\n# Definition of a simple class hierarchy.\n# Note the overlap with a dataclass if we use properties.\n\n\nclass Card:\n    insure = False\n\n    def __init__(self, rank: str, suit: Any) -> None:\n        self.suit = suit\n        self.rank = rank\n        self.hard, self.soft = self._points()\n\n    def __eq__(self, other: Any) -> bool:\n        return (\n            self.suit == cast(\"Card\", other).suit\n            and self.rank == cast(\"Card\", other).rank\n            and self.hard == cast(\"Card\", other).hard\n            and self.soft == cast(\"Card\", other).soft\n        )\n\n    def __repr__(self) -> str:\n        return f\"{self.__class__.__name__}(suit={self.suit!r}, rank={self.rank!r})\"\n\n    def __str__(self) -> str:\n        return f\"{self.rank}{self.suit}\"\n\n    def _points(self) -> Tuple[int, int]:\n        return int(self.rank), int(self.rank)\n\n\nclass AceCard(Card):\n    insure = True\n\n    def _points(self) -> Tuple[int, int]:\n        return 1, 11\n\n\nclass FaceCard(Card):\n\n    def _points(self) -> Tuple[int, int]:\n        return 10, 10\n\n\n# We can create cards like this\n\ntest_card = \"\"\"\n    >>> Suit.Club\n    <Suit.Club: '♣'>\n    >>> d1 = [AceCard('A', '♠'), Card('2', '♠'), FaceCard('Q', '♠'), ]\n    >>> d1\n    [AceCard(suit='♠', rank='A'), Card(suit='♠', rank='2'), FaceCard(suit='♠', rank='Q')]\n    >>> Card('2', '♠')\n    Card(suit='♠', rank='2')\n    >>> str(Card('2', '♠'))\n    '2♠'\n\"\"\"\n\n# Instead of strings, we can use an enum\n\nfrom enum import Enum\n\n\nclass Suit(str, Enum):\n    Club = \"♣\"\n    Diamond = \"♦\"\n    Heart = \"♥\"\n    Spade = \"♠\"\n\n\n# We can create cards like this\n\ntest_card_suit = \"\"\"\n    >>> cards = [AceCard('A', Suit.Spade), Card('2', Suit.Spade), FaceCard('Q', Suit.Spade),]\n    >>> cards\n    [AceCard(suit=<Suit.Spade: '♠'>, rank='A'), Card(suit=<Suit.Spade: '♠'>, rank='2'), FaceCard(suit=<Suit.Spade: '♠'>, rank='Q')]\n\"\"\"\n\ntest_suit_value = \"\"\"\n    >>> Suit.Heart.value\n    '♥'\n    >>> Suit.Heart.value = 'H'  # doctest: +IGNORE_EXCEPTION_DETAIL\n    Traceback (most recent call last):\n      File \"/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py\", line 1329, in __run\n        compileflags, 1), test.globs)\n      File \"<doctest __main__.__test__.test_suit_value[1]>\", line 1, in <module>\n        Suit.Heart.value = 'H'\n      File \"/Users/slott/miniconda3/envs/py37/lib/python3.7/types.py\", line 175, in __set__\n        raise AttributeError(\"can't set attribute\")\n    AttributeError: can't set attribute\n    \n\"\"\"\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_2/ch02_ex3.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 2. Example 3.\n\"\"\"\n\nfrom Chapter_2.ch02_ex2 import *\nfrom typing import cast, Iterable, Iterator\n\n# Factory Function\n\n\ndef card(rank: int, suit: Suit) -> Card:\n    if rank == 1:\n        return AceCard(\"A\", suit)\n    elif 2 <= rank < 11:\n        return Card(str(rank), suit)\n    elif 11 <= rank < 14:\n        name = {11: \"J\", 12: \"Q\", 13: \"K\"}[rank]\n        return FaceCard(name, suit)\n    raise Exception(\"Design Failure\")\n\n\n# This function builds a Card from a numeric rank and a Suit object. We can now # build cards very simply.\n\ntest_card = \"\"\"\n    >>> deck = [card(rank, suit) for rank in range(1, 14) for suit in (Suit.Club, Suit.Diamond, Suit.Heart, Suit.Spade)]\n    >>> len(deck)\n    52\n    >>> sorted(set(c.suit for c in deck))\n    [<Suit.Spade: '♠'>, <Suit.Club: '♣'>, <Suit.Heart: '♥'>, <Suit.Diamond: '♦'>]\n    >>> sorted(set(c.rank for c in deck))\n    ['10', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'J', 'K', 'Q']\n\"\"\"\n\ndeck = [\n    card(rank, suit) for rank in range(1, 14) for suit in cast(Iterable[Suit], Suit)\n]\ndeck_l = [\n    card(rank, suit) for rank in range(1, 14) for suit in iter(Suit)\n]\n\n\n# Here's a less desirable form of the factory function.\n# It harbors a hidden bug because the else assumes too much.\n\n\ndef card2(rank: int, suit: Suit) -> Card:\n    if rank == 1:\n        return AceCard(\"A\", suit)\n    elif 2 <= rank < 11:\n        return Card(str(rank), suit)\n    else:\n        name = {11: \"J\", 12: \"Q\", 13: \"K\"}[rank]\n        return FaceCard(name, suit)\n\n\ntest_card2 = \"\"\"\n    >>> deck2 = [card2(rank, suit) for rank in range(13) for suit in Suit]  # doctest: +IGNORE_EXCEPTION_DETAIL\n    Traceback (most recent call last):\n    KeyError: 0\n\"\"\"\n\n# Here's a more consistent factory function that doesn't mix elif and a mapping.\n\n\ndef card3(rank: int, suit: Suit) -> Card:\n    if rank == 1:\n        return AceCard(\"A\", suit)\n    elif 2 <= rank < 11:\n        return Card(str(rank), suit)\n    elif rank == 11:\n        return FaceCard(\"J\", suit)\n    elif rank == 12:\n        return FaceCard(\"Q\", suit)\n    elif rank == 13:\n        return FaceCard(\"K\", suit)\n    else:\n        raise Exception(\"Rank out of range\")\n\n\n# Note... This works, but mypy doesn't completely understand the simple form.\n# To help mypy, we use this: cast(Iterable[Suit], Suit)\n# This makes it clear an Enum subclass is an iterable over the enumerated values.\n\ntest_card3 = \"\"\"\n    >>> deck3 = [card3(rank, suit) for rank in range(1, 14) for suit in Suit]\n    >>> len(deck3)\n    52\n    >>> sorted(set(c.suit for c in deck3))\n    [<Suit.Spade: '♠'>, <Suit.Club: '♣'>, <Suit.Heart: '♥'>, <Suit.Diamond: '♦'>]\n    >>> sorted(set(c.rank for c in deck3))\n    ['10', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'J', 'K', 'Q']\n\"\"\"\n\ndeck3 = [\n    card3(rank, suit) for rank in range(1, 14) for suit in cast(Iterable[Suit], Suit)\n]\n\n\n# Here's an incomplete, but more consistent factory that uses just a mapping.\n# This doesn't properly translate rank to a string.\n\n\ndef card4(rank: int, suit: Suit) -> Card:\n    class_ = {1: AceCard, 11: FaceCard, 12: FaceCard, 13: FaceCard}.get(rank, Card)\n    return class_(str(rank), suit)\n\n\ntest_card4 = \"\"\"\n    >>> deck4 = [card4(rank, suit) for rank in range(1, 14) for suit in Suit]\n    >>> len(deck4)\n    52\n    >>> sorted(set(c.suit for c in deck4))\n    [<Suit.Spade: '♠'>, <Suit.Club: '♣'>, <Suit.Heart: '♥'>, <Suit.Diamond: '♦'>]\n    >>> sorted(set(c.rank for c in deck4))\n    ['1', '10', '11', '12', '13', '2', '3', '4', '5', '6', '7', '8', '9']\n\"\"\"\n\n# Here's the two-parallel mapping version.\n\n\ndef card5(rank: int, suit: Suit) -> Card:\n    class_ = {1: AceCard, 11: FaceCard, 12: FaceCard, 13: FaceCard}.get(rank, Card)\n    rank_str = {1: \"A\", 11: \"J\", 12: \"Q\", 13: \"K\"}.get(rank, str(rank))\n    return class_(rank_str, suit)\n\n\ntest_card5 = \"\"\"\n    >>> deck5 = [card5(rank, suit) for rank in range(1, 14) for suit in Suit]\n    >>> len(deck5)\n    52\n    >>> sorted(set(c.suit for c in deck5))\n    [<Suit.Spade: '♠'>, <Suit.Club: '♣'>, <Suit.Heart: '♥'>, <Suit.Diamond: '♦'>]\n    >>> sorted(set(c.rank for c in deck5))\n    ['10', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'J', 'K', 'Q']\n\"\"\"\n\n# Here's the mapping two a 2-tuple version.\n\n\ndef card6(rank: int, suit: Suit) -> Card:\n    class_, rank_str = {\n        1: (AceCard, \"A\"), 11: (FaceCard, \"J\"), 12: (FaceCard, \"Q\"), 13: (FaceCard, \"K\")\n    }.get(\n        rank, (Card, str(rank))\n    )\n    return class_(rank_str, suit)\n\n\ntest_card6 = \"\"\"\n    >>> deck6 = [card6(rank, suit) for rank in range(1, 14) for suit in Suit]\n    >>> len(deck6)\n    52\n    >>> sorted(set(c.suit for c in deck6))\n    [<Suit.Spade: '♠'>, <Suit.Club: '♣'>, <Suit.Heart: '♥'>, <Suit.Diamond: '♦'>]\n    >>> sorted(set(c.rank for c in deck6))\n    ['10', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'J', 'K', 'Q']\n\"\"\"\n\n# Here's the mapping to a partial version.\n\n# While this is appealing, it doesn't work well for class instance creation.\n# We get TypeError: 'AceCard' object is not callable\n\n# from functools import partial\n# part_class = partial(AceCard, 'A')\n# card = part_class(Suit.Heart)\n\n# Instead, we'll use lambdas as our partials.\n\n\ndef card7(rank: int, suit: Suit) -> Card:\n    class_rank = {\n        1: lambda suit: AceCard(\"A\", suit),\n        11: lambda suit: FaceCard(\"J\", suit),\n        12: lambda suit: FaceCard(\"Q\", suit),\n        13: lambda suit: FaceCard(\"K\", suit),\n    }.get(\n        rank, lambda suit: Card(str(rank), suit)\n    )\n    return class_rank(suit)\n\n\ntest_card7 = \"\"\"\n    >>> deck7 = [card7(rank, suit) for rank in range(1, 14) for suit in Suit]\n    >>> len(deck7)\n    52\n    >>> sorted(set(c.suit for c in deck7))\n    [<Suit.Spade: '♠'>, <Suit.Club: '♣'>, <Suit.Heart: '♥'>, <Suit.Diamond: '♦'>]\n    >>> sorted(set(c.rank for c in deck7))\n    ['10', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'J', 'K', 'Q']\n\"\"\"\n\n# Here's a stateful card factory that uses a fluent interface\n# to build cards. Note the methods **must** be called in the right\n# order, a common limitation of fluent interfaces.\n\n\nclass CardFactory:\n\n    def rank(self, rank: int) -> \"CardFactory\":\n        self.class_, self.rank_str = {\n            1: (AceCard, \"A\"),\n            11: (FaceCard, \"J\"),\n            12: (FaceCard, \"Q\"),\n            13: (FaceCard, \"K\"),\n        }.get(\n            rank, (Card, str(rank))\n        )\n        return self\n\n    def suit(self, suit: Suit) -> Card:\n        return self.class_(self.rank_str, suit)\n\n\ntest_card8 = \"\"\"\n    >>> card8 = CardFactory()\n    >>> deck8 = [card8.rank(r + 1).suit(s) for r in range(13) for s in Suit]\n    >>> len(deck8)\n    52\n    >>> sorted(set(c.suit for c in deck8))\n    [<Suit.Spade: '♠'>, <Suit.Club: '♣'>, <Suit.Heart: '♥'>, <Suit.Diamond: '♦'>]\n    >>> sorted(set(c.rank for c in deck8))\n    ['10', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'J', 'K', 'Q']\n\"\"\"\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n    doctest.testmod(verbose=False)\n\n    deck = [\n        card(rank, suit) for rank in range(1, 14) for suit in cast(Iterable[Suit], Suit)\n    ]\n    deck3 = [\n        card3(rank, suit)\n        for rank in range(1, 14)\n        for suit in cast(Iterable[Suit], Suit)\n    ]\n    deck4 = [\n        card4(rank, suit)\n        for rank in range(1, 14)\n        for suit in cast(Iterable[Suit], Suit)\n    ]\n    deck5 = [\n        card5(rank, suit)\n        for rank in range(1, 14)\n        for suit in cast(Iterable[Suit], Suit)\n    ]\n    deck6 = [\n        card6(rank, suit)\n        for rank in range(1, 14)\n        for suit in cast(Iterable[Suit], Suit)\n    ]\n    deck7 = [\n        card7(rank, suit)\n        for rank in range(1, 14)\n        for suit in cast(Iterable[Suit], Suit)\n    ]\n    card8 = CardFactory()\n    deck8 = [\n        card8.rank(r + 1).suit(s) for r in range(13) for s in cast(Iterable[Suit], Suit)\n    ]\n"
  },
  {
    "path": "Chapter_2/ch02_ex4.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 2. Example 4.\n\"\"\"\n\n# Alternative Designs for the Initialization\n\nfrom Chapter_2.ch02_ex3 import card, Suit\nfrom typing import Any, cast, Iterator\nfrom abc import abstractmethod\n\n# Subclass only. Omitting the superclass __init__, while legal, baffles mypy badly.\n# Omitting the __init__ is not suggested.\n\n\nclass Card2:\n\n    @abstractmethod\n    def __init__(self, rank: int, suit: Suit) -> None:\n        self.rank: str\n        self.suit: Suit\n        self.hard: int\n        self.soft: int\n\n    def __eq__(self, other: Any) -> bool:\n        return (\n            self.suit == cast(\"Card2\", other).suit\n            and self.rank == cast(\"Card2\", other).rank\n            and self.hard == cast(\"Card2\", other).hard\n            and self.soft == cast(\"Card2\", other).soft\n        )\n\n    def __repr__(self) -> str:\n        return f\"suit={self.suit!r}, rank={self.rank!r}, hard={self.hard!r}, soft={self.soft!r}\"\n\n\nclass NumberCard2(Card2):\n\n    def __init__(self, rank: int, suit: Suit) -> None:\n        self.suit = suit\n        self.rank = str(rank)\n        self.hard = self.soft = rank\n\n\nclass AceCard2(Card2):\n\n    def __init__(self, rank: int, suit: Suit) -> None:\n        self.suit = suit\n        self.rank = \"A\"\n        self.hard, self.soft = 1, 11\n\n\nclass FaceCard2(Card2):\n\n    def __init__(self, rank: int, suit: Suit) -> None:\n        self.suit = suit\n        self.rank = {11: \"J\", 12: \"Q\", 13: \"K\"}[rank]\n        self.hard = self.soft = 10\n\n\ndef card9(rank: int, suit: Suit) -> Card2:\n    if rank == 1:\n        return AceCard2(rank, suit)\n    elif 2 <= rank < 11:\n        return NumberCard2(rank, suit)\n    elif 11 <= rank < 14:\n        return FaceCard2(rank, suit)\n    else:\n        raise Exception(\"Rank out of range\")\n\n\ntest_compare_card9_with_card = \"\"\"\n    >>> # Compare with an example 2 deck\n    >>> deck = [card(rank, suit) for rank in range(1, 14) for suit in (Suit.Club, Suit.Diamond, Suit.Heart, Suit.Spade)]\n    >>> deck9 = [card9(rank, suit) for rank in range(1, 14) for suit in Suit]\n    >>> for c9, c in zip(deck9, deck):\n    ...    assert c9 == c, f\"{c9!r} != {c!r}\"\n    >>> assert deck9 == deck\n\"\"\"\n\n# Mixed subclass and superclass.\n\n# It's abstract in principle, but technically concrete.\n# This parallels a dataclass.\n\n\nclass Card3:\n\n    def __init__(self, rank: str, suit: Suit, hard: int, soft: int) -> None:\n        self.rank = rank\n        self.suit = suit\n        self.hard = hard\n        self.soft = soft\n\n    def __eq__(self, other: Any) -> bool:\n        return (\n            self.suit == cast(\"Card3\", other).suit\n            and self.rank == cast(\"Card3\", other).rank\n            and self.hard == cast(\"Card3\", other).hard\n            and self.soft == cast(\"Card3\", other).soft\n        )\n\n\nclass NumberCard3(Card3):\n\n    def __init__(self, rank: int, suit: Suit) -> None:\n        super().__init__(str(rank), suit, rank, rank)\n\n\nclass AceCard3(Card3):\n\n    def __init__(self, rank: int, suit: Suit) -> None:\n        super().__init__(\"A\", suit, 1, 11)\n\n\nclass FaceCard3(Card3):\n\n    def __init__(self, rank: int, suit: Suit) -> None:\n        rank_str = {11: \"J\", 12: \"Q\", 13: \"K\"}[rank]\n        super().__init__(rank_str, suit, 10, 10)\n\n\ndef card10(rank: int, suit: Suit) -> Card3:\n    if rank == 1:\n        return AceCard3(rank, suit)\n    elif 2 <= rank < 11:\n        return NumberCard3(rank, suit)\n    elif 11 <= rank < 14:\n        return FaceCard3(rank, suit)\n    else:\n        raise Exception(\"Rank out of range\")\n\n\ntest_compare_card10_with_card = \"\"\"\n    >>> # Compare with an example 2 deck\n    >>> deck = [card(rank, suit) for rank in range(1, 14) for suit in (Suit.Club, Suit.Diamond, Suit.Heart, Suit.Spade)]\n    >>> deck10 = [card10(rank, suit) for rank in range(1, 14) for suit in Suit]\n    >>> assert deck10 == deck\n\"\"\"\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n    doctest.testmod(verbose=False)\n\n    deck9 = [\n        card9(rank, suit)\n        for rank in range(1, 14)\n        for suit in cast(Iterator[Suit], Suit)\n    ]\n    deck10 = [\n        card10(rank, suit)\n        for rank in range(1, 14)\n        for suit in cast(Iterator[Suit], Suit)\n    ]\n"
  },
  {
    "path": "Chapter_2/ch02_ex5.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 2. Example 5.\n\"\"\"\n\n# Alternative Designs for the Initialization\n\nfrom Chapter_2.ch02_ex3 import Card, card, Suit\nfrom typing import List, Iterable, cast, Union, NamedTuple, Tuple, Optional, overload\n\nimport random\n\n# While Card should be immutable, that's a topic for the next chapter.\n\n# Composite Objects: Deck\n# ====================================\n\n# A simple Deck definition\n\ntest_no_deck = \"\"\"\n    >>> random.seed(42)\n    >>> d = [card(r + 1, s) for r in range(13) for s in iter(Suit)]\n    >>> random.shuffle(d)\n    >>> hand = [d.pop(), d.pop()]\n    >>> hand\n    [FaceCard(suit=<Suit.Club: '♣'>, rank='J'), Card(suit=<Suit.Spade: '♠'>, rank='2')]\n\"\"\"\n\n\nclass Deck:\n\n    def __init__(self) -> None:\n        self._cards = [card(r + 1, s) for r in range(13) for s in iter(Suit)]\n        random.shuffle(self._cards)\n\n    def pop(self) -> Card:\n        return self._cards.pop()\n\n\ntest_deck = \"\"\"\n    >>> random.seed(42)\n    >>> d = Deck()\n    >>> hand = [d.pop(), d.pop()]\n    >>> hand\n    [FaceCard(suit=<Suit.Club: '♣'>, rank='J'), Card(suit=<Suit.Spade: '♠'>, rank='2')]\n\"\"\"\n\n\n# A subclass of list definition\n\n\nclass Deck2(list):\n\n    def __init__(self) -> None:\n        super().__init__(\n            card(r + 1, s) for r in range(13) for s in cast(Iterable[Suit], Suit)\n        )\n        random.shuffle(self)\n\n\ntest_deck2 = \"\"\"\n    >>> random.seed(42)\n    >>> d = Deck2()\n    >>> hand = [d.pop(), d.pop()]\n    >>> hand\n    [FaceCard(suit=<Suit.Club: '♣'>, rank='J'), Card(suit=<Suit.Spade: '♠'>, rank='2')]\n\"\"\"\n\n\n# A better subclass of list which has the necessary additional features of\n# multiple sets of cards plus not dealing the entire deck.\n\n\nclass Deck3(list):\n\n    def __init__(self, decks: int = 1) -> None:\n        super().__init__()\n        for i in range(decks):\n            self.extend(card(r + 1, s) for r in range(13) for s in iter(Suit))\n        random.shuffle(self)\n        burn = random.randint(1, 52)\n        for i in range(burn):\n            self.pop()\n\n\ntest_deck3 = \"\"\"\n    >>> random.seed(42)\n    >>> d = Deck3()\n    >>> hand = [d.pop(), d.pop()]\n    >>> hand\n    [Card(suit=<Suit.Spade: '♠'>, rank='9'), FaceCard(suit=<Suit.Heart: '♥'>, rank='K')]\n\"\"\"\n\n\nclass Deck3a(list):\n\n    def __init__(self, decks: int = 1) -> None:\n        super().__init__(\n            card(r + 1, s) for r in range(13) for s in iter(Suit) for d in range(decks)\n        )\n        random.shuffle(self)\n        burn = random.randint(1, 52)\n        for i in range(burn):\n            self.pop()\n\n\ntest_deck3a = \"\"\"\n    >>> random.seed(42)\n    >>> d = Deck3a()\n    >>> hand = [d.pop(), d.pop()]\n    >>> hand\n    [Card(suit=<Suit.Spade: '♠'>, rank='9'), FaceCard(suit=<Suit.Heart: '♥'>, rank='K')]\n\"\"\"\n\n\n# Composite Objects: Hand\n# ===================================\n\n# A simplistic Hand without a proper initialization of the cards.\n\n\nclass Hand:\n\n    def __init__(self, dealer_card: Card) -> None:\n        self.dealer_card: Card = dealer_card\n        self.cards: List[Card] = []\n\n    def hard_total(self) -> int:\n        return sum(c.hard for c in self.cards)\n\n    def soft_total(self) -> int:\n        return sum(c.soft for c in self.cards)\n\n    def __repr__(self) -> str:\n        return f\"{self.__class__.__name__} {self.dealer_card} {self.cards}\"\n\n\ntest_hand = \"\"\"\n    >>> random.seed(42)\n    >>> d = Deck()\n    >>> h = Hand(d.pop())\n    >>> h.cards.append(d.pop())\n    >>> h.cards.append(d.pop())\n    >>> h\n    Hand J♣ [Card(suit=<Suit.Spade: '♠'>, rank='2'), AceCard(suit=<Suit.Diamond: '♦'>, rank='A')]\n\"\"\"\n\n\n# A Better Hand with a complete initialization of the cards.\n# This works better with serialization.\n\n\nclass Hand2:\n\n    def __init__(self, dealer_card: Card, *cards: Card) -> None:\n        self.dealer_card = dealer_card\n        self.cards = list(cards)\n\n    def card_append(self, card: Card) -> None:\n        self.cards.append(card)\n\n    def hard_total(self) -> int:\n        return sum(c.hard for c in self.cards)\n\n    def soft_total(self) -> int:\n        return sum(c.soft for c in self.cards)\n\n    def __repr__(self) -> str:\n        return f\"{self.__class__.__name__}({self.dealer_card!r}, *{self.cards})\"\n\n\ntest_hand2 = \"\"\"\n    >>> random.seed(42)\n    >>> d = Deck()\n    >>> h = Hand2(d.pop(), d.pop(), d.pop())\n    >>> h\n    Hand2(FaceCard(suit=<Suit.Club: '♣'>, rank='J'), *[Card(suit=<Suit.Spade: '♠'>, rank='2'), AceCard(suit=<Suit.Diamond: '♦'>, rank='A')])\"\"\"\n\n\n# A Hand which can be built from another Hand or a collection of Cards.\n# This allows us to freeze the hand or build a memento version of the hand.\n\n\nclass Hand3:\n\n    @overload\n    def __init__(self, arg1: \"Hand3\") -> None:\n        ...\n\n    @overload\n    def __init__(self, arg1: Card, arg2: Card, arg3: Card) -> None:\n        ...\n\n    def __init__(\n        self,\n        arg1: Union[Card, \"Hand3\"],\n        arg2: Optional[Card] = None,\n        arg3: Optional[Card] = None,\n    ) -> None:\n        self.dealer_card: Card\n        self.cards: List[Card]\n\n        if isinstance(arg1, Hand3) and not arg2 and not arg3:\n            # Clone an existing hand\n            self.dealer_card = arg1.dealer_card\n            self.cards = arg1.cards\n        elif (\n            isinstance(arg1, Card) and isinstance(arg2, Card) and isinstance(arg3, Card)\n        ):\n            # Build a fresh, new hand.\n            self.dealer_card = cast(Card, arg1)\n            self.cards = [arg2, arg3]\n\n    def __repr__(self) -> str:\n        return f\"{self.__class__.__name__}({self.dealer_card!r}, *{self.cards})\"\n\n\ntest_hand3 = \"\"\"\n    >>> random.seed(42)\n    >>> d = Deck()\n    >>> h = Hand3(d.pop(), d.pop(), d.pop())\n    >>> memento = Hand3(h)\n    >>> memento\n    Hand3(FaceCard(suit=<Suit.Club: '♣'>, rank='J'), *[Card(suit=<Suit.Spade: '♠'>, rank='2'), AceCard(suit=<Suit.Diamond: '♦'>, rank='A')])\n\"\"\"\n\n# A Hand which can be built from another Hand.\n# Or a split from another hand.\n# Or individual cards.\n\n# Note the complexity of the initialization is nearly impossible\n# to specify clearly.\n# - __init__(self, arg1 : Hand4) -> None: ...\n# - __init__(self, arg1 : Hand4, arg2: Card, *, split: int) -> None: ...\n# - __init__(self, arg1 : Card, arg2: Card, arg3: Card) -> None: ...\n# - __init__(self, arg1 : Union[Hand4, Card], arg2: Optional[Card]=None, arg3: Optional[Card] = None, split: Optional[int] = None) -> None: ...\n\n# This is an indication of a need to have staticmethods for creating instances.\n# See the next example for this.\n\n\nclass Hand4:\n\n    @overload\n    def __init__(self, arg1: \"Hand4\") -> None:\n        ...\n\n    @overload\n    def __init__(self, arg1: \"Hand4\", arg2: Card, *, split: int) -> None:\n        ...\n\n    @overload\n    def __init__(self, arg1: Card, arg2: Card, arg3: Card) -> None:\n        ...\n\n    def __init__(\n        self,\n        arg1: Union[\"Hand4\", Card],\n        arg2: Optional[Card] = None,\n        arg3: Optional[Card] = None,\n        split: Optional[int] = None,\n    ) -> None:\n        self.dealer_card: Card\n        self.cards: List[Card]\n        if isinstance(arg1, Hand4):\n            # Clone an existing hand\n            self.dealer_card = arg1.dealer_card\n            self.cards = arg1.cards\n        elif isinstance(arg1, Hand4) and isinstance(arg2, Card) and \"split\" is not None:\n            # Split an existing hand\n            self.dealer_card = arg1.dealer_card\n            self.cards = [arg1.cards[split], arg2]\n        elif isinstance(arg1, Card) and isinstance(arg2, Card) and isinstance(\n            arg3, Card\n        ):\n            # Build a fresh, new hand from three cards\n            self.dealer_card = arg1\n            self.cards = [arg2, arg3]\n        else:\n            raise TypeError(\"Invalid constructor {arg1!r} {arg2!r} {arg3!r}\")\n\n    def __str__(self) -> str:\n        return \", \".join(map(str, self.cards))\n\n\ntest_hand4 = \"\"\"\n    >>> import random\n    >>> random.seed(42)\n    >>> d = Deck()\n    >>> h = Hand4(d.pop(), d.pop(), d.pop())\n    >>> s1 = Hand4(h, d.pop(), split=0)\n    >>> s2 = Hand4(h, d.pop(), split=1)\n    >>> print(\"start\", h, \"split1\", s1, \"split2\", s2)\n    start 2♠, A♦ split1 2♠, A♦ split2 2♠, A♦\n\"\"\"\n\n# A Hand with static methods to split or frozen as a memento.\n\n\nclass Hand5:\n\n    def __init__(self, dealer_card: Card, *cards: Card) -> None:\n        self.dealer_card = dealer_card\n        self.cards = list(cards)\n\n    @staticmethod\n    def freeze(other) -> \"Hand5\":\n        hand = Hand5(other.dealer_card, *other.cards)\n        return hand\n\n    @staticmethod\n    def split(other, card0, card1) -> Tuple[\"Hand5\", \"Hand5\"]:\n        hand0 = Hand5(other.dealer_card, other.cards[0], card0)\n        hand1 = Hand5(other.dealer_card, other.cards[1], card1)\n        return hand0, hand1\n\n    def __str__(self) -> str:\n        return \", \".join(map(str, self.cards))\n\n\ntest_hand_5 = \"\"\"\n    >>> import random\n    >>> random.seed(42)\n    >>> d = Deck()\n    >>> h = Hand5(d.pop(), d.pop(), d.pop())\n    >>> s1, s2 = Hand5.split(h, d.pop(), d.pop())\n    >>> print(\"start\", h, \"split1\", s1, \"split2\", s2)\n    start 2♠, A♦ split1 2♠, Q♠ split2 A♦, 5♦\n\"\"\"\n\n# Composite Objects: Betting Strategy\n# ==============================================\n\n# A strategy class hierarchy for Betting.\n\n\nclass BettingStrategy:\n\n    def bet(self) -> int:\n        raise NotImplementedError(\"No bet method\")\n\n    def record_win(self) -> None:\n        pass\n\n    def record_loss(self) -> None:\n        pass\n\n\nclass Flat(BettingStrategy):\n\n    def bet(self) -> int:\n        return 1\n\n\ntest_flat = \"\"\"\n    >>> flat_bet = Flat()\n    >>> flat_bet.bet()\n    1\n\"\"\"\n\nimport abc\nfrom abc import abstractmethod\n\n\nclass BettingStrategy2(metaclass=abc.ABCMeta):\n\n    @abstractmethod\n    def bet(self) -> int:\n        return 1\n\n    def record_win(self):\n        pass\n\n    def record_loss(self):\n        pass\n\n\n# A strategy class hierarchy for Play.\n\n\nclass GameStrategy:\n\n    def insurance(self, hand: Hand) -> bool:\n        return False\n\n    def split(self, hand: Hand) -> bool:\n        return False\n\n    def double(self, hand: Hand) -> bool:\n        return False\n\n    def hit(self, hand: Hand) -> bool:\n        return sum(c.hard for c in hand.cards) <= 17\n\n\ntest_game = \"\"\"\n    >>> dumb = GameStrategy()\n    >>> dumb.insurance(Hand2(card(1, Suit.Heart), card(1, Suit.Spade), card(13, Suit.Spade)))\n    False\n    >>> h17 = Hand2(card(1, Suit.Heart), card(10, Suit.Heart), card(7, Suit.Club))\n    >>> [f\"{c}: {c.hard}\" for c in h17.cards]\n    ['10♥: 10', '7♣: 7']\n    >>> [f\"{c}: {c.soft}\" for c in h17.cards]\n    ['10♥: 10', '7♣: 7']\n    >>> dumb.hit(Hand2(card(1, Suit.Heart), card(10, Suit.Heart), card(7, Suit.Club)))\n    True\n    >>> dumb.hit(Hand2(card(1, Suit.Heart), card(10, Suit.Heart), card(8, Suit.Club)))\n    False\n    >>> s18 = Hand2(card(1, Suit.Heart), card(1, Suit.Heart), card(7, Suit.Club))\n    >>> [f\"{c}: {c.hard}\" for c in s18.cards]\n    ['A♥: 1', '7♣: 7']\n    >>> [f\"{c}: {c.soft}\" for c in s18.cards]\n    ['A♥: 11', '7♣: 7']\n\"\"\"\n\n\n# A simple outline for the Table.\n\n\nclass Table:\n\n    def __init__(self) -> None:\n        self.deck = Deck()\n\n    def place_bet(self, amount: int) -> None:\n        print(\"Bet\", amount)\n\n    def get_hand(self) -> Hand2:\n        try:\n            self.hand = Hand2(self.deck.pop(), self.deck.pop(), self.deck.pop())\n            self.hole_card = self.deck.pop()\n        except IndexError:\n            # Out of cards: need to shuffle.\n            # This is not technically correct: cards currently in play should not appear in the next deck.\n            self.deck = Deck()\n            return self.get_hand()\n        print(\"Deal\", self.hand)\n        return self.hand\n\n    def can_insure(self, hand: Hand) -> bool:\n        return hand.dealer_card.insure\n\n\n# A Player definition\n\n\nclass Player:\n\n    def __init__(\n        self,\n        table: Table,\n        bet_strategy: BettingStrategy,\n        game_strategy: GameStrategy\n    ) -> None:\n        self.bet_strategy = bet_strategy\n        self.game_strategy = game_strategy\n        self.table = table\n\n    def game(self):\n        self.table.place_bet(self.bet_strategy.bet())\n        self.hand = self.table.get_hand()\n        if self.table.can_insure(self.hand):\n            if self.game_strategy.insurance(self.hand):\n                self.table.insure(self.bet_strategy.bet())\n        # etc.\n\n\n# Typical Use Case\n\ntest_table_player = \"\"\"\n    >>> random.seed(42)\n    >>> table = Table()\n    >>> flat_bet = Flat()\n    >>> dumb = GameStrategy()\n    >>> p = Player(table, flat_bet, dumb)\n    >>> p.game()\n    Bet 1\n    Deal Hand2(FaceCard(suit=<Suit.Club: '♣'>, rank='J'), *[Card(suit=<Suit.Spade: '♠'>, rank='2'), AceCard(suit=<Suit.Diamond: '♦'>, rank='A')])\n\"\"\"\n\n# A Player definition using wide-open keyword definitions.\n# While the following is *technically* possible, a Very Bad Idea for type checking.\n#         self.__dict__.update(kw)\n\n\nclass Player2(Player):\n\n    def __init__(self, **kw) -> None:\n        \"\"\"Must provide table, bet_strategy, game_strategy.\"\"\"\n        self.bet_strategy: BettingStrategy = kw[\"bet_strategy\"]\n        self.game_strategy: GameStrategy = kw[\"game_strategy\"]\n        self.table: Table = kw[\"table\"]\n        self.log_name: Optional[str] = kw.get(\"log_name\")\n\n    def game(self) -> None:\n        self.table.place_bet(self.bet_strategy.bet())\n        self.hand = self.table.get_hand()\n\n\n# Typical Use Case.\n\n\ntest_table_player2 = \"\"\"\n    >>> random.seed(42)\n    >>> table = Table()\n    >>> flat_bet = Flat()\n    >>> dumb = GameStrategy()\n    >>> p2 = Player2(table=table, bet_strategy=flat_bet, game_strategy=dumb)\n    >>> p2.game()\n    Bet 1\n    Deal Hand2(FaceCard(suit=<Suit.Club: '♣'>, rank='J'), *[Card(suit=<Suit.Spade: '♠'>, rank='2'), AceCard(suit=<Suit.Diamond: '♦'>, rank='A')])\n\"\"\"\n\nclass Player2x(Player):\n\n    def __init__(self, **kw) -> None:\n        \"\"\"Must provide table, bet_strategy, game_strategy.\"\"\"\n        self.bet_strategy: BettingStrategy = kw[\"bet_strategy\"]\n        self.game_strategy: GameStrategy = kw[\"game_strategy\"]\n        self.table: Table = kw[\"table\"]\n        self.log_name: Optional[str] = kw.get(\"log_name\")\n\n    def game(self) -> None:\n        self.table.place_bet(self.bet_strategy.bet())\n        self.hand = self.table.get_hand()\n\n\n# Bonus Use Case. Set an additional attribute.\n\ntest_table_player2_extra = \"\"\"\n    >>> random.seed(42)\n    >>> table = Table()\n    >>> flat_bet = Flat()\n    >>> dumb = GameStrategy()\n    >>> p2 = Player2x(table=table, bet_strategy=flat_bet, game_strategy=dumb, log_name=\"Flat/Dumb\")\n    >>> p2.game()\n    Bet 1\n    Deal Hand2(FaceCard(suit=<Suit.Club: '♣'>, rank='J'), *[Card(suit=<Suit.Spade: '♠'>, rank='2'), AceCard(suit=<Suit.Diamond: '♦'>, rank='A')])\n    >>> print(p2.log_name, p2.hand)\n    Flat/Dumb Hand2(FaceCard(suit=<Suit.Club: '♣'>, rank='J'), *[Card(suit=<Suit.Spade: '♠'>, rank='2'), AceCard(suit=<Suit.Diamond: '♦'>, rank='A')])\n\"\"\"\n\n\n# A Player definition using wide-open keyword definitions.\n# While ``self.__dict__.update(extras)`` is *technically* possible, it's a bad idea.\n\n\nclass Player3(Player):\n\n    def __init__(\n        self,\n        table: Table,\n        bet_strategy: BettingStrategy,\n        game_strategy: GameStrategy,\n        **extras,\n    ) -> None:\n        self.bet_strategy = bet_strategy\n        self.game_strategy = game_strategy\n        self.table = table\n        # Bad: self.__dict__.update(extras)\n        # Slightly better?\n        for name in extras:\n            setattr(self, name, extras[name])\n        # Much Better\n        self.log_name: str = extras.pop(\"log_name\", self.__class__.__name__)\n        if extras:\n            raise TypeError(f\"Extra **kw arguments: {extras!r}\")\n\n\ntest_table_player3 = \"\"\"\n    >>> random.seed(42)\n    >>> table = Table()\n    >>> flat_bet = Flat()\n    >>> dumb = GameStrategy()\n    >>> p3 = Player3(table, flat_bet, dumb, log_name=\"Flat/Dumb\")\n    >>> p3.game()\n    Bet 1\n    Deal Hand2(FaceCard(suit=<Suit.Club: '♣'>, rank='J'), *[Card(suit=<Suit.Spade: '♠'>, rank='2'), AceCard(suit=<Suit.Diamond: '♦'>, rank='A')])\n    >>> print(p3.log_name, p3.hand)\n    Flat/Dumb Hand2(FaceCard(suit=<Suit.Club: '♣'>, rank='J'), *[Card(suit=<Suit.Spade: '♠'>, rank='2'), AceCard(suit=<Suit.Diamond: '♦'>, rank='A')])\n\"\"\"\n\n# Bad Ideas\n# ====================\n\n# class-level Validation\n#\n# Run-time complexity for little real value. These are design issues. Use mypy and do it only once.\n\n\nclass ValidPlayer:\n\n    def __init__(self, table, bet_strategy, game_strategy):\n        assert isinstance(table, Table)\n        assert isinstance(bet_strategy, BettingStrategy)\n        assert isinstance(game_strategy, GameStrategy)\n\n        self.bet_strategy = bet_strategy\n        self.game_strategy = game_strategy\n        self.table = table\n\n\ntest_table_valid_player = \"\"\"\n    >>> import random\n    >>> random.seed(42)\n    >>> table = Table()\n    >>> flat_bet = Flat()\n    >>> dumb = GameStrategy()\n    >>> p4 = ValidPlayer(table, flat_bet, dumb)\n\"\"\"\n\n\nclass Player4:\n\n    def __init__(\n        self, table: Table, bet_strategy: BettingStrategy, game_strategy: GameStrategy\n    ) -> None:\n        \"\"\"Creates a new player associated with a table, and configured with\n        proper betting and play strategies\n\n        :param table: an instance of :class:`Table`\n        :param bet_strategy: an instance of :class:`BettingStrategy`\n        :param game_strategy: an instance of :class:`GameStrategy`\n        \"\"\"\n        self.bet_strategy = bet_strategy\n        self.game_strategy = game_strategy\n        self.table = table\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_20/README.rst",
    "content": "A Mini Python Project\n=====================\n\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 20. Example 1.\n\nThis example shows some of the directory structure of a larger Python project.\n\nThe tests can be run as follows::\n\n    PYTHONPATH=Chapter_20/src pytest Chapter_20\n\nThe documentation can be built as follows::\n\n    cd docs\n    PYTHONPATH=../src make html\n"
  },
  {
    "path": "Chapter_20/__init__.py",
    "content": ""
  },
  {
    "path": "Chapter_20/combo.py",
    "content": "# #############\n# Combinations\n# #############\n#\n# ..  contents::\n#\n# Definition\n# ==========\n#\n# For some deeper statistical calculations,\n# we need the number of combinations of *n* things\n# taken *k* at a time, :math:`\\binom{n}{k}`.\n#\n# ..  math::\n#\n#     \\binom{n}{k} = \\dfrac{n!}{k!(n-k)!}\n#\n# The function will use an internal ``fact()`` function because\n# we don't need factorial anywhere else in the application.\n#\n# We'll rely on a simplistic factorial function without memoization.\n#\n# Test Case\n# =========\n#\n# Here are two simple unit tests for this function provided\n# as doctest examples.\n#\n# >>> from combo import combinations\n# >>> combinations(4,2)\n# 6\n# >>> combinations(8,4)\n# 70\n#\n# Implementation\n# ===============\n#\n# Here's the essential function definition, with docstring:\n# ::\n\ndef combinations(n: int, k: int) -> int:\n    \"\"\"Compute :math:`\\binom{n}{k}`, the number of\n    combinations of *n* things taken *k* at a time.\n\n    :param n: integer size of population\n    :param k: groups within the population\n    :returns: :math:`\\binom{n}{k}`\n    \"\"\"\n\n# An important consideration here is that someone hasn't confused\n# the two argument values.\n# ::\n\n    assert k <= n\n\n# Here's the embedded factorial function. It's recursive. The Python\n# stack limit is a limitation on the size of numbers we can use.\n# ::\n\n    def fact(a: int) -> int:\n        if a == 0: return 1\n        return a*fact(a-1)\n\n# Here's the final calculation. Note that we're using integer division.\n# Otherwise, we'd get an unexpected conversion to float.\n# ::\n\n    return fact(n)//(fact(k)*fact(n-k))\n\n# We can make this more efficient by treating the factorial product\n# as a *Multiset* of individual integer values. The product updates the\n# multiset. The division removes items from the multiset. Once the final\n# set of factors is available, the final product can be computed more efficiently.\n"
  },
  {
    "path": "Chapter_20/combo.py.html",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"en\" lang=\"en\">\n<head>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n<meta name=\"generator\" content=\"Docutils 0.14: http://docutils.sourceforge.net/\" />\n<title>Combinations</title>\n<style type=\"text/css\">\n\n/*\n:Author: David Goodger (goodger@python.org)\n:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $\n:Copyright: This stylesheet has been placed in the public domain.\n\nDefault cascading style sheet for the HTML output of Docutils.\n\nSee http://docutils.sf.net/docs/howto/html-stylesheets.html for how to\ncustomize this style sheet.\n*/\n\n/* used to remove borders from tables and images */\n.borderless, table.borderless td, table.borderless th {\n  border: 0 }\n\ntable.borderless td, table.borderless th {\n  /* Override padding for \"table.docutils td\" with \"! important\".\n     The right padding separates the table cells. */\n  padding: 0 0.5em 0 0 ! important }\n\n.first {\n  /* Override more specific margin styles with \"! important\". */\n  margin-top: 0 ! important }\n\n.last, .with-subtitle {\n  margin-bottom: 0 ! important }\n\n.hidden {\n  display: none }\n\n.subscript {\n  vertical-align: sub;\n  font-size: smaller }\n\n.superscript {\n  vertical-align: super;\n  font-size: smaller }\n\na.toc-backref {\n  text-decoration: none ;\n  color: black }\n\nblockquote.epigraph {\n  margin: 2em 5em ; }\n\ndl.docutils dd {\n  margin-bottom: 0.5em }\n\nobject[type=\"image/svg+xml\"], object[type=\"application/x-shockwave-flash\"] {\n  overflow: hidden;\n}\n\n/* Uncomment (and remove this text!) to get bold-faced definition list terms\ndl.docutils dt {\n  font-weight: bold }\n*/\n\ndiv.abstract {\n  margin: 2em 5em }\n\ndiv.abstract p.topic-title {\n  font-weight: bold ;\n  text-align: center }\n\ndiv.admonition, div.attention, div.caution, div.danger, div.error,\ndiv.hint, div.important, div.note, div.tip, div.warning {\n  margin: 2em ;\n  border: medium outset ;\n  padding: 1em }\n\ndiv.admonition p.admonition-title, div.hint p.admonition-title,\ndiv.important p.admonition-title, div.note p.admonition-title,\ndiv.tip p.admonition-title {\n  font-weight: bold ;\n  font-family: sans-serif }\n\ndiv.attention p.admonition-title, div.caution p.admonition-title,\ndiv.danger p.admonition-title, div.error p.admonition-title,\ndiv.warning p.admonition-title, .code .error {\n  color: red ;\n  font-weight: bold ;\n  font-family: sans-serif }\n\n/* Uncomment (and remove this text!) to get reduced vertical space in\n   compound paragraphs.\ndiv.compound .compound-first, div.compound .compound-middle {\n  margin-bottom: 0.5em }\n\ndiv.compound .compound-last, div.compound .compound-middle {\n  margin-top: 0.5em }\n*/\n\ndiv.dedication {\n  margin: 2em 5em ;\n  text-align: center ;\n  font-style: italic }\n\ndiv.dedication p.topic-title {\n  font-weight: bold ;\n  font-style: normal }\n\ndiv.figure {\n  margin-left: 2em ;\n  margin-right: 2em }\n\ndiv.footer, div.header {\n  clear: both;\n  font-size: smaller }\n\ndiv.line-block {\n  display: block ;\n  margin-top: 1em ;\n  margin-bottom: 1em }\n\ndiv.line-block div.line-block {\n  margin-top: 0 ;\n  margin-bottom: 0 ;\n  margin-left: 1.5em }\n\ndiv.sidebar {\n  margin: 0 0 0.5em 1em ;\n  border: medium outset ;\n  padding: 1em ;\n  background-color: #ffffee ;\n  width: 40% ;\n  float: right ;\n  clear: right }\n\ndiv.sidebar p.rubric {\n  font-family: sans-serif ;\n  font-size: medium }\n\ndiv.system-messages {\n  margin: 5em }\n\ndiv.system-messages h1 {\n  color: red }\n\ndiv.system-message {\n  border: medium outset ;\n  padding: 1em }\n\ndiv.system-message p.system-message-title {\n  color: red ;\n  font-weight: bold }\n\ndiv.topic {\n  margin: 2em }\n\nh1.section-subtitle, h2.section-subtitle, h3.section-subtitle,\nh4.section-subtitle, h5.section-subtitle, h6.section-subtitle {\n  margin-top: 0.4em }\n\nh1.title {\n  text-align: center }\n\nh2.subtitle {\n  text-align: center }\n\nhr.docutils {\n  width: 75% }\n\nimg.align-left, .figure.align-left, object.align-left, table.align-left {\n  clear: left ;\n  float: left ;\n  margin-right: 1em }\n\nimg.align-right, .figure.align-right, object.align-right, table.align-right {\n  clear: right ;\n  float: right ;\n  margin-left: 1em }\n\nimg.align-center, .figure.align-center, object.align-center {\n  display: block;\n  margin-left: auto;\n  margin-right: auto;\n}\n\ntable.align-center {\n  margin-left: auto;\n  margin-right: auto;\n}\n\n.align-left {\n  text-align: left }\n\n.align-center {\n  clear: both ;\n  text-align: center }\n\n.align-right {\n  text-align: right }\n\n/* reset inner alignment in figures */\ndiv.align-right {\n  text-align: inherit }\n\n/* div.align-center * { */\n/*   text-align: left } */\n\n.align-top    {\n  vertical-align: top }\n\n.align-middle {\n  vertical-align: middle }\n\n.align-bottom {\n  vertical-align: bottom }\n\nol.simple, ul.simple {\n  margin-bottom: 1em }\n\nol.arabic {\n  list-style: decimal }\n\nol.loweralpha {\n  list-style: lower-alpha }\n\nol.upperalpha {\n  list-style: upper-alpha }\n\nol.lowerroman {\n  list-style: lower-roman }\n\nol.upperroman {\n  list-style: upper-roman }\n\np.attribution {\n  text-align: right ;\n  margin-left: 50% }\n\np.caption {\n  font-style: italic }\n\np.credits {\n  font-style: italic ;\n  font-size: smaller }\n\np.label {\n  white-space: nowrap }\n\np.rubric {\n  font-weight: bold ;\n  font-size: larger ;\n  color: maroon ;\n  text-align: center }\n\np.sidebar-title {\n  font-family: sans-serif ;\n  font-weight: bold ;\n  font-size: larger }\n\np.sidebar-subtitle {\n  font-family: sans-serif ;\n  font-weight: bold }\n\np.topic-title {\n  font-weight: bold }\n\npre.address {\n  margin-bottom: 0 ;\n  margin-top: 0 ;\n  font: inherit }\n\npre.literal-block, pre.doctest-block, pre.math, pre.code {\n  margin-left: 2em ;\n  margin-right: 2em }\n\npre.code .ln { color: grey; } /* line numbers */\npre.code, code { background-color: #eeeeee }\npre.code .comment, code .comment { color: #5C6576 }\npre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }\npre.code .literal.string, code .literal.string { color: #0C5404 }\npre.code .name.builtin, code .name.builtin { color: #352B84 }\npre.code .deleted, code .deleted { background-color: #DEB0A1}\npre.code .inserted, code .inserted { background-color: #A3D289}\n\nspan.classifier {\n  font-family: sans-serif ;\n  font-style: oblique }\n\nspan.classifier-delimiter {\n  font-family: sans-serif ;\n  font-weight: bold }\n\nspan.interpreted {\n  font-family: sans-serif }\n\nspan.option {\n  white-space: nowrap }\n\nspan.pre {\n  white-space: pre }\n\nspan.problematic {\n  color: red }\n\nspan.section-subtitle {\n  /* font-size relative to parent (h1..h6 element) */\n  font-size: 80% }\n\ntable.citation {\n  border-left: solid 1px gray;\n  margin-left: 1px }\n\ntable.docinfo {\n  margin: 2em 4em }\n\ntable.docutils {\n  margin-top: 0.5em ;\n  margin-bottom: 0.5em }\n\ntable.footnote {\n  border-left: solid 1px black;\n  margin-left: 1px }\n\ntable.docutils td, table.docutils th,\ntable.docinfo td, table.docinfo th {\n  padding-left: 0.5em ;\n  padding-right: 0.5em ;\n  vertical-align: top }\n\ntable.docutils th.field-name, table.docinfo th.docinfo-name {\n  font-weight: bold ;\n  text-align: left ;\n  white-space: nowrap ;\n  padding-left: 0 }\n\n/* \"booktabs\" style (no vertical lines) */\ntable.docutils.booktabs {\n  border: 0px;\n  border-top: 2px solid;\n  border-bottom: 2px solid;\n  border-collapse: collapse;\n}\ntable.docutils.booktabs * {\n  border: 0px;\n}\ntable.docutils.booktabs th {\n  border-bottom: thin solid;\n  text-align: left;\n}\n\nh1 tt.docutils, h2 tt.docutils, h3 tt.docutils,\nh4 tt.docutils, h5 tt.docutils, h6 tt.docutils {\n  font-size: 100% }\n\nul.auto-toc {\n  list-style-type: none }\n\n</style>\n<style type=\"text/css\">\n\n/*\n*   math2html: convert LaTeX equations to HTML output.\n*\n*   Copyright (C) 2009,2010 Alex Fernández\n*\n*   Released under the terms of the `2-Clause BSD license'_, in short:\n*   Copying and distribution of this file, with or without modification,\n*   are permitted in any medium without royalty provided the copyright\n*   notice and this notice are preserved.\n*   This file is offered as-is, without any warranty.\n*\n* .. _2-Clause BSD license: http://www.spdx.org/licenses/BSD-2-Clause\n*\n*   Based on eLyXer: convert LyX source files to HTML output.\n*   http://elyxer.nongnu.org/\n*/\n/* --end--\n* CSS file for LaTeX formulas.\n*/\n\n/* Formulas */\n.formula {\n\ttext-align: center;\n\tfont-family: \"Droid Serif\", \"DejaVu Serif\", \"STIX\", serif;\n\tmargin: 1.2em 0;\n}\nspan.formula {\n\twhite-space: nowrap;\n}\ndiv.formula {\n\tpadding: 0.5ex;\n\tmargin-left: auto;\n\tmargin-right: auto;\n}\n\n/* Basic features */\na.eqnumber {\n\tdisplay: inline-block;\n\tfloat: right;\n\tclear: right;\n\tfont-weight: bold;\n}\nspan.unknown {\n\tcolor: #800000;\n}\nspan.ignored, span.arraydef {\n\tdisplay: none;\n}\n.formula i {\n\tletter-spacing: 0.1ex;\n}\n\n/* Alignment */\n.align-left, .align-l {\n\ttext-align: left;\n}\n.align-right, .align-r {\n\ttext-align: right;\n}\n.align-center, .align-c {\n\ttext-align: center;\n}\n\n/* Structures */\nspan.overline, span.bar {\n\ttext-decoration: overline;\n}\n.fraction, .fullfraction {\n\tdisplay: inline-block;\n\tvertical-align: middle;\n\ttext-align: center;\n}\n.fraction .fraction {\n\tfont-size: 80%;\n\tline-height: 100%;\n}\nspan.numerator {\n\tdisplay: block;\n}\nspan.denominator {\n\tdisplay: block;\n\tpadding: 0ex;\n\tborder-top: thin solid;\n}\nsup.numerator, sup.unit {\n\tfont-size: 70%;\n\tvertical-align: 80%;\n}\nsub.denominator, sub.unit {\n\tfont-size: 70%;\n\tvertical-align: -20%;\n}\nspan.sqrt {\n\tdisplay: inline-block;\n\tvertical-align: middle;\n\tpadding: 0.1ex;\n}\nsup.root {\n\tfont-size: 70%;\n\tposition: relative;\n\tleft: 1.4ex;\n}\nspan.radical {\n\tdisplay: inline-block;\n\tpadding: 0ex;\n\tfont-size: 150%;\n\tvertical-align: top;\n}\nspan.root {\n\tdisplay: inline-block;\n\tborder-top: thin solid;\n\tpadding: 0ex;\n\tvertical-align: middle;\n}\nspan.symbol {\n\tline-height: 125%;\n\tfont-size: 125%;\n}\nspan.bigsymbol {\n\tline-height: 150%;\n\tfont-size: 150%;\n}\nspan.largesymbol {\n\tfont-size: 175%;\n}\nspan.hugesymbol {\n\tfont-size: 200%;\n}\nspan.scripts {\n\tdisplay: inline-table;\n\tvertical-align: middle;\n}\n.script {\n\tdisplay: table-row;\n\ttext-align: left;\n\tline-height: 150%;\n}\nspan.limits {\n\tdisplay: inline-table;\n\tvertical-align: middle;\n}\n.limit {\n\tdisplay: table-row;\n\tline-height: 99%;\n}\nsup.limit, sub.limit {\n\tline-height: 100%;\n}\nspan.symbolover {\n\tdisplay: inline-block;\n\ttext-align: center;\n\tposition: relative;\n\tfloat: right;\n\tright: 100%;\n\tbottom: 0.5em;\n\twidth: 0px;\n}\nspan.withsymbol {\n\tdisplay: inline-block;\n}\nspan.symbolunder {\n\tdisplay: inline-block;\n\ttext-align: center;\n\tposition: relative;\n\tfloat: right;\n\tright: 80%;\n\ttop: 0.3em;\n\twidth: 0px;\n}\n\n/* Environments */\nspan.array, span.bracketcases, span.binomial, span.environment {\n\tdisplay: inline-table;\n\ttext-align: center;\n\tborder-collapse: collapse;\n\tmargin: 0em;\n\tvertical-align: middle;\n}\nspan.arrayrow, span.binomrow {\n\tdisplay: table-row;\n\tpadding: 0ex;\n\tborder: 0ex;\n}\nspan.arraycell, span.bracket, span.case, span.binomcell, span.environmentcell {\n\tdisplay: table-cell;\n\tpadding: 0ex 0.2ex;\n\tline-height: 99%;\n\tborder: 0ex;\n}\n/*\n* CSS file for LaTeX formulas, extra stuff:\n* binomials, vertical braces, stackrel, fonts and colors.\n*/\n\n/* Inline binomials */\nspan.binom {\n\tdisplay: inline-block;\n\tvertical-align: middle;\n\ttext-align: center;\n\tfont-size: 80%;\n}\nspan.binomstack {\n\tdisplay: block;\n\tpadding: 0em;\n}\n\n/* Over- and underbraces */\nspan.overbrace {\n\tborder-top: 2pt solid;\n}\nspan.underbrace {\n\tborder-bottom: 2pt solid;\n}\n\n/* Stackrel */\nspan.stackrel {\n\tdisplay: inline-block;\n\ttext-align: center;\n}\nspan.upstackrel {\n\tdisplay: block;\n\tpadding: 0em;\n\tfont-size: 80%;\n\tline-height: 64%;\n\tposition: relative;\n\ttop: 0.15em;\n\n}\nspan.downstackrel {\n\tdisplay: block;\n\tvertical-align: bottom;\n\tpadding: 0em;\n}\n\n/* Fonts */\nspan.mathsf, span.textsf {\n\tfont-style: normal;\n\tfont-family: sans-serif;\n}\nspan.mathrm, span.textrm {\n\tfont-style: normal;\n\tfont-family: serif;\n}\nspan.text, span.textnormal {\n\tfont-style: normal;\n}\nspan.textipa {\n\tcolor: #008080;\n}\nspan.fraktur {\n\tfont-family: \"Lucida Blackletter\", eufm10, blackletter;\n}\nspan.blackboard {\n\tfont-family: Blackboard, msbm10, serif;\n}\nspan.scriptfont {\n\tfont-family: \"Monotype Corsiva\", \"Apple Chancery\", \"URW Chancery L\", cursive;\n\tfont-style: italic;\n}\n\n/* Colors */\nspan.colorbox {\n\tdisplay: inline-block;\n\tpadding: 5px;\n}\nspan.fbox {\n\tdisplay: inline-block;\n\tborder: thin solid black;\n\tpadding: 2px;\n}\nspan.boxed, span.framebox {\n\tdisplay: inline-block;\n\tborder: thin solid black;\n\tpadding: 5px;\n}\n\n\n</style>\n</head>\n<body>\n<div class=\"document\" id=\"combinations\">\n<h1 class=\"title\">Combinations</h1>\n\n<div class=\"contents topic\" id=\"contents\">\n<p class=\"topic-title first\">Contents</p>\n<ul class=\"simple\">\n<li><a class=\"reference internal\" href=\"#definition\" id=\"id1\">Definition</a></li>\n<li><a class=\"reference internal\" href=\"#test-case\" id=\"id2\">Test Case</a></li>\n<li><a class=\"reference internal\" href=\"#implementation\" id=\"id3\">Implementation</a></li>\n</ul>\n</div>\n<div class=\"section\" id=\"definition\">\n<h1><a class=\"toc-backref\" href=\"#id1\">Definition</a></h1>\n<p>For some deeper statistical calculations,\nwe need the number of combinations of <em>n</em> things\ntaken <em>k</em> at a time, <span class=\"formula\"><span class=\"bigsymbol\">(</span><span class=\"binom\"><span class=\"binomstack\"><i>n</i></span><span class=\"binomstack\"><i>k</i></span></span><span class=\"bigsymbol\">)</span></span>.</p>\n<div class=\"formula\">\n<span class=\"bigsymbol\">(</span><span class=\"binom\"><span class=\"binomstack\"><i>n</i></span><span class=\"binomstack\"><i>k</i></span></span><span class=\"bigsymbol\">)</span> = <span class=\"fullfraction\"><span class=\"ignored\">(</span><span class=\"numerator\"><i>n</i>!</span><span class=\"ignored\">)/(</span><span class=\"denominator\"><i>k</i>!(<i>n</i> − <i>k</i>)!</span><span class=\"ignored\">)</span></span>\n</div>\n<p>The function will use an internal <tt class=\"docutils literal\">fact()</tt> function because\nwe don't need factorial anywhere else in the application.</p>\n<p>We'll rely on a simplistic factorial function without memoization.</p>\n</div>\n<div class=\"section\" id=\"test-case\">\n<h1><a class=\"toc-backref\" href=\"#id2\">Test Case</a></h1>\n<p>Here are two simple unit tests for this function provided\nas doctest examples.</p>\n<pre class=\"doctest-block\">\n&gt;&gt;&gt; from combo import combinations\n&gt;&gt;&gt; combinations(4,2)\n6\n&gt;&gt;&gt; combinations(8,4)\n70\n</pre>\n</div>\n<div class=\"section\" id=\"implementation\">\n<h1><a class=\"toc-backref\" href=\"#id3\">Implementation</a></h1>\n<p>Here's the essential function definition, with docstring:</p>\n<pre class=\"literal-block\">\ndef combinations(n: int, k: int) -&gt; int:\n    &quot;&quot;&quot;Compute :math:`\\binom{n}{k}`, the number of\n    combinations of *n* things taken *k* at a time.\n\n    :param n: integer size of population\n    :param k: groups within the population\n    :returns: :math:`\\binom{n}{k}`\n    &quot;&quot;&quot;\n</pre>\n<p>An important consideration here is that someone hasn't confused\nthe two argument values.</p>\n<pre class=\"literal-block\">\nassert k &lt;= n\n</pre>\n<p>Here's the embedded factorial function. It's recursive. The Python\nstack limit is a limitation on the size of numbers we can use.</p>\n<pre class=\"literal-block\">\ndef fact(a: int) -&gt; int:\n    if a == 0: return 1\n    return a*fact(a-1)\n</pre>\n<p>Here's the final calculation. Note that we're using integer division.\nOtherwise, we'd get an unexpected conversion to float.</p>\n<pre class=\"literal-block\">\nreturn fact(n)//(fact(k)*fact(n-k))\n</pre>\n<p>We can make this more efficient by treating the factorial product\nas a <em>Multiset</em> of individual integer values. The product updates the\nmultiset. The division removes items from the multiset. Once the final\nset of factors is available, the final product can be computed more efficiently.</p>\n</div>\n</div>\n</body>\n</html>\n"
  },
  {
    "path": "Chapter_20/combo.py.txt",
    "content": "#############\nCombinations\n#############\n\n..  contents::\n\nDefinition\n==========\n\nFor some deeper statistical calculations,\nwe need the number of combinations of *n* things\ntaken *k* at a time, :math:`\\binom{n}{k}`.\n\n..  math::\n\n    \\binom{n}{k} = \\dfrac{n!}{k!(n-k)!}\n\nThe function will use an internal ``fact()`` function because\nwe don't need factorial anywhere else in the application.\n\nWe'll rely on a simplistic factorial function without memoization.\n\nTest Case\n=========\n\nHere are two simple unit tests for this function provided\nas doctest examples.\n\n>>> from combo import combinations\n>>> combinations(4,2)\n6\n>>> combinations(8,4)\n70\n\nImplementation\n===============\n\nHere's the essential function definition, with docstring:\n::\n\n  def combinations(n: int, k: int) -> int:\n      \"\"\"Compute :math:`\\binom{n}{k}`, the number of\n      combinations of *n* things taken *k* at a time.\n\n      :param n: integer size of population\n      :param k: groups within the population\n      :returns: :math:`\\binom{n}{k}`\n      \"\"\"\n\nAn important consideration here is that someone hasn't confused\nthe two argument values.\n::\n\n      assert k <= n\n\nHere's the embedded factorial function. It's recursive. The Python\nstack limit is a limitation on the size of numbers we can use.\n::\n\n      def fact(a: int) -> int:\n          if a == 0: return 1\n          return a*fact(a-1)\n\nHere's the final calculation. Note that we're using integer division.\nOtherwise, we'd get an unexpected conversion to float.\n::\n\n      return fact(n)//(fact(k)*fact(n-k))\n\nWe can make this more efficient by treating the factorial product\nas a *Multiset* of individual integer values. The product updates the\nmultiset. The division removes items from the multiset. Once the final\nset of factors is available, the final product can be computed more efficiently.\n"
  },
  {
    "path": "Chapter_20/docs/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD   = sphinx-build\nSOURCEDIR     = .\nBUILDDIR      = _build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)"
  },
  {
    "path": "Chapter_20/docs/conf.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Configuration file for the Sphinx documentation builder.\n#\n# This file does only contain a selection of the most common options. For a\n# full list see the documentation:\n# http://www.sphinx-doc.org/en/master/config\n\n# -- Path setup --------------------------------------------------------------\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n#\n# import os\n# import sys\n# sys.path.insert(0, os.path.abspath('.'))\n\n\n# -- Project information -----------------------------------------------------\n\nproject = 'Chapter 20'\ncopyright = '2019, S.Lott'\nauthor = 'S.Lott'\n\n# The short X.Y version\nversion = ''\n# The full version, including alpha/beta/rc tags\nrelease = '2.0'\n\n\n# -- General configuration ---------------------------------------------------\n\n# If your documentation needs a minimal Sphinx version, state it here.\n#\n# needs_sphinx = '1.0'\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = [\n    'sphinx.ext.autodoc',\n    'sphinx.ext.doctest',\n    'sphinx.ext.todo',\n    'sphinx.ext.mathjax',\n    'sphinx.ext.viewcode',\n    'sphinx.ext.githubpages',\n]\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = ['_templates']\n\n# The suffix(es) of source filenames.\n# You can specify multiple suffix as a list of string:\n#\n# source_suffix = ['.rst', '.md']\nsource_suffix = '.rst'\n\n# The master toctree document.\nmaster_doc = 'index'\n\n# The language for content autogenerated by Sphinx. Refer to documentation\n# for a list of supported languages.\n#\n# This is also used if you do content translation via gettext catalogs.\n# Usually you set \"language\" from the command line for these cases.\nlanguage = None\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This pattern also affects html_static_path and html_extra_path.\nexclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']\n\n# The name of the Pygments (syntax highlighting) style to use.\npygments_style = None\n\n\n# -- Options for HTML output -------------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\n#\nhtml_theme = 'alabaster'\n\n# Theme options are theme-specific and customize the look and feel of a theme\n# further.  For a list of options available for each theme, see the\n# documentation.\n#\n# html_theme_options = {}\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\nhtml_static_path = ['_static']\n\n# Custom sidebar templates, must be a dictionary that maps document names\n# to template names.\n#\n# The default sidebars (for documents that don't match any pattern) are\n# defined by theme itself.  Builtin themes are using these templates by\n# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',\n# 'searchbox.html']``.\n#\n# html_sidebars = {}\n\n\n# -- Options for HTMLHelp output ---------------------------------------------\n\n# Output file base name for HTML help builder.\nhtmlhelp_basename = 'Chapter20doc'\n\n\n# -- Options for LaTeX output ------------------------------------------------\n\nlatex_elements = {\n    # The paper size ('letterpaper' or 'a4paper').\n    #\n    # 'papersize': 'letterpaper',\n\n    # The font size ('10pt', '11pt' or '12pt').\n    #\n    # 'pointsize': '10pt',\n\n    # Additional stuff for the LaTeX preamble.\n    #\n    # 'preamble': '',\n\n    # Latex figure (float) alignment\n    #\n    # 'figure_align': 'htbp',\n}\n\n# Grouping the document tree into LaTeX files. List of tuples\n# (source start file, target name, title,\n#  author, documentclass [howto, manual, or own class]).\nlatex_documents = [\n    (master_doc, 'Chapter20.tex', 'Chapter 20 Documentation',\n     'S.Lott', 'manual'),\n]\n\n\n# -- Options for manual page output ------------------------------------------\n\n# One entry per manual page. List of tuples\n# (source start file, name, description, authors, manual section).\nman_pages = [\n    (master_doc, 'chapter20', 'Chapter 20 Documentation',\n     [author], 1)\n]\n\n\n# -- Options for Texinfo output ----------------------------------------------\n\n# Grouping the document tree into Texinfo files. List of tuples\n# (source start file, target name, title, author,\n#  dir menu entry, description, category)\ntexinfo_documents = [\n    (master_doc, 'Chapter20', 'Chapter 20 Documentation',\n     author, 'Chapter20', 'One line description of project.',\n     'Miscellaneous'),\n]\n\n\n# -- Options for Epub output -------------------------------------------------\n\n# Bibliographic Dublin Core info.\nepub_title = project\n\n# The unique identifier of the text. This can be a ISBN number\n# or the project homepage.\n#\n# epub_identifier = ''\n\n# A unique identification for the text.\n#\n# epub_uid = ''\n\n# A list of files that should not be packed into the epub file.\nepub_exclude_files = ['search.html']\n\n\n# -- Extension configuration -------------------------------------------------\n\n# -- Options for todo extension ----------------------------------------------\n\n# If true, `todo` and `todoList` produce output, else they produce nothing.\ntodo_include_todos = True\n"
  },
  {
    "path": "Chapter_20/docs/implementation.rst",
    "content": "Implementation\n==============\n\nHere's a reference to the `inception document <_static/inception_doc/index.html>`_\n\nHere's a reference to the :ref:`user_story`\n\nThe ch20_ex1 module\n-------------------\n\n..  automodule:: ch20_ex1\n    :members:\n    :undoc-members:\n    :special-members:\n\nSome Other Module\n-----------------\n\nWe'd have an ``..  automodule::`` directive here, for ``some_other_module``."
  },
  {
    "path": "Chapter_20/docs/index.rst",
    "content": ".. Chapter 20 documentation master file, created by\n   sphinx-quickstart on Wed Apr  3 16:18:57 2019.\n   You can adapt this file completely to your liking, but it should at least\n   contain the root `toctree` directive.\n\nWelcome to Chapter 20's documentation!\n======================================\n\n.. toctree::\n   :maxdepth: 2\n   :caption: Contents:\n\n   user_story\n   implementation\n\n\nIndices and tables\n==================\n\n* :ref:`genindex`\n* :ref:`modindex`\n* :ref:`search`\n"
  },
  {
    "path": "Chapter_20/docs/user_story.rst",
    "content": "..  _user_story:\n\nUser Stories\n============\n\nThe user generally has three tasks: customize the simulation's parameters,\nrun a simulation, and analyze the results of a simulation.\n"
  },
  {
    "path": "Chapter_20/src/__init__.py",
    "content": ""
  },
  {
    "path": "Chapter_20/src/ch20_ex1.py",
    "content": "#!/usr/bin/env python3.7\n# Mastering Object-Oriented Python 2e\n#\n# Code Examples for Mastering Object-Oriented Python 2nd Edition\n#\n# Chapter 20. Example 1.\n#\n\"\"\"\nBlackjack Cards and Decks\n=========================\n\nThis module contains a definition of :class:`Card`, \n:class:`Deck` and :class:`Shoe` suitable for Blackjack.\n\nThe :class:`Card` class hierarchy\n---------------------------------\n\nThe :class:`Card` class hierarchy includes the following class definitions.\n\n:class:`Card` is the superclass as well as being the class for number cards.\n:class:`FaceCard` defines face cards: J, Q and K.\n:class:`AceCard` defines the Ace. This is special in Blackjack because it creates a soft total for a hand.\n\nWe create cards using the :func:`card` factory function to create the proper\n:class:`Card` subclass instances from a rank and suit.\n\nThe :class:`Suit` enumeration has all of the Suit instances.\n\n::\n\n    >>> from ch20_ex1 import cards\n    >>> ace_clubs= cards.card( 1, cards.suits[0] )\n    >>> ace_clubs\n    'A♣'\n    >>> ace_diamonds= cards.card( 1, cards.suits[1] )\n    >>> ace_clubs.rank ==  ace_diamonds.rank\n    True\n\nThe :class:`Deck` and :class:`Shoe` class hierarchy\n---------------------------------------------------\n\nThe basic :class:`Deck` creates a single 52-card deck. The :class:`Shoe` subclass creates a given number of decks. A :class:`Deck`\ncan be shuffled before the cards can be extracted with the :meth:`pop` method. A :class:`Shoe` must be shuffled and\n*burned*. The burn operation sequesters a random number of cards based on a mean and standard deviation. The mean is\na number of cards (52 is the default.) The standard deviation for the burn is also given as a number of cards (2 is\nthe default.)\n\n\"\"\"\n\n# Example Sphinx-style Documentation\n# -------------------------------------\n\n# Imports\nfrom enum import Enum\nfrom typing import Optional\n\n\nclass Suit(str, Enum):\n    \"\"\"\n    Enumeration of all possible values for a card's suit.\n    \"\"\"\n    Club = \"♣\"\n    Diamond = \"♦\"\n    Heart = \"♥\"\n    Spade = \"♠\"\n\n\nclass Card:\n    \"\"\"\n    Definition of a numeric rank playing card.\n    Subclasses will define :py:class:`FaceCard` and :py:class:`AceCard`.\n\n    :ivar rank: int rank of the card\n    :ivar suit: Suit suit of the card\n    :ivar hard: int Hard point total for a card\n    :ivar soft: int Soft total; same as hard for all cards except Aces.\n    \"\"\"\n\n    def __init__(\n        self, rank: int, suit: Suit, hard: int, soft: Optional[int] = None\n    ) -> None:\n        \"\"\"Define the values for this card.\n\n        :param rank: Numeric rank in the range 1-13.\n        :param suit: Suit object (often a character from '♣♡♢♠')\n        :param hard: Hard point total (or 10 for FaceCard or 1 for AceCard)\n        :param soft: The soft total for AceCard, otherwise defaults to hard.\n        \"\"\"\n        self.rank = rank\n        self.suit = suit\n        self.hard = hard\n        self.soft = soft if soft is not None else hard\n\n    def __str__(self) -> str:\n        return f\"{self.rank}{self.suit}\"\n\n    def __repr__(self) -> str:\n        return f\"{self.__class__.__name__}(rank={self.rank}, suit={self.suit})\"\n\n\nclass FaceCard(Card):\n    \"\"\"\n    Subclass of :py:class:`Card` with Ranks 11-13 represented by J, Q, and K.\n    \"\"\"\n    rank_str = {11: \"J\", 12: \"Q\", 13: \"K\"}\n\n    def __str__(self) -> str:\n        return f\"{self.rank_str[self.rank]}{self.suit}\"\n\n\nclass AceCard(Card):\n    \"\"\"\n    Subclass of :py:class:`Card` with rank of 1 represented by A.\n    \"\"\"\n\n    def __str__(self) -> str:\n        return f\"A{self.suit}\"\n\n\ndef card(rank: int, suit: Suit) -> Card:\n    \"\"\"\n    Create a :py:class:`Card` instance from rank and suit.\n    Can raise :py:exc:`TypeError` for ranks out of the range 1 to 13, inclusive.\n\n    :param suit: Suit object\n    :param rank: Numeric rank in the range 1-13\n    :returns: :py:class:`Card` instance\n    :raises TypeError: rank out of range\nc\n    >>> from Chapter_20.ch20_ex1 import card\n    >>> str(card(3, Suit.Heart))\n    '3♥'\n    >>> str(card(1, Suit.Heart))\n    'A♥'\n    \"\"\"\n    if rank == 1:\n        return AceCard(rank, suit, 1, 11)\n    elif 2 <= rank < 11:\n        return Card(rank, suit, rank)\n    elif 11 <= rank < 14:\n        return FaceCard(rank, suit, 10)\n    else:\n        raise TypeError\n"
  },
  {
    "path": "Chapter_20/tests/test_ch20.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 20. Example 2.\n\"\"\"\n\n# NOTE. This expects Chapter_20/src to be on the ``PYTHONPATH``\n\nfrom pytest import *\nfrom ch20_ex1 import *\n\ndef test_card_factory():\n    c_3h = card(3, Suit.Heart)\n    assert str(c_3h) == '3♥'\n    c_1h = card(1, Suit.Heart)\n    'assert str(c_1h) ==A♥'\n"
  },
  {
    "path": "Chapter_3/__init__.py",
    "content": ""
  },
  {
    "path": "Chapter_3/ch03_ex1.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 3. Example 1.\n\"\"\"\n\nfrom typing import Any, cast, Callable\nfrom enum import Enum\nimport sys\n\n# Mutable and Immutable Objects\n# ==============================\n\n# Card Class with anomalies\n# ############################\n\n# Definition of a simple class hierarchy for immutable objects\n# without a formal equality test or hash function. This will\n# somewhat work as expected. However, there will also be anomalies.\n\n\nclass Card:\n    insure = False\n\n    def __init__(self, rank: str, suit: \"Suit\", hard: int, soft: int) -> None:\n        self.rank = rank\n        self.suit = suit\n        self.hard = hard\n        self.soft = soft\n\n    def __repr__(self) -> str:\n        return f\"{self.__class__.__name__}(suit={self.suit!r}, rank={self.rank!r})\"\n\n    def __str__(self) -> str:\n        return f\"{self.rank}{self.suit}\"\n\n\nclass NumberCard(Card):\n\n    def __init__(self, rank: int, suit: \"Suit\") -> None:\n        super().__init__(str(rank), suit, rank, rank)\n\n\nclass AceCard(Card):\n    insure = True\n\n    def __init__(self, rank: int, suit: \"Suit\") -> None:\n        super().__init__(\"A\", suit, 1, 11)\n\n\nclass FaceCard(Card):\n\n    def __init__(self, rank: int, suit: \"Suit\") -> None:\n        rank_str = {11: \"J\", 12: \"Q\", 13: \"K\"}[rank]\n        super().__init__(rank_str, suit, 10, 10)\n\n\nclass Suit(str, Enum):\n    Club = \"\\N{BLACK CLUB SUIT}\"\n    Diamond = \"\\N{BLACK DIAMOND SUIT}\"\n    Heart = \"\\N{BLACK HEART SUIT}\"\n    Spade = \"\\N{BLACK SPADE SUIT}\"\n\n\n# Some Use cases for two seemingly equal cards.\n\ntest_card = \"\"\"\n    >>> c1 = AceCard(1, Suit.Club)\n    >>> c2 = AceCard(1, Suit.Club)\n\n    >>> id(c1) == id(c2)\n    False\n    >>> c1 is c2\n    False\n    >>> hash(c1) == hash(c2)\n    False\n    >>> hash(c1), hash(c2)  # doctest: +ELLIPSIS\n    (..., ...)\n    >>> c1 == c2\n    False\n    >>> set([c1, c2])\n    {AceCard(suit=<Suit.Club: '♣'>, rank='A'), AceCard(suit=<Suit.Club: '♣'>, rank='A')}\n\"\"\"\n\n# Better Card Class\n# ############################\n\n# Definition of a more sophisticated class hierarchy\n# with a formal equality and hash test. This will create\n# properly immutable objects. There will be no anomalies with\n# seemingly equal objects.\n\n# We'll look at a far better solution using typing.NamedTuple below.\n\n\nclass Card2:\n    insure = False\n\n    def __init__(self, rank: str, suit: \"Suit\", hard: int, soft: int) -> None:\n        self.rank = rank\n        self.suit = suit\n        self.hard = hard\n        self.soft = soft\n\n    def __repr__(self) -> str:\n        return f\"{self.__class__.__name__}(suit={self.suit!r}, rank={self.rank!r})\"\n\n    def __str__(self) -> str:\n        return f\"{self.rank}{self.suit}\"\n\n    def __eq__(self, other: Any) -> bool:\n        return (\n            self.suit == cast(Card2, other).suit\n            and self.rank == cast(Card2, other).rank\n        )\n\n    def __hash__(self) -> int:\n        return (hash(self.suit) + 4*hash(self.rank)) % sys.hash_info.modulus\n\n    def __format__(self, format_spec: str) -> str:\n        if format_spec == \"\":\n            return str(self)\n        rs = (\n            format_spec.replace(\"%r\", self.rank)\n                       .replace(\"%s\", self.suit)\n                       .replace(\"%%\", \"%\")\n        )\n        return rs\n\n    def __bytes__(self) -> bytes:\n        class_code = self.__class__.__name__[0]\n        rank_number_str = {\"A\": \"1\", \"J\": \"11\", \"Q\": \"12\", \"K\": \"13\"}.get(\n            self.rank, self.rank\n        )\n        string = f\"({' '.join([class_code, rank_number_str, self.suit])})\"\n        return bytes(string, encoding=\"utf-8\")\n\n\nclass NumberCard2(Card2):\n\n    def __init__(self, rank: int, suit: \"Suit\") -> None:\n        super().__init__(str(rank), suit, rank, rank)\n\n\nclass AceCard2(Card2):\n    insure = True\n\n    def __init__(self, rank: int, suit: \"Suit\") -> None:\n        super().__init__(\"A\", suit, 1, 11)\n\n\nclass FaceCard2(Card2):\n\n    def __init__(self, rank: int, suit: \"Suit\") -> None:\n        rank_str = {11: \"J\", 12: \"Q\", 13: \"K\"}[rank]\n        super().__init__(rank_str, suit, 10, 10)\n\n\ndef card2(rank: int, suit: Suit) -> Card2:\n    class_ = {1: AceCard2, 11: FaceCard2, 12: FaceCard2, 13: FaceCard2}.get(\n        rank, NumberCard2\n    )\n    return class_(rank, suit)\n\n\n# Some Use cases for two seemingly equal cards.\n\ntest_card2 = \"\"\"\n    >>> c1 = AceCard2(1, Suit.Club)\n    >>> c2 = AceCard2(1, Suit.Club)\n\n    >>> id(c1), id(c2)  # doctest: +ELLIPSIS\n    (..., ...)\n\n    >>> id(c1) == id(c2)\n    False\n    >>> c1 is c2\n    False\n    >>> hash(c1) == hash(c2)\n    True\n    >>> c1 == c2\n    True\n    >>> set([c1, c2])\n    {AceCard2(suit=<Suit.Club: '♣'>, rank='A')}\n    \n    >>> bytes(c1)\n    b'(A 1 \\xe2\\x99\\xa3)'\n\"\"\"\n\n\n# Another Poorly-Designed Card Class\n# ##################################\n\n# Definition of a weird class hierarchy\n# with a formal equality but no hash test. This will create\n# properly mutable objects that can't be put into sets.\n\n\nclass Card3:\n    insure = False\n\n    def __init__(self, rank: str, suit: \"Suit\", hard: int, soft: int) -> None:\n        self.rank = rank\n        self.suit = suit\n        self.hard = hard\n        self.soft = soft\n\n    def __repr__(self) -> str:\n        return f\"{self.__class__.__name__}(suit={self.suit!r}, rank={self.rank!r})\"\n\n    def __str__(self) -> str:\n        return f\"{self.rank}{self.suit}\"\n\n    def __eq__(self, other: Any) -> bool:\n        return (\n            self.suit == cast(Card3, other).suit\n            and self.rank == cast(Card3, other).rank\n        )\n\n    # __hash__ = None  # mypy balks at this.\n\n\nclass AceCard3(Card3):\n    insure = True\n\n    def __init__(self, rank: int, suit: \"Suit\") -> None:\n        super().__init__(\"A\", suit, 1, 11)\n\n\nclass NumberCard3(Card3):\n\n    def __init__(self, rank: int, suit: \"Suit\") -> None:\n        super().__init__(str(rank), suit, rank, rank)\n\n\nclass FaceCard3(Card3):\n\n    def __init__(self, rank: int, suit: \"Suit\") -> None:\n        rank_str = {11: \"J\", 12: \"Q\", 13: \"K\"}[rank]\n        super().__init__(rank_str, suit, 10, 10)\n\n\n# Some Use cases for two seemingly equal cards that cannot be hashed.\n\ntest_card3 = \"\"\"\n    >>> c1 = AceCard3(1, Suit.Club)\n    >>> c2 = AceCard3(1, Suit.Club)\n\n    >>> id(c1) == id(c2)\n    False\n    >>> c1 is c2\n    False\n    >>> hash(c1) == hash(c2)  # doctest: +IGNORE_EXCEPTION_DETAIL\n    Traceback (most recent call last):\n    TypeError: unhashable type: 'AceCard3'\n    >>> c1 == c2\n    True\n    >>> set([c1, c2])  # doctest: +IGNORE_EXCEPTION_DETAIL\n    Traceback (most recent call last):\n    TypeError: unhashable type: 'AceCard3'\n\"\"\"\n\n__test__ = {\n    name: value for name, value in locals().items() if name.startswith(\"test_\")\n}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_3/ch03_ex2.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 3. Example 2.\n\"\"\"\n\nfrom typing import NamedTuple, Tuple, cast, Iterable\nfrom enum import Enum\n\n# Properly Immutable Card\n# =======================\n\n\nclass Suit(str, Enum):\n    Club = \"\\N{BLACK CLUB SUIT}\"\n    Diamond = \"\\N{BLACK DIAMOND SUIT}\"\n    Heart = \"\\N{BLACK HEART SUIT}\"\n    Spade = \"\\N{BLACK SPADE SUIT}\"\n\n\nclass Card(NamedTuple):\n    rank: str\n    suit: Suit\n\n    def __str__(self) -> str:\n        return f\"{self.rank}{self.suit.value}\"\n\n    @property\n    def insure(self) -> bool:\n        return False\n\n    @property\n    def hard(self) -> int:\n        hard, soft = self._points()\n        return hard\n\n    @property\n    def soft(self) -> int:\n        hard, soft = self._points()\n        return soft\n\n    def _points(self) -> Tuple[int, int]:\n        pass\n\n\nclass NumberCard(Card):\n\n    def _points(self) -> Tuple[int, int]:\n        return int(self.rank), int(self.rank)\n\n\nclass AceCard(Card):\n\n    @property\n    def insure(self) -> bool:\n        return True\n\n    def _points(self) -> Tuple[int, int]:\n        return 1, 11\n\n\nclass FaceCard(Card):\n\n    def _points(self) -> Tuple[int, int]:\n        return 10, 10\n\n\ndef card(rank: int, suit: Suit) -> Card:\n    class_, rank_str = {\n        1: (AceCard, \"A\"), 11: (FaceCard, \"J\"), 12: (FaceCard, \"Q\"), 13: (FaceCard, \"K\")\n    }.get(\n        rank, (NumberCard, str(rank))\n    )\n    return class_(rank_str, suit)\n\n\ntest_card = \"\"\"\n    >>> deck = [card(r, s) for r in range(1, 14) for s in cast(Iterable[Suit], Suit)]\n    >>> len(deck)\n    52\n    >>> s_1 = card(1, Suit.Spade)\n    >>> s_1\n    AceCard(rank='A', suit=<Suit.Spade: '♠'>)\n    >>> s_1.insure\n    True\n    >>> s_1.hard\n    1\n    >>> s_1.soft\n    11\n    >>> s_j = card(11, Suit.Spade)\n    >>> s_j\n    FaceCard(rank='J', suit=<Suit.Spade: '♠'>)\n    >>> s_j.insure\n    False\n    >>> s_j.hard\n    10\n    >>> s_j.soft\n    10\n\"\"\"\n\n# Some Use cases for two seemingly equal cards.\n\ntest_card_equality = \"\"\"\n    >>> c1 = card(1, Suit.Club)\n    >>> c2 = card(1, Suit.Club)\n\n    >>> id(c1) == id(c2)\n    False\n    >>> c1 is c2\n    False\n    >>> hash(c1) == hash(c2)\n    True\n    >>> c1 == c2\n    True\n    >>> set([c1, c2])\n    {AceCard(rank='A', suit=<Suit.Club: '♣'>)}\n\"\"\"\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_3/ch03_ex3.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 3. Example 3.\n\"\"\"\n\nimport sys\nimport random\nfrom typing import cast, Iterable, Any, Callable\nfrom Chapter_3.ch03_ex1 import Suit, Card2, card2\n\n# Complex Mutable Object Example\n# ==============================\n\n# Definition of a simple class hierarchy\n# with a formal equality and hash test.\n\n\nclass Hand:\n\n    def __init__(self, dealer_card: Card2, *cards: Card2) -> None:\n        self.dealer_card = dealer_card\n        self.cards = list(cards)\n\n    def __str__(self) -> str:\n        return \", \".join(map(str, self.cards))\n\n    def __repr__(self) -> str:\n        cards_text = \", \".join(map(repr, self.cards))\n        return f\"{self.__class__.__name__}({self.dealer_card!r}, {cards_text})\"\n\n    def __format__(self, spec: str) -> str:\n        if spec == \"\":\n            return str(self)\n        return \", \".join(f\"{c:{spec}}\" for c in self.cards)\n\n    def __eq__(self, other: Any) -> bool:\n        if isinstance(other, int):\n            return self.total() == other\n        try:\n            return (\n                self.cards == cast(Hand, other).cards\n                and self.dealer_card == cast(Hand, other).dealer_card\n            )\n        except AttributeError:\n            return NotImplemented\n\n    def __lt__(self, other: Any) -> bool:\n        if isinstance(other, int):\n            return self.total() < other\n        try:\n            return self.total() < cast(Hand, other).total()\n        except AttributeError:\n            return NotImplemented\n\n    def __le__(self, other: Any) -> bool:\n        if isinstance(other, int):\n            return self.total() <= other\n        try:\n            return self.total() <= cast(Hand, other).total()\n        except AttributeError:\n            return NotImplemented\n\n    # __hash__: Callable[[], int] = None\n\n    def total(self) -> int:\n        delta_soft = max(c.soft - c.hard for c in self.cards)\n        hard = sum(c.hard for c in self.cards)\n        if hard + delta_soft <= 21:\n            return hard + delta_soft\n        return hard\n\n\nclass FrozenHand(Hand):\n\n    def __init__(self, *args, **kw) -> None:\n        if len(args) == 1 and isinstance(args[0], Hand):\n            # Clone a hand\n            other = cast(Hand, args[0])\n            self.dealer_card = other.dealer_card\n            self.cards = other.cards\n        else:\n            # Build a fresh Hand from Card instances.\n            super().__init__(*args, **kw)\n\n    def __hash__(self) -> int:\n        return sum(hash(c) for c in self.cards) % sys.hash_info.modulus\n\n\nclass Deck(list):\n\n    def __init__(self) -> None:\n        super().__init__(\n            card2(r + 1, s) for r in range(13) for s in cast(Iterable[Suit], Suit)\n        )\n        random.shuffle(self)\n\n\ntest_frozen_hand = \"\"\"\n    >>> from collections import defaultdict\n    >>> random.seed(1138)\n    >>> d = Deck()\n    >>> h = Hand(d.pop(), d.pop(), d.pop())\n    \n    >>> print(\"Player: {hand:%r%s}\".format(hand=h))\n    Player: K♦, 9♥\n\n    >>> stats = defaultdict(int)\n    >>> stats[h] += 1  # doctest: +IGNORE_EXCEPTION_DETAIL\n    Traceback (most recent call last):\n    TypeError: unhashable type: 'Hand'\n    \n    \n    >>> h_f = FrozenHand(h)\n    >>> stats = defaultdict(int)\n    >>> stats[h_f] += 1\n    >>> print(stats)\n    defaultdict(<class 'int'>, {FrozenHand(NumberCard2(suit=<Suit.Heart: '♥'>, rank='5'), FaceCard2(suit=<Suit.Diamond: '♦'>, rank='K'), NumberCard2(suit=<Suit.Heart: '♥'>, rank='9')): 1})\n\"\"\"\n\n__test__ = {\n    name: value for name, value in locals().items() if name.startswith(\"test_\")\n}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_3/ch03_ex4.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 3. Example 4.\n\"\"\"\n\nfrom typing import Iterable, cast, Any, Union, Type, Tuple\nimport random\nfrom collections import defaultdict\nfrom Chapter_3.ch03_ex1 import card2, Suit, Card2, AceCard2, FaceCard2, NumberCard2\nfrom Chapter_3.ch03_ex3 import Hand, FrozenHand\n\n\nclass Deck(list):\n\n    def __init__(self) -> None:\n        super().__init__(\n            card2(r + 1, s) for r in range(13) for s in cast(Iterable[Suit], Suit)\n        )\n        random.shuffle(self)\n\n# __hash__\n# =========\n\ntest_hash = \"\"\"\n    >>> v1 = 123_456_789\n    >>> v2 = 2_305_843_009_337_150_740\n    >>> hash(v1)\n    123456789\n    >>> hash(v2)\n    123456789\n    >>> v2 == v1\n    False\n\"\"\"\n\n# __bool__\n# ====================\n\n# Not much to show here. The default works as expected.\n\n# __format__\n# =====================\n#\n# Really, this belongs with __str__() and __repr__()\n#\n\n# Examples of how __format__ gets invoked\n\ntest_format = \"\"\"\n    >>> c = card2(2, Suit.Club)\n    >>> print(\"function\", format(c))\n    function 2♣\n    >>> print(f\"Card plain {c}\")\n    Card plain 2♣\n    >>> print(f\"Card !r {c!r}\")\n    Card !r NumberCard2(suit=<Suit.Club: '♣'>, rank='2')\n    >>> print(f\"Card !s {c!s}\")\n    Card !s 2♣\n\n    # Our own unique formatting language uses \"r\" and \"s\" for rank and suit.\n\n    >>> print(\"Card :%s {0:%s}\".format(c))\n    Card :%s ♣\n    >>> print(\"Card :%r {0:%r}\".format(c))\n    Card :%r 2\n    >>> print(\"Card :%r of %s {0:%r of %s}\".format(c))\n    Card :%r of %s 2 of ♣\n    >>> print(\"Card :%s%r {0:%s%r}\".format(c))\n    Card :%s%r ♣2\n\n    # Extra literals we leave alone.\n\n    >>> print(\"Card nested {0:{fill}{align}16s}\".format(c, fill=\"*\", align=\"<\"))\n    Card nested *<16s\n\"\"\"\n\n# RE to parse the specification.\n\nimport re\n\nspec_pat = re.compile(\n    r\"(?P<fill_align>.?[\\<\\>=\\^])?\"\n    \"(?P<sign>[-+ ])?\"\n    \"(?P<alt>#)?\"\n    \"(?P<padding>0)?\"\n    \"(?P<width>\\d*)\"\n    \"(?P<comma>,)?\"\n    \"(?P<precision>\\.\\d*)?\"\n    \"(?P<type>[bcdeEfFgGnosxX%])?\"\n)\n\ntest_spec_pat = \"\"\"\n    >>> for spec in (\n    ...     \"<30\",\n    ...     \">30\",\n    ...     \"^30\",\n    ...     \"*^30\",\n    ...     \"+f\",\n    ...     \"-f\",\n    ...     \" f\",\n    ...     \"d\",\n    ...     \"x\",\n    ...     \"o\",\n    ...     \"b\",\n    ...     \"#x\",\n    ...     \"#o\",\n    ...     \"#b\",\n    ...     \",\",\n    ...     \".2%\",\n    ...     \"06.4f\",\n    ... ):\n    ...     print(spec, spec_pat.match(spec).groupdict())\n    <30 {'fill_align': '<', 'sign': None, 'alt': None, 'padding': None, 'width': '30', 'comma': None, 'precision': None, 'type': None}\n    >30 {'fill_align': '>', 'sign': None, 'alt': None, 'padding': None, 'width': '30', 'comma': None, 'precision': None, 'type': None}\n    ^30 {'fill_align': '^', 'sign': None, 'alt': None, 'padding': None, 'width': '30', 'comma': None, 'precision': None, 'type': None}\n    *^30 {'fill_align': '*^', 'sign': None, 'alt': None, 'padding': None, 'width': '30', 'comma': None, 'precision': None, 'type': None}\n    +f {'fill_align': None, 'sign': '+', 'alt': None, 'padding': None, 'width': '', 'comma': None, 'precision': None, 'type': 'f'}\n    -f {'fill_align': None, 'sign': '-', 'alt': None, 'padding': None, 'width': '', 'comma': None, 'precision': None, 'type': 'f'}\n     f {'fill_align': None, 'sign': ' ', 'alt': None, 'padding': None, 'width': '', 'comma': None, 'precision': None, 'type': 'f'}\n    d {'fill_align': None, 'sign': None, 'alt': None, 'padding': None, 'width': '', 'comma': None, 'precision': None, 'type': 'd'}\n    x {'fill_align': None, 'sign': None, 'alt': None, 'padding': None, 'width': '', 'comma': None, 'precision': None, 'type': 'x'}\n    o {'fill_align': None, 'sign': None, 'alt': None, 'padding': None, 'width': '', 'comma': None, 'precision': None, 'type': 'o'}\n    b {'fill_align': None, 'sign': None, 'alt': None, 'padding': None, 'width': '', 'comma': None, 'precision': None, 'type': 'b'}\n    #x {'fill_align': None, 'sign': None, 'alt': '#', 'padding': None, 'width': '', 'comma': None, 'precision': None, 'type': 'x'}\n    #o {'fill_align': None, 'sign': None, 'alt': '#', 'padding': None, 'width': '', 'comma': None, 'precision': None, 'type': 'o'}\n    #b {'fill_align': None, 'sign': None, 'alt': '#', 'padding': None, 'width': '', 'comma': None, 'precision': None, 'type': 'b'}\n    , {'fill_align': None, 'sign': None, 'alt': None, 'padding': None, 'width': '', 'comma': ',', 'precision': None, 'type': None}\n    .2% {'fill_align': None, 'sign': None, 'alt': None, 'padding': None, 'width': '', 'comma': None, 'precision': '.2', 'type': '%'}\n    06.4f {'fill_align': None, 'sign': None, 'alt': None, 'padding': '0', 'width': '6', 'comma': None, 'precision': '.4', 'type': 'f'}\n\"\"\"\n\n# Nested {}'s\n\ntest_nested_curlies = \"\"\"\n    >>> random.seed(9973)\n    >>> stats = defaultdict(int)\n    >>> d = Deck()\n    >>> h1 = FrozenHand(d.pop(), d.pop(), d.pop())\n    >>> stats[h1] += 1\n    >>> h2 = FrozenHand(d.pop(), d.pop(), d.pop())\n    >>> stats[h2] += 1\n\n    >>> width = 6\n    >>> for hand, count in stats.items():\n    ...     print(\"{hand:%r%s} {count:{width}d}\".format(hand=hand, count=count, width=width))\n    5♦, 5♠      1\n    8♠, 9♦      1\n\"\"\"\n\n# __bytes__\n# =====================\n#\n\n# Export Card2 instance as a bytes. Recover a Card2 instance from bytes.\n\n\ndef card_from_bytes(buffer: bytes) -> Card2:\n    \"\"\"Parses bytes to rebuild the original Card2 instance.\"\"\"\n    string = buffer.decode(\"utf8\")\n    try:\n        if not (string[0] == \"(\" and string[-1] == \")\"):\n            raise ValueError\n        code, rank_number, suit_value = string[1:-1].split()\n        if int(rank_number) not in range(1, 14):\n            raise ValueError\n        class_ = {\"A\": AceCard2, \"N\": NumberCard2, \"F\": FaceCard2}[code]\n        return class_(int(rank_number), Suit(suit_value))\n    except (IndexError, KeyError, ValueError) as ex:\n        raise ValueError(f\"{buffer!r} isn't a Card2 instance\")\n\n\ntest_bytes = \"\"\"\n    >>> random.seed(1138)\n    >>> d = Deck()\n    >>> c = d.pop()\n    >>> c\n    NumberCard2(suit=<Suit.Heart: '♥'>, rank='5')\n    >>> b = bytes(c)\n    >>> print(b)\n    b'(N 5 \\\\xe2\\\\x99\\\\xa5)'\n\n    >>> data = b'(N 5 \\\\xe2\\\\x99\\\\xa5)'\n    >>> c2 = card_from_bytes(data)\n    >>> c2\n    NumberCard2(suit=<Suit.Heart: '♥'>, rank='5')\n    \n    >>> card_from_bytes(b'random')  # doctest: +IGNORE_EXCEPTION_DETAIL\n    Traceback (most recent call last):\n    ValueError: b'random' isn't a Card2 instance\n    >>> card_from_bytes(b'(less random)')\n    Traceback (most recent call last):\n    ValueError: b'(less random)' isn't a Card2 instance\n    >>> card_from_bytes(b'(X 5 \\\\xe2\\\\x99\\\\xa5)')\n    Traceback (most recent call last):\n    ValueError: b'(X 5 \\\\xe2\\\\x99\\\\xa5)' isn't a Card2 instance\n    >>> card_from_bytes(b'(N 25 \\\\xe2\\\\x99\\\\xa5)')\n    Traceback (most recent call last):\n    ValueError: b'(N 25 \\\\xe2\\\\x99\\\\xa5)' isn't a Card2 instance\n    >>> card_from_bytes(b'(N 5 nope)')\n    Traceback (most recent call last):\n    ValueError: b'(N 5 nope)' isn't a Card2 instance\n\"\"\"\n\n# Comparison\n# ====================\n\n# The object resolution for comparison special methods.\n# A partial class to see what happens.\n\n\nclass BlackJackCard_p:\n\n    def __init__(self, rank: int, suit: Suit) -> None:\n        self.rank = rank\n        self.suit = suit\n\n    def __lt__(self, other: Any) -> bool:\n        print(f\"Compare {self} < {other}\")\n        return self.rank < cast(BlackJackCard_p, other).rank\n\n    def __str__(self) -> str:\n        return f\"{self.rank}{self.suit}\"\n\n\ntest_blackjackcard_partial = \"\"\"\n    >>> two = BlackJackCard_p(2, Suit.Spade)\n    >>> three = BlackJackCard_p(3, Suit.Spade)\n    >>> two < three\n    Compare 2♠ < 3♠\n    True\n    >>> two > three\n    Compare 3♠ < 2♠\n    False\n    >>> two == three\n    False\n    \n    >>> two <= three  # doctest: +IGNORE_EXCEPTION_DETAIL\n    Traceback (most recent call last):\n      File \"/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py\", line 1329, in __run\n        compileflags, 1), test.globs)\n      File \"<doctest __main__.__test__.test_blackjackcard_partial[5]>\", line 1, in <module>\n        print(\"{0} <= {1} :: {2!r}\".format(two, three, two <= three))  # doctest: +IGNORE_EXCEPTION_DETAIL\n    TypeError: '<=' not supported between instances of 'BlackJackCard_p' and 'BlackJackCard_p'\n    \n    >>> two_c = BlackJackCard_p(2, Suit.Club)\n    >>> two_c == BlackJackCard_p(2, Suit.Club)\n    False\n\"\"\"\n\n# A more complete class to show same-class comparisons.\n\n\nclass BlackJackCard:\n\n    def __init__(self, rank: int, suit: Suit, hard: int, soft: int) -> None:\n        self.rank = rank\n        self.suit = suit\n        self.hard = hard\n        self.soft = soft\n\n    def __lt__(self, other: Any) -> bool:\n        if not isinstance(other, BlackJackCard):\n            return NotImplemented\n        return self.rank < other.rank\n\n    def __le__(self, other: Any) -> bool:\n        try:\n            return self.rank <= cast(BlackJackCard, other).rank\n        except AttributeError:\n            return NotImplemented\n\n    def __gt__(self, other: Any) -> bool:\n        if not isinstance(other, BlackJackCard):\n            return NotImplemented\n        return self.rank > other.rank\n\n    def __ge__(self, other: Any) -> bool:\n        try:\n            return self.rank >= cast(BlackJackCard, other).rank\n        except AttributeError:\n            return NotImplemented\n\n    def __eq__(self, other: Any) -> bool:\n        if not isinstance(other, BlackJackCard):\n            return NotImplemented\n        return (self.rank == other.rank\n                and self.suit == other.suit)\n\n    def __ne__(self, other: Any) -> bool:\n        if not isinstance(other, BlackJackCard):\n            return NotImplemented\n        return (self.rank != other.rank\n                or self.suit != other.suit)\n\n    def __str__(self) -> str:\n        return f\"{self.rank}{self.suit}\"\n\n    def __repr__(self) -> str:\n        return (f\"{self.__class__.__name__}\"\n                f\"(rank={self.rank!r}, suit={self.suit!r}, \"\n                f\"hard={self.hard!r}, soft={self.soft!r})\")\n\n\nclass Ace21Card(BlackJackCard):\n\n    def __init__(self, rank: int, suit: Suit) -> None:\n        super().__init__(rank, suit, 1, 11)\n\n    def __str__(self) -> str:\n        return f\"A{self.suit}\"\n\n    def __repr__(self) -> str:\n        return f\"{self.__class__.__name__}(rank={self.rank!r}, suit={self.suit!r}, hard={self.hard!r}, soft={self.soft!r})\"\n\n\nclass Face21Card(BlackJackCard):\n\n    FACE_MAP = {11: \"J\", 12: \"Q\", 13: \"K\"}\n\n    def __init__(self, rank: int, suit: Suit) -> None:\n        super().__init__(rank, suit, 10, 10)\n\n    def __str__(self) -> str:\n        return f\"{self.FACE_MAP[self.rank]}{self.suit}\"\n\n    def __repr__(self) -> str:\n        return f\"{self.__class__.__name__}(rank={self.rank!r}, suit={self.suit!r}, hard={self.hard!r}, soft={self.soft!r})\"\n\n\nclass Number21Card(BlackJackCard):\n\n    def __init__(self, rank: int, suit: Suit) -> None:\n        super().__init__(rank, suit, rank, rank)\n\n\ndef card21(rank: int, suit: Suit) -> BlackJackCard:\n    if rank == 1:\n        return Ace21Card(rank, suit)\n    elif 2 <= rank < 11:\n        return Number21Card(rank, suit)\n    elif 11 <= rank < 14:\n        return Face21Card(rank, suit)\n    else:\n        raise TypeError\n\n\ntest_blackjack_full = \"\"\"\n    >>> two = card21(2, \"♠\")\n    >>> three = card21(3, \"♠\")\n    >>> f\"{two} <  {three} is {two < three}\"\n    '2♠ <  3♠ is True'\n    >>> f\"{two} >  {three} is {two > three}\"\n    '2♠ >  3♠ is False'\n    >>> f\"{two} == {three} is {two == three}\"\n    '2♠ == 3♠ is False'\n    >>> f\"{two} <= {three} is {two <= three}\"\n    '2♠ <= 3♠ is True'\n\n    >>> two_c = card21(2, \"♣\")\n    >>> f\"{two} == {two_c} is {two == two_c}\"\n    '2♠ == 2♣ is False'\n    >>> two.rank == two_c.rank\n    True\n \n    >>> # A mixed class comparison with int\n    >>> f\"2 <  {three} is {2 < three}\"  # doctest: +IGNORE_EXCEPTION_DETAIL\n    Traceback (most recent call last):\n      File \"/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py\", line 1329, in __run\n        compileflags, 1), test.globs)\n      File \"<doctest __main__.__test__.test_blackjack_full[8]>\", line 1, in <module>\n        print(\"{0} <  {1} :: {2!r}\".format(2, three, 2 < three))  # doctest: +IGNORE_EXCEPTION_DETAIL\n    TypeError: '<' not supported between instances of 'int' and 'Number21Card'\n    >>> two < 2  # doctest: +IGNORE_EXCEPTION_DETAIL\n    Traceback (most recent call last):\n      File \"/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py\", line 1329, in __run\n        compileflags, 1), test.globs)\n      File \"<doctest __main__.__test__.test_blackjack_full[10]>\", line 1, in <module>\n        two < 2\n    TypeError: '<' not supported between instances of 'Number21Card' and 'int'\n    >>> two > 2  # doctest: +IGNORE_EXCEPTION_DETAIL\n    Traceback (most recent call last):\n      File \"/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py\", line 1329, in __run\n        compileflags, 1), test.globs)\n      File \"<doctest __main__.__test__.test_blackjack_full[11]>\", line 1, in <module>\n        two > 2\n    TypeError: '>' not supported between instances of 'Number21Card' and 'int'\n    >>> two == 2\n    False\n    >>> 2 == two\n    False\n\"\"\"\n\ntest_hand = \"\"\"\n    >>> two = card21(2, Suit.Spade) \n    >>> three = card21(3, Suit.Spade) \n    >>> two_c = card21(2, Suit.Club) \n    >>> ace = card21(1, Suit.Club) \n    >>> cards = [ace, two, two_c, three] \n\n    >>> h = Hand( card21(10,'♠'), *cards ) \n    >>> print(h) \n    A♣, 2♠, 2♣, 3♠\n    >>> h.total()\n    18\n\n\"\"\"\n\n\n# Destruction and __del__()\n# ====================================\n\n# Noisy Exit\n\n\nclass Noisy:\n\n    def __del__(self) -> None:\n        print(f\"Removing {id(self)}\")\n\n\n# Simple create and delete.\n\ntest_noisy_1 = \"\"\"\n    >>> x = Noisy()\n    >>> del x  # doctest: +ELLIPSIS\n    Removing ...\n\"\"\"\n\n# Shallow copy and multiple references.\n\n\ntest_noisy_2 = \"\"\"\n    >>> ln = [Noisy(), Noisy()]\n    >>> ln2 = ln[:]\n    >>> del ln  # doctest: +ELLIPSIS\n    >>> del ln2  # doctest: +ELLIPSIS\n    Removing ...\n    Removing ...\n\"\"\"\n\n# Circularity\n\n\nclass Parent:\n\n    def __init__(self, *children: 'Child') -> None:\n        for child in children:\n            child.parent = self\n        self.children = {c.id: c for c in children}\n\n    def __del__(self) -> None:\n        print(\n            f\"Removing {self.__class__.__name__} {id(self):d}\"\n        )\n\n\nclass Child:\n\n    def __init__(self, id: str) -> None:\n        self.id = id\n        self.parent: Parent = cast(Parent, None)\n\n    def __del__(self) -> None:\n        print(\n            f\"Removing {self.__class__.__name__} {id(self):d}\"\n        )\n\n\ntest_circularity_fail = \"\"\"\n    >>> p = Parent(Child('a'), Child('b'))\n    >>> del p  # doctest: +ELLIPSIS\n    \n    >>> p_0 = Parent()\n    >>> del p_0  # doctest: +ELLIPSIS\n    Removing Parent ...\n\n    >>> import gc\n    >>> gc.collect()  # doctest: +ELLIPSIS\n    Removing Child ...\n    Removing Child ...\n    Removing Parent ...\n    ...\n\n    >>> print(gc.garbage)\n    []\n\"\"\"\n\n# No circularity via weak references.\n\n\nfrom weakref import ref\n\n\nclass Parent2:\n\n    def __init__(self, *children: 'Child2') -> None:\n        for child in children:\n            child.parent = ref(self)\n        self.children = {c.id: c for c in children}\n\n    def __del__(self) -> None:\n        print(\n            f\"Removing {self.__class__.__name__} {id(self):d}\"\n        )\n\nclass Child2:\n\n    def __init__(self, id: str) -> None:\n        self.id = id\n        self.parent: ref[Parent2] = cast(ref[Parent2], None)\n\n    def __del__(self) -> None:\n        print(\n            f\"Removing {self.__class__.__name__} {id(self):d}\"\n        )\n\n\ntest_circularity_pass = \"\"\"\n    >>> p = Parent2(Child('a'), Child('b'))\n    >>> del p    # doctest: +ELLIPSIS\n    Removing Parent2 ...\n    Removing Child ...\n    Removing Child ...\n\"\"\"\n\n# Immutable init and __new__()\n# ========================================\n\n# Doesn't work. Can't use this form of __init__ with immutable classes.\n\nclass Float_Fail(float):\n\n    def __init__(self, value: float, unit: str) -> None:\n        super().__init__(value)\n        self.unit = unit\n\n\ntest_float_fail = \"\"\"\n    >>> x = Float_Fail(6.8, \"knots\")  # doctest: +IGNORE_EXCEPTION_DETAIL\n    Traceback (most recent call last):\n      File \"/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py\", line 1329, in __run\n        compileflags, 1), test.globs)\n      File \"<doctest __main__.__test__.test_float_fail[0]>\", line 1, in <module>\n        x = Float_Fail(6.8, \"knots\")\n    TypeError: float expected at most 1 arguments, got 2\n\"\"\"\n\n\n# This is how we can tweak an immutable object before the __init__ is invoked.\n\n# See https://github.com/python/mypy/issues/1053\n# This *should* work since v0.3.1\n\n# Adding type hints will report mypy errors.\n# float is (implicitly) a subclass of object.\n# object.__new__() takes no arguments.\n# float.__new__() is *really* more like type.__new__\n\nclass Float_Units(float):\n\n    def __new__(cls, value, unit):\n        obj = super().__new__(cls, float(value))\n        obj.unit = unit\n        return obj\n\nfrom typing import overload, Optional, SupportsFloat\n\nclass Float_Units_Ugly(float):\n\n    unit: str\n\n    def __new__(cls: Type, value: SupportsFloat, unit: str) -> 'Float_Units_Ugly':\n        obj = cast('Float_Units_Ugly', cast(type, super()).__new__(cls, float(value)))\n        obj.unit = unit\n        return obj\n\n\ntest_float_pass = \"\"\"\n    >>> speed = Float_Units(6.8, \"knots\")\n    >>> speed*2\n    13.6\n    >>> speed.unit\n    'knots'\n\"\"\"\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_3/ch03_ex5.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 3. Example 5.\n\"\"\"\n\nfrom typing import Iterable, cast, Any, Union, Type, Tuple\nimport random\nfrom collections import defaultdict\nfrom Chapter_3.ch03_ex1 import card2, Suit, Card2, AceCard2, FaceCard2, NumberCard2\nfrom Chapter_3.ch03_ex3 import Hand, FrozenHand\n\n\n# Immutable init and __new__()\n# ========================================\n\n# Doesn't work. Can't use this form of __init__ with immutable classes.\n\n\nclass Float_Fail(float):\n\n    def __init__(self, value: float, unit: str) -> None:\n        super().__init__(value)\n        self.unit = unit\n\n\ntest_float_fail = \"\"\"\n    >>> x = Float_Fail(6.8, \"knots\")  # doctest: +IGNORE_EXCEPTION_DETAIL\n    Traceback (most recent call last):\n      File \"/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py\", line 1329, in __run\n        compileflags, 1), test.globs)\n      File \"<doctest __main__.__test__.test_float_fail[0]>\", line 1, in <module>\n        x = Float_Fail(6.8, \"knots\")\n    TypeError: float expected at most 1 arguments, got 2\n\"\"\"\n\n\n# This is how we can tweak an immutable object before the __init__ is invoked.\n\n# See https://github.com/python/mypy/issues/1053\n# This *should* work since v0.3.1\n\n# Adding type hints will report mypy errors.\n# float is (implicitly) a subclass of object.\n# object.__new__() takes no arguments.\n# float.__new__() is *really* more like type.__new__\n\n\nclass Float_Units(float):\n\n    def __new__(cls, value, unit):\n        obj = super().__new__(cls, float(value))\n        obj.unit = unit\n        return obj\n\n\ntest_float_units = \"\"\"\n    >>> speed = Float_Units(6.8, \"knots\")\n    >>> speed*2\n    13.6\n    >>> speed.unit\n    'knots'\n\"\"\"\n\n# Option 2...\n# A number of casts to work with mypy.\n# While this \"works\" the cast(type, super() doesn't make sense.\n\nfrom typing import overload, Optional, SupportsFloat, Dict\n\n\nclass Float_Units_Ugly(float):\n\n    unit: str\n\n    def __new__(cls: Type, value: SupportsFloat, unit: str) -> \"Float_Units_Ugly\":\n        # print(f\"Float_Units_Ugly {cls}\")\n        obj = cast(\"Float_Units_Ugly\", cast(type, super()).__new__(cls, float(value)))\n        obj.unit = unit\n        return obj\n\n\ntest_float_units = \"\"\"\n    >>> speed = Float_Units_Ugly(6.8, \"knots\")\n    >>> speed*2\n    13.6\n    >>> speed.unit\n    'knots'\n\"\"\"\n\n# Option 3...\n# Metaclass to adjust structure.\n# Also relevant to the more complex example that follows.\n\n\nclass AddUnitMeta(type):\n\n    def __new__(\n        cls: Type, name: str, bases: Tuple[Type, ...], namespace: Dict[str, Any], **kwds\n    ) -> \"Float_Units2\":\n        namespace[\"unit\"] = None\n        result = cast(\"Float_Units2\", super().__new__(cls, name, bases, namespace))\n        return result\n\n\nclass Float_Units2(float, metaclass=AddUnitMeta):\n\n    def withUnit(self, unit):\n        self.unit = unit\n        return self\n\n\ntest_float_units_2 = \"\"\"\n    >>> speed = Float_Units2(6.8).withUnit(\"knots\")\n    >>> speed*2\n    13.6\n    >>> speed.unit\n    'knots'\n\"\"\"\n\n# Metaclass and __new__()\n# ===================================\n\n# Example 1. Classes with pre-built loggers.\n\nimport logging\n\n\nclass LoggedMeta(type):\n\n    def __new__(\n        cls: Type, name: str, bases: Tuple[Type, ...], namespace: Dict[str, Any]\n    ) -> \"Logged\":\n        result = cast(\"Logged\", super().__new__(cls, name, bases, namespace))\n        result.logger = logging.getLogger(name)\n        return result\n\n\nclass Logged(metaclass=LoggedMeta):\n    logger: logging.Logger\n\n\nclass SomeApplicationClass(Logged):\n\n    def __init__(self, v1: int, v2: int) -> None:\n        self.logger.info(\"v1=%r, v2=%r\", v1, v2)\n        self.v1 = v1\n        self.v2 = v2\n        self.v3 = v1 * v2\n        self.logger.info(\"product=%r\", self.v3)\n\n\ntest_meta = \"\"\"\n    >>> import sys\n    >>> logging.basicConfig(stream=sys.stdout, level=logging.INFO)\n    >>> sa = SomeApplicationClass(6, 7)\n    INFO:SomeApplicationClass:v1=6, v2=7\n    INFO:SomeApplicationClass:product=42\n    >>> logging.shutdown()\n\"\"\"\n\n__test__ = {\n    name: value for name, value in locals().items() if name.startswith(\"test_\")\n}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_4/__init__.py",
    "content": ""
  },
  {
    "path": "Chapter_4/ch04_ex1.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 4. Example 1.\n\"\"\"\nfrom enum import Enum\nimport random\nfrom typing import Any, cast, NamedTuple, Callable, Iterable, Union\nfrom dataclasses import dataclass\n\n# Immutability\n# ======================\n\n# Yes, this is out of order from the book.\n# It's presented first to make the code below work out nicely.\n\n# A simple-looking card class with comparisons.\n# Uses __slots__ to constrain the definition.\n\n\nclass Suit(str, Enum):\n    Club = \"\\N{BLACK CLUB SUIT}\"\n    Diamond = \"\\N{BLACK DIAMOND SUIT}\"\n    Heart = \"\\N{BLACK HEART SUIT}\"\n    Spade = \"\\N{BLACK SPADE SUIT}\"\n\n\nclass BlackJackCard:\n    \"\"\"Abstract Superclass.\"\"\"\n\n    # Note: __slots__ isn't inherited and must be repeated.\n    # THe alternative is kind of hideous.\n    __slots__ = (\"rank\", \"suit\", \"hard\", \"soft\")\n\n    def __init__(self, rank: str, suit: \"Suit\", hard: int, soft: int) -> None:\n        self.rank = rank\n        self.suit = suit\n        self.hard = hard\n        self.soft = soft\n\n    def __repr__(self) -> str:\n        return f\"{self.__class__.__name__}(rank={self.rank}, suit={self.suit!r}, hard={self.hard}, soft={self.soft}\"\n\n    def __str__(self) -> str:\n        return f\"{self.rank}{self.suit}\"\n\n    # if we don't want to repeat __slots__ we can use this to prevent attributes being set.\n\n    # def __setattr__(self, name: str, value: Any) -> NoReturn:\n    #    raise AttributeError(\n    #        f\"{self.__class__.__name__} has no attribute {name!r}\"\n    #    )\n\n    def __lt__(self, other: Any) -> bool:\n        # Bad idea\n        if not issubclass(other.__class__, BlackJackCard):\n            return NotImplemented\n        return self.rank < cast(BlackJackCard, other).rank\n\n    def __le__(self, other: Any) -> bool:\n        # Better idea\n        try:\n            return self.rank <= cast(BlackJackCard, other).rank\n        except AttributeError:\n            return NotImplemented\n\n    def __eq__(self, other: Any) -> bool:\n        try:\n            return self.rank == cast(BlackJackCard, other).rank and self.suit == cast(\n                BlackJackCard, other\n            ).suit\n        except AttributeError:\n            return NotImplemented\n\n\nclass Ace21Card(BlackJackCard):\n    __slots__ = (\"rank\", \"suit\", \"hard\", \"soft\")\n\n    def __init__(self, rank: int, suit: Suit) -> None:\n        super().__init__(\"A\", suit, 1, 11)\n\n    def __repr__(self) -> str:\n        return f\"{self.__class__.__name__}(rank=1, suit={self.suit!r})\"\n\n\nclass Face21Card(BlackJackCard):\n    __slots__ = (\"rank\", \"suit\", \"hard\", \"soft\")\n\n    def __init__(self, rank: int, suit: Suit) -> None:\n        rank_str = {11: \"J\", 12: \"Q\", 13: \"K\"}[rank]\n        super().__init__(rank_str, suit, 10, 10)\n\n    def __repr__(self) -> str:\n        rank_num = {\"J\": 11, \"Q\": 12, \"K\": 13}[self.rank]\n        return f\"{self.__class__.__name__}(rank={rank_num}, suit={self.suit!r})\"\n\n\nclass Number21Card(BlackJackCard):\n    __slots__ = (\"rank\", \"suit\", \"hard\", \"soft\")\n\n    def __init__(self, rank: int, suit: Suit) -> None:\n        super().__init__(str(rank), suit, rank, rank)\n\n    def __repr__(self) -> str:\n        return f\"{self.__class__.__name__}(rank={self.rank}, suit={self.suit!r})\"\n\n\ndef card21(rank: int, suit: Suit) -> BlackJackCard:\n    if rank == 1:\n        return Ace21Card(rank, suit)\n    elif 2 <= rank < 11:\n        return Number21Card(rank, suit)\n    elif 11 <= rank < 14:\n        return Face21Card(rank, suit)\n    else:\n        raise TypeError\n\n\ndef compare(a: Any, b: Any) -> None:\n    print(f\"{a} == {b} {a==b}, {a} < {b} {a<b}, {a} <= {b} {a <= b}\")\n    print(f\"{a} != {b} {a!=b}, {a} > {b} {a>b}, {a} >= {b} {a >= b}\")\n\n\ntest_comparisons_21 = \"\"\"\n    >>> c = Ace21Card(\"A\", Suit.Spade)\n\n    >>> c.label = \"no slot named label\"  # doctest: +IGNORE_EXCEPTION_DETAIL\n    Traceback (most recent call last):\n      File \"/Users/slott/Documents/Writing/Python/Mastering OO Python 2e/mastering-oo-python-2e/Chapter_4/ch04_ex1.py\", line 173, in <module>\n        card2d.label = \"no slot named label\"\n    AttributeError: 'Number21Card' object has no attribute 'label'\n\n    >>> print(c.label)  # doctest: +IGNORE_EXCEPTION_DETAIL\n    Traceback (most recent call last):\n      File \"/Users/slott/Documents/Writing/Python/Mastering OO Python 2e/mastering-oo-python-2e/Chapter_4/ch04_ex1.py\", line 173, in <module>\n        card2d.label = \"no slot named label\"\n    AttributeError: 'Number21Card' object has no attribute 'label'\n\n    >>> c\n    Ace21Card(rank=1, suit=<Suit.Spade: '♠'>)\n    \n    >>> c.rank = 2\n\n    >>> card2d = card21(2, Suit.Diamond)\n    >>> card2s = card21(2, Suit.Spade)\n    >>> cardkd = card21(13, Suit.Diamond)\n\n    >>> card2d\n    Number21Card(rank=2, suit=<Suit.Diamond: '♦'>)\n\n    >>> compare(card2d, card2s)\n    2♦ == 2♠ False, 2♦ < 2♠ False, 2♦ <= 2♠ True\n    2♦ != 2♠ True, 2♦ > 2♠ False, 2♦ >= 2♠ True\n    >>> compare(card2s, cardkd)\n    2♠ == K♦ False, 2♠ < K♦ True, 2♠ <= K♦ True\n    2♠ != K♦ True, 2♠ > K♦ False, 2♠ >= K♦ False\n    >>> compare(card2d, 2)    # doctest: +IGNORE_EXCEPTION_DETAIL\n    Traceback (most recent call last):\n      File \"/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py\", line 1329, in __run\n        compileflags, 1), test.globs)\n      File \"<doctest __main__.__test__.test_comparisons_21[10]>\", line 1, in <module>\n        compare(card2d, 2)    # doctest: +IGNORE_EXCEPTION_DETAIL\n      File \"/Users/slott/Documents/Writing/Python/Mastering OO Python 2e/mastering-oo-python-2e/Chapter_4/ch04_ex1.py\", line 117, in compare\n        print(f\"{a} == {b} {a==b}, {a} < {b} {a<b}, {a} <= {b} {a <= b}\")\n    TypeError: '<' not supported between instances of 'Number21Card' and 'int'\n\"\"\"\n\n\nclass Deck(list):\n\n    def __init__(\n        self, decks: int = 6, factory: Callable[[int, Suit], BlackJackCard] = card21\n    ) -> None:\n        super().__init__()\n        for i in range(decks):\n            self.extend(factory(r + 1, s) for r in range(13) for s in cast(Iterable[Suit], Suit))\n        random.shuffle(self)\n        burn = random.randint(1, 52)\n        for i in range(burn):\n            self.pop()\n\n\nclass AceCard2(NamedTuple):\n    rank: str\n    suit: Suit\n    hard: int = 1\n    soft: int = 11\n\n    def __str__(self) -> str:\n        return f\"{self.rank}{self.suit}\"\n\n\nclass FaceCard2(NamedTuple):\n    rank: str\n    suit: Suit\n    hard: int = 10\n    soft: int = 10\n\n    def __str__(self) -> str:\n        return f\"{self.rank}{self.suit}\"\n\n\nclass NumberCard2(NamedTuple):\n    rank: str\n    suit: Suit\n    hard: int\n    soft: int\n\n    def __str__(self) -> str:\n        return f\"{self.rank}{self.suit}\"\n\n\ndef card2(rank: int, suit: Suit) -> Union[AceCard2, FaceCard2, NumberCard2]:\n    \"\"\"No parent class... \"\"\"\n    if rank == 1:\n        return AceCard2(\"A\", suit)\n    elif 2 <= rank < 11:\n        return NumberCard2(str(rank), suit, rank, rank)\n    elif 11 <= rank < 14:\n        rank_str = {11: \"J\", 12: \"Q\", 13: \"K\"}[rank]\n        return FaceCard2(rank_str, suit)\n    else:\n        raise TypeError\n\n\ntest_comparisons_2 = \"\"\"\n    >>> c = AceCard2(\"A\", Suit.Spade)\n    >>> c.rank\n    'A'\n    >>> c.suit\n    <Suit.Spade: '♠'>\n    >>> c.hard\n    1\n    >>> c.not_allowed = 2  # doctest: +IGNORE_EXCEPTION_DETAIL\n    Traceback (most recent call last):\n      File \"/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py\", line 1329, in __run\n        compileflags, 1), test.globs)\n      File \"<doctest __main__.__test__.test_comparisons_2[3]>\", line 1, in <module>\n        c.not_allowed = 2\n    AttributeError: 'AceCard2' object has no attribute 'not_allowed'\n    >>> c.rank = 3  # doctest: +IGNORE_EXCEPTION_DETAIL\n    Traceback (most recent call last):\n      File \"/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py\", line 1329, in __run\n        compileflags, 1), test.globs)\n      File \"<doctest __main__.__test__.test_comparisons_2[4]>\", line 1, in <module>\n        c.rank = 3\n    AttributeError: can't set attribute\n\n    >>> c.label = \"no slot named label\"  # doctest: +IGNORE_EXCEPTION_DETAIL\n    Traceback (most recent call last):\n      File \"/Users/slott/Documents/Writing/Python/Mastering OO Python 2e/mastering-oo-python-2e/Chapter_4/ch04_ex1.py\", line 173, in <module>\n        card2d.label = \"no slot named label\"\n    AttributeError: 'AceCard2' object has no attribute 'label'\n\n    >>> print(c.label)  # doctest: +IGNORE_EXCEPTION_DETAIL\n    Traceback (most recent call last):\n      File \"/Users/slott/Documents/Writing/Python/Mastering OO Python 2e/mastering-oo-python-2e/Chapter_4/ch04_ex1.py\", line 173, in <module>\n        card2d.label = \"no slot named label\"\n    AttributeError: 'AceCard2' object has no attribute 'label'\n\n    >>> c\n    AceCard2(rank='A', suit=<Suit.Spade: '♠'>, hard=1, soft=11)\n\n    >>> card2d = card2(2, Suit.Diamond)\n    >>> card2s = card2(2, Suit.Spade)\n    >>> cardkd = card2(13, Suit.Diamond)\n\n    >>> card2d\n    NumberCard2(rank='2', suit=<Suit.Diamond: '♦'>, hard=2, soft=2)\n\n    >>> compare(card2d, card2s)\n    2♦ == 2♠ False, 2♦ < 2♠ False, 2♦ <= 2♠ False\n    2♦ != 2♠ True, 2♦ > 2♠ True, 2♦ >= 2♠ True\n    >>> compare(card2s, cardkd)\n    2♠ == K♦ False, 2♠ < K♦ True, 2♠ <= K♦ True\n    2♠ != K♦ True, 2♠ > K♦ False, 2♠ >= K♦ False\n    >>> compare(card2d, 2)    # doctest: +IGNORE_EXCEPTION_DETAIL\n    Traceback (most recent call last):\n    TypeError: '<' not supported between instances of 'NumberCard2' and 'int'\n\"\"\"\n\n\n@dataclass(eq=True, order=True, frozen=True)\nclass AceCard3:\n    rank: str\n    suit: Suit\n    hard: int = 1\n    soft: int = 11\n\n\n@dataclass(eq=True, order=True, frozen=True)\nclass FaceCard3:\n    rank: str\n    suit: Suit\n    hard: int = 10\n    soft: int = 10\n\n\n@dataclass(eq=True, order=True, frozen=True)\nclass NumberCard3:\n    rank: str\n    suit: Suit\n    hard: int\n    soft: int\n\n\ndef card3(rank, suit) -> Union[AceCard3, FaceCard3, NumberCard3]:\n    if rank == 1:\n        return AceCard3(\"A\", suit)\n    elif 2 <= rank < 11:\n        return NumberCard3(str(rank), suit, rank, rank)\n    elif 11 <= rank < 14:\n        rank_str = {11: \"J\", 12: \"Q\", 13: \"K\"}[rank]\n        return FaceCard3(rank_str, suit)\n    else:\n        raise TypeError\n\n\ntest_comparisons_3 = \"\"\"\n    >>> c = AceCard3(\"A\", Suit.Spade)\n    \n    >>> c.label = \"no slot named label\"   \n    Traceback (most recent call last):\n    dataclasses.FrozenInstanceError: cannot assign to field 'label'\n    >>> print(c.label)\n    Traceback (most recent call last):\n    AttributeError: 'AceCard3' object has no attribute 'label'\n        \n    >>> c\n    AceCard3(rank='A', suit=<Suit.Spade: '♠'>, hard=1, soft=11)\n\n    >>> card2d = card3(2, Suit.Diamond)\n    >>> card2s = card3(2, Suit.Spade)\n    >>> cardkd = card3(13, Suit.Diamond)\n    \n    >>> card2d\n    NumberCard3(rank='2', suit=<Suit.Diamond: '♦'>, hard=2, soft=2)\n\n    >>> compare(card2d, card2s)\n    NumberCard3(rank='2', suit=<Suit.Diamond: '♦'>, hard=2, soft=2) == NumberCard3(rank='2', suit=<Suit.Spade: '♠'>, hard=2, soft=2) False, NumberCard3(rank='2', suit=<Suit.Diamond: '♦'>, hard=2, soft=2) < NumberCard3(rank='2', suit=<Suit.Spade: '♠'>, hard=2, soft=2) False, NumberCard3(rank='2', suit=<Suit.Diamond: '♦'>, hard=2, soft=2) <= NumberCard3(rank='2', suit=<Suit.Spade: '♠'>, hard=2, soft=2) False\n    NumberCard3(rank='2', suit=<Suit.Diamond: '♦'>, hard=2, soft=2) != NumberCard3(rank='2', suit=<Suit.Spade: '♠'>, hard=2, soft=2) True, NumberCard3(rank='2', suit=<Suit.Diamond: '♦'>, hard=2, soft=2) > NumberCard3(rank='2', suit=<Suit.Spade: '♠'>, hard=2, soft=2) True, NumberCard3(rank='2', suit=<Suit.Diamond: '♦'>, hard=2, soft=2) >= NumberCard3(rank='2', suit=<Suit.Spade: '♠'>, hard=2, soft=2) True\n    >>> compare(card2s, cardkd)\n    Traceback (most recent call last):\n      File \"/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py\", line 1329, in __run\n        compileflags, 1), test.globs)\n      File \"<doctest __main__.__test__.test_comparisons_3[9]>\", line 1, in <module>\n        compare(card2s, cardkd)\n      File \"/Users/slott/Documents/Writing/Python/Mastering OO Python 2e/mastering-oo-python-2e/Chapter_4/ch04_ex1.py\", line 117, in compare\n        print(f\"{a} == {b} {a==b}, {a} < {b} {a<b}, {a} <= {b} {a <= b}\")\n    TypeError: '<' not supported between instances of 'NumberCard3' and 'FaceCard3'\n    >>> compare(card2d, 2)    # doctest: +IGNORE_EXCEPTION_DETAIL\n    Traceback (most recent call last):\n    TypeError: '<' not supported between instances of 'NumberCard3' and 'int'\n\"\"\"\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_4/ch04_ex2.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 4. Example 2.\n\"\"\"\nimport random\nfrom typing import List\n\nfrom Chapter_4.ch04_ex1 import Deck, BlackJackCard\n\n\n# Property Decorator\n# ==============================\n#\n# Definition of Hand using a property for the total.\n\n\nclass Hand:\n\n    def __init__(\n            self,\n             dealer_card: BlackJackCard,\n             *cards: BlackJackCard\n        ) -> None:\n        self.dealer_card: BlackJackCard = dealer_card\n        self._cards: List[BlackJackCard] = list(cards)\n\n    def __str__(self) -> str:\n        return \", \".join(map(str, self.card))\n\n    def __repr__(self) -> str:\n        return (\n            f\"{self.__class__.__name__}\"\n            f\"({self.dealer_card!r}, \" \n            f\"{', '.join(map(repr, self.card))})\"\n        )\n\n    @property\n    def card(self) -> List[BlackJackCard]:\n        return self._cards\n\n    @card.setter\n    def card(self, aCard: BlackJackCard) -> None:\n        raise NotImplementedError\n\n    @card.deleter\n    def card(self) -> None:\n        raise NotImplementedError\n\n    def split(self, deck: Deck) -> \"Hand\":\n        \"\"\"Updates this hand and also returns the new hand.\"\"\"\n        assert self._cards[0].rank == self._cards[1].rank\n        c1 = self._cards[-1]\n        del self.card\n        self.card = deck.pop()\n        h_new = self.__class__(self.dealer_card, c1, deck.pop())\n        return h_new\n\n\nclass Hand_Lazy(Hand):\n\n    @property\n    def total(self) -> int:\n        delta_soft = max(c.soft - c.hard for c in self._cards)\n        hard_total = sum(c.hard for c in self._cards)\n        if hard_total + delta_soft <= 21:\n            return hard_total + delta_soft\n        return hard_total\n\n    @property\n    def card(self) -> List[BlackJackCard]:\n        return self._cards\n\n    @card.setter\n    def card(self, aCard: BlackJackCard) -> None:\n        self._cards.append(aCard)\n\n    @card.deleter\n    def card(self) -> None:\n        self._cards.pop(-1)\n\n\n# We can now work with the total value of a hand using Hand.total\n# instead of hand.total().\n#\n\n\ntest_hand_lazy = \"\"\"\n    >>> random.seed(9973)\n    >>> d = Deck()\n    >>> h = Hand_Lazy(d.pop(), d.pop(), d.pop())\n    >>> print(h.total)\n    14\n    >>> h.card = d.pop()\n    >>> print(h.total)\n    18\n\"\"\"\n\n# What's the advantage?\n# Simpler syntax. We can still have lazy vs. eager calculation of\n# the total value of the hand.\n\n\nclass Hand_Eager(Hand):\n\n    def __init__(\n            self,\n            dealer_card: BlackJackCard,\n            *cards: BlackJackCard\n    ) -> None:\n        self.dealer_card = dealer_card\n        self.total = 0\n        self._delta_soft = 0\n        self._hard_total = 0\n        self._cards: List[BlackJackCard] = list()\n        for c in cards:\n            # Mypy cannot discern the actual type of the setter.\n            # https://github.com/python/mypy/issues/4167\n            self.card = c  # type: ignore\n\n    @property\n    def card(self) -> List[BlackJackCard]:\n        return self._cards\n\n    @card.setter\n    def card(self, aCard: BlackJackCard) -> None:\n        self._cards.append(aCard)\n        self._delta_soft = max(aCard.soft - aCard.hard, self._delta_soft)\n        self._hard_total = self._hard_total + aCard.hard\n        self._set_total()\n\n    @card.deleter\n    def card(self) -> None:\n        removed = self._cards.pop(-1)\n        self._hard_total -= removed.hard\n        # Issue: was this the only ace?\n        self._delta_soft = max(c.soft - c.hard for c in self._cards)\n        self._set_total()\n\n    def _set_total(self) -> None:\n        if self._hard_total + self._delta_soft <= 21:\n            self.total = self._hard_total + self._delta_soft\n        else:\n            self.total = self._hard_total\n\n\ntest_hand_eager_and_lazy = \"\"\"\n    >>> random.seed(9973)\n    >>> d = Deck()\n    >>> h = Hand_Eager(d.pop(), d.pop(), d.pop())\n    >>> print(h.total)\n    14\n    >>> h.card = d.pop()\n    >>> print(h.total)\n    18\n\n    >>> random.seed(9973)\n    >>> d = Deck()\n    >>> c = d.pop()\n    >>> h = Hand_Lazy(d.pop(), c, c)  # Force splittable hand\n    >>> h2 = h.split(d)\n\n    >>> print(h)\n    6♦, A♦\n    >>> print(h2)\n    6♦, 4♠\n\"\"\"\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_4/ch04_ex3.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 4. Example 3.\n\"\"\"\nfrom typing import Any, Optional\nfrom dataclasses import dataclass\n\nfrom Chapter_4.ch04_ex1 import Deck, BlackJackCard\n\n# Eagerly Computed Attributes\n# ============================\n\n# Compute early and often. This is rather complex because it\n# derives any one from the other two. The ``Optional[float]`` is misleading.\n\n\n@dataclass\nclass RateTimeDistance:\n\n    rate: Optional[float] = None\n    time: Optional[float] = None\n    distance: Optional[float] = None\n\n    def __post_init__(self) -> None:\n        if self.rate is not None and self.time is not None:\n            self.distance = self.rate * self.time\n        elif self.rate is not None and self.distance is not None:\n            self.time = self.distance / self.rate\n        elif self.time is not None and self.distance is not None:\n            self.rate = self.distance / self.time\n\n\ntest_rtd = \"\"\"\n    >>> rtd = RateTimeDistance(rate=6.3, time=8.25, distance=None)\n    >>> print(f\"Rate={rtd.rate}, Time={rtd.time}, Distance={rtd.distance}\")\n    Rate=6.3, Time=8.25, Distance=51.975\n    \n    >>> RateTimeDistance(rate=5.2, time=9.5)\n    RateTimeDistance(rate=5.2, time=9.5, distance=49.4)\n    >>> RateTimeDistance(distance=48.5, rate=6.1)\n    RateTimeDistance(rate=6.1, time=7.950819672131148, distance=48.5)\n    \n    >>> r1 = RateTimeDistance(time=1, rate=0)\n    >>> r1.distance = -99\n    >>> r1\n    RateTimeDistance(rate=0, time=1, distance=-99)\n\"\"\"\n\n# Dynamic Attributes\n# ==================\n\n\nclass RTD_Dynamic:\n\n    def __init__(self) -> None:\n        self.rate: float\n        self.time: float\n        self.distance: float\n\n        super().__setattr__(\"rate\", None)\n        super().__setattr__(\"time\", None)\n        super().__setattr__(\"distance\", None)\n\n    def __repr__(self) -> str:\n        clauses = []\n        if self.rate:\n            clauses.append(f\"rate={self.rate}\")\n        if self.time:\n            clauses.append(f\"time={self.time}\")\n        if self.distance:\n            clauses.append(f\"distance={self.distance}\")\n        return (f\"{self.__class__.__name__}\" f\"({', '.join(clauses)})\")\n\n    def __setattr__(self, name: str, value: float) -> None:\n        if name == \"rate\":\n            super().__setattr__(\"rate\", value)\n        elif name == \"time\":\n            super().__setattr__(\"time\", value)\n        elif name == \"distance\":\n            super().__setattr__(\"distance\", value)\n\n        if self.rate and self.time:\n            super().__setattr__(\"distance\", self.rate * self.time)\n        elif self.rate and self.distance:\n            super().__setattr__(\"time\", self.distance / self.rate)\n        elif self.time and self.distance:\n            super().__setattr__(\"rate\", self.distance / self.time)\n\n\ntest_rtd_dynamic = \"\"\"\n    >>> rtd = RTD_Dynamic()\n    >>> rtd.time = 9.5\n    >>> rtd\n    RTD_Dynamic(time=9.5)\n    >>> rtd.rate = 6.25\n    >>> rtd\n    RTD_Dynamic(rate=6.25, time=9.5, distance=59.375)\n    >>> rtd.distance\n    59.375\n    \n    >>> rtd.time = None\n    >>> rtd.rate = 6.125\n    >>> rtd\n    RTD_Dynamic(rate=6.125, time=9.5, distance=58.1875)\n    \n\"\"\"\n\n# Descriptors\n# =====================\n\n# A Non-Data descriptor example where the descriptor object\n# reads a local cache file to set class-level parameters\n\nfrom pathlib import Path\nfrom typing import Type\n\n\nclass PersistentState:\n    \"\"\"Abstract superclass to use a StateManager object\"\"\"\n    _saved: Path\n\n\nclass StateManager:\n    \"\"\"May create a directory. Sets _saved in the instance.\"\"\"\n\n    def __init__(self, base: Path) -> None:\n        self.base = base\n\n    def __get__(self, instance: PersistentState, owner: Type) -> Path:\n        if not hasattr(instance, \"_saved\"):\n            class_path = self.base / owner.__name__\n            class_path.mkdir(exist_ok=True, parents=True)\n            instance._saved = class_path / str(id(instance))\n        return instance._saved\n\n\nclass PersistentClass(PersistentState):\n    state_path = StateManager(Path.cwd() / \"data\" / \"state\")\n\n    def __init__(self, a: int, b: float) -> None:\n        self.a = a\n        self.b = b\n        self.c: Optional[float] = None\n        self.state_path.write_text(repr(vars(self)))\n\n    def calculate(self, c: float) -> float:\n        self.c = c\n        self.state_path.write_text(repr(vars(self)))\n        return self.a * self.b + self.c\n\n    def __str__(self) -> str:\n        return self.state_path.read_text()\n\n\ntest_persist = \"\"\"\n    >>> x = PersistentClass(1, 2)\n    >>> str(x)  # doctest: +ELLIPSIS\n    \"{'a': 1, 'b': 2, 'c': None, '_saved': ...)}\"\n    >>> x.calculate(3)\n    5\n    >>> str(x)  # doctest: +ELLIPSIS\n    \"{'a': 1, 'b': 2, 'c': 3, '_saved': ...)}\"\n\"\"\"\n\n# A data descriptor example with data in the containing instance.\n\n\nclass Conversion:\n    \"\"\"Depends on a standard value.\"\"\"\n    conversion: float\n    standard: str\n\n    def __get__(self, instance: Any, owner: type) -> float:\n        return getattr(instance, self.standard) * self.conversion\n\n    def __set__(self, instance: Any, value: float) -> None:\n        setattr(instance, self.standard, value / self.conversion)\n\n\nclass Standard(Conversion):\n    \"\"\"Defines a standard value.\"\"\"\n    conversion = 1.0\n\n\nclass Speed(Conversion):\n    standard = \"standard_speed\"  # KPH\n\n\nclass KPH(Standard, Speed):\n    pass\n\n\nclass Knots(Speed):\n    conversion = 0.5399568\n\n\nclass MPH(Speed):\n    conversion = 0.62137119\n\n\nclass Trip:\n    kph = KPH()\n    knots = Knots()\n    mph = MPH()\n\n    def __init__(\n        self,\n        distance: float,\n        kph: Optional[float] = None,\n        mph: Optional[float] = None,\n        knots: Optional[float] = None,\n    ) -> None:\n        self.distance = distance  # Nautical Miles\n        if kph:\n            self.kph = kph\n        elif mph:\n            self.mph = mph\n        elif knots:\n            self.knots = knots\n        else:\n            raise TypeError(\"Impossible pattern of None values\")\n        self.time = self.distance / self.knots\n\n    def __str__(self) -> str:\n        return (\n            f\"distance: {self.distance} nm, \"\n            f\"rate: {self.kph} \"\n            f\"kph = {self.mph} \"\n            f\"mph = {self.knots} knots, \"\n            f\"time = {self.time} hrs\"\n        )\n\n\ntest_trip = \"\"\"\n    >>> m2 = Trip(distance=13.2, knots=5.9)\n    >>> print(m2)\n    distance: 13.2 nm, rate: 10.92680006993152 kph = 6.789598762345432 mph = 5.9 knots, time = 2.23728813559322 hrs\n    >>> print(f\"Speed: {m2.mph:.3f} mph\")\n    Speed: 6.790 mph\n    >>> m2.standard_speed\n    10.92680006993152\n\"\"\"\n\n__test__ = {\n    name: value for name, value in locals().items() if name.startswith(\"test_\")\n}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_4/ch04_ex4.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 4. Example 4.\n\"\"\"\nfrom typing import Any, Optional\n\nfrom Chapter_4.ch04_ex1 import Suit\n\n\nclass Card:\n    \"\"\"A poor replacement for polymorphism.\"\"\"\n\n    def __init__(cls, rank: int, suit: Suit) -> None:\n        super().__setattr__(\"_cache\", {\"suit\": suit, \"rank\": rank})\n\n    def __setattr__(self, name: str, value: Any) -> None:\n        raise TypeError(\"Can't Touch That\")\n\n    def __getattr__(self, name: str) -> Any:\n        if name in self._cache:\n            return self._cache[name]\n        elif name == \"hard\":\n            if self.rank in (11, 12, 13):\n                return 10\n            elif self.rank == 1:\n                return 1\n            elif self.rank in range(2, 10):\n                return self.rank\n            else:\n                raise ValueError(\"Invalid Rank\")\n        elif name == \"soft\":\n            if self.rank in (11, 12, 13):\n                return 10\n            elif self.rank == 1:\n                return 11\n            elif self.rank in range(2, 10):\n                return self.rank\n            else:\n                raise ValueError(\"Invalid Rank\")\n        else:\n            raise AttributeError(name)\n\n\ntest_card = \"\"\"\n    >>> c = Card(2, Suit.Club)\n    >>> c.rank = 3  # doctest: +IGNORE_EXCEPTION_DETAIL\n    Traceback (most recent call last):\n      File \"/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py\", line 1329, in __run\n        compileflags, 1), test.globs)\n      File \"<doctest __main__.__test__.test_card[2]>\", line 1, in <module>\n        c.rank = 3\n      File \"/Users/slott/Documents/Writing/Python/Mastering OO Python 2e/mastering-oo-python-2e/Chapter_4/ch04_ex4.py\", line 17, in __setattr__\n        raise TypeError(\"Can't Touch That\")\n    TypeError: Can't Touch That\n    >>> c.extra = 3  # doctest: +IGNORE_EXCEPTION_DETAIL\n    Traceback (most recent call last):\n      File \"/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py\", line 1329, in __run\n        compileflags, 1), test.globs)\n      File \"<doctest __main__.__test__.test_card[2]>\", line 1, in <module>\n        c.rank = 3\n      File \"/Users/slott/Documents/Writing/Python/Mastering OO Python 2e/mastering-oo-python-2e/Chapter_4/ch04_ex4.py\", line 17, in __setattr__\n        raise TypeError(\"Can't Touch That\")\n    TypeError: Can't Touch That\n    >>> c.extra  # doctest: +IGNORE_EXCEPTION_DETAIL\n    Traceback (most recent call last):\n      File \"/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py\", line 1329, in __run\n        compileflags, 1), test.globs)\n      File \"<doctest __main__.__test__.test_card[3]>\", line 1, in <module>\n        c.extra\n      File \"/Users/slott/Documents/Writing/Python/Mastering OO Python 2e/mastering-oo-python-2e/Chapter_4/ch04_ex4.py\", line 44, in __getattr__\n        raise AttributeError(name)\n    AttributeError: extra\n    \n    >>> c.rank\n    2\n    >>> c.hard\n    2\n    >>> c.soft\n    2\n\"\"\"\n\n\nclass RTD_Solver:\n\n    def __init__(\n        self, *, rate: float = None, time: float = None, distance: float = None\n    ) -> None:\n        if rate:\n            self.rate = rate\n        if time:\n            self.time = time\n        if distance:\n            self.distance = distance\n\n    def __getattr__(self, name: str) -> float:\n        if name == \"rate\":\n            print(\"Computing Rate\")\n            return self.distance / self.time\n        elif name == \"time\":\n            return self.distance / self.rate\n        elif name == \"distance\":\n            return self.rate * self.time\n        else:\n            raise AttributeError(f\"Can't compute {name}\")\n\n\ntest_rtd = \"\"\"\n    >>> r1 = RTD_Solver(rate=6.25, distance=10.25)\n    >>> r1.time\n    1.64\n    >>> r1.rate\n    6.25\n\"\"\"\n\n\nclass SuperSecret:\n\n    def __init__(self, hidden: Any, exposed: Any) -> None:\n        self._hidden = hidden\n        self.exposed = exposed\n\n    def __getattribute__(self, item: str):\n        if (len(item) >= 2 and item[0] == \"_\"\n                and item[1] != \"_\"):\n            raise AttributeError(item)\n        return super().__getattribute__(item)\n\n\ntest_secret = \"\"\"\n    >>> x = SuperSecret('onething', 'another')\n    >>> x.exposed\n    'another'\n    >>> x._hidden  # doctest: +IGNORE_EXCEPTION_DETAIL\n    Traceback (most recent call last):\n      File \"/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py\", line 1329, in __run\n        compileflags, 1), test.globs)\n      File \"<doctest __main__.__test__.test_secret[3]>\", line 1, in <module>\n        x._hidden  #\n      File \"/Users/slott/Documents/Writing/Python/Mastering OO Python 2e/mastering-oo-python-2e/Chapter_4/ch04_ex4.py\", line 132, in __getattribute__\n        raise AttributeError(item)\n    AttributeError: _hidden\n\n    >>> dir(x)  # doctest: +ELLIPSIS\n    [..., '_hidden', 'exposed']\n    >>> vars(x)\n    {'_hidden': 'onething', 'exposed': 'another'}\n\"\"\"\n\n__test__ = {\n    name: value for name, value in locals().items() if name.startswith(\"test_\")\n}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_4/ch04_ex5.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 4. Example 5.\n\"\"\"\n\nfrom dataclasses import dataclass\nfrom enum import Enum\nfrom typing import Iterator, cast, Iterable, Optional\n\n\n@dataclass\nclass RTD:\n    rate: Optional[float]\n    time: Optional[float]\n    distance: Optional[float]\n\n    def compute(self) -> \"RTD\":\n        if (\n            self.distance is None and self.rate is not None and self.time is not None\n        ):\n            self.distance = self.rate * self.time\n        elif (\n            self.rate is None and self.distance is not None and self.time is not None\n        ):\n            self.rate = self.distance / self.time\n        elif (\n            self.time is None and self.distance is not None and self.rate is not None\n        ):\n            self.time = self.distance / self.rate\n        return self\n\n\ntest_rtd = \"\"\"\n    >>> r = RTD(distance=13.5, rate=6.1, time=None)\n    >>> r.compute()\n    RTD(rate=6.1, time=2.2131147540983607, distance=13.5)\n\"\"\"\n\n\nclass Suit(str, Enum):\n    Club = \"\\N{BLACK CLUB SUIT}\"\n    Diamond = \"\\N{BLACK DIAMOND SUIT}\"\n    Heart = \"\\N{BLACK HEART SUIT}\"\n    Spade = \"\\N{BLACK SPADE SUIT}\"\n\n\n@dataclass(frozen=True, order=True)\nclass Card:\n    rank: int\n    suit: str\n\n    @property\n    def points(self) -> int:\n        return self.rank\n\n\nclass Ace(Card):\n\n    @property\n    def points(self) -> int:\n        return 1\n\n\nclass Face(Card):\n\n    @property\n    def points(self) -> int:\n        return 10\n\n\ndef deck() -> Iterator[Card]:\n    for rank in range(1, 14):\n        for suit in cast(Iterable[Suit], Suit):\n            if rank == 1:\n                yield Ace(rank, suit)\n            elif rank >= 11:\n                yield Face(rank, suit)\n            else:\n                yield Card(rank, suit)\n\n\ntest_dataclass = \"\"\"\n    >>> a = Card(7, Suit.Heart)\n    >>> a.rank\n    7\n    >>> a.suit\n    <Suit.Heart: '♥'>\n    >>> b = Card(7, Suit.Heart)\n    >>> a == b\n    True\n    >>> a < Card(8, Suit.Spade)\n    True\n\"\"\"\n\ntest_hand = \"\"\"\n    >>> import random\n    >>> random.seed(16)\n    >>> cards = list(deck())\n    >>> random.shuffle(cards)\n    >>> hand = cards[:5]\n    >>> any(c.rank == 1 for c in hand)\n    True\n    >>> any(c.points == 10 for c in hand)\n    True\n    >>> sum(c.points for c in hand)\n    34\n    >>> for c in hand:\n    ...     print(f\"{c!r}: {c.points}\")\n    Card(rank=3, suit=<Suit.Heart: '♥'>): 3\n    Ace(rank=1, suit=<Suit.Spade: '♠'>): 1\n    Face(rank=11, suit=<Suit.Club: '♣'>): 10\n    Face(rank=13, suit=<Suit.Spade: '♠'>): 10\n    Face(rank=12, suit=<Suit.Diamond: '♦'>): 10\n    >>> Ace(1, Suit.Spade) in set(hand)\n    True\n\"\"\"\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_5/__init__.py",
    "content": ""
  },
  {
    "path": "Chapter_5/ch05_ex1.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 5. Example 1.\n\"\"\"\nfrom typing import Any, Union\n\n# Metaclass\n# ======================\n\n# Abstract Base Class Example.\n\nfrom abc import ABCMeta, abstractmethod, ABC\n\nclass Card:\n    pass\n\n\nclass Hand(list):\n    def __init__(self, *cards: Card) -> None:\n        super().__init__(cards)\n\n\nclass AbstractBettingStrategy(metaclass=ABCMeta):\n\n    @abstractmethod\n    def bet(self, hand: Hand) -> int:\n        return 1\n\n    @abstractmethod\n    def record_win(self, hand: Hand) -> None:\n        pass\n\n    @abstractmethod\n    def record_loss(self, hand: Hand) -> None:\n        pass\n\nclass AbstractBettingStrategy2(ABC):\n\n    @abstractmethod\n    def bet(self, hand: Hand) -> int:\n        return 1\n\n    @abstractmethod\n    def record_win(self, hand: Hand) -> None:\n        pass\n\n    @abstractmethod\n    def record_loss(self, hand: Hand) -> None:\n        pass\n\n    @classmethod\n    def __subclasshook__(cls, subclass: type) -> bool:\n        \"\"\"Validate the class definition is complete.\"\"\"\n        if cls is AbstractBettingStrategy2:\n            has_bet = any(hasattr(B, \"bet\") for B in subclass.__mro__)\n            has_record_win =  any(hasattr(B, \"record_win\") for B in subclass.__mro__)\n            has_record_loss = any(hasattr(B, \"record_loss\") for B in subclass.__mro__)\n            if has_bet and has_record_win and has_record_loss:\n                return True\n        # print(f\"has_bet {has_bet}, has_record_win {has_record_win}, has_record_loss {has_record_loss}\")\n        return False\n\ntest_broken = \"\"\"\n    >>> class Simple_Broken(AbstractBettingStrategy):\n    ...     def bet(self, hand: Hand) -> int:\n    ...         return 1\n    >>> simple = Simple_Broken()  # doctest: +IGNORE_EXCEPTION_DETAIL\n    Traceback (most recent call last):\n      File \"/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py\", line 1329, in __run\n        compileflags, 1), test.globs)\n      File \"<doctest __main__.__test__.test_broken[0]>\", line 1, in <module>\n        simple = Simple_Broken()  # doctest: +IGNORE_EXCEPTION_DETAIL\n    TypeError: Can't instantiate abstract class Simple_Broken with abstract methods record_loss, record_win\n\"\"\"\n\n\ntest_broken_2 = \"\"\"\n    >>> class Simple_Broken2(AbstractBettingStrategy2):\n    ...     def bet(self, hand: Hand) -> int:\n    ...         return 1\n    ...\n    >>> simple2 = Simple_Broken2()  # doctest: +IGNORE_EXCEPTION_DETAIL\n    Traceback (most recent call last):\n      File \"/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py\", line 1329, in __run\n        compileflags, 1), test.globs)\n      File \"<doctest __main__.__test__.test_broken_2[1]>\", line 1, in <module>\n        simple2 = Simple_Broken2()\n    TypeError: Can't instantiate abstract class Simple_Broken2 with abstract methods record_loss, record_win     \n\"\"\"\n\n\nclass Simple(AbstractBettingStrategy):\n\n    def bet(self, hand: Hand) -> int:\n        return 1\n\n    def record_win(self, hand: Hand) -> None:\n        pass\n\n    def record_loss(self, hand: Hand) -> None:\n        pass\n\ntest_proper = \"\"\"\n    >>> simple = Simple()\n\"\"\"\n\nfrom typing import Tuple, Iterator\n\nclass LikeAbstract:\n    def aMethod(self, arg: int) -> int:\n        raise NotImplementedError\n\n# The following will raise a mypy error.\n# Chapter_5/ch05_ex1.py:114: error: Signature of \"aMethod\" incompatible with supertype \"LikeAbstract\"\nclass LikeConcrete(LikeAbstract):\n    def aMethod(self, arg1: str, arg2: Tuple[int, int]) -> Iterator[Any]:\n        pass\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_5/ch05_ex2.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 5. Example 2.\n\"\"\"\nimport numbers\nimport decimal\nimport collections.abc\n\ntest_membership = \"\"\"\n>>> isinstance(42, numbers.Number) \nTrue\n>>> 355/113             \n3.1415929203539825\n>>> isinstance(355/113, numbers.Number) \nTrue\n\n>>> issubclass(decimal.Decimal, numbers.Number)\nTrue\n>>> issubclass(decimal.Decimal, numbers.Integral) \nFalse\n>>> issubclass(decimal.Decimal, numbers.Real) \nFalse\n>>> issubclass(decimal.Decimal, numbers.Complex) \nFalse\n>>> issubclass(decimal.Decimal, numbers.Rational) \nFalse\n\n\"\"\"\n\ntest_iterator = \"\"\"\n>>> x = [1, 2, 3]\n>>> iter(x)  # doctest: +ELLIPSIS\n<list_iterator object at ...>\n>>> x_iter = iter(x)\n>>> next(x_iter)\n1\n>>> next(x_iter) \n2\n>>> next(x_iter) \n3\n>>> next(x_iter)  # doctest: +IGNORE_EXCEPTION_DETAIL\nTraceback (most recent call last):\n  File \"/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py\", line 1329, in __run\n    compileflags, 1), test.globs)\n  File \"<doctest __main__.__test__.test_iterator[6]>\", line 1, in <module>\n    next(x_iter)\nStopIteration\n>>> isinstance(x_iter, collections.abc.Iterator) \nTrue\n\n\"\"\"\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_6/__init__.py",
    "content": ""
  },
  {
    "path": "Chapter_6/ch06_ex1.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 6. Example 1.\n\"\"\"\nfrom typing import Callable, Dict\n\n# Callable\n# ======================\n\n# Callable Example #1. Inefficient.  But. It does work.\n\nIntExp = Callable[[int, int], int]\n\nclass Power1:\n\n    def __call__(self, x: int, n: int) -> int:\n        p = 1\n        for i in range(n):\n            p *= x\n        return p\n\npow1: IntExp = Power1()\n\ntest_power1 = \"\"\"\n    >>> pow1 = Power1()\n    >>> pow1(2, 1024)\n    179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137216\n\"\"\"\n\n# Example 2. Subtle error, can be detected by the class definition\n# Disliked by mypy as an 'Invalid base class' error: we're forced to ignore it.\nfrom collections.abc import Callable as CallableClass\nclass Power2(CallableClass):  # type: ignore\n\n    def __call_(self, x: int, n: int) -> int:\n        p = 1\n        for i in range(n):\n            p *= x\n        return p\n\ntest_power2 = \"\"\"\n    >>> pow2 = Power2()  # doctest: +IGNORE_EXCEPTION_DETAIL\n    Traceback (most recent call last):\n      File \"/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py\", line 1329, in __run\n        compileflags, 1), test.globs)\n      File \"<doctest __main__.__test__.test_power2[0]>\", line 1, in <module>\n        pow2 = Power2()\n      File \"/Users/slott/miniconda3/envs/py37/lib/python3.7/typing.py\", line 813, in __new__\n        obj = super().__new__(cls, *args, **kwds)\n    TypeError: Can't instantiate abstract class Power2 with abstract methods __call__\n\"\"\"\n\n# Example 3. Subtle error, detectable by mypy.\nclass Power3:\n\n    def __call_(self, x: int, n: int) -> int:\n        p = 1\n        for i in range(n):\n            p *= x\n        return p\n\n# mypy will detect this problem.\n# Chapter_6/ch06_ex1.py:68: error: Incompatible types in assignment (expression has type \"Power3\", variable has type \"Callable[[int, int], int]\")\npow3: IntExp = Power3()\n\ntest_power3 = \"\"\"\n    >>> pow3 = Power3()\n    >>> pow3(2, 1024)  # doctest: +IGNORE_EXCEPTION_DETAIL\n    Traceback (most recent call last):\n      File \"/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py\", line 1329, in __run\n        compileflags, 1), test.globs)\n      File \"<doctest __main__.__test__.test_power3[1]>\", line 1, in <module>\n        pow3(2, 1024)\n    TypeError: 'Power3' object is not callable\n\"\"\"\n\nclass Power4:\n\n    def __call__(self, x: int, n: int) -> int:\n        if n == 0:\n            return 1\n        elif n % 2 == 1:\n            return self.__call__(x, n - 1) * x\n        else:  # n % 2 == 0:\n            t = self.__call__(x, n // 2)\n            return t * t\n\npow4: IntExp = Power4()\n\ntest_power4 = \"\"\"\n    >>> pow4(2, 1024)\n    179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137216\n\"\"\"\n\n\n# Example 4, iterative, also super efficient.\n\n\nclass Power4i:\n\n    def __call__(self, x: int, n: int) -> int:\n        p = 1\n        while n != 0:\n            if n % 2 == 1:\n                p *= x\n                n -= 1\n            else:  # n % 2 == 0:\n                t = self.__call__(x, n // 2)\n                p *= t\n                p *= t\n                n = 0\n        return p\n\npow4i: IntExp = Power4i()\n\ntest_power4i = \"\"\"\n    >>> pow4i(2, 1024)\n    179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137216\n\"\"\"\n\n# Example 5, memoization\n\n\nclass Power5:\n\n    def __init__(self) -> None:\n        self.memo: Dict[int, int] = {}\n\n    def __call__(self, x: int, n: int) -> int:\n        if (x, n) not in self.memo:\n            if n == 0:\n                self.memo[x, n] = 1\n            elif n % 2 == 1:\n                self.memo[x, n] = self.__call__(x, n - 1) * x\n            elif n % 2 == 0:\n                t = self.__call__(x, n // 2)\n                self.memo[x, n] = t * t\n            else:\n                raise Exception(\"Logic Error\")\n        return self.memo[x, n]\n\npow5: IntExp = Power5()\n\ntest_power5 = \"\"\"\n    >>> pow5(2, 1024)\n    179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137216\n\"\"\"\n\n# Example 6, functools memoization\nfrom functools import lru_cache\n\n@lru_cache()\ndef pow6(x: int, n: int) -> int:\n    if n == 0:\n        return 1\n    elif n % 2 == 1:\n        return pow6(x, n - 1) * x\n    else:  # n % 2 == 0:\n        t = pow6(x, n // 2)\n        return t * t\n\ntest_power6 = \"\"\"\n    >>> pow6(2, 1024)\n    179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137216\n\"\"\"\n\n\ndef performance() -> None:\n    \"\"\"Timeit results\"\"\"\n    import timeit\n\n    iterative = timeit.timeit(\n        \"pow1(2, 1024)\",\n        \"\"\"\nclass Power1:\n    def __call__(self, x: int, n: int) -> int:\n        p = 1\n        for i in range(n):\n            p *= x\n        return p\n\npow1= Power1()\n    \"\"\",\n        number=100_000,\n    )  # otherwise it takes 2 minutes\n    print(\"Iterative\", iterative)\n\n    recursive = timeit.timeit(\n        \"pow4(2,1024)\",\n        \"\"\"\nclass Power4:\n    def __call__(self, x: int, n: int) -> int:\n        if n == 0: return 1\n        elif n % 2 == 1: return self.__call__(x, n-1) * x\n        else: # n % 2 == 0:\n            t= self.__call__(x, n//2)\n            return t*t\n\npow4= Power4()\n    \"\"\",\n        number=100_000,\n    )\n    print(\"Recursive\", recursive)\n\n    memoized = timeit.timeit(\n        \"pow5(2,1024)\",\n        \"\"\"\nclass Power5:\n    def __init__( self ) -> None:\n        self.memo = {}\n    def __call__(self, x: int, n: int) -> int:\n        if (x,n) not in self.memo:\n            if n == 0:\n                self.memo[x,n]= 1\n            elif n % 2 == 1:\n                self.memo[x,n]= self.__call__(x, n-1) * x\n            elif n % 2 == 0:\n                t= self.__call__(x, n//2)\n                self.memo[x,n]= t*t\n            else:\n                raise Exception(\"Logic Error\")\n        return self.memo[x,n]\n\npow5 = Power5()\n    \"\"\",\n        number=100_000,\n    )\n    print(\"Memoized\", memoized)\n\n\n# Some additional Callable Examples\n# ---------------------------------\n\n# The BetingStrategy superclass.\nclass BettingStrategy:\n\n    def __init__(self) -> None:\n        self._win = 0\n        self._loss = 0\n\n    @property\n    def win(self) -> int:\n        return self._win\n\n    @win.setter\n    def win(self, value: int) -> None:\n        self._win = value\n        self.stage = 1\n\n    @property\n    def loss(self) -> int:\n        return self._loss\n\n    @loss.setter\n    def loss(self, value: int) -> None:\n        self._loss = value\n\n    def __call__(self) -> int:\n        return 1\n\ntest_flat_betting_strategy = \"\"\"\n    >>> bet = BettingStrategy()\n    >>> bet()\n    1\n    >>> bet.win += 1\n    >>> bet()\n    1\n    >>> bet.loss += 1\n    >>> bet()\n    1\n\"\"\"\n\n# A stateful betting strategy. Property-based\nclass BettingMartingale(BettingStrategy):\n\n    def __init__(self) -> None:\n        self._win = 0\n        self._loss = 0\n        self.stage = 1\n\n    @property\n    def win(self) -> int:\n        return self._win\n\n    @win.setter\n    def win(self, value: int) -> None:\n        self._win = value\n        self.stage = 1\n\n    @property\n    def loss(self) -> int:\n        return self._loss\n\n    @loss.setter\n    def loss(self, value: int) -> None:\n        self._loss = value\n        self.stage *= 2\n\n    def __call__(self) -> int:\n        return self.stage\n\ntest_martingale_betting_strategy = \"\"\"\n    >>> bet = BettingMartingale()\n    >>> bet()\n    1\n    >>> bet.win += 1\n    >>> bet()\n    1\n    >>> bet.loss += 1\n    >>> bet()\n    2\n    >>> bet.loss += 1\n    >>> bet()\n    4\n    >>> bet.win += 1\n    >>> bet()\n    1\n\"\"\"\n\n# Another stateful betting strategy, using ``__setattr__()`` instead\n# if properties.\nclass BettingMartingale2(BettingStrategy):\n\n    def __init__(self) -> None:\n        self.win = 0\n        self.loss = 0\n        self.stage = 1\n\n    def __setattr__(self, name: str, value: int) -> None:\n        if name == \"win\":\n            self.stage = 1\n        elif name == \"loss\":\n            self.stage *= 2\n        super().__setattr__(name, value)\n\n    def __call__(self) -> int:\n        return self.stage\n\ntest_martingale2_betting_strategy = \"\"\"\n    >>> bet = BettingMartingale2()\n    >>> bet()\n    1\n    >>> bet.win += 1\n    >>> bet()\n    1\n    >>> bet.loss += 1\n    >>> bet()\n    2\n    >>> bet.loss += 1\n    >>> bet()\n    4\n    >>> bet.win += 1\n    >>> bet()\n    1\n\"\"\"\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n    doctest.testmod(verbose=False)\n\n    # Takes 12 seconds.\n    # performance()"
  },
  {
    "path": "Chapter_6/ch06_ex2.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 6. Example 2.\n\"\"\"\nfrom typing import Callable, TypeVar, Iterable, Iterator, cast, Dict, Match\nfrom pathlib import Path\n\n# Contexts\n# =================\n\n# With statement example 1 -- file processing\n\ndef slow(source=\"itmaybeahack.com.bkup-Feb-2012.gz\") -> int:\n    from pathlib import Path\n    source_path = Path.cwd()/\"data\"/source\n    target_path = Path.cwd()/\"data\"/\"subset.csv\"\n\n    import re\n\n    format_1_pat = re.compile(\n        r\"([\\d\\.]+)\\s+\"  # digits and .'s: host\n        r\"(\\S+)\\s+\"  # non-space: logname\n        r\"(\\S+)\\s+\"  # non-space: user\n        r\"\\[(.+?)\\]\\s+\"  # Everything in []: time\n        r'\"(.+?)\"\\s+'  # Everything in \"\": request\n        r\"(\\d+)\\s+\"  # digits: status\n        r\"(\\S+)\\s+\"  # non-space: bytes\n        r'\"(.*?)\"\\s+'  # Everything in \"\": referrer\n        r'\"(.*?)\"\\s*'  # Everything in \"\": user agent\n    )\n    import gzip\n    import csv\n\n    T = Optional[Match[str]]\n    class Counter:\n        def __init__(self, source: Iterable[T]) -> None:\n            self.source_iter = source\n            self.count = 0\n        def __iter__(self) -> Iterator[T]:\n            for item in self.source_iter:\n                yield item\n                self.count += 1\n\n    with target_path.open('w', newline='') as target:\n        wtr = csv.writer(target)\n        with gzip.open(source_path, \"r\") as source:\n            line_iter = (b.decode() for b in source)\n            row_iter = Counter(format_1_pat.match(line) for line in line_iter)\n            non_empty_rows: Iterator[Match] = filter(None, row_iter)\n            wtr.writerows(m.groups() for m in non_empty_rows)\n    return row_iter.count\n\ntest_file_proc = \"\"\"\n    >>> import time\n    >>> start = time.perf_counter()\n    >>> rows = slow()\n    >>> end = time.perf_counter()    \n    >>> print(f\"Wrote {rows:,} summary rows in {end-start:.3f} seconds\")  # doctest: +ELLIPSIS\n    Wrote 380,517 summary rows in ... seconds\n\"\"\"\n\n# With statement example 2 -- decimal contexts\n\ntest_decimal = \"\"\"\n    >>> import decimal\n\n    >>> PENNY = decimal.Decimal(\"0.00\")\n    >>> price = decimal.Decimal(\"15.99\")\n    >>> rate = decimal.Decimal(\"0.0075\")\n    >>> print(f\"Tax={(price * rate).quantize(PENNY)}, Fully={price * rate}\")\n    Tax=0.12, Fully=0.119925\n    >>> with decimal.localcontext() as ctx:\n    ...     ctx.rounding = decimal.ROUND_DOWN\n    ...     tax = (price * rate).quantize(PENNY)\n    >>> print(f\"Tax={tax}\")\n    Tax=0.11\n\"\"\"\n\n# With statement example 3 -- logging level change -- perhaps not ideal\n# There are better ways to accomplish this.\n\nimport logging, sys\n\nclass Debugging:\n\n    def __init__(self, aName=None):\n        self.logname = aName\n\n    def __enter__(self):\n        self.default = logging.getLogger(self.logname).getEffectiveLevel()\n        logging.getLogger().setLevel(logging.DEBUG)\n\n    def __exit__(self, exc_type, exc_value, traceback):\n        logging.getLogger(self.logname).setLevel(self.default)\n\ntest_debugging = \"\"\"\n    >>> import io\n    >>> log_file = io.StringIO()\n    >>> logging.basicConfig(stream=log_file, level=logging.INFO)\n    >>> logging.info(\"Before\")\n    >>> logging.debug(\"Silenced before\")\n    >>> with Debugging():\n    ...     logging.info(\"During\")\n    ...     logging.debug(\"Enabled during\")\n    >>> logging.info(\"Between\")\n    >>> logging.debug(\"Silenced between\")\n    >>> with Debugging():\n    ...    logging.info(\"Again\")\n    ...    logging.debug(\"Enabled Again\")\n    >>> logging.info(\"Done\")\n    >>> logging.debug(\"Silenced at the end\")\n    >>> print(log_file.getvalue())\n    INFO:root:Before\n    INFO:root:During\n    DEBUG:root:Enabled during\n    INFO:root:Between\n    INFO:root:Again\n    DEBUG:root:Enabled Again\n    INFO:root:Done\n    <BLANKLINE>\n\"\"\"\n\n# With statement example 4 -- sets the random seed value.\n\nimport random\nfrom typing import Optional, Type\nfrom types import TracebackType\n\nclass KnownSequence:\n\n    def __init__(self, seed: int = 0) -> None:\n        self.seed = 0\n\n    def __enter__(self) -> 'KnownSequence':\n        self.was = random.getstate()\n        random.seed(self.seed, version=1)\n        return self\n\n    def __exit__(\n            self,\n            exc_type: Optional[Type[BaseException]],\n            exc_value: Optional[BaseException],\n            traceback: Optional[TracebackType]\n    ) -> Optional[bool]:\n        random.setstate(self.was)\n        return False\n\ntest_known_sequence = \"\"\"\n    >>> print(tuple(random.randint(-1, 36) for i in range(5)))  # doctest: +ELLIPSIS\n    (...)\n    >>> with KnownSequence():\n    ...    print(tuple(random.randint(-1, 36) for i in range(5)))\n    (23, 25, 1, 15, 31)\n    >>> print(tuple(random.randint(-1, 36) for i in range(5)))  # doctest: +ELLIPSIS\n    (...)\n    >>> with KnownSequence():\n    ...    print(tuple(random.randint(-1, 36) for i in range(5)))\n    (23, 25, 1, 15, 31)\n    >>> print(tuple(random.randint(-1, 36) for i in range(5)))  # doctest: +ELLIPSIS\n    (...)\n\"\"\"\n\n# Some classes for example 5\n\nfrom typing import NamedTuple\nfrom enum import Enum\n\nclass Suit(Enum):\n    Clubs = \"♣\"\n    Diamonds = \"♦\"\n    Hearts = \"♥\"\n    Spades = \"♠\"\n\nclass Card(NamedTuple):\n    rank: int\n    suit: Suit\n\nclass Deck(list):\n\n    def __init__(self, size: int = 1) -> None:\n        super().__init__()\n        for d in range(size):\n            cards = [Card(r, s) for r in range(13) for s in cast(Iterable[Suit], Suit)]\n            super().extend(cards)\n        random.shuffle(self)\n\n\n# Exam[le 5 -- A Context Manager as Factory example\n\nclass Deterministic_Deck:\n\n    def __init__(self, *args, **kw) -> None:\n        self.args = args\n        self.kw = kw\n\n    def __enter__(self) -> Deck:\n        self.was = random.getstate()\n        random.seed(0, version=1)\n        return Deck(*self.args, **self.kw)\n\n    def __exit__(\n            self,\n            exc_type: Optional[Type[BaseException]],\n            exc_value: Optional[BaseException],\n            traceback: Optional[TracebackType]\n    ) -> Optional[bool]:\n        random.setstate(self.was)\n        return False\n\n\ntest_deterministic_deck = \"\"\"\n    Random\n    >>> for i in range(3):\n    ...    d1 = Deck()\n    ...    print(d1.pop(), d1.pop(), d1.pop())  # doctest: +ELLIPSIS\n    Card(rank=..., suit=...) Card(rank=..., suit=...) Card(rank=..., suit=...)\n    Card(rank=..., suit=...) Card(rank=..., suit=...) Card(rank=..., suit=...)\n    Card(rank=..., suit=...) Card(rank=..., suit=...) Card(rank=..., suit=...)\n\n    Known\n    >>> for i in range(3):\n    ...     with Deterministic_Deck(1) as dd1:\n    ...        print(dd1.pop(), dd1.pop(), dd1.pop())\n    Card(rank=6, suit=<Suit.Clubs: '♣'>) Card(rank=12, suit=<Suit.Clubs: '♣'>) Card(rank=6, suit=<Suit.Hearts: '♥'>)\n    Card(rank=6, suit=<Suit.Clubs: '♣'>) Card(rank=12, suit=<Suit.Clubs: '♣'>) Card(rank=6, suit=<Suit.Hearts: '♥'>)\n    Card(rank=6, suit=<Suit.Clubs: '♣'>) Card(rank=12, suit=<Suit.Clubs: '♣'>) Card(rank=6, suit=<Suit.Hearts: '♥'>)\n\n\"\"\"\n\n# Example 6 -- A Context Manager as Mixin\n\nclass Deck2(list, KnownSequence):\n\n    def __init__(self, size: int = 1) -> None:\n        super().__init__()\n        for d in range(size):\n            cards = [Card(r, s) for r in range(13) for s in cast(Iterable[Suit], Suit)]\n            super().extend(cards)\n        self.raw = True\n        KnownSequence.__init__(self)\n\n    def pop(self, *args, **kw) -> Card:\n        if self.raw:\n            random.shuffle(self)\n            self.raw = False\n        return super().pop(*args, **kw)\n\ntest_context_mixin = \"\"\"\n    Random\n    >>> for i in range(3):\n    ...    dd2r = Deck2()\n    ...    print(dd2r.pop(), dd2r.pop(), dd2r.pop())  # doctest: +ELLIPSIS\n    Card(rank=..., suit=...) Card(rank=..., suit=...) Card(rank=..., suit=...)\n    Card(rank=..., suit=...) Card(rank=..., suit=...) Card(rank=..., suit=...)\n    Card(rank=..., suit=...) Card(rank=..., suit=...) Card(rank=..., suit=...)\n    \n    Known \n    >>> for i in range(3):\n    ...     with Deck2(1) as dd2k:\n    ...        print(dd2k.pop(), dd2k.pop(), dd2k.pop())\n    Card(rank=6, suit=<Suit.Clubs: '♣'>) Card(rank=12, suit=<Suit.Clubs: '♣'>) Card(rank=6, suit=<Suit.Hearts: '♥'>)\n    Card(rank=6, suit=<Suit.Clubs: '♣'>) Card(rank=12, suit=<Suit.Clubs: '♣'>) Card(rank=6, suit=<Suit.Hearts: '♥'>)\n    Card(rank=6, suit=<Suit.Clubs: '♣'>) Card(rank=12, suit=<Suit.Clubs: '♣'>) Card(rank=6, suit=<Suit.Hearts: '♥'>)\n\"\"\"\n\n# Example 7 -- A Context Manager for a File Copy\n\nfrom pathlib import Path\nfrom typing import Optional\n\nclass Updating:\n\n    def __init__(self, target: Path) -> None:\n        self.target: Path = target\n        self.previous: Optional[Path] = None\n\n    def __enter__(self) -> None:\n        try:\n            self.previous = (\n                self.target.parent\n                    / (self.target.stem + \" backup\")\n                ).with_suffix(self.target.suffix)\n            self.target.rename(self.previous)\n        except FileNotFoundError:\n            # Target doesn't exist. That's okay.\n            self.previous = None\n\n    def __exit__(\n            self,\n            exc_type: Optional[Type[BaseException]],\n            exc_value: Optional[BaseException],\n            traceback: Optional[TracebackType]\n    ) -> Optional[bool]:\n        if exc_type is not None:\n            # An Exception Occurred: Preserve the erroneous file, if possible.\n            try:\n                self.failure = (\n                    self.target.parent / (self.target.stem + \" error\")\n                    ).with_suffix(self.target.suffix)\n                self.target.rename(self.failure)\n            except FileNotFoundError:\n                pass  # Never even got created.\n            # If there was a previous file, put the old file back in place.\n            if self.previous:\n                self.previous.rename(self.target)\n        return False\n\ndef some_update(important_path):\n    with Updating(important_file):\n        with important_file.open('w') as revision:\n            revision.write(\"Attempted Update\\\\n\")\n            raise Exception(\"oops\")\n\n\ntest_updating_context = \"\"\"\n    Our file. Make sure it's gone.\n    >>> important_file = Path.cwd()/\"data\"/\"some_file.txt\"\n    >>> try:\n    ...     important_file.unlink()\n    ... except IOError as e:\n    ...     pass\n    \n    First. Create the data.\n    >>> with important_file.open('w') as original:\n    ...     _ = original.write(\"Original data\\\\n\")\n    \n    Second. Try the update.\n    >>> try:\n    ...     with Updating(important_file):\n    ...         with important_file.open('w') as revision:\n    ...             _ = revision.write(\"Attempted Update\\\\n\")\n    ...             raise Exception(\"oops\")\n    ... except Exception as ex:\n    ...     print(ex)\n    oops\n    \n    # ``some_file error.txt`` left for us to examine.\n    # ``some_file.txt`` left intact\n    >>> important_file.read_text()\n    'Original data\\\\n'\n    \n    >>> (Path.cwd()/\"data\"/\"some_file error.txt\").read_text()\n    'Attempted Update\\\\n'\n\"\"\"\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_7/__init__.py",
    "content": ""
  },
  {
    "path": "Chapter_7/ch07_defaults.json",
    "content": "{\n    \"decks\": 6,\n    \"table_limit\": 50,\n    \"playerclass\": \"Passive\"\n}\n"
  },
  {
    "path": "Chapter_7/ch07_ex1.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 7. Example 1.\n\"\"\"\n\n# Existing Classes\n# ##############################\n\nfrom pathlib import Path\nfrom enum import Enum\nfrom typing import (\n    cast,\n    Iterable,\n    List,\n    TypeVar,\n    Dict,\n    Optional,\n    Iterator,\n    Union,\n    overload,\n)\nfrom collections.abc import MutableSequence\n\n\nclass Suit(str, Enum):\n    Clubs = \"♣\"\n    Diamonds = \"♦\"\n    Hearts = \"♥\"\n    Spades = \"♠\"\n\n\n# collections.namedtuple and typing.NamedTuple\n# ============================================\n\nfrom collections import namedtuple\n\nBlackjackCard = namedtuple(\"BlackjackCard\", \"rank,suit,hard,soft\")\n\n\nfrom typing import NamedTuple\n\nclass BlackjackCard_T(NamedTuple):\n    rank: str\n    suit: Suit\n    hard: int\n    soft: int\n    def is_ace(self) -> bool:\n        return False\n\n\ndef card(rank: int, suit: Suit) -> BlackjackCard:\n    if rank == 1:\n        return BlackjackCard(\"A\", suit, 1, 11)\n    elif 2 <= rank < 11:\n        return BlackjackCard(str(rank), suit, rank, rank)\n    elif rank == 11:\n        return BlackjackCard(\"J\", suit, 10, 10)\n    elif rank == 12:\n        return BlackjackCard(\"Q\", suit, 10, 10)\n    elif rank == 13:\n        return BlackjackCard(\"K\", suit, 10, 10)\n    else:\n        raise ValueError(f\"Invalid Rank {rank}\")\n\n\ndef card_t(rank: int, suit: Suit) -> BlackjackCard_T:\n    if rank == 1:\n        return BlackjackCard_T(\"A\", suit, 1, 11)\n    elif 2 <= rank < 11:\n        return BlackjackCard_T(str(rank), suit, rank, rank)\n    elif rank == 11:\n        return BlackjackCard_T(\"J\", suit, 10, 10)\n    elif rank == 12:\n        return BlackjackCard_T(\"Q\", suit, 10, 10)\n    elif rank == 13:\n        return BlackjackCard_T(\"K\", suit, 10, 10)\n    else:\n        raise ValueError(f\"Invalid Rank {rank}\")\n\n\ntest_namedtuple = \"\"\"\n    >>> c = card(10, Suit.Spades)\n    >>> print(c)\n    BlackjackCard(rank='10', suit=<Suit.Spades: '♠'>, hard=10, soft=10)\n\n    >>> c_t = card_t(10, Suit.Spades)\n    >>> print(c_t)\n    BlackjackCard_T(rank='10', suit=<Suit.Spades: '♠'>, hard=10, soft=10)\n    >>> c_t.is_ace()\n    False\n\"\"\"\n\n# This doesn't work out well. The parent class cannot be defined\n# with a method.\nclass AceCard(BlackjackCard):\n    def is_ace(self) -> bool:\n        return True\n\nclass AceCard_T(BlackjackCard_T):\n    def is_ace(self) -> bool:\n        return True\n\ntest_subclass = \"\"\"\n    >>> c_1 = AceCard(\"A\", Suit.Spades, 1, 11)\n    >>> print(c_1)\n    AceCard(rank='A', suit=<Suit.Spades: '♠'>, hard=1, soft=11)\n    \n    >>> c_1t = AceCard_T(\"A\", Suit.Spades, 1, 11)\n    >>> print(c_1t)\n    AceCard_T(rank='A', suit=<Suit.Spades: '♠'>, hard=1, soft=11)\n    >>> c_1t.is_ace()\n    True\n\"\"\"\n\ntest_immutable = \"\"\"\n    >>> c_1 = AceCard(1, Suit.Spades, 1, 11)\n    >>> c_1.rank = 12  # doctest: +IGNORE_EXCEPTION_DETAIL\n    Traceback (most recent call last):\n      File \"/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py\", line 1329, in __run\n        compileflags, 1), test.globs)\n      File \"<doctest __main__.__test__.test_immutable[1]>\", line 1, in <module>\n        c_1.rank = 12  # doctest: +IGNORE_EXCEPTION_DETAIL\n    AttributeError: can't set attribute\n    \n    >>> c_1t = AceCard_T(1, Suit.Spades, 1, 11)\n    >>> c_1t.rank = 12  # doctest: +IGNORE_EXCEPTION_DETAIL\n    Traceback (most recent call last):\n      File \"/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py\", line 1329, in __run\n        compileflags, 1), test.globs)\n      File \"<doctest __main__.__test__.test_immutable[3]>\", line 1, in <module>\n        c_1t.rank = 12  # doctest: +IGNORE_EXCEPTION_DETAIL\n    AttributeError: can't set attribute\n\"\"\"\n\n# deque\n# ================================\n\n# Example of Deck built from deque.\n\n\nclass Card(NamedTuple):\n    rank: int\n    suit: Suit\n\n\nimport random\nfrom collections import deque\n\n\nclass MultiDeck(list):\n    \"\"\"A sequence of decks. Each shuffled separately.\n    \"\"\"\n\n    def __init__(self, size: int = 5) -> None:\n        super().__init__()\n        for d in range(size):\n            deck = list(\n                card(r, s) for r in range(1, 14) for s in cast(Iterable[Suit], Suit)\n            )\n            random.shuffle(deck)\n            while deck:\n                super().append(deck.pop())\n\n\ntest_multideck = \"\"\"\n    >>> random.seed(9973)\n    >>> d = MultiDeck()\n    >>> print(d.pop(), d.pop(), d.pop())\n    BlackjackCard(rank='4', suit=<Suit.Diamonds: '♦'>, hard=4, soft=4) BlackjackCard(rank='A', suit=<Suit.Diamonds: '♦'>, hard=1, soft=11) BlackjackCard(rank='J', suit=<Suit.Diamonds: '♦'>, hard=10, soft=10)\n    >>> more_cards = [d.pop() for _ in range(49)]\n    >>> print(d.pop(), d.pop(), d.pop())\n    BlackjackCard(rank='10', suit=<Suit.Hearts: '♥'>, hard=10, soft=10) BlackjackCard(rank='3', suit=<Suit.Clubs: '♣'>, hard=3, soft=3) BlackjackCard(rank='6', suit=<Suit.Clubs: '♣'>, hard=6, soft=6)\n\"\"\"\n\n# ChainMap\n# =====================\n\nimport argparse\nimport json\nimport os\nimport sys\nfrom collections import ChainMap\nfrom typing import Dict, Any\n\n\ndef get_options(argv: List[str] = sys.argv[1:]) -> ChainMap:\n    \"\"\"Four Sources: comand line, file, OS environ, defaults.\"\"\"\n    parser = argparse.ArgumentParser(\n        description=\"Process some integers.\")\n    parser.add_argument(\n        \"-c\", \"--configuration\", type=open, nargs=\"?\")\n    parser.add_argument(\n        \"-p\", \"--playerclass\", type=str, nargs=\"?\",\n        default=\"Simple\")\n    cmdline = parser.parse_args(argv)\n\n    if cmdline.configuration:\n        config_file = json.load(cmdline.configuration)\n        cmdline.configuration.close()\n    else:\n        config_file = {}\n\n    default_path = (Path.cwd() / \"Chapter_7\" / \"ch07_defaults.json\")\n    with default_path.open() as default_file:\n        defaults = json.load(default_file)\n\n    combined = ChainMap(\n        vars(cmdline), config_file, os.environ, defaults)\n    return combined\n\n\ntest_options = \"\"\"\n    >>> options = get_options(['-p', 'Aggressive'])\n    >>> print(\"combined\", options['playerclass'])\n    combined Aggressive\n    >>> print(\"cmdline playerclass\", options.maps[0].get('playerclass', None))\n    cmdline playerclass Aggressive\n    >>> print(\"config_file playerclass\", options.maps[1].get('playerclass', None))\n    config_file playerclass None\n    >>> print(\"os environ playerclass\", options.maps[2].get('playerclass', None))\n    os environ playerclass None\n    >>> print(\"default playerclass\", options.maps[3].get('playerclass', None))\n    default playerclass Passive\n\"\"\"\n\n# OrderedDict\n# ======================\n\n# No longer necessary. A ``dict`` does this, also.\n\n# Some Sample XML\nsource = \"\"\"\n<blog>\n    <topics>\n        <entry ID=\"UUID98765\"><title>first</title><body>more words</body></entry>\n        <entry ID=\"UUID87654\"><title>second</title><body>more words</body></entry>\n        <entry ID=\"UUID65432\"><title>third</title><body>more words</body></entry>\n    </topics>\n    <indices>\n        <bytag>\n            <tag text=\"#sometag\">\n                <entry IDREF=\"UUID87654\"/>\n                <entry IDREF=\"UUID98765\"/>\n            </tag>\n            <tag text=\"#anothertag\">\n                <entry IDREF=\"UUID98765\"/>\n                <entry IDREF=\"UUID65432\"/>\n            </tag>\n        </bytag>\n        <bylocation>\n            <location text=\"Somewhere\">\n                <entry IDREF=\"UUID98765\"/>\n                <entry IDREF=\"UUID87654\"/>\n            </location>\n            <location text=\"Somewhere Else\">\n                <entry IDREF=\"UUID98765\"/>\n                <entry IDREF=\"UUID87654\"/>\n            </location>\n        </bylocation>\n    </indices>\n</blog>\n\"\"\"\n\n# Parsing\nfrom collections import OrderedDict\nimport xml.etree.ElementTree as etree\n\ntest_ordered_dict = \"\"\"\n    >>> doc = etree.XML(source)  # Parse\n    >>> \n    >>> topics = OrderedDict()  # Gather <entry> tags within <topic>\n    >>> for topic in doc.findall(\"topics/entry\"):\n    ...     topics[topic.attrib['ID']] = topic\n\n    >>> # Order of entry is preserved. Always.\n    >>> for topic in topics:  # Display <title> tags within each <topic>\n    ...     print(topic, topics[topic].find(\"title\").text)\n    UUID98765 first\n    UUID87654 second\n    UUID65432 third\n\n    >>> # We can also lookup by a key.\n    >>> for tag in doc.findall(\"indices/bytag/tag\"):\n    ...     print(tag.attrib['text'])\n    ...     for e in tag.findall(\"entry\"):\n    ...         print(' ', e.attrib['IDREF'], topics[e.attrib['IDREF']].find(\"title\").text)\n    #sometag\n      UUID87654 second\n      UUID98765 first\n    #anothertag\n      UUID98765 first\n      UUID65432 third\n\"\"\"\n\n# The point is to keep the topics in an ordereddict by their original positions\n# in the document and also reference them by ID.\n# We can reference them from other places without scrambling\n# the original order.\n\ntest_dict_ordering = \"\"\"\n    >>> some_dict = {'zzz': 1, 'aaa': 2}\n    >>> some_dict['mmm'] = 3\n    >>> some_dict\n    {'zzz': 1, 'aaa': 2, 'mmm': 3}\n    >>> sorted(some_dict)\n    ['aaa', 'mmm', 'zzz']\n\"\"\"\n\n# Defaultdict\n# =====================\n\nfrom collections import defaultdict\nfrom typing import DefaultDict\n\nmessages: Dict[str, str] = defaultdict(lambda: \"N/A\")\nmessages[\"error1\"] = \"Full Error Text\"\nmessages[\"other\"]\nmessages[\"error2\"] = \"Another Error Text\"\n\ntest_default_dict = \"\"\"\n    >>> messages_with_default = [k for k in messages if messages[k] == \"N/A\"]\n    >>> messages_with_default\n    ['other']\n    >>> messages['error1']\n    'Full Error Text'\n    >>> messages['weird']\n    'N/A'\n\"\"\"\n\nfrom typing import Dict, List, Tuple\ndef dice_examples(n: int=12, seed: Any=None) -> DefaultDict[int, List]:\n    if seed:\n        random.seed(seed)\n    Roll = Tuple[int, int]\n    outcomes: DefaultDict[int, List[Roll]] = defaultdict(list)\n    for _ in range(n):\n        d1, d2 = random.randint(1, 6), random.randint(1, 6)\n        outcomes[d1+d2].append((d1, d2))\n    return outcomes\n\ntest_default_dict_2 = \"\"\"\n    >>> d = dice_examples(12, seed=42)\n    >>> d\n    defaultdict(<class 'list'>, {7: [(6, 1), (1, 6), (6, 1), (2, 5)], 5: [(3, 2)], 4: [(2, 2)], 12: [(6, 6)], 6: [(5, 1), (5, 1)], 9: [(5, 4)], 2: [(1, 1)], 3: [(1, 2)]})\n\"\"\"\n\n\n# Counter\n# ==================\n\n# Extension of defaultdict(int)\n\n# A Data Source\nimport random\n\n\ndef value_iterator(count=100, seed=4000) -> Iterable[str]:\n    random.seed(seed, version=1)\n    for i in range(count):\n        yield str(random.randint(1, 6) + random.randint(1, 6))\n\n\nfrom collections import defaultdict\n\nT = TypeVar(\"T\")\n\n\ndef freq_ordered(values: Iterable[T]) -> Dict[int, List[T]]:\n    \"\"\"\n    Shows ties as list of pair values with the same frequency.\n    \"\"\"\n    frequency: Dict[T, int] = defaultdict(int)\n    for p in values:\n        frequency[p] += 1\n\n    rank_by_value: Dict[int, List[T]] = defaultdict(list)\n    for pair, freq in frequency.items():\n        rank_by_value[freq].append(pair)\n    return rank_by_value\n\n\nfrom collections import Counter\n\nfreq_2: Dict[str, int] = Counter(value_iterator())\n\ntest_counter = \"\"\"\n    >>> freq_1 = freq_ordered(value_iterator())\n    >>> for freq in sorted(freq_1, reverse=True):\n    ...     for v in freq_1[freq]:\n    ...         print(repr(v), freq)\n    '7' 19\n    '6' 17\n    '8' 16\n    '10' 10\n    '4' 9\n    '11' 8\n    '5' 8\n    '9' 5\n    '3' 4\n    '12' 2\n    '2' 2\n\n    >>> freq_2 = Counter(value_iterator())\n    >>> for k, freq in freq_2.most_common():\n    ...     print(repr(k), freq)\n    '7' 19\n    '6' 17\n    '8' 16\n    '10' 10\n    '4' 9\n    '11' 8\n    '5' 8\n    '9' 5\n    '3' 4\n    '12' 2\n    '2' 2\n\"\"\"\n\ndef bag_demo() -> None:\n    \"\"\"\n    >>> bag_demo()\n    Counter({'a': 2, 'r': 1, 'd': 1, 'w': 1, 'o': 1, 'l': 1, 'v': 1, 'e': 1, 's': 1})\n    Counter({'o': 2, 'z': 1, 'y': 1, 'm': 1, 'l': 1, 'g': 1, 'i': 1, 'e': 1, 's': 1})\n    Counter({'o': 3, 'a': 2, 'l': 2, 'e': 2, 's': 2, 'r': 1, 'd': 1, 'w': 1, 'v': 1, 'z': 1, 'y': 1, 'm': 1, 'g': 1, 'i': 1})\n    Counter({'a': 2, 'r': 1, 'd': 1, 'w': 1, 'v': 1})\n    Counter({'z': 1, 'y': 1, 'm': 1, 'o': 1, 'g': 1, 'i': 1})\n    \"\"\"\n    bag1 = Counter(\"aardwolves\")\n    bag2 = Counter(\"zymologies\")\n    print(bag1)\n    print(bag2)\n    print(bag1+bag2)\n    print(bag1-bag2)\n    print(bag2-bag1)\n\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n    doctest.testmod(verbose=False)\n\n    # performance()\n"
  },
  {
    "path": "Chapter_7/ch07_ex2.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 7. Example 2.\n\"\"\"\nfrom typing import List, cast, Any, Optional, Iterable, overload, Union, Iterator\n\n# Extending Classes\n# ##############################\n\n# Basic Stats formulae\nimport math\n\n\ndef mean(outcomes: List[float]) -> float:\n    return sum(outcomes) / len(outcomes)\n\n\ndef stdev(outcomes: List[float]) -> float:\n    n = float(len(outcomes))\n    return math.sqrt(n * sum(x ** 2 for x in outcomes) - sum(outcomes) ** 2) / n\n\n\ntest_stats = \"\"\"\n    >>> sample_data = [2, 4, 4, 4, 5, 5, 7, 9]\n    >>> mean(sample_data)\n    5.0\n    >>> stdev(sample_data)\n    2.0\n\"\"\"\n\n\n# A simple (lazy) stats list class.\n# Note the difficulty in expressing a type constraint: List[float].\n\n\nclass StatsList(list):\n\n    def __init__(self, iterable: Optional[Iterable[float]]) -> None:\n        super().__init__(cast(Iterable[Any], iterable))\n\n    @property\n    def mean(self) -> float:\n        return sum(self) / len(self)\n\n    @property\n    def stdev(self) -> float:\n        n = len(self)\n        return math.sqrt(n * sum(x ** 2 for x in self) - sum(self) ** 2) / n\n\n\ntest_lazy_stats_list = \"\"\"\n    >>> sl = StatsList([2, 4, 4, 4, 5, 5, 7, 9])\n    >>> sl.mean\n    5.0\n    >>> sl.stdev\n    2.0\n    >>> sl[2] = 10\n    >>> round(sl.mean, 2)\n    5.75\n    >>> round(sl.stdev, 2)\n    2.54\n\"\"\"\n\nimport random\n\n\ndef data_gen() -> int:\n    return random.randint(1, 6) + random.randint(1, 6)\n\n\ndef demo_statslist() -> None:\n    \"\"\"\n    >>> random.seed(42)\n    >>> demo_statslist()\n    mean = 7.000000\n    stdev= 2.328\n    \"\"\"\n    random.seed(42)\n    data = [data_gen() for _ in range(100)]\n    stats = StatsList(data)\n    print(f\"mean = {stats.mean:f}\")\n    print(f\"stdev= {stats.stdev:.3f}\")\n\n\nclass Explore(list):\n\n    # There are two overloaded definitions, the type hints tend to be complex for this case\n    def __getitem__(self, index):\n        print(index, index.indices(len(self)))\n        return super().__getitem__(index)\n\n\ntest_explore = \"\"\"\n>>> x= Explore('abcdefg')\n>>> x[:]\nslice(None, None, None) (0, 7, 1)\n['a', 'b', 'c', 'd', 'e', 'f', 'g']\n>>> x[:-1]\nslice(None, -1, None) (0, 6, 1)\n['a', 'b', 'c', 'd', 'e', 'f']\n>>> x[1:]\nslice(1, None, None) (1, 7, 1)\n['b', 'c', 'd', 'e', 'f', 'g']\n>>> x[::2]\nslice(None, None, 2) (0, 7, 2)\n['a', 'c', 'e', 'g']\n\"\"\"\n\n# Eager Stats List class\n# Note the difficulty in expressing a type constraint: List[float].\n\n\nclass StatsList2(list):\n    \"\"\"Eager Stats.\"\"\"\n\n    def __init__(self, iterable: Optional[Iterable[float]]) -> None:\n        self.sum0 = 0  # len(self), sometimes called \"N\"\n        self.sum1 = 0.0  # sum(self)\n        self.sum2 = 0.0  # sum(x**2 for x in self)\n        super().__init__(cast(Iterable[Any], iterable))\n        for x in self:\n            self._new(x)\n\n    def _new(self, value: float) -> None:\n        self.sum0 += 1\n        self.sum1 += value\n        self.sum2 += value * value\n\n    def _rmv(self, value: float) -> None:\n        self.sum0 -= 1\n        self.sum1 -= value\n        self.sum2 -= value * value\n\n    def insert(self, index: int, value: float) -> None:\n        super().insert(index, value)\n        self._new(value)\n\n    def pop(self, index: int = 0) -> None:\n        value = super().pop(index)\n        self._rmv(value)\n        return value\n\n    def append(self, value: float) -> None:\n        super().append(value)\n        self._new(value)\n\n    def extend(self, sequence: Iterable[float]) -> None:\n        super().extend(sequence)\n        for value in sequence:\n            self._new(value)\n\n    def remove(self, value: float) -> None:\n        super().remove(value)\n        self._rmv(value)\n\n    def __iadd__(self, sequence: Iterable[float]) -> \"StatsList2\":\n        for v in sequence:\n            self.append(v)\n        return self\n\n    def __add__(self, sequence: Iterable[float]) -> \"StatsList2\":\n        generic = super().__add__(cast(StatsList2, sequence))\n        result = StatsList2(generic)\n        return result\n\n    # reveal_type(list.__iadd__)\n    # reveal_type(list.__add__)\n    # reveal_type(StatsList2.__iadd__)\n    # reveal_type(StatsList2.__add__)\n\n    @property\n    def mean(self) -> float:\n        return self.sum1 / self.sum0\n\n    @property\n    def stdev(self) -> float:\n        return math.sqrt(self.sum0 * self.sum2 - self.sum1 * self.sum1) / self.sum0\n\n    @overload\n    def __setitem__(self, index: int, value: float) -> None:\n        ...\n\n    @overload\n    def __setitem__(self, index: slice, value: Iterable[float]) -> None:\n        ...\n\n    def __setitem__(self, index, value) -> None:\n        if isinstance(index, slice):\n            start, stop, step = index.indices(len(self))\n            olds = [self[i] for i in range(start, stop, step)]\n            super().__setitem__(index, value)\n            for x in olds:\n                self._rmv(x)\n            for x in value:\n                self._new(x)\n        else:\n            old = self[index]\n            super().__setitem__(index, value)\n            self._rmv(old)\n            self._new(value)\n\n    def __delitem__(self, index: Union[int, slice]) -> None:\n        # Index may be a single integer, or a slice\n        if isinstance(index, slice):\n            start, stop, step = index.indices(len(self))\n            olds = [self[i] for i in range(start, stop, step)]\n            super().__delitem__(index)\n            for x in olds:\n                self._rmv(x)\n        else:\n            old = self[index]\n            super().__delitem__(index)\n            self._rmv(old)\n\n    # reveal_type(list.__setitem__)\n    # reveal_type(MutableSequence.__setitem__)\n    # reveal_type(StatsList2.__setitem__)\n\n    # reveal_type(list.__delitem__)\n    # reveal_type(StatsList2.__delitem__)\n\n\ntest_eager_stats_list = \"\"\"\n    >>> sl2 = StatsList2([2, 4, 3, 4, 5, 5, 7, 9, 10])\n    >>> print(\"start\", sl2, sl2.sum0, sl2.sum1, sl2.sum2)\n    start [2, 4, 3, 4, 5, 5, 7, 9, 10] 9 49.0 325.0\n\n    >>> sl2[2] = 4\n    >>> print(\"replace\", sl2, sl2.sum0, sl2.sum1, sl2.sum2)\n    replace [2, 4, 4, 4, 5, 5, 7, 9, 10] 9 50.0 332.0\n\n    >>> del sl2[-1]\n    >>> print(\"remove\", sl2, sl2.sum0, sl2.sum1, sl2.sum2)\n    remove [2, 4, 4, 4, 5, 5, 7, 9] 8 40.0 232.0\n\n    >>> sl2.insert(0, -1)\n    >>> print(\"insert\", sl2, sl2.sum0, sl2.sum1, sl2.sum2)\n    insert [-1, 2, 4, 4, 4, 5, 5, 7, 9] 9 39.0 233.0\n    \n    >>> r = sl2.pop()\n    >>> print(\"pop\", sl2, sl2.sum0, sl2.sum1, sl2.sum2)\n    pop [2, 4, 4, 4, 5, 5, 7, 9] 8 40.0 232.0\n\n    >>> sl2.append(1)\n    >>> print(\"append\", sl2, sl2.sum0, sl2.sum1, sl2.sum2)\n    append [2, 4, 4, 4, 5, 5, 7, 9, 1] 9 41.0 233.0\n    \n    >>> sl2.extend([10, 11, 12])\n    >>> print(\"extend\", sl2, sl2.sum0, sl2.sum1, sl2.sum2)\n    extend [2, 4, 4, 4, 5, 5, 7, 9, 1, 10, 11, 12] 12 74.0 598.0\n    \n    >>> sl2.remove(-2)  # doctest: +IGNORE_EXCEPTION_DETAIL\n    Traceback (most recent call last):\n      File \"/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py\", line 1329, in __run\n        compileflags, 1), test.globs)\n      File \"<doctest __main__.__test__.test_eager_stats_list[14]>\", line 1, in <module>\n        sl2.remove(-2)  # doctest: +IGNORE_EXCEPTION_DETAIL\n      File \"/Users/slott/Documents/Writing/Python/Mastering OO Python 2e/mastering-oo-python-2e/Chapter_7/ch07_ex1.py\", line 438, in remove\n        super().remove(value)\n    ValueError: list.remove(x): x not in list\n\n    >>> print(\"failed remove\", sl2, sl2.sum0, sl2.sum1, sl2.sum2)\n    failed remove [2, 4, 4, 4, 5, 5, 7, 9, 1, 10, 11, 12] 12 74.0 598.0\n    \n    >>> sl2 += [21, 22, 23]\n    >>> print(\"+=\", sl2, sl2.sum0, sl2.sum1, sl2.sum2)\n    += [2, 4, 4, 4, 5, 5, 7, 9, 1, 10, 11, 12, 21, 22, 23] 15 140.0 2052.0\n\n    >>> sl = StatsList([2, 4, 4, 4, 5, 5, 7, 9, 1, 10, 11, 12, 21, 22, 23])\n    >>> print(\"expected\", len(sl), \"actual\", sl2.sum0)\n    expected 15 actual 15\n    >>> print(\"expected\", sum(sl), \"actual\", sl2.sum1)\n    expected 140 actual 140.0\n    >>> print(\"expected\", sum(x * x for x in sl), \"actual\", sl2.sum2)\n    expected 2052 actual 2052.0\n    >>> sl.mean == sl2.mean\n    True\n    >>> sl.stdev == sl2.stdev\n    True\n\n    >>> sl2a = StatsList2([2, 4, 3, 4, 5, 5, 7, 9, 10])\n    >>> del sl2a[1:3]\n    >>> print('slice del', sl2a, sl2a.sum0, sl2a.sum1, sl2a.sum2)\n    slice del [2, 4, 5, 5, 7, 9, 10] 7 42.0 300.0\n\"\"\"\n\n# Wrapping Classes\n# ##############################\n\n# Stats List Wrapper\nclass StatsList3:\n\n    def __init__(self) -> None:\n        self._list: List[float] = list()\n        self.sum0 = 0  # len(self), sometimes called \"N\"\n        self.sum1 = 0.  # sum(self)\n        self.sum2 = 0.  # sum(x**2 for x in self)\n\n    def append(self, value: float) -> None:\n        self._list.append(value)\n        self.sum0 += 1\n        self.sum1 += value\n        self.sum2 += value * value\n\n    # etc.\n\n    def __getitem__(self, index: int) -> float:\n        return self._list.__getitem__(index)\n\n    @property\n    def mean(self) -> float:\n        return self.sum1 / self.sum0\n\n    @property\n    def stdev(self) -> float:\n        return math.sqrt(self.sum0 * self.sum2 - self.sum1 * self.sum1) / self.sum0\n\n\ntest_wrapper_stats_list = \"\"\"\n    >>> sl3 = StatsList3()\n    >>> for data in 2, 4, 4, 4, 5, 5, 7, 9:\n    ...    sl3.append(data)\n    >>> print(f\"Mean {sl3.mean:.1f}, Standard Deviation {sl3.stdev:.1f}\")\n    Mean 5.0, Standard Deviation 2.0\n\"\"\"\n\n# Heading 4 -- Extending Classes\n# ##############################\n\n\n# Stats Counter\nimport math\nfrom collections import Counter\n\n\nclass StatsCounter(Counter):\n\n    @property\n    def mean(self) -> float:\n        sum0 = sum(v for k, v in self.items())\n        sum1 = sum(k * v for k, v in self.items())\n        return sum1 / sum0\n\n    @property\n    def stdev(self) -> float:\n        sum0 = sum(v for k, v in self.items())\n        sum1 = sum(k * v for k, v in self.items())\n        sum2 = sum(k * k * v for k, v in self.items())\n        return math.sqrt(sum0 * sum2 - sum1 * sum1) / sum0\n\n    @property\n    def median(self) -> Any:\n        all = list(sorted(self.elements()))\n        return all[len(all) // 2]\n\n    @property\n    def median2(self) -> Optional[float]:\n        mid = sum(self.values()) // 2\n        low = 0\n        for k, v in sorted(self.items()):\n            if low <= mid < low + v:\n                return k\n            low += v\n        return None\n\n\ntest_stats_counter = \"\"\"\n    >>> sc = StatsCounter([2, 4, 4, 4, 5, 5, 7, 9])\n    >>> print(sc.mean, sc.stdev, sc.most_common(), sc.median, sc.median2)\n    5.0 2.0 [(4, 3), (5, 2), (2, 1), (7, 1), (9, 1)] 5 5\n\"\"\"\n\n__test__ = {\n    name: value for name, value in locals().items() if name.startswith(\"test_\")\n}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n\n    # performance()\n"
  },
  {
    "path": "Chapter_7/ch07_ex3.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 7. Example 3.\n\"\"\"\nfrom typing import List, cast, Any, Optional, Iterable, overload, Union, Iterator\n\n# Extending Classes\n# ##############################\n\n# Basic Stats formulae\nimport math\n\n\n# New Sequence from Scratch.\n# ======================================\n\n# A Binary Search Tree.\n#\n# http://en.wikipedia.org/wiki/Binary_search_tree\n#\nimport collections.abc\nimport weakref\nfrom abc import ABCMeta, abstractmethod\nfrom typing import TypeVar, Any\n\n\nclass Comparable(metaclass=ABCMeta):\n\n    @abstractmethod\n    def __lt__(self, other: Any) -> bool:\n        ...\n\n    def __ge__(self, other: Any) -> bool:\n        ...\n\n\n# In case we need a type variable that maps to Comparable\nNodeItem = TypeVar(\"NodeItem\", bound=Comparable)\n\n\nclass TreeNode:\n    \"\"\"\n    Ideally, there's weakref to the tree;\n    tree has the key() function.\n    \"\"\"\n\n    def __init__(\n        self,\n        item: Optional[Comparable],\n        less: Optional[\"TreeNode\"] = None,\n        more: Optional[\"TreeNode\"] = None,\n        parent: Optional[\"TreeNode\"] = None,\n    ) -> None:\n        self.item = item\n        self.less = less\n        self.more = more\n        if parent:\n            # Can't create a weakref to a None value. Only set if there's a value\n            self.parent = parent\n\n    @property\n    def parent(self) -> Optional[\"TreeNode\"]:\n        return self.parent_ref()\n\n    @parent.setter\n    def parent(self, value: \"TreeNode\") -> None:\n        self.parent_ref = weakref.ref(value)\n\n    def __repr__(self) -> str:\n        return f\"TreeNode({self.item!r}, {self.less!r}, {self.more!r})\"\n\n    def find(self, item: Comparable) -> \"TreeNode\":\n        if self.item is None:  # Root\n            if self.more:\n                return self.more.find(item)\n        elif self.item == item:\n            return self\n        elif self.item > item and self.less:\n            return self.less.find(item)\n        elif self.item < item and self.more:\n            return self.more.find(item)\n        raise KeyError\n\n    def __iter__(self) -> Iterator[Comparable]:\n        if self.less:\n            yield from self.less\n        if self.item:\n            yield self.item\n        if self.more:\n            yield from self.more\n\n    def add(self, item: Comparable) -> None:\n        if self.item is None:  # Root Special Case\n            if self.more:\n                self.more.add(item)\n            else:\n                self.more = TreeNode(item, parent=self)\n        elif self.item >= item:\n            if self.less:\n                self.less.add(item)\n            else:\n                self.less = TreeNode(item, parent=self)\n        elif self.item < item:\n            if self.more:\n                self.more.add(item)\n            else:\n                self.more = TreeNode(item, parent=self)\n\n    def remove(self, item: Comparable) -> None:\n        # Recursive search for node\n        if self.item is None or item > self.item:\n            if self.more:\n                self.more.remove(item)\n            else:\n                raise KeyError\n        elif item < self.item:\n            if self.less:\n                self.less.remove(item)\n            else:\n                raise KeyError\n        else:  # self.item == item\n            if self.less and self.more:  # Two children are present\n                successor = self.more._least()\n                self.item = successor.item\n                if successor.item:\n                    successor.remove(successor.item)\n            elif self.less:  # One child on less\n                self._replace(self.less)\n            elif self.more:  # One child on more\n                self._replace(self.more)\n            else:  # Zero children\n                self._replace(None)\n\n    def _least(self) -> \"TreeNode\":\n        if self.less is None:\n            return self\n        return self.less._least()\n\n    def _replace(self, new: Optional[\"TreeNode\"] = None) -> None:\n        if self.parent:\n            if self == self.parent.less:\n                self.parent.less = new\n            else:\n                self.parent.more = new\n        if new is not None:\n            new.parent = self.parent\n\n\nclass Tree(collections.abc.MutableSet):\n\n    def __init__(self, source: Iterable[Comparable] = None) -> None:\n        self.root = TreeNode(None)\n        self.size = 0\n        if source:\n            for item in source:\n                self.root.add(item)\n                self.size += 1\n\n    def add(self, item: Comparable) -> None:\n        self.root.add(item)\n        self.size += 1\n\n    def discard(self, item: Comparable) -> None:\n        if self.root.more:\n            try:\n                self.root.more.remove(item)\n                self.size -= 1\n            except KeyError:\n                pass\n        else:\n            pass\n\n    def __contains__(self, item: Any) -> bool:\n        if self.root.more:\n            self.root.more.find(cast(Comparable, item))\n            return True\n        else:\n            return False\n\n    def __iter__(self) -> Iterator[Comparable]:\n        if self.root.more:\n            for item in iter(self.root.more):\n                yield item\n        # Otherwise, the tree is empty.\n\n    def __len__(self) -> int:\n        return self.size\n\n\ntest_tree = \"\"\"\n    >>> bt = Tree()\n    >>> bt.add(\"Number 1\")\n    >>> print(list(iter(bt)))\n    ['Number 1']\n    >>> bt.add(\"Number 3\")\n    >>> print(list(iter(bt)))\n    ['Number 1', 'Number 3']\n    >>> bt.add(\"Number 2\")\n    >>> print(list(iter(bt)))\n    ['Number 1', 'Number 2', 'Number 3']\n    \n    >>> print(repr(bt.root))\n    TreeNode(None, None, TreeNode('Number 1', None, TreeNode('Number 3', TreeNode('Number 2', None, None), None)))\n    >>> print(\"Number 2\" in bt)\n    True\n    >>> print(len(bt))\n    3\n    >>> bt.remove(\"Number 3\")\n    >>> print(list(iter(bt)))\n    ['Number 1', 'Number 2']\n    >>> bt.discard(\"Number 3\")  # Should be silent\n    >>> bt.remove(\"Number 3\")  # doctest: +IGNORE_EXCEPTION_DETAIL\n    Traceback (most recent call last):\n      File \"/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py\", line 1329, in __run\n        compileflags, 1), test.globs)\n      File \"<doctest __main__.__test__.test_tree[13]>\", line 1, in <module>\n        bt.remove(\"Number 3\")  # doctest: +IGNORE_EXCEPTION_DETAIL\n      File \"/Users/slott/miniconda3/envs/py37/lib/python3.7/_collections_abc.py\", line 583, in remove\n        raise KeyError(value)\n    KeyError: 'Number 3'\n\n    >>> bt.add(\"Number 1\")\n    >>> print(list(iter(bt)))\n    ['Number 1', 'Number 1', 'Number 2']\n\"\"\"\n\ntest_tree_256_randomized_insert_delete = \"\"\"\n    >>> import random\n\n    >>> for i in range(256):\n    ...     values = [random.random() for _ in range(i)]\n    ...     random.shuffle(values)\n    ...     bt = Tree()\n    ...     for i in values:\n    ...         bt.add(i)\n    ...     assert list(bt) == list(sorted(values)), f\"IN: {values}, OUT: {list(bt)}\"\n    ...     random.shuffle(values)\n    ...     for i in values:\n    ...         bt.remove(i)\n    ...         values.remove(i)\n    ...         assert list(bt) == list(sorted(values)), f\"IN: {values}, OUT: {list(bt)}\"\n\"\"\"\n\ntest_tree_merge = \"\"\"\n    >>> s1 = Tree([\"Item 1\", \"Another\", \"Middle\"])\n    >>> s2 = Tree([\"Another\", \"More\", \"Yet More\"])\n    >>> print(list(s1))\n    ['Another', 'Item 1', 'Middle']\n    >>> print(list(s2))\n    ['Another', 'More', 'Yet More']\n    >>> print(list(iter(s1 | s2)))\n    ['Another', 'Another', 'Item 1', 'Middle', 'More', 'Yet More']\n    >>> union = s1 | s2\n    >>> list(union)\n    ['Another', 'Another', 'Item 1', 'Middle', 'More', 'Yet More']\n    >>> len(union)\n    6\n    >>> union.remove('Another')\n    >>> list(union)\n    ['Another', 'Item 1', 'Middle', 'More', 'Yet More']\n    \n\"\"\"\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n    doctest.testmod(verbose=False)\n\n\n    # performance()\n"
  },
  {
    "path": "Chapter_7/ch07_ex4.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 7. Example 4.\n\"\"\"\n\n# Comparisons\n# ======================================\n\n# Using a list vs. a set\n\nimport timeit\n\ndef performance() -> None:\n    list_time = timeit.timeit(\"l.remove(10); l.append(10)\", \"l = list(range(20))\")\n    set_time = timeit.timeit(\"l.remove(10); l.add(10)\", \"l = set(range(20))\")\n    print(f\"append; remove: list {list_time:.3f}, set {set_time:.3f}\")\n\n    # Using two parallel lists vs. a mapping\n\n    list_2_time = timeit.timeit(\n        \"i= k.index(10); v[i]= 0\", \"k=list(range(20)); v=list(range(20))\"\n    )\n    dict_time = timeit.timeit(\"m[10]= 0\", \"m=dict(zip(list(range(20)),list(range(20))))\")\n    print(f\"setitem: two lists {list_2_time:.3f}, one dict {dict_time:.3f}\")\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n    doctest.testmod(verbose=False)\n\n    performance()\n"
  },
  {
    "path": "Chapter_8/__init__.py",
    "content": ""
  },
  {
    "path": "Chapter_8/ch08_ex1.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 8. Example 1.\n\"\"\"\n\n# noisyfloat\n# ================================\n\nimport sys\n\n\ndef trace(frame, event, arg):\n    if frame.f_code.co_name.startswith(\"__\"):\n        print(frame.f_code.co_name, frame.f_code.co_filename, event)\n\n\n# sys.settrace(trace)\n\n\nclass NoisyFloat(float):\n\n    def __add__(self, other: float) -> 'NoisyFloat':\n        print(self, \"+\", other)\n        return NoisyFloat(super().__add__(other))\n\n    def __radd__(self, other: float) -> 'NoisyFloat':\n        print(self, \"r+\", other)\n        return NoisyFloat(super().__radd__(other))\n\ntest_noisy_float = \"\"\"\n    >>> x = NoisyFloat(2)\n    >>> y = NoisyFloat(3)\n    >>> x + y + 2.5\n    2.0 + 3.0\n    5.0 + 2.5\n    7.5\n\"\"\"\n\n\n# Fixed Point\n# =================================\n\nimport numbers\nimport math\nfrom typing import Union, Optional, Any\n\nclass FixedPoint(numbers.Rational):\n    __slots__ = (\"value\", \"scale\", \"default_format\")\n\n    def __init__(self, value: Union['FixedPoint', int, float], scale: int = 100) -> None:\n        self.value: int\n        self.scale: int\n        if isinstance(value, FixedPoint):\n            self.value = value.value\n            self.scale = value.scale\n        elif isinstance(value, int):\n            self.value = value\n            self.scale = scale\n        elif isinstance(value, float):\n            self.value = int(scale * value + .5)  # Round half up\n            self.scale = scale\n        else:\n            raise TypeError(f\"Can't build FixedPoint from {value!r} of {type(value)}\")\n        digits = int(math.log10(scale))\n        self.default_format = \"{{0:.{digits}f}}\".format(digits=digits)\n\n    def __str__(self) -> str:\n        return self.__format__(self.default_format)\n\n    def __repr__(self) -> str:\n        return f\"{self.__class__.__name__:s}({self.value:d},scale={self.scale:d})\"\n\n    def __format__(self, specification: str) -> str:\n        if specification == \"\":\n            specification = self.default_format\n        return specification.format(self.value / self.scale)  # no rounding\n\n    def numerator(self) -> int:\n        return self.value\n\n    def denominator(self) -> int:\n        return self.scale\n\n    def __add__(self, other: Union['FixedPoint', int]) -> 'FixedPoint':\n        if not isinstance(other, FixedPoint):\n            new_scale = self.scale\n            new_value = self.value + other * self.scale\n        else:\n            new_scale = max(self.scale, other.scale)\n            new_value = self.value * (new_scale // self.scale) + other.value * (\n                new_scale // other.scale\n            )\n        return FixedPoint(int(new_value), scale=new_scale)\n\n    def __sub__(self, other: Union['FixedPoint', int]) -> 'FixedPoint':\n        if not isinstance(other, FixedPoint):\n            new_scale = self.scale\n            new_value = self.value - other * self.scale\n        else:\n            new_scale = max(self.scale, other.scale)\n            new_value = self.value * (new_scale // self.scale) - other.value * (\n                new_scale // other.scale\n            )\n        return FixedPoint(int(new_value), scale=new_scale)\n\n    def __mul__(self, other: Union['FixedPoint', int]) -> 'FixedPoint':\n        if not isinstance(other, FixedPoint):\n            new_scale = self.scale\n            new_value = self.value * other\n        else:\n            new_scale = self.scale * other.scale\n            new_value = self.value * other.value\n        return FixedPoint(int(new_value), scale=new_scale)\n\n    def __truediv__(self, other: Union['FixedPoint', int]) -> 'FixedPoint':\n        if not isinstance(other, FixedPoint):\n            new_value = int(self.value / other)\n        else:\n            new_value = int(self.value / (other.value / other.scale))\n        return FixedPoint(new_value, scale=self.scale)\n\n    def __floordiv__(self, other: Union['FixedPoint', int]) -> 'FixedPoint':\n        if not isinstance(other, FixedPoint):\n            new_value = int(self.value // other)\n        else:\n            new_value = int(self.value // (other.value / other.scale))\n        return FixedPoint(new_value, scale=self.scale)\n\n    def __mod__(self, other: Union['FixedPoint', int]) -> 'FixedPoint':\n        if not isinstance(other, FixedPoint):\n            new_value = (self.value / self.scale) % other\n        else:\n            new_value = self.value % (other.value / other.scale)\n        return FixedPoint(new_value, scale=self.scale)\n\n    def __pow__(self, other: Union['FixedPoint', int]) -> 'FixedPoint':\n        if not isinstance(other, FixedPoint):\n            new_value = (self.value / self.scale) ** other\n        else:\n            new_value = (self.value / self.scale) ** (other.value / other.scale)\n        return FixedPoint(int(new_value) * self.scale, scale=self.scale)\n\n    def __abs__(self) -> 'FixedPoint':\n        return FixedPoint(abs(self.value), self.scale)\n\n    def __float__(self) -> float:\n        return self.value / self.scale\n\n    def __int__(self) -> int:\n        return int(self.value / self.scale)\n\n    def __trunc__(self) -> int:\n        return int(math.trunc(self.value / self.scale))\n\n    def __ceil__(self) -> int:\n        return int(math.ceil(self.value / self.scale))\n\n    def __floor__(self) -> int:\n        return int(math.floor(self.value / self.scale))\n\n    # reveal_type(numbers.Rational.__round__)\n\n    def __round__(self, ndigits: Optional[int] = 0) -> Any:\n        return FixedPoint(round(self.value / self.scale, ndigits=ndigits), self.scale)\n\n    def __neg__(self) -> 'FixedPoint':\n        return FixedPoint(-self.value, self.scale)\n\n    def __pos__(self) -> 'FixedPoint':\n        return self\n\n    # Note equality among floats isn't a good idea.\n    # Also, should FixedPoint(123, 100) equal FixedPoint(1230, 1000)?\n    def __eq__(self, other: Any) -> bool:\n        if isinstance(other, FixedPoint):\n            if self.scale == other.scale:\n                return self.value == other.value\n            else:\n                return self.value * other.scale // self.scale == other.value\n        else:\n            return abs(self.value / self.scale - float(other)) < .5 / self.scale\n\n    def __ne__(self, other: Any) -> bool:\n        return not (self == other)\n\n    def __le__(self, other: 'FixedPoint') -> bool:\n        return self.value / self.scale <= float(other)\n\n    def __lt__(self, other: 'FixedPoint') -> bool:\n        return self.value / self.scale < float(other)\n\n    def __ge__(self, other: 'FixedPoint') -> bool:\n        return self.value / self.scale >= float(other)\n\n    def __gt__(self, other: 'FixedPoint') -> bool:\n        return self.value / self.scale > float(other)\n\n    def __hash__(self) -> int:\n        P = sys.hash_info.modulus\n        m, n = self.value, self.scale\n        # Remove common factors of P.  (Unnecessary if m and n already coprime.)\n        while m % P == n % P == 0:\n            m, n = m // P, n // P\n\n        if n % P == 0:\n            hash_ = sys.hash_info.inf\n        else:\n            # Fermat's Little Theorem: pow(n, P-1, P) is 1, so\n            # pow(n, P-2, P) gives the inverse of n modulo P.\n            hash_ = (abs(m) % P) * pow(n, P - 2, P) % P\n        if m < 0:\n            hash_ = -hash_\n        if hash_ == -1:\n            hash_ = -2\n        return hash_\n\n    def __radd__(self, other: Union['FixedPoint', int]) -> 'FixedPoint':\n        if not isinstance(other, FixedPoint):\n            new_scale = self.scale\n            new_value = other * self.scale + self.value\n        else:\n            new_scale = max(self.scale, other.scale)\n            new_value = other.value * (new_scale // other.scale) + self.value * (\n                new_scale // self.scale\n            )\n        return FixedPoint(int(new_value), scale=new_scale)\n\n    def __rsub__(self, other: Union['FixedPoint', int]) -> 'FixedPoint':\n        if not isinstance(other, FixedPoint):\n            new_scale = self.scale\n            new_value = other * self.scale - self.value\n        else:\n            new_scale = max(self.scale, other.scale)\n            new_value = other.value * (new_scale // other.scale) - self.value * (\n                new_scale // self.scale\n            )\n        return FixedPoint(int(new_value), scale=new_scale)\n\n    def __rmul__(self, other: Union['FixedPoint', int]) -> 'FixedPoint':\n        if not isinstance(other, FixedPoint):\n            new_scale = self.scale\n            new_value = other * self.value\n        else:\n            new_scale = self.scale * other.scale\n            new_value = other.value * self.value\n        return FixedPoint(int(new_value), scale=new_scale)\n\n    def __rtruediv__(self, other: Union['FixedPoint', int]) -> 'FixedPoint':\n        if not isinstance(other, FixedPoint):\n            new_value = self.scale * int(other / (self.value / self.scale))\n        else:\n            new_value = int((other.value / other.scale) / self.value)\n        return FixedPoint(new_value, scale=self.scale)\n\n    def __rfloordiv__(self, other: Union['FixedPoint', int]) -> 'FixedPoint':\n        if not isinstance(other, FixedPoint):\n            new_value = self.scale * int(other // (self.value / self.scale))\n        else:\n            new_value = int((other.value / other.scale) // self.value)\n        return FixedPoint(new_value, scale=self.scale)\n\n    def __rmod__(self, other: Union['FixedPoint', int]) -> 'FixedPoint':\n        if not isinstance(other, FixedPoint):\n            new_value = other % (self.value / self.scale)\n        else:\n            new_value = (other.value / other.scale) % (self.value / self.scale)\n        return FixedPoint(new_value, scale=self.scale)\n\n    def __rpow__(self, other: Union['FixedPoint', int]) -> 'FixedPoint':\n        if not isinstance(other, FixedPoint):\n            new_value = other ** (self.value / self.scale)\n        else:\n            new_value = (other.value / other.scale) ** self.value / self.scale\n        return FixedPoint(int(new_value) * self.scale, scale=self.scale)\n\n    def round_to(self, new_scale: int) -> 'FixedPoint':\n        f = new_scale / self.scale\n        return FixedPoint(int(self.value * f + .5), scale=new_scale)\n\n\n# test cases to show that ``FixedPoint`` numbers work properly.\ntest_fp = \"\"\"\n    >>> f1 = FixedPoint(12.34, 100)\n    >>> f2 = FixedPoint(1234, 100)\n    >>> print(f1, repr(f1))\n    12.34 FixedPoint(1234,scale=100)\n    >>> print(f2, repr(f2))\n    12.34 FixedPoint(1234,scale=100)\n    >>> print(f1 * f2, f1 + f2, f1 - f2, f1 / f2)\n    152.2756 24.68 0.00 1.00\n    >>> print(f1 + 101, f1 * 2, f1 - 101, f1 / 2, f1 % 1, f1 // 2)\n    113.34 24.68 -88.66 6.17 0.34 6.17\n    >>> print(101 + f2, 2 * f2, 101 - f1, 25 / f1, 1334 % f1, 25 // f1)\n    113.34 24.68 88.66 2.00 1.28 2.00\n    >>> print(\"round\", round(f1))\n    round 12.00\n    >>> print(\"ceil\", math.ceil(f1))\n    ceil 13\n    >>> print(\"floor\", math.floor(f1))\n    floor 12\n    >>> print(\"trunc\", math.trunc(f1))\n    trunc 12\n\n    >>> print(\"==\", f1 == f2, f1 == 12.34, f1 == 1234 / 100, f1 == FixedPoint(12340, 1000))\n    == True True True True\n    >>> print(hash(f1), hash(f2), hash(FixedPoint(12340, 1000)))\n    1521856386081038020 1521856386081038020 1521856386081038020\n\n    >>> f3 = FixedPoint(200, 100)\n    >>> print(f3 * f3 * f3, f3 ** 3, 3 ** f3)\n    8.000000 8.00 9.00\n    \n    >>> price = FixedPoint(1299, 100)\n    >>> tax_rate = FixedPoint(725, 1000)\n    >>> tax = price * tax_rate\n    >>> print(tax, tax.round_to(100))\n    9.41775 9.42\n\"\"\"\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_9/__init__.py",
    "content": ""
  },
  {
    "path": "Chapter_9/ch09_ex1.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 9. Example 1.\n\"\"\"\n\n# Decorator Example 1\n# ================================\n\n# Use of builtin decorators\n\nfrom typing import Any, cast, Optional, Type\nfrom types import TracebackType\nimport math\nimport random\nfrom Chapter_6.ch06_ex2 import KnownSequence\n\n\nclass Angle(float):\n    __slots__ = (\"_degrees\",)\n\n    @staticmethod\n    def from_radians(value: float) -> \"Angle\":\n        return Angle(180 * value / math.pi)\n\n    def __init__(self, degrees: float) -> None:\n        self._degrees = degrees\n\n    @property\n    def radians(self) -> float:\n        return math.pi * self._degrees / 180\n\n    @property\n    def degrees(self) -> float:\n        return self._degrees\n\n\ntest_angle = \"\"\"\n    >>> a = Angle(22.5)\n    >>> round(a.radians/math.pi, 3)\n    0.125\n    >>> b = Angle.from_radians(.227)\n    >>> round(b.degrees, 1)\n    13.0\n    >>> b.radians\n    0.227\n\"\"\"\n\n# Decorator Example 2\n# ================================\n\n# Use of library decorators.\n# Some preliminary definitions\n\nfrom enum import Enum\n\n\nclass Suit(Enum):\n    Clubs = \"♣\"\n    Diamonds = \"♦\"\n    Hearts = \"♥\"\n    Spades = \"♠\"\n\n\n# Using functools.total_ordering\n# Not a good idea. Use dataclasses instead.\nimport functools\n\n\n@functools.total_ordering\nclass CardTO:\n    __slots__ = (\"rank\", \"suit\")\n\n    def __init__(self, rank: int, suit: Suit) -> None:\n        self.rank = rank\n        self.suit = suit\n\n    def __eq__(self, other: Any) -> bool:\n        return self.rank == cast(CardTO, other).rank\n\n    def __lt__(self, other: Any) -> bool:\n        return self.rank < cast(CardTO, other).rank\n\n    def __str__(self) -> str:\n        return f\"{self.rank:d}{self.suit:s}\"\n\n\ntest_total_ordering = \"\"\"\n    >>> c1 = CardTO(3, Suit.Clubs)\n    >>> c2 = CardTO(3, Suit.Hearts)\n    >>> c1 == c2\n    True\n    >>> c1 < c2\n    False\n    >>> c1 <= c2\n    True\n    >>> c1 >= c2\n    True\n    >>> c1 > c2\n    False\n    >>> c1 != c2\n    False\n\"\"\"\n\nfrom dataclasses import dataclass\n\n\n@dataclass(frozen=True)\nclass CardDC:\n    rank: int\n    suit: Suit\n\n    def __eq__(self, other: Any) -> bool:\n        return self.rank == cast(CardTO, other).rank\n\n    def __lt__(self, other: Any) -> bool:\n        return self.rank < cast(CardTO, other).rank\n\n    def __le__(self, other: Any) -> bool:\n        return self.rank <= cast(CardTO, other).rank\n\n    def __str__(self) -> str:\n        return f\"{self.rank:d}{self.suit:s}\"\n\n\ntest_dc_ordering = \"\"\"\n    >>> c1 = CardDC(3, Suit.Clubs)\n    >>> c2 = CardDC(3, Suit.Hearts)\n    >>> c1 == c2\n    True\n    >>> c1 < c2\n    False\n    >>> c1 <= c2\n    True\n    >>> c1 >= c2\n    True\n    >>> c1 > c2\n    False\n    >>> c1 != c2\n    False\n\"\"\"\n\n# For later examples\n\n\nclass Deck(list):\n\n    def __init__(self, size: int = 1) -> None:\n        for d in range(size):\n            cards = [CardDC(r, s) for r in range(1, 14) for s in Suit]\n            super().extend(cards)\n        random.shuffle(self)\n\n\n# Mixin example 1\n# =======================\n\n# Mixin using enums\n\nfrom typing import Type, List\n\nfrom enum import Enum\n\n\nclass EnumDomain:\n\n    @classmethod\n    def domain(cls: Type) -> List[str]:\n        return [m.value for m in cls]\n\n\nclass SuitD(str, EnumDomain, Enum):\n    Clubs = \"♣\"\n    Diamonds = \"♦\"\n    Hearts = \"♥\"\n    Spades = \"♠\"\n\n\ntest_enum = \"\"\"\n    >>> SuitD.domain()\n    ['♣', '♦', '♥', '♠']\n    >>> SuitD.Clubs.center(5)\n    '  ♣  '\n    \n    >>> Suit.Clubs.center(5)  # doctest: +IGNORE_EXCEPTION_DETAIL\n    Traceback (most recent call last):\n      File \"/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py\", line 1329, in __run\n        compileflags, 1), test.globs)\n      File \"<doctest __main__.__test__.test_enum[2]>\", line 1, in <module>\n        Suit.Clubs.center(5)\n    AttributeError: 'Suit' object has no attribute 'center'\n\n    >>> Suit.Clubs.value.center(5)\n    '  ♣  '\n\n\"\"\"\n\n# Decorator Example 1\n# ==============================\n\n# Simple function decorator\nimport logging, sys\nimport functools\nfrom typing import Callable, TypeVar, List\n\n\nFuncType = Callable[..., Any]\nF = TypeVar(\"F\", bound=FuncType)\n\n\ndef debug(function: F) -> F:\n\n    @functools.wraps(function)\n    def logged_function(*args, **kw):\n        logging.debug(\"%s(%r, %r)\", function.__name__, args, kw)\n        result = function(*args, **kw)\n        logging.debug(\"%s = %r\", function.__name__, result)\n        return result\n\n    return cast(F, logged_function)\n\n\n@debug\ndef ackermann(m: int, n: int) -> int:\n    if m == 0:\n        return n + 1\n    elif m > 0 and n == 0:\n        return ackermann(m - 1, 1)\n    elif m > 0 and n > 0:\n        return ackermann(m - 1, ackermann(m, n - 1))\n    else:\n        raise Exception(f\"Design Error: {vars()}\")\n\n\ntest_debug_1 = \"\"\"\n    >>> logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)\n    >>> ackermann(2, 4)\n    11\n    >>> logging.shutdown()\n\"\"\"\n\n# Decorator Example 2\n# ==============================\n\n\ndef debug2(function: F) -> F:\n    log = logging.getLogger(function.__name__)\n\n    @functools.wraps(function)\n    def logged_function(*args, **kw):\n        log.debug(\"call(%r, %r)\", args, kw)\n        result = function(*args, **kw)\n        log.debug(\"result = %r\", result)\n        return result\n\n    return cast(F, logged_function)\n\n\n@debug2\ndef ackermann2(m: int, n: int) -> int:\n    if m == 0:\n        return n + 1\n    elif m > 0 and n == 0:\n        return ackermann2(m - 1, 1)\n    elif m > 0 and n > 0:\n        return ackermann2(m - 1, ackermann2(m, n - 1))\n    else:\n        raise Exception(f\"Design Error: {vars()}\")\n\n\n@debug2\ndef simpler(x: int, y: int) -> int:\n    return 2 * x + y\n\n\ntest_debug_2 = \"\"\"\n    >>> logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)\n    >>> ackermann2(2, 4)\n    11\n    >>> simpler(20, 2)\n    42\n    >>> logging.shutdown()\n\"\"\"\n\n# Decorator Example 3\n# ==============================\n\n# Parameterized decorator\ndef decorator(config) -> Callable[[F], F]:\n\n    def concrete_decorator(function: F) -> F:\n\n        def wrapped(*args, **kw):\n            return function(*args, **kw)\n\n        return cast(F, wrapped)\n\n    return concrete_decorator\n\n\ndef debug_named(log_name: str) -> Callable[[F], F]:\n    log = logging.getLogger(log_name)\n\n    def concrete_decorator(function: F) -> F:\n\n        @functools.wraps(function)\n        def wrapped(*args, **kw):\n            log.debug(\"%s(%r, %r)\", function.__name__, args, kw)\n            result = function(*args, **kw)\n            log.debug(\"%s = %r\", function.__name__, result)\n            return result\n\n        return cast(F, wrapped)\n\n    return concrete_decorator\n\n\n@debug_named(\"recursion\")\ndef ackermann3(m: int, n: int) -> int:\n    if m == 0:\n        return n + 1\n    elif m > 0 and n == 0:\n        return ackermann3(m - 1, 1)\n    elif m > 0 and n > 0:\n        return ackermann3(m - 1, ackermann3(m, n - 1))\n    else:\n        raise Exception(f\"Design Error: {vars()}\")\n\n\ntest_debug_3 = \"\"\"\n    >>> logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)\n    >>> ackermann3(2, 4)\n    11\n    >>> logging.shutdown()\n\"\"\"\n\n# Class Decorator 1\n# ==============================\n\n# Unit and Standard Unit\n\n\ndef standard(class_: Type) -> Type:\n    class_.standard = class_\n    return class_\n\n\ndef nonstandard(based_on: Type) -> Callable[[Type], Type]:\n\n    def concrete_decorator(class_: Type) -> Type:\n        class_.standard = based_on\n        return class_\n\n    return concrete_decorator\n\n\nclass Unit:\n    factor = 1.0\n\n    @classmethod\n    def value(class_, value: float) -> float:\n        if value is None:\n            return None\n        return value / class_.factor\n\n    @classmethod\n    def convert(class_, value: float) -> float:\n        if value is None:\n            return None\n        return value * class_.factor\n\n\n@standard\nclass INCH(Unit):\n    \"\"\"inch\"\"\"\n    name = \"in\"\n\n\n@nonstandard(INCH)\nclass FOOT(Unit):\n    \"\"\"foot\"\"\"\n    name = \"ft\"\n    factor = 1 / 12\n\n\ntest_class_decorator = \"\"\"\n    >>> length = INCH.value(18)\n    >>> print(FOOT.convert(length), FOOT.name, \"=\", INCH.convert(length), INCH.name)\n    1.5 ft = 18.0 in\n\"\"\"\n\n# Method Decorator\n# =============================\n\n\ndef audit(method: F) -> F:\n\n    @functools.wraps(method)\n    def wrapper(self, *args, **kw):\n        template = \"%s\\n     before %s\\n     after %s\"\n        audit_log = logging.getLogger(\"audit\")\n        before = repr(self)  # a kind of deep copy to preserve state\n        try:\n            result = method(self, *args, **kw)\n        except Exception as e:\n            after = repr(self)\n            audit_log.exception(template, method.__qualname__, before, after)\n            raise\n        after = repr(self)\n        audit_log.info(template, method.__qualname__, before, after)\n        return result\n\n    return cast(F, wrapper)\n\n\nclass Hand:\n\n    def __init__(self, *cards: CardDC) -> None:\n        self._cards = list(cards)\n\n    @audit\n    def __iadd__(self, card: CardDC) -> \"Hand\":\n        self._cards.append(card)\n        self._cards.sort(key=lambda c: c.rank)\n        return self\n\n    def __repr__(self) -> str:\n        cards = \", \".join(map(str, self._cards))\n        return f\"{self.__class__.__name__}({cards})\"\n\n\ntest_audit = \"\"\"\n    >>> logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)\n    >>> with KnownSequence():\n    ...     d = Deck()\n    ...     h = Hand(d.pop(), d.pop())\n    ...     h += d.pop()\n    ...     print(h)\n    Hand(7Suit.Clubs, 7Suit.Hearts, 13Suit.Clubs)\n        \n    >>> with KnownSequence():\n    ...     d = Deck()\n    ...     h = Hand(d.pop(), d.pop())\n    ...     h += \"Not A Card!\"  # doctest: +IGNORE_EXCEPTION_DETAIL\n    Traceback (most recent call last):\n      File \"/Users/slott/Documents/Writing/Python/Mastering OO Python 2e/mastering-oo-python-2e/Chapter_9/ch09_ex1.py\", line 390, in wrapper\n        result = method(self, *args, **kw)\n          File \"/Users/slott/Documents/Writing/Python/Mastering OO Python 2e/mastering-oo-python-2e/Chapter_9/ch09_ex1.py\", line 390, in wrapper\n      File \"/Users/slott/Documents/Writing/Python/Mastering OO Python 2e/mastering-oo-python-2e/Chapter_9/ch09_ex1.py\", line 410, in __iadd__\n            result = method(self, *args, **kw)\n        self._cards.sort(key=lambda c: c.rank)\n          File \"/Users/slott/Documents/Writing/Python/Mastering OO Python 2e/mastering-oo-python-2e/Chapter_9/ch09_ex1.py\", line 410, in __iadd__\n      File \"/Users/slott/Documents/Writing/Python/Mastering OO Python 2e/mastering-oo-python-2e/Chapter_9/ch09_ex1.py\", line 410, in <lambda>\n            self._cards.sort(key=lambda c: c.rank)\n        self._cards.sort(key=lambda c: c.rank)\n    AttributeError: 'str' object has no attribute 'rank'\n    \n    >>> logging.shutdown()\n\"\"\"\n\n# More Complex Decoration\n# ==================================\n\n# This decorator's effect is effectively hidden from mypy.\n# The new method is injected dynamically.\n\n\ndef memento(class_: Type) -> Type:\n\n    def memento_method(self):\n        return (\n            f\"{self.__class__.__qualname__}\"\n            f\"(**{vars(self)!r})\"\n        )\n\n    class_.memento = memento_method\n    return class_\n\n\n@memento\nclass StatefulClass:\n\n    def __init__(self, value: Any) -> None:\n        self.value = value\n\n    def __repr__(self) -> str:\n        return f\"{self.value}\"\n\n\ntest_memento_1 = \"\"\"\n    >>> st = StatefulClass(2.7)\n    >>> print(st.memento())\n    StatefulClass(**{'value': 2.7})\n\"\"\"\n\n\nclass Memento:\n\n    def memento(self) -> str:\n        return f\"{self.__class__.__qualname__}(**{vars(self)!r})\"\n\n\nclass StatefulClass2(Memento):\n\n    def __init__(self, value):\n        self.value = value\n\n    def __repr__(self):\n        return f\"{self.value}\"\n\n\ntest_memento_2 = \"\"\"\n    >>> st2 = StatefulClass2(2.7)\n    >>> print(st2.memento())\n    StatefulClass2(**{'value': 2.7})\n\"\"\"\n__test__ = {\n    name: value for name, value in locals().items() if name.startswith(\"test_\")\n}\n\nif __name__ == \"__main__\":\n    import doctest\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "Chapter_9/ch09_ex2.py",
    "content": "#!/usr/bin/env python3.7\n\"\"\"\nMastering Object-Oriented Python 2e\n\nCode Examples for Mastering Object-Oriented Python 2nd Edition\n\nChapter 9. Example 2.\n\"\"\"\nfrom typing import Any, Type\n\n# Class Decorator 2 -- Logger\n# ==============================\n\nimport logging\nimport sys\n\n# Wordy - but visible to mypy\nclass UglyClass1:\n\n    def __init__(self) -> None:\n        self.logger = logging.getLogger(self.__class__.__qualname__)\n        self.logger.info(\"New thing\")\n\n    def method(self, *args: Any) -> int:\n        self.logger.info(\"method %r\", args)\n        return 42\n\n\n# Non-DRY -- class name is repeated\nclass UglyClass2:\n    logger = logging.getLogger(\"UglyClass2\")\n\n    def __init__(self) -> None:\n        self.logger.info(\"New thing\")\n\n    def method(self, *args: Any) -> int:\n        self.logger.info(\"method %r\", args)\n        return 42\n\n\n# Less Ugly, more DRY\n# However... mypy can't see this attribute -- not a solution\n# Chapter_9/ch09_ex2.py:54: error: \"SomeClass\" has no attribute \"logger\"\n# Chapter_9/ch09_ex2.py:57: error: \"SomeClass\" has no attribute \"logger\"\ndef logged(class_: Type) -> Type:\n    class_.logger = logging.getLogger(class_.__qualname__)\n    return class_\n\n\n@logged\nclass SomeClass:\n\n    def __init__(self) -> None:\n        self.logger.info(\"New thing\")  # mypy error\n\n    def method(self, *args: Any) -> int:\n        self.logger.info(\"method %r\", args)  # mypy error\n        return 42\n\n\n# More DRY. And visible to mypy.\nclass LoggedInstance:\n    logger: logging.Logger\n\n    def __new__(cls):\n        instance = super().__new__(cls)\n        instance.logger = logging.getLogger(cls.__qualname__)\n        return instance\n\n\nclass SomeClass2(LoggedInstance):\n\n    def __init__(self) -> None:\n        self.logger.info(\"New thing\")\n\n    def method(self, *args: Any) -> int:\n        self.logger.info(\"method %r\", args)\n        return 42\n\n\n# And a class-level logger, just to be complete.\n\n\nclass LoggedClassMeta(type):\n\n    def __new__(cls, name, bases, namespace, **kwds):\n        result = type.__new__(cls, name, bases, dict(namespace))\n        result.logger = logging.getLogger(result.__qualname__)\n        return result\n\n\nclass LoggedClass(metaclass=LoggedClassMeta):\n    logger: logging.Logger\n    pass\n\n\nclass SomeClass3(LoggedClass):\n\n    def __init__(self) -> None:\n        self.logger.info(\"New thing\")\n\n    def method(self, *args: Any) -> int:\n        self.logger.info(\"method %r\", args)\n        return 42\n\n\nclass LoggedWithHook:\n    def __init_subclass__(cls, name=None):\n        cls.logger = logging.getLogger(name or cls.__qualname__)\n\n\nclass SomeClass4(LoggedWithHook):\n\n    def __init__(self) -> None:\n        self.logger.info(\"New thing\")\n\n    def method(self, *args: Any) -> int:\n        self.logger.info(\"method %r\", args)\n        return 42\n\nclass SomeClass4s(LoggedWithHook, name='special'):\n\n    def __init__(self) -> None:\n        self.logger.info(\"New thing\")\n\n    def method(self, *args: Any) -> int:\n        self.logger.info(\"method %r\", args)\n        return 42\n\n\ntest_logged_class = \"\"\"\n    >>> logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)\n    >>> uc1 = UglyClass1()\n    >>> uc1.method(355 / 113)\n    42\n    >>> uc2 = UglyClass2()\n    >>> uc2.method(355 / 113)\n    42\n    >>> sc = SomeClass()\n    >>> sc.method(355 / 113)\n    42\n    >>> sc2 = SomeClass2()\n    >>> sc2.method(355 / 113)\n    42\n    >>> sc3 = SomeClass3()\n    >>> sc3.method(355 / 113)\n    42\n    >>> sc4 = SomeClass4()\n    >>> sc4.method(365 / 113)\n    42\n    >>> sc4s = SomeClass4s()\n    >>> sc4s.method(365 / 113)\n    42\n    >>> logging.shutdown()\n\"\"\"\n\n__test__ = {name: value for name, value in locals().items() if name.startswith(\"test_\")}\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod(verbose=False)\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019 Packt\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "\n\n\n# Mastering Object-Oriented Python - Second Edition \n\n<a href=\"https://www.packtpub.com/programming/mastering-object-oriented-python-second-edition?utm_source=github&utm_medium=repository&utm_campaign=9781789531367\"><img src=\"https://www.packtpub.com/media/catalog/product/cache/e4d64343b1bc593f1c5348fe05efa4a6/9/7/9781789531367-original.jpeg\" alt=\"Mastering Object-Oriented Python - Second Edition \" height=\"256px\" align=\"right\"></a>\n\nThis is the code repository for [Mastering Object-Oriented Python - Second Edition](https://www.packtpub.com/programming/mastering-object-oriented-python-second-edition?utm_source=github&utm_medium=repository&utm_campaign=9781789531367), published by Packt.\n\n**Build powerful applications with reusable code using OOP design patterns and Python 3.7**\n\n## What is this book about?\nObject-oriented programming (OOP) is a relatively complex discipline to master, and it can be difficult to see how general principles apply to each language's unique features. With the help of the latest edition of Mastering Objected-Oriented Python, you'll be shown how to effectively implement OOP in Python, and even explore Python 3.x. \n\nThis book covers the following exciting features:\n* Explore a variety of different design patterns for the __init__() method \n* Learn to use Flask to build a RESTful web service \n* Discover SOLID design patterns and principles \n* Use the features of Python 3's abstract base \n* Create classes for your own applications \n* Design testable code using pytest and fixtures \n* Understand how to design context managers that leverage the 'with' statement \n* Create a new type of collection using standard library and design techniques \n* Develop new number types above and beyond the built-in classes of numbers\n\nIf you feel this book is for you, get your [copy](https://www.amazon.com/dp/1789531365) today!\n\n<a href=\"https://www.packtpub.com/?utm_source=github&utm_medium=banner&utm_campaign=GitHubBanner\"><img src=\"https://raw.githubusercontent.com/PacktPublishing/GitHub/master/GitHub.png\" \nalt=\"https://www.packtpub.com/\" border=\"5\" /></a>\n\n## Instructions and Navigations\nAll of the code is organized into folders. For example, Chapter02.\n\nThe code will look like the following:\n```\ndef F(n: int) -> int:\nif n in (0, 1):\nreturn 1\nelse:\nreturn F(n-1) + F(n-2)\n```\n\n**Following is what you need for this book:**\nThis book is for developers who want to use Python to create efficient programs. A good understanding of Python programming is required to make the most out of this book. Knowledge of concepts related to object-oriented design patterns will also be useful.\n\nWith the following software and hardware list you can run all code files present in the book (Chapter 1-20).\n### Software and Hardware List\n| Chapter | Software required | OS required |\n| -------- | ------------------------------------ | ----------------------------------- |\n| 1-20 | Python 3.7 | Any |\n\n### Related products\n* Python 3 Object-Oriented Programming - Third Edition  [[Packt]](https://www.packtpub.com/application-development/python-3-object-oriented-programming-third-edition?utm_source=github&utm_medium=repository&utm_campaign=9781789615852) [[Amazon]](https://www.amazon.com/dp/1789615852)\n\n* Learn Python Programming - Second Edition  [[Packt]](https://www.packtpub.com/application-development/learn-python-programming-second-edition?utm_source=github&utm_medium=repository&utm_campaign=9781788996662) [[Amazon]](https://www.amazon.com/dp/1788996666)\n\n## Get to Know the Author\n**Steven F. Lott**\nhas been programming since the 1970s, when computers were large, expensive, and rare. As a contract software developer and architect, he has worked on hundreds of projects, from very small to very large ones. He's been using Python to solve business problems for over 10 years. His other titles with Packt include Python Essentials, Mastering Object-Oriented Python, Functional Python Programming Second Edition, Python for Secret Agents, and Python for Secret Agents II. Steven is currently a technomad who lives in various places on the East Coast of the US. You can follow him on Twitter via the handle @s_lott.\n\n## Other books by the author\n[Modern Python Cookbook ](https://www.packtpub.com/application-development/modern-python-cookbook?utm_source=github&utm_medium=repository&utm_campaign=9781786469250)\n\n[Functional Python Programming - Second Edition ](https://www.packtpub.com/application-development/functional-python-programming-second-edition?utm_source=github&utm_medium=repository&utm_campaign=9781788627061)\n\n### Suggestions and Feedback\n[Click here](https://docs.google.com/forms/d/e/1FAIpQLSdy7dATC6QmEL81FIUuymZ0Wy9vH1jHkvpY57OiMeKGqib_Ow/viewform) if you have any feedback or suggestions.\n### Download a free PDF\n\n <i>If you have already purchased a print or Kindle version of this book, you can get a DRM-free PDF version at no cost.<br>Simply click on the link to claim your free PDF.</i>\n<p align=\"center\"> <a href=\"https://packt.link/free-ebook/9781789531367\">https://packt.link/free-ebook/9781789531367 </a> </p>"
  },
  {
    "path": "data/ch17_data.csv",
    "content": "rate_in,time_in,distance_in,rate_out,time_out,distance_out\n2,3,,2,3,6\n5,,7,5,1.4,7\n,11,13,1.18,11,13\n"
  },
  {
    "path": "data/ch17_sample.csv",
    "content": "not_player,bet,rounds,final\ndata,1,1,1\n"
  },
  {
    "path": "environment.yaml",
    "content": "name: mastering\nchannels:\n  - defaults\ndependencies:\n  - alabaster=0.7.12=py37_0\n  - asn1crypto=0.24.0=py37_0\n  - astroid=2.0.4=py37_0\n  - atomicwrites=1.2.1=py37_0\n  - attrs=18.2.0=py37h28b3542_0\n  - babel=2.6.0=py37_0\n  - ca-certificates=2019.1.23=0\n  - certifi=2019.3.9=py37_0\n  - cffi=1.11.5=py37h6174b99_1\n  - chardet=3.0.4=py37_1\n  - cryptography=2.5=py37ha12b0ac_0\n  - docutils=0.14=py37_0\n  - flask=1.0.2=py37_1\n  - idna=2.7=py37_0\n  - imagesize=1.1.0=py37_0\n  - isort=4.3.4=py37_0\n  - itsdangerous=1.1.0=py37_0\n  - jinja2=2.10=py37_0\n  - lazy-object-proxy=1.3.1=py37h1de35cc_2\n  - libcxx=4.0.1=h579ed51_0\n  - libcxxabi=4.0.1=hebd6815_0\n  - libedit=3.1.20170329=hb402a30_2\n  - libffi=3.2.1=h475c297_4\n  - markupsafe=1.0=py37h1de35cc_1\n  - mccabe=0.6.1=py37_1\n  - more-itertools=4.3.0=py37_0\n  - mypy_extensions=0.4.1=py37_0\n  - ncurses=6.1=h0a44026_0\n  - openssl=1.1.1b=h1de35cc_1\n  - packaging=18.0=py37_0\n  - pluggy=0.7.1=py37h28b3542_0\n  - psutil=5.4.7=py37h1de35cc_0\n  - py=1.7.0=py37_0\n  - pycparser=2.19=py37_0\n  - pygments=2.2.0=py37_0\n  - pylint=2.1.1=py37_0\n  - pyopenssl=18.0.0=py37_0\n  - pyparsing=2.2.2=py37_0\n  - pysocks=1.6.8=py37_0\n  - pytest=3.8.2=py37_0\n  - python=3.7.2=haf84260_0\n  - pytz=2018.5=py37_0\n  - pyyaml=5.1=py37h1de35cc_0\n  - readline=7.0=h1de35cc_5\n  - requests=2.21.0=py37_0\n  - setuptools=40.8.0=py37_0\n  - six=1.11.0=py37_1\n  - snowballstemmer=1.2.1=py37_0\n  - sphinx=1.8.5=py37_0\n  - sphinxcontrib=1.0=py37_1\n  - sphinxcontrib-websupport=1.1.0=py37_1\n  - sqlalchemy=1.2.12=py37h1de35cc_0\n  - sqlite=3.26.0=ha441bb4_0\n  - tk=8.6.8=ha441bb4_0\n  - urllib3=1.23=py37_0\n  - werkzeug=0.14.1=py37_0\n  - wheel=0.33.1=py37_0\n  - wrapt=1.10.11=py37h1de35cc_2\n  - xz=5.2.4=h1de35cc_4\n  - yaml=0.1.7=hc338f04_2\n  - zlib=1.2.11=hf3cbc9b_2\n  - pip:\n    - appdirs==1.4.3\n    - black==18.9b0\n    - click==7.0\n    - mypy==0.670\n    - pip==19.0.2\n    - toml==0.10.0\n    - typed-ast==1.3.1\n\nprefix: /Users/slott/miniconda3/envs/mastering\n\n"
  },
  {
    "path": "requirements.txt",
    "content": "# This file may be used to create an environment using:\n# $ conda create --name <env> --file <this file>\n# platform: osx-64\nalabaster=0.7.12=py37_0\nappdirs=1.4.3=pypi_0\nasn1crypto=0.24.0=py37_0\nastroid=2.0.4=py37_0\natomicwrites=1.2.1=py37_0\nattrs=18.2.0=py37h28b3542_0\nbabel=2.6.0=py37_0\nblack=18.9b0=pypi_0\nca-certificates=2019.1.23=0\ncertifi=2019.3.9=py37_0\ncffi=1.11.5=py37h6174b99_1\nchardet=3.0.4=py37_1\nclick=7.0=pypi_0\ncryptography=2.5=py37ha12b0ac_0\ndocutils=0.14=py37_0\nflask=1.0.2=py37_1\nidna=2.7=py37_0\nimagesize=1.1.0=py37_0\nisort=4.3.4=py37_0\nitsdangerous=1.1.0=py37_0\njinja2=2.10=py37_0\nlazy-object-proxy=1.3.1=py37h1de35cc_2\nlibcxx=4.0.1=h579ed51_0\nlibcxxabi=4.0.1=hebd6815_0\nlibedit=3.1.20170329=hb402a30_2\nlibffi=3.2.1=h475c297_4\nmarkupsafe=1.0=py37h1de35cc_1\nmccabe=0.6.1=py37_1\nmore-itertools=4.3.0=py37_0\nmypy=0.670=pypi_0\nmypy_extensions=0.4.1=py37_0\nncurses=6.1=h0a44026_0\nopenssl=1.1.1b=h1de35cc_1\npackaging=18.0=py37_0\npip=19.0.2=pypi_0\npluggy=0.7.1=py37h28b3542_0\npsutil=5.4.7=py37h1de35cc_0\npy=1.7.0=py37_0\npycparser=2.19=py37_0\npygments=2.2.0=py37_0\npylint=2.1.1=py37_0\npyopenssl=18.0.0=py37_0\npyparsing=2.2.2=py37_0\npysocks=1.6.8=py37_0\npytest=3.8.2=py37_0\npython=3.7.2=haf84260_0\npytz=2018.5=py37_0\npyyaml=5.1=py37h1de35cc_0\nreadline=7.0=h1de35cc_5\nrequests=2.21.0=py37_0\nsetuptools=40.8.0=py37_0\nsix=1.11.0=py37_1\nsnowballstemmer=1.2.1=py37_0\nsphinx=1.8.5=py37_0\nsphinxcontrib=1.0=py37_1\nsphinxcontrib-websupport=1.1.0=py37_1\nsqlalchemy=1.2.12=py37h1de35cc_0\nsqlite=3.26.0=ha441bb4_0\ntk=8.6.8=ha441bb4_0\ntoml=0.10.0=pypi_0\ntyped-ast=1.3.1=pypi_0\nurllib3=1.23=py37_0\nwerkzeug=0.14.1=py37_0\nwheel=0.33.1=py37_0\nwrapt=1.10.11=py37h1de35cc_2\nxz=5.2.4=h1de35cc_4\nyaml=0.1.7=hc338f04_2\nzlib=1.2.11=hf3cbc9b_2\n"
  },
  {
    "path": "show_hierarchies.py",
    "content": "\"\"\"\nCreate ASCII Art for hierarchies\n\nUses asciitree. https://pypi.org/project/asciitree/0.3.3/\n\"\"\"\n\nfrom asciitree import LeftAligned\nfrom asciitree.drawing import BOX_HEAVY\n\nrendering = LeftAligned()\nrendering.draw.gfx = BOX_HEAVY\n\nsimple = {\n    'module.py': {\n        'class A:': {'def method(self): ...': {}},\n        'class B:': {'def method(self): ...': {}},\n        'def function():': {}}\n}\nprint(rendering(simple))\n\npackage = {\n    'package': {\n        '__init__.py': {},\n        'module1.py': {\n            'class A:': {'def method(self): ...': {}},\n            'def function():': {}},\n        'module2.py': {'...': {}},\n    }\n}\nprint(rendering(package))\n\ncpx = {\n    'gemma': {\n        'baseline': {'code.py': {}},\n        'my-first-project': {'code.py': {}},\n        'another-project': {'code.py': {}},\n        'os-upgrade': {'other files...': {}}\n    }\n}\nprint(rendering(cpx))\n"
  },
  {
    "path": "stubs/sqlite3.pyi",
    "content": "\"\"\"A start of a stub file to correct the error in the current sqlite3 definition.\"\"\"\n\nfrom typing import Union, Type, Optional\nfrom pathlib import Path\nimport sqlite3\n\ndef connect(\n        database: Union[bytes, str, Path],\n        timeout: Optional[float] = None,\n        detect_types: Optional[int] = None,\n        isolation_level: Optional[str] = None,\n        check_same_thread: Optional[bool] = None,\n        factory: Optional[Type[sqlite3.dbapi2.Connection]] = None,\n        cached_statements: Optional[int] = None,\n        uri: Optional[bool] = None\n    ) -> sqlite3.dbapi2.Connection: ...\n"
  },
  {
    "path": "test_all.py",
    "content": "#!/usr/bin/env python3\n\"\"\"Run all the chapter modules, doctests or performance() function\n\nThis is run from the top-level directory, where all of the sample\ndata files are also located.\n\nWhen runnning individual examples, working directory is expected\nto be this top-level directory.\n\"\"\"\nimport doctest\nimport runpy\nimport unittest\nimport sys\nimport time\nfrom enum import Enum\nimport importlib\nfrom pathlib import Path\nfrom typing import Any, Iterator, Tuple, Iterable\nimport pytest\n\nDEBUG = False # Can't easily use logging -- can conflict with chapters on logging.\n\nDOCTEST_EXCLUDE = {\n    'ch13_ex4'  # Requires a separate server to be started, too complex for this script\n}\n\ndef package_module_iter(packages: Iterable[Path]) -> Iterator[Tuple[Path, Iterator[Path]]]:\n    \"\"\"For a given list of packages, emit the package name and a generator\n    for all modules in the package. Structured like ``itertools.groupby()``.\n    With a filter to reject caches of various kinds.\n\n    keep = lambda path: not all(\n        [filename.stem.startswith(\"__\"), filename.stem.endswith(\"__\"), filename.suffix == \".py\"]\n    )\n    yield package, filter(keep, package.glob(\"*.py\"))\n    \"\"\"\n    def module_iter(package: Path, module_iter: Iterable[Path]) -> Iterator[Path]:\n        \"\"\"\n        A filter to reject __init__.py and similar names.\n        \"\"\"\n        if DEBUG:\n            print(f\"Package {package}\")\n        for filename in module_iter:\n            if (\n                filename.stem.startswith(\"__\")\n                and filename.stem.endswith(\"__\")\n                and filename.suffix == \".py\"\n            ):\n                continue\n            if DEBUG:\n                print(f\"  file {filename.name} module {filename.stem}\")\n            yield filename\n\n    for package in packages:\n        yield (package,\n               module_iter(package, package.glob(\"*.py\")))\n\ndef run(pkg_mod_iter: Iterable[Tuple[Path, Iterable[Path]]]) -> None:\n    \"\"\"Run each module, with a few exclusions.\"\"\"\n    for package, module_iter in pkg_mod_iter:\n        print()\n        print(package.name)\n        print(\"=\"*len(package.name))\n        print()\n        for module in module_iter:\n            if module.stem in DOCTEST_EXCLUDE:\n                print(f\"Excluding {module}\")\n                continue\n            status = runpy.run_path(module, run_name=\"__main__\")\n            if status != 0:\n                sys.exit(f\"Failure: {module}\")\nimport subprocess\n\ndef run_doctest_suite(pkg_mod_iter: Iterable[Tuple[Path, Iterable[Path]]]) -> None:\n    \"\"\"Doctest each module individually. With a few exclusions.\n\n    Might be simpler to use doctest.testfile()? However, the examples aren't laid out for this.\n    \"\"\"\n    for package, module_iter in pkg_mod_iter:\n        print()\n        print(package.name)\n        print(\"=\"*len(package.name))\n        print()\n        for module_path in module_iter:\n            if module_path.stem in DOCTEST_EXCLUDE:\n                print(f\"Excluding {module_path}\")\n                continue\n            result = subprocess.run(['python3', '-m', 'doctest', str(module_path)])\n            if result.returncode != 0:\n                sys.exit(f\"Failure {result!r} in {module_path}\")\n\nclass PytestExit(int, Enum):\n    Success = 0\n    Failures = 1\n    Interrupted = 2\n    InternalError = 3\n    CommandLineError = 4\n    NoTests = 5\n\ndef run_pytest_suite(pkg_mod_iter: Iterable[Tuple[Path, Iterable[Path]]]) -> None:\n    \"\"\"Pytest each module's modules.\n    \"\"\"\n    for package, module_iter in pkg_mod_iter:\n        print()\n        print(package.name)\n        print(\"=\"*len(package.name))\n        print()\n        names = [f\"{m.parent.name}/{m.name}\" for m in (module_iter)]\n        print(names)\n        status = pytest.main(names)\n        if status not in (PytestExit.Success, PytestExit.NoTests):\n            sys.exit(f\"Failure {PytestExit(status)!r} in {names}\")\n\n\ndef run_performance(pkg_mod_iter: Iterable[Tuple[Path, Iterable[Path]]]) -> None:\n    \"\"\"Locate a performance() function in each module and run it.\"\"\"\n    for package, module_iter in pkg_mod_iter:\n        print()\n        print(package.name)\n        print(\"=\"*len(package.name))\n        print()\n        for module in module_iter:\n            print(module)\n            try:\n                imported_module = __import__(\n                    f\"{package.name}.{module.stem}\", fromlist=[module.stem, \"performance\"])\n                imported_module.performance()\n            except AttributeError:\n                pass # no performance() function in the module.\n\ndef master_test_suite(pkg_mod_iter: Iterable[Tuple[Path, Iterable[Path]]]) -> None:\n    \"\"\"Deprecated. Use pytest, instead.\n\n    Build a master unittest test suite from all modules and run that.\n    \"\"\"\n    master_suite = unittest.TestSuite()\n    for package, module_iter in pkg_mod_iter:\n        for module in module_iter:\n            print(f\"{package.name}.{module.stem}\", file=sys.stderr)\n            suite = doctest.DocTestSuite(f\"{package.name}.{module.stem}\")\n            print(\"  \", suite, file=sys.stderr)\n            master_suite.addTests(suite)\n    runner = unittest.TextTestRunner(verbosity=1)\n    runner.run(master_suite)\n\ndef chap_key(name: Path) -> int:\n    _, _, n = name.stem.partition(\"_\")\n    return int(n)\n\nif __name__ == \"__main__\":\n    content = sorted(Path.cwd().glob(\"Chapter_*\"), key=chap_key)\n    if DEBUG:\n        print(content, file=sys.stderr)\n    run_doctest_suite(package_module_iter(content))\n    run_pytest_suite(package_module_iter(content))\n"
  },
  {
    "path": "tox.ini",
    "content": "[tox]\nskipsdist = True\nenvlist = py37\n\n[testenv]\ndeps =\n    pytest\n    flask\n    requests\n    jinja2\n    sqlalchemy\n    pyyaml\n    mypy\nsetenv   =\n    PYTHONPATH = {env:PYTHONPATH:}{:}Chapter_20/src\ncommands =\n    python3 test_all.py\n\n    python3 -m doctest Chapter_17/ch17_ex1.py\n    python3 -m doctest Chapter_17/test_ch17.py\n\n    # mypy can't be run on all files...\n    # It might be slightly cleaner to provide a separate mypy config file.\n\n    mypy Chapter_1\n    mypy Chapter_2\n    mypy Chapter_3\n    mypy Chapter_4\n    # Chapter_5/ch05_ex1.py:114: error: Signature of \"aMethod\" incompatible with supertype \"LikeAbstract\"\n    mypy Chapter_5/ch05_ex2.py\n    # Chapter_6/ch06_ex1.py:68: error: Incompatible types in assignment (expression has type \"Power3\", variable has type \"Callable[[int, int], int]\")\n    mypy Chapter_6/ch06_ex2.py\n    mypy Chapter_7\n    mypy Chapter_8\n    mypy Chapter_9/ch09_ex1.py\n    # Chapter_9/ch09_ex2.py:54: error: \"SomeClass\" has no attribute \"logger\"\n    # Chapter_9/ch09_ex2.py:57: error: \"SomeClass\" has no attribute \"logger\"\n    mypy Chapter_10\n    mypy Chapter_11\n    mypy Chapter_12/ch12_ex1.py Chapter_12/ch12_ex2.py Chapter_12/ch12_ex3.py\n    # Chapter_12/ch12_ex4.py relies on SQLAlchemy which has no stubs\n    mypy Chapter_13/ch13_ex1.py Chapter_13/ch13_ex2.py Chapter_13/ch13_ex3.py Chapter_13/ch13_ex5.py Chapter_13/ch13_ex6.py\n    # Chapter_13/ch13_ex4.py:37: error: No library stub file for module 'pytest'\n    mypy --ignore-missing-imports Chapter_13/ch13_ex4.py\n    mypy Chapter_14\n    mypy Chapter_15\n    mypy Chapter_16/ch16_ex1.py Chapter_16/ch16_ex3.py Chapter_16/ch16_ex4.py Chapter_16/ch16_ex5.py\n    mypy Chapter_16/ch16_ex6.py Chapter_16/ch16_ex9.py Chapter_16/ch16_ex10.py\n    mypy --ignore-missing-imports Chapter_16/ch16_ex7.py\n    # Chapter_16/ch16_ex2.py:41: error: \"Player\" has no attribute \"audit\"\n    # Chapter_16/ch16_ex2.py:42: error: \"Player\" has no attribute \"verbose\"\n    # Chapter_16/ch16_ex2.py:50: error: \"Table\" has no attribute \"security\"\n    # Chapter_16/ch16_ex8.py:29: error: \"TailHandler\" has no attribute \"flushLevel\"\n    # Chapter_16/ch16_ex8.py:31: error: \"TailHandler\" has no attribute \"buffer\"\n    # Chapter_16/ch16_ex8.py:31: error: \"TailHandler\" has no attribute \"capacity\"\n    # Chapter_16/ch16_ex8.py:34: error: \"TailHandler\" has no attribute \"buffer\"\n\n    mypy --ignore-missing-imports Chapter_17/ch17_ex1.py\n    # Chapter_17/ch17_ex1.py:493: error: No library stub file for module 'pytest'\n    mypy --ignore-missing-imports --follow-imports=skip Chapter_17/ch17_ex2.py\n    # Chapter_17/ch17_ex2.py relies on SQLAlchemy which has no stubs\n    mypy --ignore-missing-imports --follow-imports=skip Chapter_17/test_ch17.py\n    # Chapter_17/test_ch17.py relies on test discovery for ch12_ex4 with SQL Alchemy, also.\n\n    mypy --ignore-missing-imports Chapter_18\n    # Chapter_18/ch18_ex1.py:148: error: No library stub file for module 'pytest'\n    mypy Chapter_19/some_algorithm\n    mypy Chapter_19/ch19_ex1.py\n    mypy --ignore-missing-imports Chapter_19/ch19_ex2.py\n    # Chapter_19/ch19_ex2.py:10: error: No library stub file for module 'pytest'\n    mypy Chapter_20\n\n[testenv:doc]\ndeps =\n    sphinx\nchangedir = Chapter_20/docs\nsetenv   =\n    PYTHONPATH = {env:PYTHONPATH}{:}{toxinidir}/Chapter_20/src\ncommands =\n    sphinx-build -M html . _build"
  }
]