Repository: PacktPublishing/Mastering-Object-Oriented-Python-Second-Edition Branch: master Commit: f6d6517952d5 Files: 142 Total size: 582.5 KB Directory structure: gitextract_if6v5z7l/ ├── .pylintrc ├── Chapter_1/ │ ├── ch01_ex1.py │ ├── ch01_ex2.py │ ├── ch01_ex3.py │ ├── ch01_ex4.py │ ├── ch01_ex5.py │ └── getting_started.rst ├── Chapter_10/ │ ├── __init__.py │ ├── ch10_bonus.py │ ├── ch10_ex1.py │ ├── ch10_ex2.py │ ├── ch10_ex2a.py │ ├── ch10_ex2b.py │ ├── ch10_ex2c.py │ ├── ch10_ex3.py │ ├── ch10_ex4.py │ ├── ch10_ex5.py │ └── ch10_ex6.py ├── Chapter_11/ │ ├── __init__.py │ ├── ch11_ex1.py │ └── ch11_ex2.py ├── Chapter_12/ │ ├── __init__.py │ ├── ch12_ex1.py │ ├── ch12_ex2.py │ ├── ch12_ex3.py │ └── ch12_ex4.py ├── Chapter_13/ │ ├── __init__.py │ ├── cards_openapi.json │ ├── ch13_e1_ex2.py │ ├── ch13_e1_ex3.py │ ├── ch13_e1_ex4.py │ ├── ch13_ex1.py │ ├── ch13_ex2.py │ ├── ch13_ex3.py │ ├── ch13_ex4.py │ ├── ch13_ex5.py │ ├── ch13_ex6.py │ ├── dice_openapi.json │ ├── dominoes_openapi.json │ └── simulation_model.py ├── Chapter_14/ │ ├── __init__.py │ ├── ch14_ex1.py │ ├── ch14_ex2.py │ ├── ch14_ex3.py │ ├── ch14_ex4.py │ ├── ch14_ex5.py │ ├── ch14_ex6.py │ ├── simulation_model.py │ └── someapp.config ├── Chapter_15/ │ ├── __init__.py │ ├── ch15_ex1.py │ └── ch15_ex2.py ├── Chapter_16/ │ ├── __init__.py │ ├── ch16_ex1.py │ ├── ch16_ex10.py │ ├── ch16_ex2.py │ ├── ch16_ex3.py │ ├── ch16_ex4.py │ ├── ch16_ex5.py │ ├── ch16_ex6.py │ ├── ch16_ex7.py │ ├── ch16_ex8.py │ └── ch16_ex9.py ├── Chapter_17/ │ ├── __init__.py │ ├── ch17_data.csv │ ├── ch17_ex1.py │ ├── ch17_ex2.py │ └── test_ch17.py ├── Chapter_18/ │ ├── __init__.py │ ├── ch18_demo.py │ ├── ch18_ex1.py │ ├── ch18_ex2.py │ ├── ch18_ex3.py │ ├── ch18app.yaml │ └── opt/ │ └── ch18app.yaml ├── Chapter_19/ │ ├── __init__.py │ ├── ch19_ex1.py │ ├── ch19_ex2.py │ ├── some_algorithm/ │ │ ├── __init__.py │ │ ├── abstraction.py │ │ ├── long_version.py │ │ └── short_version.py │ └── tests/ │ ├── __init__.py │ └── test_all.py ├── Chapter_2/ │ ├── __init__.py │ ├── ch02_ex1.py │ ├── ch02_ex2.py │ ├── ch02_ex3.py │ ├── ch02_ex4.py │ └── ch02_ex5.py ├── Chapter_20/ │ ├── README.rst │ ├── __init__.py │ ├── combo.py │ ├── combo.py.html │ ├── combo.py.txt │ ├── docs/ │ │ ├── Makefile │ │ ├── conf.py │ │ ├── implementation.rst │ │ ├── index.rst │ │ └── user_story.rst │ ├── src/ │ │ ├── __init__.py │ │ └── ch20_ex1.py │ └── tests/ │ └── test_ch20.py ├── Chapter_3/ │ ├── __init__.py │ ├── ch03_ex1.py │ ├── ch03_ex2.py │ ├── ch03_ex3.py │ ├── ch03_ex4.py │ └── ch03_ex5.py ├── Chapter_4/ │ ├── __init__.py │ ├── ch04_ex1.py │ ├── ch04_ex2.py │ ├── ch04_ex3.py │ ├── ch04_ex4.py │ └── ch04_ex5.py ├── Chapter_5/ │ ├── __init__.py │ ├── ch05_ex1.py │ └── ch05_ex2.py ├── Chapter_6/ │ ├── __init__.py │ ├── ch06_ex1.py │ └── ch06_ex2.py ├── Chapter_7/ │ ├── __init__.py │ ├── ch07_defaults.json │ ├── ch07_ex1.py │ ├── ch07_ex2.py │ ├── ch07_ex3.py │ └── ch07_ex4.py ├── Chapter_8/ │ ├── __init__.py │ └── ch08_ex1.py ├── Chapter_9/ │ ├── __init__.py │ ├── ch09_ex1.py │ └── ch09_ex2.py ├── LICENSE ├── README.md ├── data/ │ ├── ch17_data.csv │ └── ch17_sample.csv ├── environment.yaml ├── requirements.txt ├── show_hierarchies.py ├── stubs/ │ └── sqlite3.pyi ├── test_all.py └── tox.ini ================================================ FILE CONTENTS ================================================ ================================================ FILE: .pylintrc ================================================ [MASTER] # A comma-separated list of package or module names from where C extensions may # be loaded. Extensions are loading into the active Python interpreter and may # run arbitrary code. extension-pkg-whitelist= # Add files or directories to the blacklist. They should be base names, not # paths. ignore=CVS # Add files or directories matching the regex patterns to the blacklist. The # regex matches against base names, not paths. ignore-patterns= # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). #init-hook= # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the # number of processors available to use. jobs=1 # Control the amount of potential inferred values when inferring a single # object. This can help the performance when dealing with large functions or # complex, nested conditions. limit-inference-results=100 # List of plugins (as comma separated values of python modules names) to load, # usually to register additional checkers. load-plugins= # Pickle collected data for later comparisons. persistent=yes # Specify a configuration file. #rcfile= # When enabled, pylint would attempt to guess common misconfiguration and emit # user-friendly hints instead of false-positive error messages. suggestion-mode=yes # Allow loading of arbitrary C extensions. Extensions are imported into the # active Python interpreter and may run arbitrary code. unsafe-load-any-extension=no [MESSAGES CONTROL] # Only show warnings with the listed confidence levels. Leave empty to show # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. confidence= # Disable the message, report, category or checker with the given id(s). You # can either give multiple identifiers separated by comma (,) or put this # option multiple times (only on the command line, not in the configuration # file where it should appear only once). You can also use "--disable=all" to # disable everything first and then reenable specific checks. For example, if # you want to run only the similarities checker, you can use "--disable=all # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use "--disable=all --enable=classes # --disable=W". disable=invalid-name, missing-docstring, print-statement, parameter-unpacking, unpacking-in-except, old-raise-syntax, backtick, long-suffix, old-ne-operator, old-octal-literal, import-star-module-level, non-ascii-bytes-literal, raw-checker-failed, bad-inline-option, locally-disabled, locally-enabled, file-ignored, suppressed-message, useless-suppression, deprecated-pragma, use-symbolic-message-instead, too-few-public-methods, unused-import, apply-builtin, basestring-builtin, buffer-builtin, cmp-builtin, coerce-builtin, execfile-builtin, file-builtin, long-builtin, raw_input-builtin, reduce-builtin, standarderror-builtin, unicode-builtin, xrange-builtin, coerce-method, delslice-method, getslice-method, setslice-method, no-absolute-import, old-division, dict-iter-method, dict-view-method, next-method-called, metaclass-assignment, indexing-exception, raising-string, reload-builtin, oct-method, hex-method, nonzero-method, cmp-method, input-builtin, round-builtin, intern-builtin, unichr-builtin, map-builtin-not-iterating, zip-builtin-not-iterating, range-builtin-not-iterating, filter-builtin-not-iterating, using-cmp-argument, eq-without-hash, div-method, idiv-method, rdiv-method, exception-message-attribute, invalid-str-codec, sys-max-int, bad-python3-import, deprecated-string-function, deprecated-str-translate-call, deprecated-itertools-function, deprecated-types-field, next-method-defined, dict-items-not-iterating, dict-keys-not-iterating, dict-values-not-iterating, deprecated-operator-function, deprecated-urllib-function, xreadlines-attribute, deprecated-sys-function, exception-escape, comprehension-escape, wrong-import-order # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option # multiple time (only on the command line, not in the configuration file where # it should appear only once). See also the "--disable" option for examples. enable=c-extension-no-member [REPORTS] # Python expression which should return a note less than 10 (10 is the highest # note). You have access to the variables errors warning, statement which # respectively contain the number of errors / warnings messages and the total # number of statements analyzed. This is used by the global evaluation report # (RP0004). evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) # Template used to display messages. This is a python new-style format string # used to format the message information. See doc for all details. #msg-template= # Set the output format. Available formats are text, parseable, colorized, json # and msvs (visual studio). You can also give a reporter class, e.g. # mypackage.mymodule.MyReporterClass. output-format=text # Tells whether to display a full report or only the messages. reports=no # Activate the evaluation score. score=yes [REFACTORING] # Maximum number of nested blocks for function / method body max-nested-blocks=5 # Complete name of functions that never returns. When checking for # inconsistent-return-statements if a never returning function is called then # it will be considered as an explicit return statement and no message will be # printed. never-returning-functions=sys.exit [LOGGING] # Logging modules to check that the string format arguments are in logging # function parameter format. logging-modules=logging [SPELLING] # Limits count of emitted suggestions for spelling mistakes. max-spelling-suggestions=4 # Spelling dictionary name. Available dictionaries: none. To make it working # install python-enchant package.. spelling-dict= # List of comma separated words that should not be checked. spelling-ignore-words= # A path to a file that contains private dictionary; one word per line. spelling-private-dict-file= # Tells whether to store unknown words to indicated private dictionary in # --spelling-private-dict-file option instead of raising a message. spelling-store-unknown-words=no [MISCELLANEOUS] # List of note tags to take in consideration, separated by a comma. notes=FIXME, XXX, TODO [TYPECHECK] # List of decorators that produce context managers, such as # contextlib.contextmanager. Add to this list to register other decorators that # produce valid context managers. contextmanager-decorators=contextlib.contextmanager # List of members which are set dynamically and missed by pylint inference # system, and so shouldn't trigger E1101 when accessed. Python regular # expressions are accepted. generated-members= # Tells whether missing members accessed in mixin class should be ignored. A # mixin class is detected if its name ends with "mixin" (case insensitive). ignore-mixin-members=yes # Tells whether to warn about missing members when the owner of the attribute # is inferred to be None. ignore-none=yes # This flag controls whether pylint should warn about no-member and similar # checks whenever an opaque object is returned when inferring. The inference # can return multiple potential results while evaluating a Python object, but # some branches might not be evaluated, which results in partial inference. In # that case, it might be useful to still emit no-member and other checks for # the rest of the inferred objects. ignore-on-opaque-inference=yes # List of class names for which member attributes should not be checked (useful # for classes with dynamically set attributes). This supports the use of # qualified names. ignored-classes=optparse.Values,thread._local,_thread._local # List of module names for which member attributes should not be checked # (useful for modules/projects where namespaces are manipulated during runtime # and thus existing member attributes cannot be deduced by static analysis. It # supports qualified module names, as well as Unix pattern matching. ignored-modules= # Show a hint with possible names when a member name was not found. The aspect # of finding the hint is based on edit distance. missing-member-hint=yes # The minimum edit distance a name should have in order to be considered a # similar match for a missing member name. missing-member-hint-distance=1 # The total number of similar names that should be taken in consideration when # showing a hint for a missing member. missing-member-max-choices=1 [VARIABLES] # List of additional names supposed to be defined in builtins. Remember that # you should avoid to define new builtins when possible. additional-builtins= # Tells whether unused global variables should be treated as a violation. allow-global-unused-variables=yes # List of strings which can identify a callback function by name. A callback # name must start or end with one of those strings. callbacks=cb_, _cb # A regular expression matching the name of dummy variables (i.e. expected to # not be used). dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ # Argument names that match this expression will be ignored. Default to name # with leading underscore. ignored-argument-names=_.*|^ignored_|^unused_ # Tells whether we should check for unused import in __init__ files. init-import=no # List of qualified module names which can have objects that can redefine # builtins. redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io [FORMAT] # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. expected-line-ending-format= # Regexp for a line that is allowed to be longer than the limit. ignore-long-lines=^\s*(# )??$ # Number of spaces of indent required inside a hanging or continued line. indent-after-paren=4 # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 # tab). indent-string=' ' # Maximum number of characters on a single line. max-line-length=100 # Maximum number of lines in a module. max-module-lines=1000 # List of optional constructs for which whitespace checking is disabled. `dict- # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. # `trailing-comma` allows a space between comma and closing bracket: (a, ). # `empty-line` allows space-only lines. no-space-check=trailing-comma, dict-separator # Allow the body of a class to be on the same line as the declaration if body # contains single statement. single-line-class-stmt=no # Allow the body of an if to be on the same line as the test if there is no # else. single-line-if-stmt=no [SIMILARITIES] # Ignore comments when computing similarities. ignore-comments=yes # Ignore docstrings when computing similarities. ignore-docstrings=yes # Ignore imports when computing similarities. ignore-imports=no # Minimum lines number of a similarity. min-similarity-lines=4 [BASIC] # Naming style matching correct argument names. argument-naming-style=snake_case # Regular expression matching correct argument names. Overrides argument- # naming-style. #argument-rgx= # Naming style matching correct attribute names. attr-naming-style=snake_case # Regular expression matching correct attribute names. Overrides attr-naming- # style. #attr-rgx= # Bad variable names which should always be refused, separated by a comma. bad-names=foo, bar, baz, toto, tutu, tata # Naming style matching correct class attribute names. class-attribute-naming-style=any # Regular expression matching correct class attribute names. Overrides class- # attribute-naming-style. #class-attribute-rgx= # Naming style matching correct class names. class-naming-style=PascalCase # Regular expression matching correct class names. Overrides class-naming- # style. #class-rgx= # Naming style matching correct constant names. const-naming-style=UPPER_CASE # Regular expression matching correct constant names. Overrides const-naming- # style. #const-rgx= # Minimum line length for functions/classes that require docstrings, shorter # ones are exempt. docstring-min-length=-1 # Naming style matching correct function names. function-naming-style=snake_case # Regular expression matching correct function names. Overrides function- # naming-style. #function-rgx= # Good variable names which should always be accepted, separated by a comma. good-names=i, j, k, ex, Run, _ # Include a hint for the correct naming format with invalid-name. include-naming-hint=no # Naming style matching correct inline iteration names. inlinevar-naming-style=any # Regular expression matching correct inline iteration names. Overrides # inlinevar-naming-style. #inlinevar-rgx= # Naming style matching correct method names. method-naming-style=snake_case # Regular expression matching correct method names. Overrides method-naming- # style. #method-rgx= # Naming style matching correct module names. module-naming-style=snake_case # Regular expression matching correct module names. Overrides module-naming- # style. #module-rgx= # Colon-delimited sets of names that determine each other's naming style when # the name regexes allow several styles. name-group= # Regular expression which should only match function or class names that do # not require a docstring. no-docstring-rgx=^_ # List of decorators that produce properties, such as abc.abstractproperty. Add # to this list to register other decorators that produce valid properties. # These decorators are taken in consideration only for invalid-name. property-classes=abc.abstractproperty # Naming style matching correct variable names. variable-naming-style=snake_case # Regular expression matching correct variable names. Overrides variable- # naming-style. #variable-rgx= [IMPORTS] # Allow wildcard imports from modules that define __all__. allow-wildcard-with-all=no # Analyse import fallback blocks. This can be used to support both Python 2 and # 3 compatible code, which means that the block might have code that exists # only in one or another interpreter, leading to false positives when analysed. analyse-fallback-blocks=no # Deprecated modules which should not be used, separated by a comma. deprecated-modules=optparse,tkinter.tix # Create a graph of external dependencies in the given file (report RP0402 must # not be disabled). ext-import-graph= # Create a graph of every (i.e. internal and external) dependencies in the # given file (report RP0402 must not be disabled). import-graph= # Create a graph of internal dependencies in the given file (report RP0402 must # not be disabled). int-import-graph= # Force import order to recognize a module as part of the standard # compatibility libraries. known-standard-library= # Force import order to recognize a module as part of a third party library. known-third-party=enchant [CLASSES] # List of method names used to declare (i.e. assign) instance attributes. defining-attr-methods=__init__, __new__, setUp # List of member names, which should be excluded from the protected access # warning. exclude-protected=_asdict, _fields, _replace, _source, _make # List of valid names for the first argument in a class method. valid-classmethod-first-arg=cls # List of valid names for the first argument in a metaclass class method. valid-metaclass-classmethod-first-arg=cls [DESIGN] # Maximum number of arguments for function / method. max-args=5 # Maximum number of attributes for a class (see R0902). max-attributes=7 # Maximum number of boolean expressions in an if statement. max-bool-expr=5 # Maximum number of branch for function / method body. max-branches=12 # Maximum number of locals for function / method body. max-locals=15 # Maximum number of parents for a class (see R0901). max-parents=7 # Maximum number of public methods for a class (see R0904). max-public-methods=20 # Maximum number of return / yield for function / method body. max-returns=6 # Maximum number of statements in function / method body. max-statements=50 # Minimum number of public methods for a class (see R0903). min-public-methods=2 [EXCEPTIONS] # Exceptions that will emit a warning when being caught. Defaults to # "Exception". overgeneral-exceptions=Exception ================================================ FILE: Chapter_1/ch01_ex1.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 1. Example 1. """ # Show the basics of timeit. import timeit method_time = timeit.timeit( "obj.method()", """ class SomeClass: def method(self): pass obj= SomeClass() """, ) function_time = timeit.timeit( "f()", """ def f(): pass """, ) if __name__ == "__main__": print(f"Method {method_time:.4f}") print(f"Function {function_time:.4f}") ================================================ FILE: Chapter_1/ch01_ex2.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 1. Example 2. """ # pylint: disable=invalid-name test_list = """ >>> f = [1, 1, 2, 3] >>> f += [f[-1] + f[-2]] >>> f [1, 1, 2, 3, 5] >>> f.__getitem__(-1) 5 >>> f.__getitem__(-1).__add__(f.__getitem__(-2)) 8 >>> f.__iadd__([8]) [1, 1, 2, 3, 5, 8] >>> f [1, 1, 2, 3, 5, 8] """ __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_1/ch01_ex3.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 1. Example 3. """ def F(n: int) -> int: if n in (0, 1): return 1 else: return F(n-1) + F(n-2) test_F_8 = """ >>> F(8) 34 """ def demo(): print("Good Use", F(8)) print("Bad Use", F(355/113)) __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_1/ch01_ex4.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 1. Example 4. """ # Simple function with docstring. def factorial(n: int) -> int: """Compute n! recursively. :param n: an integer >= 0 :returns: n! Because of Python's stack limitation, this won't compute a value larger than about 1000!. >>> factorial(5) 120 """ if n == 0: return 1 return n * factorial(n - 1) if __name__ == "__main__": import doctest doctest.testmod() ================================================ FILE: Chapter_1/ch01_ex5.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 1. Example 5. """ # Definition of some classes with doctest-based unit tests. from types import SimpleNamespace class EmptyClass: pass EmptyClass2 = SimpleNamespace EmptyClass3 = type('EmptyClass3', (object,), {}) __test__ = { 'EmptyClass': ''' >>> ec = EmptyClass() >>> ec.new_attribute = 42 >>> ec.new_attribute 42 >>> ec.undefined # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): AttributeError: 'EmptyClass' object has no attribute 'undefined' ''', 'EmptyClass2': ''' >>> ec = EmptyClass2() >>> ec.new_attribute = 42 >>> ec.new_attribute 42 >>> ec.undefined # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): AttributeError: 'EmptyClass' object has no attribute 'undefined' ''', 'EmptyClass3': ''' >>> ec = EmptyClass3() >>> ec.new_attribute = 42 >>> ec.new_attribute 42 >>> ec.undefined # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): AttributeError: 'EmptyClass' object has no attribute 'undefined' ''', } if __name__ == "__main__": import doctest doctest.testmod() ================================================ FILE: Chapter_1/getting_started.rst ================================================ In order to run the examples mentioned in this book you require the following software: - Python version 3.7 or higher with the standard suite of libraries. We'll look at some additional packages. These include PyYaml, SQLAlchemy, and Jinja2. We'll use pytest for doing unit testing. - http://pyyaml.org - 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. - http://jinja.pocoo.org/ We'll also look at some tools: - https://docs.pytest.org/en/latest/index.html - http://sphinx-doc.org - http://mypy-lang.org - https://www.pylint.org - https://github.com/ambv/black There two alternative approaches to these installations. - From Python.org 1. Install Python 3.7 from http://www.python.org. This will change your OS-level ``PATH`` settings. This usually means you need to start a new terminal session to make your new Python tools available. 2. Use your new pip3 to install the other packages: ``python3 -m pip install pyyaml sqlalchemy jinja2 pytest sphinx mypy pylint black`` If you install from Python.org, you'll have a single, default Python environment. This isn't optimal. When you need to upgrade or experiment, you'll often want to create additional environments. There are several tools for this. The conda tool seems to be the most versatile. - Use Anaconda.com's miniconda to get started. 1. Download and install the appropriate miniconda for your platform. https://conda.io/miniconda.html 2. Use miniconda to build a Python environment including the required packages. ``conda create --name mastering python=3.7 pyyaml sqlalchemy jinja2 pytest sphinx mypy pylint`` 3. Activate your new environment ``conda activate mastering`` You'll know it's active because your terminal prompt will have ``(mastering)`` as a prefix. 4. Add the ``black`` tool using a separate installation after activating the environment. ``python3 -m pip install --upgrade pip`` ``python3 -m pip install black`` ================================================ FILE: Chapter_10/__init__.py ================================================ ================================================ FILE: Chapter_10/ch10_bonus.py ================================================ """ Enumerate all Blackjack outcomes with player mirroring the dealer choice. Note that player going bust first is a loss, irrespective of what the dealer holds. The question, then, is given two cards, what are the odds of going bust using dealer rules of hit 17, stand on 18. Then bust for player is rule 1. Bust for dealer is rule 2. Otherwise it's a 50/50 proposition. """ from typing import Optional, Tuple, Dict, Counter import random from enum import Enum import collections class Suit(Enum): Clubs = "♣" Diamonds = "♦" Hearts = "♥" Spades = "♠" class Card: def __init__(self, rank: str, suit: Suit, hard: Optional[int]=None, soft: Optional[int]=None) -> None: self.rank = rank self.suit = suit self.hard = hard or int(rank) self.soft = soft or int(rank) def __str__(self) -> str: return f"{self.rank!s}{self.suit.value!s}" class AceCard(Card): def __init__(self, rank: str, suit: Suit) -> None: super().__init__(rank, suit, 1, 11) class FaceCard(Card): def __init__(self, rank: str, suit: Suit) -> None: super().__init__(rank, suit, 10, 10) def card(rank: int, suit: Suit) -> Card: if rank == 1: return AceCard("A", suit) elif rank in (11, 12, 13): rank_str = {11: "J", 12: "Q", 13: "K"}[rank] return FaceCard(rank_str, suit) else: return Card(str(rank), suit) class Deck(list): def __init__(self) -> None: super().__init__(card(r, s) for r in range(1, 14) for s in Suit) random.shuffle(self) class Hand(list): @property def hard(self) -> int: return sum(c.hard for c in self) @property def soft(self) -> int: return sum(c.soft for c in self) def __repr__(self) -> str: cards = [str(c) for c in self] return f"Hand({cards!r})" def deal_rules(deck: Deck) -> Tuple[Hand, Optional[int]]: hand = Hand([deck.pop(), deck.pop()]) while hand.hard < 21: if hand.soft == 21: return hand, 21 elif hand.hard == 21: return hand, 21 elif hand.soft < 18: hand.append(deck.pop()) elif hand.soft > 21 and hand.hard < 18: hand.append(deck.pop()) else: return hand, min(hand.hard, hand.soft) return hand, None def simulation() -> None: raw_outcomes: Counter[Tuple[Optional[int], Optional[int]]] = collections.Counter() game_payout: Counter[str] = collections.Counter() for i in range(20_000): deck = Deck() player_hand, player_result = deal_rules(deck) dealer_hand, dealer_result = deal_rules(deck) raw_outcomes[(player_result, dealer_result)] += 1 if player_result is None: game_payout['loss'] += 1 elif player_result is 21: game_payout['21'] += 1 elif dealer_result is None: game_payout['win'] += 1 elif player_result > dealer_result: game_payout['win'] += 1 elif player_result == dealer_result: game_payout['push'] += 1 else: game_payout['loss'] += 1 running = 0.0 for outcome, count in game_payout.most_common(): print(f"{running:.3f} <= r < {running+count/20_000:.3f}: {outcome}") running += count/20_000 if __name__ == "__main__": import doctest doctest.testmod() simulation() ================================================ FILE: Chapter_10/ch10_ex1.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 10. Example 1. JSON """ # Persistence Classes # ======================================== # A detail class for micro-blog posts from typing import List, Optional, Dict, Any, DefaultDict, Union, Type from pathlib import Path import datetime from dataclasses import dataclass # Technically, this is the type supported by JSON serailization. # JSON = Union[Dict[str, 'JSON'], List['JSON'], int, str, float, bool, Type[None]] JSON = Union[Dict[str, Any], List[Any], int, str, float, bool, Type[None]] @dataclass class Post: date: datetime.datetime title: str rst_text: str tags: List[str] def as_dict(self) -> Dict[str, Any]: return dict( date=str(self.date), title=self.title, underline="-" * len(self.title), rst_text=self.rst_text, tag_text=" ".join(self.tags), ) # Here's a collection of these posts. This is an extension # of list which doesn't work well with JSON. from collections import defaultdict class Blog_x(list): def __init__(self, title: str, posts: Optional[List[Post]]=None) -> None: self.title = title super().__init__(posts if posts is not None else []) def by_tag(self) -> DefaultDict[str, List[Dict[str, Any]]]: tag_index: DefaultDict[str, List[Dict[str, Any]]] = defaultdict(list) for post in self: for tag in post.tags: tag_index[tag].append(post.as_dict()) return tag_index def as_dict(self) -> Dict[str, Any]: return dict( title=self.title, entries=[p.as_dict() for p in self] ) # An example blog travel_x = Blog_x("Travel") travel_x.append( Post( date=datetime.datetime(2013, 11, 14, 17, 25), title="Hard Aground", rst_text="""Some embarrassing revelation. Including ☹ and ⚓""", tags=["#RedRanger", "#Whitby42", "#ICW"], ) ) travel_x.append( Post( date=datetime.datetime(2013, 11, 18, 15, 30), title="Anchor Follies", rst_text="""Some witty epigram. Including < & > characters.""", tags=["#RedRanger", "#Whitby42", "#Mistakes"], ) ) # JSON # ================================ # Example 1: Simple # #################### # Simple JSON dump import json test_json_1 = """ >>> print(json.dumps(travel_x.as_dict(), indent=4)) { "title": "Travel", "entries": [ { "date": "2013-11-14 17:25:00", "title": "Hard Aground", "underline": "------------", "rst_text": "Some embarrassing revelation. Including \u2639 and \u2693", "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" } ] } """ # Example 2. JSON: Flawed Container Design # ######################################## # Flawed Encoder based on flawed design of the class. def blogx_encode(object: Any) -> Dict[str, Any]: if isinstance(object, datetime.datetime): return dict( __class__="datetime.datetime", __args__=[], __kw__=dict( year=object.year, month=object.month, day=object.day, hour=object.hour, minute=object.minute, second=object.second, ), ) elif isinstance(object, Post): return dict( __class__="Post", __args__=[], __kw__=dict( date=object.date, title=object.title, rst_text=object.rst_text, tags=object.tags, ), ) elif isinstance(object, Blog_x): # Will get ignored... return dict( __class__="Blog_x", __args__=[], __kw__=dict(title=object.title, entries=tuple(object)), ) else: return object def blogx_decode(some_dict: Dict[str, Any]) -> Dict[str, Any]: if set(some_dict.keys()) == set(["__class__", "__args__", "__kw__"]): class_ = eval(some_dict["__class__"]) return class_(*some_dict["__args__"], **some_dict["__kw__"]) else: return some_dict test_json_2 = """ >>> text = json.dumps(travel_x, indent=4, default=blogx_encode) >>> print(text) [ { "__class__": "Post", "__args__": [], "__kw__": { "date": { "__class__": "datetime.datetime", "__args__": [], "__kw__": { "year": 2013, "month": 11, "day": 14, "hour": 17, "minute": 25, "second": 0 } }, "title": "Hard Aground", "rst_text": "Some embarrassing revelation. Including \u2639 and \u2693", "tags": [ "#RedRanger", "#Whitby42", "#ICW" ] } }, { "__class__": "Post", "__args__": [], "__kw__": { "date": { "__class__": "datetime.datetime", "__args__": [], "__kw__": { "year": 2013, "month": 11, "day": 18, "hour": 15, "minute": 30, "second": 0 } }, "title": "Anchor Follies", "rst_text": "Some witty epigram. Including < & > characters.", "tags": [ "#RedRanger", "#Whitby42", "#Mistakes" ] } } ] The Blog structure overall? Vanished. It's only a list >>> from pprint import pprint >>> copy = json.loads(text, object_hook=blogx_decode) >>> pprint(copy) [Post(date=datetime.datetime(2013, 11, 14, 17, 25), title='Hard Aground', rst_text='Some embarrassing revelation. Including ☹ and ⚓', tags=['#RedRanger', '#Whitby42', '#ICW']), Post(date=datetime.datetime(2013, 11, 18, 15, 30), title='Anchor Follies', rst_text='Some witty epigram. Including < & > characters.', tags=['#RedRanger', '#Whitby42', '#Mistakes'])] """ # Example 3 JSON: Better Design # ############################### # Consider this wrap-based design instead of an extension-based version # Here's another collection of these posts. # This wraps a list which works much better with JSON than extending a list. import datetime from collections import defaultdict class Blog: def __init__(self, title: str, posts: Optional[List[Post]]=None) -> None: self.title = title self.entries = posts if posts is not None else [] @property def underline(self) -> str: return '='*len(self.title) def append(self, post: Post) -> None: self.entries.append(post) def by_tag(self) -> Dict[str, List[Dict[str, Any]]]: tag_index: Dict[str, List[Dict[str, Any]]] = defaultdict(list) for post in self.entries: for tag in post.tags: tag_index[tag].append(post.as_dict()) return tag_index def as_dict(self) -> Dict[str, Any]: return dict( title=self.title, underline=self.underline, entries=[p.as_dict() for p in self.entries], ) # An example blog travel = Blog("Travel") travel.append( Post( date=datetime.datetime(2013, 11, 14, 17, 25), title="Hard Aground", rst_text="""Some embarrassing revelation. Including ☹ and ⚓︎""", tags=["#RedRanger", "#Whitby42", "#ICW"], ) ) travel.append( Post( date=datetime.datetime(2013, 11, 18, 15, 30), title="Anchor Follies", rst_text="""Some witty epigram. Including < & > characters.""", tags=["#RedRanger", "#Whitby42", "#Mistakes"], ) ) def blog_encode(object: Any) -> Dict[str, Any]: if isinstance(object, datetime.datetime): return dict( __class__="datetime.datetime", __args__=[], __kw__=dict( year=object.year, month=object.month, day=object.day, hour=object.hour, minute=object.minute, second=object.second, ), ) elif isinstance(object, Post): return dict( __class__="Post", __args__=[], __kw__=dict( date=object.date, title=object.title, rst_text=object.rst_text, tags=object.tags, ), ) elif isinstance(object, Blog): return dict( __class__="Blog", __args__=[object.title, object.entries], __kw__={} ) else: return object def blog_decode(some_dict: Dict[str, Any]) -> Dict[str, Any]: if set(some_dict.keys()) == {"__class__", "__args__", "__kw__"}: class_ = eval(some_dict["__class__"]) return class_(*some_dict["__args__"], **some_dict["__kw__"]) else: return some_dict test_json_3 = """ >>> text = json.dumps(travel, indent=4, default=blog_encode) >>> print(text) { "__class__": "Blog", "__args__": [ "Travel", [ { "__class__": "Post", "__args__": [], "__kw__": { "date": { "__class__": "datetime.datetime", "__args__": [], "__kw__": { "year": 2013, "month": 11, "day": 14, "hour": 17, "minute": 25, "second": 0 } }, "title": "Hard Aground", "rst_text": "Some embarrassing revelation. Including \u2639 and \u2693\ufe0e", "tags": [ "#RedRanger", "#Whitby42", "#ICW" ] } }, { "__class__": "Post", "__args__": [], "__kw__": { "date": { "__class__": "datetime.datetime", "__args__": [], "__kw__": { "year": 2013, "month": 11, "day": 18, "hour": 15, "minute": 30, "second": 0 } }, "title": "Anchor Follies", "rst_text": "Some witty epigram. Including < & > characters.", "tags": [ "#RedRanger", "#Whitby42", "#Mistakes" ] } } ] ], "__kw__": {} } >>> from pprint import pprint >>> copy = json.loads(text, object_hook=blog_decode) >>> print(copy.title) Travel >>> pprint(copy.entries) [Post(date=datetime.datetime(2013, 11, 14, 17, 25), title='Hard Aground', rst_text='Some embarrassing revelation. Including ☹ and ⚓︎', tags=['#RedRanger', '#Whitby42', '#ICW']), Post(date=datetime.datetime(2013, 11, 18, 15, 30), title='Anchor Follies', rst_text='Some witty epigram. Including < & > characters.', tags=['#RedRanger', '#Whitby42', '#Mistakes'])] """ # Sidebar: Demo of rendering 1 # ############################### # Here's a template for an individual post import string # Here's a way to render the entire blog in RST def rst_render(blog: Blog) -> None: post = string.Template( """ $title $underline $rst_text :date: $date :tags: $tag_text """ ) # with contextlib.redirect_stdout("some_file"): print(f"{blog.title}\n{blog.underline}\n") for p in blog.entries: print(post.substitute(**p.as_dict())) tag_index = blog.by_tag() print("Tag Index") print("=========") print() for tag in tag_index: print(f"* {tag}") print() for post_dict in tag_index[tag]: print(f" - `{post_dict['title']}`_") print() test_string_template_render = """ >>> rst_render(travel) Travel ====== Hard Aground ------------ Some embarrassing revelation. Including ☹ and ⚓︎ :date: 2013-11-14 17:25:00 :tags: #RedRanger #Whitby42 #ICW Anchor Follies -------------- Some witty epigram. Including < & > characters. :date: 2013-11-18 15:30:00 :tags: #RedRanger #Whitby42 #Mistakes Tag Index ========= * #RedRanger - `Hard Aground`_ - `Anchor Follies`_ * #Whitby42 - `Hard Aground`_ - `Anchor Follies`_ * #ICW - `Hard Aground`_ * #Mistakes - `Anchor Follies`_ """ # Sidebar: Demo of rendering 2 (using Jinja2) # ############################################ from jinja2 import Template blog_template = Template( """{{title}} {{underline}} {% for e in entries %} {{e.title}} {{e.underline}} {{e.rst_text}} :date: {{e.date}} :tags: {{e.tag_text}} {% endfor %} Tag Index ========= {% for t in tags %} * {{t}} {% for post in tags[t] %} - `{{post.title}}`_ {%- endfor %} {% endfor %} """ ) test_jinja_temple_render = """ >>> print(blog_template.render(tags=travel.by_tag(), **travel.as_dict())) Travel ====== Hard Aground ------------ Some embarrassing revelation. Including ☹ and ⚓︎ :date: 2013-11-14 17:25:00 :tags: #RedRanger #Whitby42 #ICW Anchor Follies -------------- Some witty epigram. Including < & > characters. :date: 2013-11-18 15:30:00 :tags: #RedRanger #Whitby42 #Mistakes Tag Index ========= * #RedRanger - `Hard Aground`_ - `Anchor Follies`_ * #Whitby42 - `Hard Aground`_ - `Anchor Follies`_ * #ICW - `Hard Aground`_ * #Mistakes - `Anchor Follies`_ """ # Example 4. JSON: Refactoring Encoding # ###################################### # Changes to the class definitions to add a ``_json`` method. class Post_J(Post): """Not really essential to inherit from Post, it's simply a dataclass.""" @property def _json(self) -> Dict[str, Any]: return dict( __class__=self.__class__.__name__, __kw__=dict( date=self.date, title=self.title, rst_text=self.rst_text, tags=self.tags ), __args__=[], ) class Blog_J(Blog): """Note. No explicit reference to Blog_J for entries.""" @property def _json(self) -> Dict[str, Any]: return dict( __class__=self.__class__.__name__, __kw__={}, __args__=[self.title, self.entries], ) def blog_j_encode(object: Union[Blog_J, Post_J, Any]) -> Dict[str, Any]: if isinstance(object, datetime.datetime): return dict( __class__="datetime.datetime", __args__=[], __kw__=dict( year=object.year, month=object.month, day=object.day, hour=object.hour, minute=object.minute, second=object.second, ), ) else: try: encoding = object._json except AttributeError: encoding = json.JSONEncoder().default(object) return encoding travel3 = Blog_J("Travel") travel3.append( Post_J( date=datetime.datetime(2013, 11, 14, 17, 25), title="Hard Aground", rst_text="""Some embarrassing revelation. Including ☹ and ⚓""", tags=["#RedRanger", "#Whitby42", "#ICW"], ) ) travel3.append( Post_J( date=datetime.datetime(2013, 11, 18, 15, 30), title="Anchor Follies", rst_text="""Some witty epigram.""", tags=["#RedRanger", "#Whitby42", "#Mistakes"], ) ) test_json_4 = """ >>> text = json.dumps(travel3, indent=4, default=blog_j_encode) >>> print(text) { "__class__": "Blog_J", "__kw__": {}, "__args__": [ "Travel", [ { "__class__": "Post_J", "__kw__": { "date": { "__class__": "datetime.datetime", "__args__": [], "__kw__": { "year": 2013, "month": 11, "day": 14, "hour": 17, "minute": 25, "second": 0 } }, "title": "Hard Aground", "rst_text": "Some embarrassing revelation. Including \u2639 and \u2693", "tags": [ "#RedRanger", "#Whitby42", "#ICW" ] }, "__args__": [] }, { "__class__": "Post_J", "__kw__": { "date": { "__class__": "datetime.datetime", "__args__": [], "__kw__": { "year": 2013, "month": 11, "day": 18, "hour": 15, "minute": 30, "second": 0 } }, "title": "Anchor Follies", "rst_text": "Some witty epigram.", "tags": [ "#RedRanger", "#Whitby42", "#Mistakes" ] }, "__args__": [] } ] ] } """ # Example 5: JSON: Super-Flexible Date Encoding # ############################################# # Right at the edge of the envelope for dates. This may be too much flexibility. # There's an ISO standard for dates, and using it is simpler. # For other unique data objects, however, this kind of pattern may be helpful # for providing a way to parse complex strings. # Changes to the class definitions def blog_j2_encode(object: Union[Blog_J, Post_J, Any]) -> Dict[str, Any]: if isinstance(object, datetime.datetime): return dict( __class__="datetime.datetime.strptime", __args__=[object.strftime("%Y-%m-%dT%H:%M:%S"), "%Y-%m-%dT%H:%M:%S"], __kw__={}, ) else: try: encoding = object._json except AttributeError: encoding = json.JSONEncoder().default(object) return encoding test_json_5 = """ >>> text = json.dumps(travel3, indent=4, default=blog_j2_encode) >>> print(text) { "__class__": "Blog_J", "__kw__": {}, "__args__": [ "Travel", [ { "__class__": "Post_J", "__kw__": { "date": { "__class__": "datetime.datetime.strptime", "__args__": [ "2013-11-14T17:25:00", "%Y-%m-%dT%H:%M:%S" ], "__kw__": {} }, "title": "Hard Aground", "rst_text": "Some embarrassing revelation. Including \u2639 and \u2693", "tags": [ "#RedRanger", "#Whitby42", "#ICW" ] }, "__args__": [] }, { "__class__": "Post_J", "__kw__": { "date": { "__class__": "datetime.datetime.strptime", "__args__": [ "2013-11-18T15:30:00", "%Y-%m-%dT%H:%M:%S" ], "__kw__": {} }, "title": "Anchor Follies", "rst_text": "Some witty epigram.", "tags": [ "#RedRanger", "#Whitby42", "#Mistakes" ] }, "__args__": [] } ] ] } >>> from pprint import pprint >>> copy = json.loads(text, object_hook=blog_decode) >>> print(copy.title) Travel >>> pprint(copy.entries) [Post_J(date=datetime.datetime(2013, 11, 14, 17, 25), title='Hard Aground', rst_text='Some embarrassing revelation. Including ☹ and ⚓', tags=['#RedRanger', '#Whitby42', '#ICW']), Post_J(date=datetime.datetime(2013, 11, 18, 15, 30), title='Anchor Follies', rst_text='Some witty epigram.', tags=['#RedRanger', '#Whitby42', '#Mistakes'])] """ with (Path.cwd()/"data"/"ch10.json").open("w", encoding="UTF-8") as target: json.dump(travel3, target, separators=(",", ":"), default=blog_j2_encode) __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_10/ch10_ex2.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 10. Example 2. YAML. Base Definitions """ # Persistence Classes # ======================================== from typing import List, Optional, Dict, Any # Example 2: Cards # ################### from enum import Enum class Suit(str, Enum): Clubs = "♣" Diamonds = "♦" Hearts = "♥" Spades = "♠" class Card: def __init__(self, rank: str, suit: Suit, hard: Optional[int]=None, soft: Optional[int]=None) -> None: self.rank = rank self.suit = suit self.hard = hard or int(rank) self.soft = soft or int(rank) def __str__(self) -> str: return f"{self.rank!s}{self.suit.value!s}" class AceCard(Card): def __init__(self, rank: str, suit: Suit) -> None: super().__init__(rank, suit, 1, 11) class FaceCard(Card): def __init__(self, rank: str, suit: Suit) -> None: super().__init__(rank, suit, 10, 10) __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_10/ch10_ex2a.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 10. Example 2. YAML (part a) """ # Persistence Classes # ======================================== # A detail class for micro-blog posts import datetime from typing import List, Optional, Dict, Any from dataclasses import dataclass from pathlib import Path from Chapter_10.ch10_ex1 import Post, Blog, travel, rst_render from Chapter_10.ch10_ex2 import Suit, Card, FaceCard, AceCard # YAML # =================== import yaml # Example 1: That's it. # ###################### # Start with original definitions test_yaml = """ >>> text = yaml.dump(travel) >>> print(text) !!python/object:Chapter_10.ch10_ex1.Blog entries: - !!python/object:Chapter_10.ch10_ex1.Post date: 2013-11-14 17:25:00 rst_text: "Some embarrassing revelation. Including \\u2639 and \\u2693\\uFE0E" tags: - '#RedRanger' - '#Whitby42' - '#ICW' title: Hard Aground - !!python/object:Chapter_10.ch10_ex1.Post date: 2013-11-18 15:30:00 rst_text: Some witty epigram. Including < & > characters. tags: - '#RedRanger' - '#Whitby42' - '#Mistakes' title: Anchor Follies title: Travel >>> copy = yaml.load(text) >>> print(type(copy), copy.title) Travel >>> for p in copy.entries: ... print(p.date.year, p.date.month, p.date.day, p.title, p.tags) 2013 11 14 Hard Aground ['#RedRanger', '#Whitby42', '#ICW'] 2013 11 18 Anchor Follies ['#RedRanger', '#Whitby42', '#Mistakes'] >>> text2 = yaml.dump(travel, allow_unicode=True) >>> print(text2) !!python/object:Chapter_10.ch10_ex1.Blog entries: - !!python/object:Chapter_10.ch10_ex1.Post date: 2013-11-14 17:25:00 rst_text: Some embarrassing revelation. Including ☹ and ⚓︎ tags: - '#RedRanger' - '#Whitby42' - '#ICW' title: Hard Aground - !!python/object:Chapter_10.ch10_ex1.Post date: 2013-11-18 15:30:00 rst_text: Some witty epigram. Including < & > characters. tags: - '#RedRanger' - '#Whitby42' - '#Mistakes' title: Anchor Follies title: Travel """ with (Path.cwd()/"data"/"ch10.yaml").open("w", encoding="UTF-8") as target: yaml.dump(travel, target) # Example 2: Cards # ################### deck = [AceCard("A", Suit.Clubs), Card("2", Suit.Hearts), FaceCard("K", Suit.Diamonds)] test_yaml_dump = """ >>> text = yaml.dump(deck, allow_unicode=True) >>> print(text) - !!python/object:Chapter_10.ch10_ex2.AceCard hard: 1 rank: A soft: 11 suit: !!python/object/apply:Chapter_10.ch10_ex2.Suit - ♣ - !!python/object:Chapter_10.ch10_ex2.Card hard: 2 rank: '2' soft: 2 suit: !!python/object/apply:Chapter_10.ch10_ex2.Suit - ♥ - !!python/object:Chapter_10.ch10_ex2.FaceCard hard: 10 rank: K soft: 10 suit: !!python/object/apply:Chapter_10.ch10_ex2.Suit - ♦ """ __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_10/ch10_ex2b.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 10. Example 2. YAML (part b) """ # Persistence Classes # ======================================== # A detail class for micro-blog posts import datetime from typing import List, Optional, Dict, Any from dataclasses import dataclass from pathlib import Path from Chapter_10.ch10_ex2 import Suit, Card, AceCard, FaceCard # YAML -- 2b cards with custom representations # ============================================= deck = [AceCard("A", Suit.Clubs), Card("2", Suit.Hearts), FaceCard("K", Suit.Diamonds)] import yaml def card_representer(dumper: Any, card: Card) -> str: return dumper.represent_scalar( "!Card", f"{card.rank!s}{card.suit.value!s}") def acecard_representer(dumper: Any, card: Card) -> str: return dumper.represent_scalar( "!AceCard", f"{card.rank!s}{card.suit.value!s}") def facecard_representer(dumper: Any, card: Card) -> str: return dumper.represent_scalar( "!FaceCard", f"{card.rank!s}{card.suit.value!s}") def card_constructor(loader: Any, node: Any) -> Card: value = loader.construct_scalar(node) rank, suit = value[:-1], value[-1] return Card(rank, Suit(suit)) def acecard_constructor(loader: Any, node: Any) -> Card: value = loader.construct_scalar(node) rank, suit = value[:-1], value[-1] return AceCard(rank, Suit(suit)) def facecard_constructor(loader: Any, node: Any) -> Card: value = loader.construct_scalar(node) rank, suit = value[:-1], value[-1] return FaceCard(rank, Suit(suit)) # Changes to the yaml module will apply throughout the application. # And this test run, also. # We can also add this yaml.add_representer(Card, card_representer) yaml.add_representer(AceCard, acecard_representer) yaml.add_representer(FaceCard, facecard_representer) yaml.add_constructor("!Card", card_constructor) yaml.add_constructor("!AceCard", acecard_constructor) yaml.add_constructor("!FaceCard", facecard_constructor) test_yaml_dump_load = """ >>> print(*map(str, deck)) A♣ 2♥ K♦ >>> text = yaml.dump(deck, allow_unicode=True) >>> print(text) - !AceCard 'A♣' - !Card '2♥' - !FaceCard 'K♦' >>> copy = yaml.load(text, Loader=yaml.Loader) >>> print(*map(str, copy)) A♣ 2♥ K♦ """ __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_10/ch10_ex2c.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 10. Example 2. YAML (part c) """ # Persistence Classes # ======================================== # A detail class for micro-blog posts import datetime from typing import List, Optional, Dict, Any from dataclasses import dataclass from pathlib import Path from Chapter_10.ch10_ex2 import Suit, Card, AceCard, FaceCard from Chapter_10.ch10_ex2b import facecard_representer, acecard_representer, card_representer # YAML -- 2c cards with safe custom representations # ================================================== import yaml class Card2(yaml.YAMLObject): yaml_tag = "!Card2" yaml_loader = yaml.SafeLoader def __init__(self, rank, suit, hard=None, soft=None) -> None: self.rank = rank self.suit = suit self.hard = hard or int(rank) self.soft = soft or int(rank) def __str__(self) -> str: return "{0.rank!s}{0.suit!s}".format(self) class AceCard2(Card2): yaml_tag = "!AceCard2" def __init__(self, rank, suit) -> None: super().__init__(rank, suit, 1, 11) class FaceCard2(Card2): yaml_tag = "!FaceCard2" def __init__(self, rank, suit) -> None: super().__init__(rank, suit, 10, 10) deck2 = [AceCard2("A", "♣"), Card2("2", "♥"), FaceCard2("K", "♦")] test_yaml_dump_safe_load = """ # Changes to the yaml module will apply throughout the application. >>> yaml.add_representer(Card, card_representer) >>> yaml.add_representer(AceCard, acecard_representer) >>> yaml.add_representer(FaceCard, facecard_representer) >>> text2 = yaml.dump(deck2) >>> print(text2) - !AceCard2 hard: 1 rank: A soft: 11 suit: "\\u2663" - !Card2 hard: 2 rank: '2' soft: 2 suit: "\\u2665" - !FaceCard2 hard: 10 rank: K soft: 10 suit: "\\u2666" >>> copy = yaml.safe_load(text2) >>> print([str(c) for c in copy]) ['A♣', '2♥', 'K♦'] """ __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_10/ch10_ex3.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 10. Example 3. Pickle """ # Persistence Classes # ======================================== import datetime from typing import List, Optional, Dict, Any from dataclasses import dataclass from pathlib import Path from Chapter_10.ch10_ex1 import Post, Blog, travel, rst_render from Chapter_10.ch10_ex2 import FaceCard, AceCard, Card, Suit # Pickle # =================== # Example 1: Working # #################### # Use pickle to persist our microblog import pickle from pathlib import Path test_pickle = """ >>> with (Path.cwd()/"data"/"ch10_travel_blog.p").open("wb") as target: ... pickle.dump(travel, target) >>> with(Path.cwd()/"data"/"ch10_travel_blog.p").open("rb") as source: ... copy = pickle.load(source) >>> print(copy.title) Travel >>> for post in copy.entries: ... print(post) Post(date=datetime.datetime(2013, 11, 14, 17, 25), title='Hard Aground', rst_text='Some embarrassing revelation. Including ☹ and ⚓︎', tags=['#RedRanger', '#Whitby42', '#ICW']) Post(date=datetime.datetime(2013, 11, 18, 15, 30), title='Anchor Follies', rst_text='Some witty epigram. Including < & > characters.', tags=['#RedRanger', '#Whitby42', '#Mistakes']) """ # Example 2: Won't Init # ######################## import logging, sys audit_log = logging.getLogger("audit") class Hand_bad: def __init__(self, dealer_card: Card, *cards: Card) -> None: self.dealer_card = dealer_card self.cards = list(cards) for c in self.cards: audit_log.info("Initial %s", c) def append(self, card: Card) -> None: self.cards.append(card) audit_log.info("Hit %s", card) def __str__(self) -> str: cards = ", ".join(map(str, self.cards)) return f"{self.dealer_card} | {cards}" test_audit = """ >>> logging.basicConfig(stream=sys.stderr, level=logging.INFO) >>> logging.info("bad create") >>> h = Hand_bad(FaceCard("K", Suit.Diamonds), AceCard("A", Suit.Clubs), Card("9", Suit.Hearts)) >>> print(h) K♦ | A♣, 9♥ >>> b = pickle.dumps(h) >>> logging.info("bad load from pickle") >>> h2 = pickle.loads(b) >>> print(h2) K♦ | A♣, 9♥ >>> logging.shutdown() """ class Hand2: def __init__(self, dealer_card: Card, *cards: Card) -> None: self.dealer_card = dealer_card self.cards = list(cards) for c in self.cards: audit_log.info("Initial %s", c) def append(self, card: Card) -> None: self.cards.append(card) audit_log.info("Hit %s", card) def __str__(self) -> str: cards = ", ".join(map(str, self.cards)) return f"{self.dealer_card} | {cards}" def __getstate__(self) -> Dict[str, Any]: return vars(self) def __setstate__(self, state: Dict[str, Any]) -> None: # Not very secure -- hard for mypy to detect what's going on. self.__dict__.update(state) for c in self.cards: audit_log.info("Initial (unpickle) %s", c) test_audit_2 = """ >>> logging.basicConfig(stream=sys.stderr, level=logging.INFO) >>> logging.info("good create") >>> hp = Hand2(FaceCard("K", Suit.Diamonds), AceCard("A", Suit.Clubs), Card("9", Suit.Hearts)) >>> data = pickle.dumps(hp) >>> logging.info("good load from pickle") >>> h2p = pickle.loads(data) >>> print(h2p) K♦ | A♣, 9♥ >>> logging.shutdown() """ # Example 3: Secure Pickle # ######################## import builtins class RestrictedUnpickler(pickle.Unpickler): def find_class(self, module: str, name: str) -> Any: if module == "builtins": if name not in ("exec", "eval"): return getattr(builtins, name) elif module in ("__main__", "Chapter_10.ch10_ex3", "ch10_ex3"): # Valid module names depends on execution context. return globals()[name] # elif module in any of our application modules... elif module in ("Chapter_10.ch10_ex2",): return globals()[name] raise pickle.UnpicklingError( f"global '{module}.{name}' is forbidden" ) test_audit_3 = """ >>> import io >>> logging.basicConfig(stream=sys.stderr, level=logging.INFO) >>> hp = Hand2(FaceCard("K", Suit.Diamonds), AceCard("A", Suit.Clubs), Card("9", Suit.Hearts)) >>> data = pickle.dumps(hp) >>> try: ... h2s = RestrictedUnpickler(io.BytesIO(data)).load() ... except pickle.UnpicklingError as e: ... print(e) >>> print(h2s) K♦ | A♣, 9♥ Creating an unimportable pickle file requires something not in Chapter_10.ch10_ex2. >>> from Chapter_10.ch10_ex1 import travel >>> bad_data = pickle.dumps(travel) >>> try: ... travel_copy = RestrictedUnpickler(io.BytesIO(bad_data)).load() ... except pickle.UnpicklingError as e: ... print(e) global 'Chapter_10.ch10_ex1.Blog' is forbidden """ __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_10/ch10_ex4.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 10. Example 4. CSV """ # Persistence Classes # ======================================== # A detail class for micro-blog posts import datetime from typing import List, Optional, Dict, Any, Iterator from dataclasses import dataclass from pathlib import Path from Chapter_10.ch10_ex1 import Post, Blog, travel, rst_render from Chapter_10.ch10_ex2 import FaceCard, AceCard, Card import io # CSV # =================== # Example 1: GameStats # ###################### class Table: """Abstraction for games played on tables.""" def bet(self, game_state: str, amount: float) -> None: """Accepts a bet for a particular future game state.""" pass class Player_Strategy: """Abstraction for player choices. Varies by game, of course.""" pass class Betting: def __init__(self, stake: float = 100) -> None: self.stake = stake def bet(self, table: Table, game_state: str) -> None: if game_state == "ante": table.bet(game_state, 1) def win(self, amount: float) -> None: self.stake += amount def loss(self, amount: float) -> None: self.stake -= amount def push(self) -> None: pass class Flat_Bet(Betting): pass class Martingale_Bet(Betting): def __init__(self, *args) -> None: super().__init__(*args) self.stage = 1 def bet(self, table: Table, game_state: str) -> None: if game_state == "ante": try: table.bet(game_state, min(self.stage, self.stake)) except BadBet as e: limit = e.args[0] table.bet(game_state, min(limit, self.stake)) def win(self, amount) -> None: self.stage = 1 super().win(amount) def loss(self, amount) -> None: self.stage = min(self.stage * 2, 512) super().loss(amount) def push(self) -> None: super().push() import random class BadBet(Exception): pass class Broke(Exception): pass # A "Table" implementation for Blackjack. class Blackjack(Table): def __init__(self, play: Player_Strategy, betting: Betting) -> None: self.player = play self.betting = betting self.bets: Dict[str, float] = dict() self.rounds = 0 @property def stake(self) -> float: return self.betting.stake def bet(self, game_state: str, amount: float) -> None: if amount > 50: raise BadBet(50) self.bets[game_state] = amount def play_1(self) -> None: if self.betting.stake == 0: raise Broke self.betting.bet(self, "ante") bet = sum(self.bets.values()) outcome = random.random() if outcome < 0.579: self.betting.loss(bet) elif 0.579 <= outcome < 0.883: self.betting.win(bet) elif 0.883 <= outcome < 0.943: self.betting.push() else: # 0.943 <= outcome self.betting.win(bet * 2) def until_broke_or_rounds(self, limit: int) -> None: while self.rounds < limit and self.betting.stake > 0: self.play_1() self.rounds += 1 # Example 1 dumping # #################### # An application of the above definitions. from typing import NamedTuple class GameStat(NamedTuple): player: str bet: str rounds: int final: float from typing import Iterator, Type def gamestat_iter( player: Type[Player_Strategy], betting: Type[Betting], limit: int = 100 ) -> Iterator[GameStat]: for sample in range(30): random.seed(sample) # Reproducible b = Blackjack(player(), betting()) b.until_broke_or_rounds(limit) yield GameStat(player.__name__, betting.__name__, b.rounds, b.betting.stake) import csv from pathlib import Path with (Path.cwd() / "data" / "ch10_blackjack_1.csv").open("w", newline="") as target: writer = csv.DictWriter(target, GameStat._fields) writer.writeheader() for gamestat in gamestat_iter(Player_Strategy, Martingale_Bet): writer.writerow(gamestat._asdict()) data = gamestat_iter(Player_Strategy, Martingale_Bet) with (Path.cwd() / "data" / "ch10_blackjack_2.csv").open("w", newline="") as target: writer = csv.DictWriter(target, GameStat._fields) writer.writeheader() writer.writerows(g._asdict() for g in data) # Example 2 loading # ################### # Loading data from the simulator with (Path.cwd() / "data" / "ch10_blackjack_1.csv").open() as source: reader = csv.DictReader(source) assert set(reader.fieldnames) == set(GameStat._fields) for gs in (GameStat(**r) for r in reader): pass # print( gs ) def gamestat_rdr_iter( source_data: Iterator[Dict[str, str]] ) -> Iterator[GameStat]: for row in source_data: yield GameStat(row["player"], row["bet"], int(row["rounds"]), int(row["final"])) test_write_read_1 = """ >>> with (Path.cwd()/"data"/"ch10_blackjack_1.csv").open() as source: ... reader = csv.DictReader(source) ... assert set(reader.fieldnames) == set(GameStat._fields) ... for gs in gamestat_rdr_iter(reader): ... print(gs) GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=100, final=142) GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=27, final=0) GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=25, final=0) GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=100, final=157) GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=100, final=87) GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=18, final=0) GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=100, final=161) GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=10, final=0) GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=22, final=0) GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=53, final=0) GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=37, final=0) GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=27, final=0) GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=100, final=188) GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=58, final=0) GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=100, final=103) GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=28, final=0) GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=60, final=0) GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=100, final=150) GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=9, final=0) GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=13, final=0) GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=97, final=0) GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=100, final=93) GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=72, final=0) GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=12, final=0) GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=36, final=0) GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=35, final=0) GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=78, final=0) GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=68, final=0) GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=39, final=0) GameStat(player='Player_Strategy', bet='Martingale_Bet', rounds=47, final=0) """ # Example 3 blog and post one file # ################################ # There are two row types, however -- blogs and posts within a blog. # Our blog data to be saved positionally. blogs = [travel] with (Path.cwd() / "data" / "ch10_blog3.csv").open("w", newline="") as target: wtr = csv.writer(target) wtr.writerow(["__class__", "title", "date", "title", "rst_text", "tags"]) for b in blogs: wtr.writerow(["Blog", b.title, None, None, None, None]) for p in b.entries: wtr.writerow(["Post", None, p.date, p.title, p.rst_text, p.tags]) # Super-important: column order must match __init__() param order. # Hard to do in general. # And impossible to make work with mypy unless your Blog and Post structures # are reduced to List[str] with (Path.cwd() / "data" / "ch10_blog3.csv").open() as source: rdr = csv.reader(source) header = next(rdr) assert header == ["__class__", "title", "date", "title", "rst_text", "tags"] blogs = [] for r in rdr: if r[0] == "Blog": blog = Blog(*r[1:2]) # type: ignore blogs.append(blog) elif r[0] == "Post": post = Post(*r[2:]) # type: ignore blogs[-1].append(post) # Tags, however, will not be a proper tuple # The above doesn't handle Post tags properly! # Can use the following for safe eval of literals. import ast def blog_builder(row: List[str]) -> Blog: return Blog(row[1]) def post_builder(row: List[str]) -> Post: return Post( date=datetime.datetime.strptime(row[2], "%Y-%m-%d %H:%M:%S"), title=row[3], rst_text=row[4], tags=ast.literal_eval(row[5]), ) with (Path.cwd() / "data" / "ch10_blog3.csv").open() as source: rdr = csv.reader(source) header = next(rdr) assert header == ["__class__", "title", "date", "title", "rst_text", "tags"] blogs = [] for r in rdr: if r[0] == "Blog": blog = blog_builder(r) blogs.append(blog) elif r[0] == "Post": post = post_builder(r) blogs[-1].append(post) # Example 4 blog and post with better metadata and filter # ######################################################## # Loading the blog with a generator function. from typing import TextIO, cast def blog_iter(source: TextIO) -> Iterator[Blog]: rdr = csv.reader(source) header = next(rdr) assert header == ["__class__", "title", "date", "title", "rst_text", "tags"] blog: Blog = cast(Blog, None) for r in rdr: if r[0] == "Blog": if blog: yield blog blog = blog_builder(r) elif r[0] == "Post": post = post_builder(r) blog.append(post) if blog: yield blog test_blog_3 = """ >>> with (Path.cwd()/"data"/"ch10_blog3.csv").open() as source: ... for b in blog_iter(source): ... print(b.title, [p.title for p in b.entries]) Travel ['Hard Aground', 'Anchor Follies'] >>> with (Path.cwd()/"data"/"ch10_blog3.csv").open() as source: ... blogs = list(blog_iter(source)) >>> for b in blogs: ... print(b.title, [p.title for p in b.entries]) Travel ['Hard Aground', 'Anchor Follies'] """ # Example 5 Blog and Post join # ################################ # Using a "join" between Blog and Post to create a file. with (Path.cwd() / "data" / "ch10_blog5.csv").open("w", newline="") as target: wtr = csv.writer(target) wtr.writerow( ["Blog.title", "Post.date", "Post.title", "Post.tags", "Post.rst_text"] ) for b in blogs: for p in b.entries: wtr.writerow([b.title, p.date, p.title, p.tags, p.rst_text]) from typing import Union, Iterator, Tuple import ast def post_builder5(row: Dict[str, str]) -> Post: return Post( date=datetime.datetime.strptime(row["Post.date"], "%Y-%m-%d %H:%M:%S"), title=row["Post.title"], rst_text=row["Post.rst_text"], tags=ast.literal_eval(row["Post.tags"]), ) def blog_builder5(row: Dict[str, str]) -> Blog: return Blog(title=row["Blog.title"]) from typing import TextIO def blog_iter2(source: TextIO) -> Iterator[Blog]: """An iterator which fetches blogs""" rdr = csv.DictReader(source) assert ( set(rdr.fieldnames) == {"Blog.title", "Post.date", "Post.title", "Post.tags", "Post.rst_text"} ) # Fetch the first row and build the first Blog and Post from this row = next(rdr) blog = blog_builder5(row) post = post_builder5(row) blog.append(post) # Fetch all subsequent rows. # Emit completed Blogs. # Append Posts to the currently open Blog for row in rdr: if row["Blog.title"] != blog.title: yield blog blog = blog_builder5(row) post = post_builder5(row) blog.append(post) yield blog test_blog_iter_2 = """ >>> with (Path.cwd()/"data"/"ch10_blog5.csv").open() as source: ... for b in blog_iter2(source): ... print(b.title, b.as_dict()) 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'}]} """ __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_10/ch10_ex5.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 10. Example 5. EBCDIC encode/decode """ # Persistence Classes # ======================================== # A detail class for micro-blog posts import datetime from typing import List, Optional, Dict, Any, Tuple, Callable, Union from dataclasses import dataclass from pathlib import Path from Chapter_10.ch10_ex1 import Post, Blog, travel, rst_render from Chapter_10.ch10_ex2 import FaceCard, AceCard, Card from Chapter_10.ch10_ex4 import Player_Strategy, Martingale_Bet, gamestat_iter, GameStat import io # Legacy Files # =================== # We'll look at pure text and mixed text w/ packed decimal. # Example 1 dumping all text # ########################### # Metadata for Gamestat objects. # attribute name, start, size, and an output format specification. from typing import NamedTuple, BinaryIO, TextIO, Iterable, Iterator, cast from pathlib import Path class FixedField(NamedTuple): name: str offset: int length: int format_spec: str class Metadata(NamedTuple): fields: List[FixedField] reclen: int metadata_txt = Metadata( fields=[ FixedField("player", 0, 20, "{:<{size}s}"), FixedField("bet", 20, 20, "{:<{size}s}"), FixedField("rounds", 40, 5, "{:>{size}d}"), FixedField("final", 45, 8, "{:>{size}d}"), ], reclen=53, ) # A function to transform a namedtuple into a fixed-layout record. def gamestat_record(gamestat:GameStat, metadata: Metadata) -> str: record_fields = [ format_spec.format(getattr(gamestat, name), size=size) for name, start, size, format_spec in metadata.fields ] record_text = "".join(record_fields) assert len(record_text) == metadata.reclen, f"Got {len(record_text)} Should Be {metadata.reclen}" return record_text # An application of the game statistics definitions. with (Path.cwd()/"data"/"ch10_blackjack.file").open("w", encoding="cp037", newline="") as target: for gamestat in gamestat_iter(Player_Strategy, Martingale_Bet): record = gamestat_record(gamestat, metadata_txt) target.write(record) # Example 2 loading all text # ########################## # Loading data from the simulator. Part 1 -- Physical decomposition into rows. def line_iter(aFile: TextIO, metadata: Union[Metadata, 'XMetadata']) -> Iterator[str]: recBytes = aFile.read(metadata.reclen) while recBytes: yield recBytes recBytes = aFile.read(metadata.reclen) # Part 2 -- decomposition into named fields. def record_iter(aFile: TextIO, metadata: Metadata) -> Iterator[Dict[str, str]]: for line in line_iter(aFile, metadata): record = { name: line[start:start + size].strip() for name, start, size, format_spec in metadata.fields } yield record # Part 3 -- using the field to dictionary parser. test_reader_1 = """ >>> with (Path.cwd()/"data"/"ch10_blackjack.file").open("r", encoding="cp037", newline="") as source: ... for record_in in record_iter(cast(TextIO, source), metadata_txt): ... print(record_in) {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '100', 'final': '142'} {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '27', 'final': '0'} {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '25', 'final': '0'} {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '100', 'final': '157'} {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '100', 'final': '87'} {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '18', 'final': '0'} {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '100', 'final': '161'} {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '10', 'final': '0'} {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '22', 'final': '0'} {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '53', 'final': '0'} {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '37', 'final': '0'} {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '27', 'final': '0'} {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '100', 'final': '188'} {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '58', 'final': '0'} {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '100', 'final': '103'} {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '28', 'final': '0'} {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '60', 'final': '0'} {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '100', 'final': '150'} {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '9', 'final': '0'} {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '13', 'final': '0'} {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '97', 'final': '0'} {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '100', 'final': '93'} {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '72', 'final': '0'} {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '12', 'final': '0'} {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '36', 'final': '0'} {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '35', 'final': '0'} {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '78', 'final': '0'} {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '68', 'final': '0'} {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '39', 'final': '0'} {'player': 'Player_Strategy', 'bet': 'Martingale_Bet', 'rounds': '47', 'final': '0'} """ # Example 3 -- USAGE DISPLAY and USAGE COMP3 # ########################################### # Using COMP-3 expands the problem into three kinds of data # # - Alpha and Alphanumeric encoded in EBCDIC or ASCII # # - Numeric, USAGE DISPLAY, as a string of digits encoded in EBCDIC or ASCII # # - Numeric, USAGE COMP-3, as string of bytes encoded as packed decimal. # # All of which require the decimal module's Decimal class definition. from decimal import Decimal # As a convenience, we map 'ebcdic' to 'cp037' by adding a new lookup function. # import codecs def ebcdic_lookup(name, fallback=codecs.lookup): # real signature unknown if name == "ebcdic": return codecs.lookup("cp037") return fallback(name) codecs.register(ebcdic_lookup) # Alphanumeric USAGE DISPLAY conversion. # The COBOL program stored text. def alpha_decode(data: bytes, metadata: 'XMetadata', field_metadata: 'XField') -> str: """Decode alpha or alphanumeric data. metadata has encoding. field_metadata is (currently) not used. Mock metadata objects >>> import types >>> meta = types.SimpleNamespace(reclen=6, encoding='ebcdic') >>> meta.decode = codecs.getdecoder(meta.encoding) >>> field_meta = types.SimpleNamespace() # Used in other examples... >>> data = bytes([0xf9, 0xf8, 0xf7, 0xf6, 0xf5, 0x60]) >>> alpha_decode(data, meta, field_meta) '98765-' """ text, _ = metadata.decode(data) return text # Numeric USAGE DISPLAY trailing sign conversion. # The COBOL program stored text version of the number. def display_decode(data: bytes, metadata: 'XMetadata', field_metadata: 'XField') -> Decimal: """Decode USAGE DISPLAY numeric data. metadata has encoding. field_metadata has attributes name, start, size, format, precision, usage. Mock metadata objects >>> import types >>> meta= types.SimpleNamespace(reclen=6, encoding='ebcdic') >>> meta.decode = codecs.getdecoder(meta.encoding) >>> field_meta = types.SimpleNamespace(precision=2) >>> data = bytes([0xf9, 0xf8, 0xf7, 0xf6, 0xf5, 0x60]) >>> display_decode(data, meta, field_meta) Decimal('-987.65') """ text, _ = metadata.decode(data) precision = field_metadata.precision or 0 # If None, default is 0. text, sign = text[:-1], text[-1] return Decimal(sign + text[:-precision] + "." + text[-precision:]) # Numeric USAGE COMP-3 conversion. # The COBOL program encoded the number into packed decimal representation. def comp3_decode(data: bytes, metadata: 'XMetadata', field_metadata: 'XField') -> Decimal: """Decode USAGE COMP-3 data. metadata has encoding, which is not used. field_metadata has attributes name, start, size, format, precision, usage. Note that the size is the overall resulting string of bytes. NOT the number of digits involved. Mock metadata objects >>> import types >>> meta = types.SimpleNamespace() # Not used >>> field_meta = types.SimpleNamespace(precision=2) >>> data = bytes((0x98, 0x76, 0x5d)) >>> comp3_decode(data, meta, field_meta) Decimal('-987.65') """ precision = field_metadata.precision or 0 # Default when precision is omitted digits = [] for b in data[:-1]: hi, lo = divmod(b, 16) digits.append(str(hi)) digits.append(str(lo)) digit, sign_byte = divmod(data[-1], 16) digits.append(str(digit)) text = "".join(digits) sign = "-" if sign_byte in (0x0b, 0x0d) else "+" return Decimal(sign + text[:-precision] + "." + text[-precision:]) # Encoder for simple alpha or alphanumeric. def alpha_encode(data: Any, metadata: 'XMetadata', field_metadata: 'XField') -> bytes: """Encode alpha or alphanumeric data. metadata has encoding. field_metadata is not used. Mock metadata objects >>> import types >>> meta = types.SimpleNamespace(encoding='ebcdic') >>> meta.encode = codecs.getencoder(meta.encoding) >>> field_meta = types.SimpleNamespace(length=6) >>> data = '98765-' >>> actual = alpha_encode(data, meta, field_meta) >>> repr(actual) "b'\\\\xf9\\\\xf8\\\\xf7\\\\xf6\\\\xf5`'" >>> actual == bytes([0xf9, 0xf8, 0xf7, 0xf6, 0xf5, 0x60]) True """ bytes, _ = metadata.encode("{:<{size}s}".format(data, size=field_metadata.length)) return bytes # Encoder for numeric USAGE DISPLAY, trailing sign. def display_encode(data: Decimal, metadata: 'XMetadata', field_metadata: 'XField') -> bytes: """Encode numeric USAGE DISPLAY trailing sign. metadata has encoding. field_metadata has attributes name, start, size, format, precision, usage. Mock metadata objects >>> import types, decimal >>> meta = types.SimpleNamespace(encoding='ebcdic') >>> meta.encode = codecs.getencoder(meta.encoding) >>> field_meta = types.SimpleNamespace(length=6, precision=2) >>> actual = display_encode(Decimal('-987.65'), meta, field_meta) >>> repr(actual) "b'\\\\xf9\\\\xf8\\\\xf7\\\\xf6\\\\xf5`'" >>> actual == bytes([0xf9, 0xf8, 0xf7, 0xf6, 0xf5, 0x60]) True """ precision = field_metadata.precision or 0 text = "{0:0>{size}d}{1}".format( abs(int(data * Decimal(10) ** precision)), "-" if data < 0 else "+", size=field_metadata.length - 1, ) bytes, _ = metadata.encode(text) return bytes # Encoder for numeric USAGE COMP-3. def comp3_encode(data: Decimal, metadata: 'XMetadata', field_metadata: 'XField') -> bytes: """Encode numeric USAGE COMP-3. metadata has encoding which is not used. field_metadata has attributes name, start, size, format, precision, usage. Note that the size is the overall resulting string of bytes. NOT the number of digits involved. This has 2 digits per byte + a digit and a sign. Mock metadata objects >>> import types >>> meta = types.SimpleNamespace(encoding='ebcdic') >>> field_meta = types.SimpleNamespace(length=3, precision=2) >>> actual = comp3_encode(Decimal('-987.65'), meta, field_meta) >>> repr(actual) "b'\\\\x98v]'" >>> actual == bytes((0x98, 0x76, 0x5d)) True """ precision = field_metadata.precision or 0 value = abs(int(data * Decimal(10) ** precision)) digits = [0x0d if data < 0 else 0x00] # Trailing sign. nDigits = field_metadata.length * 2 - 1 for i in range(nDigits): digits = [value % 10] + digits value //= 10 b = bytes((hi * 16 + lo for hi, lo in list(zip(digits[::2], digits[1::2])))) return b # Our expanded metadata to include more refined field-level definitions. # First, we'll define some encode-decode pairs. alphanumeric = (alpha_encode, alpha_decode) usage_display = (display_encode, display_decode) usage_comp3 = (comp3_encode, comp3_decode) Encoder = Callable[[Any, Any, Any], bytes] Decoder = Callable[[bytes, Any, Any], Any] Usage = Tuple[Encoder, Decoder] # Then we'll define a more sophisticated metadata that includes the # precision and a reference to the relevant encode-decode pair. # # The overall metadata encoding name is transformed into an # encode and decode function to save lookups on a field-by-field basis. import collections class XField(NamedTuple): name: str offset: int length: int precision: Optional[int] usage: Tuple[Callable, Callable] class XMetadata(NamedTuple): fields: List[XField] reclen: int encoding: str @property def decode(self) -> Callable[[bytes], Tuple[str, int]]: return codecs.getdecoder(self.encoding) @property def encode(self) -> Callable[[str], Tuple[bytes, int]]: return codecs.getencoder(self.encoding) metadata_comp3 = XMetadata( fields=[ XField("player", 0, 20, None, alphanumeric), XField("bet", 20, 20, None, alphanumeric), XField("rounds", 40, 8, 2, usage_display), XField("final", 48, 8, 2, usage_comp3), ], reclen=56, encoding="ebcdic", # for display fields and alphanumeric fields. ) # A function to transform a namedtuple into a fixed-layout record. def gamestat_record_comp3(gamestat: GameStat, metadata: XMetadata) -> bytes: record = [ field.usage[0](getattr(gamestat, field.name), metadata, field) for field in metadata.fields ] text = b"".join(record) assert len(text) == metadata.reclen, "Got {0} != Should Be {1}".format( len(text), metadata.reclen ) return text # Example encoding app. with (Path.cwd()/"data"/"ch10_blackjack_comp3.file").open("wb") as target: for gamestat in gamestat_iter(Player_Strategy, Martingale_Bet): data_bytes = gamestat_record_comp3(gamestat, metadata_comp3) target.write(data_bytes) # Example decoding iterator using more sophisticated metadata. def record2_iter(aFile: TextIO, metadata: XMetadata) -> Iterator[Dict[str, XField]]: for line in line_iter(aFile, metadata): field_data = ( (field, line[field.offset:field.offset + field.length]) for field in metadata.fields ) record = dict( (field.name, field.usage[1](data, metadata, field)) for field, data in field_data ) yield record test_reader_2 = """ >>> with (Path.cwd()/"data"/"ch10_blackjack_comp3.file").open("rb") as source: ... for record in record2_iter(source, metadata_comp3): ... print(record) {'player': 'Player_Strategy ', 'bet': 'Martingale_Bet ', 'rounds': Decimal('100.00'), 'final': Decimal('142.00')} {'player': 'Player_Strategy ', 'bet': 'Martingale_Bet ', 'rounds': Decimal('27.00'), 'final': Decimal('0.00')} {'player': 'Player_Strategy ', 'bet': 'Martingale_Bet ', 'rounds': Decimal('25.00'), 'final': Decimal('0.00')} {'player': 'Player_Strategy ', 'bet': 'Martingale_Bet ', 'rounds': Decimal('100.00'), 'final': Decimal('157.00')} {'player': 'Player_Strategy ', 'bet': 'Martingale_Bet ', 'rounds': Decimal('100.00'), 'final': Decimal('87.00')} {'player': 'Player_Strategy ', 'bet': 'Martingale_Bet ', 'rounds': Decimal('18.00'), 'final': Decimal('0.00')} {'player': 'Player_Strategy ', 'bet': 'Martingale_Bet ', 'rounds': Decimal('100.00'), 'final': Decimal('161.00')} {'player': 'Player_Strategy ', 'bet': 'Martingale_Bet ', 'rounds': Decimal('10.00'), 'final': Decimal('0.00')} {'player': 'Player_Strategy ', 'bet': 'Martingale_Bet ', 'rounds': Decimal('22.00'), 'final': Decimal('0.00')} {'player': 'Player_Strategy ', 'bet': 'Martingale_Bet ', 'rounds': Decimal('53.00'), 'final': Decimal('0.00')} {'player': 'Player_Strategy ', 'bet': 'Martingale_Bet ', 'rounds': Decimal('37.00'), 'final': Decimal('0.00')} {'player': 'Player_Strategy ', 'bet': 'Martingale_Bet ', 'rounds': Decimal('27.00'), 'final': Decimal('0.00')} {'player': 'Player_Strategy ', 'bet': 'Martingale_Bet ', 'rounds': Decimal('100.00'), 'final': Decimal('188.00')} {'player': 'Player_Strategy ', 'bet': 'Martingale_Bet ', 'rounds': Decimal('58.00'), 'final': Decimal('0.00')} {'player': 'Player_Strategy ', 'bet': 'Martingale_Bet ', 'rounds': Decimal('100.00'), 'final': Decimal('103.00')} {'player': 'Player_Strategy ', 'bet': 'Martingale_Bet ', 'rounds': Decimal('28.00'), 'final': Decimal('0.00')} {'player': 'Player_Strategy ', 'bet': 'Martingale_Bet ', 'rounds': Decimal('60.00'), 'final': Decimal('0.00')} {'player': 'Player_Strategy ', 'bet': 'Martingale_Bet ', 'rounds': Decimal('100.00'), 'final': Decimal('150.00')} {'player': 'Player_Strategy ', 'bet': 'Martingale_Bet ', 'rounds': Decimal('9.00'), 'final': Decimal('0.00')} {'player': 'Player_Strategy ', 'bet': 'Martingale_Bet ', 'rounds': Decimal('13.00'), 'final': Decimal('0.00')} {'player': 'Player_Strategy ', 'bet': 'Martingale_Bet ', 'rounds': Decimal('97.00'), 'final': Decimal('0.00')} {'player': 'Player_Strategy ', 'bet': 'Martingale_Bet ', 'rounds': Decimal('100.00'), 'final': Decimal('93.00')} {'player': 'Player_Strategy ', 'bet': 'Martingale_Bet ', 'rounds': Decimal('72.00'), 'final': Decimal('0.00')} {'player': 'Player_Strategy ', 'bet': 'Martingale_Bet ', 'rounds': Decimal('12.00'), 'final': Decimal('0.00')} {'player': 'Player_Strategy ', 'bet': 'Martingale_Bet ', 'rounds': Decimal('36.00'), 'final': Decimal('0.00')} {'player': 'Player_Strategy ', 'bet': 'Martingale_Bet ', 'rounds': Decimal('35.00'), 'final': Decimal('0.00')} {'player': 'Player_Strategy ', 'bet': 'Martingale_Bet ', 'rounds': Decimal('78.00'), 'final': Decimal('0.00')} {'player': 'Player_Strategy ', 'bet': 'Martingale_Bet ', 'rounds': Decimal('68.00'), 'final': Decimal('0.00')} {'player': 'Player_Strategy ', 'bet': 'Martingale_Bet ', 'rounds': Decimal('39.00'), 'final': Decimal('0.00')} {'player': 'Player_Strategy ', 'bet': 'Martingale_Bet ', 'rounds': Decimal('47.00'), 'final': Decimal('0.00')} """ __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_10/ch10_ex6.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 10. Example 6. XML """ # Persistence Classes # ======================================== # A detail class for micro-blog posts import datetime from dataclasses import dataclass, field, asdict from typing import List, DefaultDict, Dict, Any from collections import defaultdict import io from Chapter_10.ch10_ex1 import travel, rst_render # XML # =================== # Example 1: XML output # ###################### from dataclasses import dataclass, field, asdict @dataclass class Post_X: date: datetime.datetime title: str rst_text: str tags: List[str] underline: str = field(init=False) tag_text: str = field(init=False) def __post_init__(self) -> None: self.underline = "-"*len(self.title) self.tag_text = ' '.join(self.tags) def as_dict(self) -> Dict[str, Any]: return asdict(self) def xml(self) -> str: tags = "".join(f"{t}" for t in self.tags) return f"""\ {self.title} {self.date} {tags} {self.rst_text} """ from dataclasses import dataclass, field, asdict @dataclass class Blog_X: title: str entries: List[Post_X] = field(default_factory=list) underline: str = field(init=False) def __post_init__(self) -> None: self.underline = "="*len(self.title) def append(self, post: Post_X) -> None: self.entries.append(post) def by_tag(self) -> DefaultDict[str, List[Dict[str, Any]]]: tag_index: DefaultDict[str, List[Dict[str, Any]]] = defaultdict(list) for post in self.entries: for tag in post.tags: tag_index[tag].append(asdict(post)) return tag_index def as_dict(self) -> Dict[str, Any]: return asdict(self) def xml(self) -> str: children = "\n".join(c.xml() for c in self.entries) return f"""\ {self.title} {children} """ travel4 = Blog_X("Travel") travel4.append( Post_X( date=datetime.datetime(2013, 11, 14, 17, 25), title="Hard Aground", rst_text="""Some embarrassing revelation. Including ☹ and ⚓""", tags=["#RedRanger", "#Whitby42", "#ICW"], ) ) travel4.append( Post_X( date=datetime.datetime(2013, 11, 18, 15, 30), title="Anchor Follies", rst_text="""Some witty epigram.""", tags=["#RedRanger", "#Whitby42", "#Mistakes"], ) ) test_xml_out_1 = """ >>> print(travel4.xml()) # doctest: +NORMALIZE_WHITESPACE Travel Hard Aground 2013-11-14 17:25:00 #RedRanger#Whitby42#ICW Some embarrassing revelation. Including ☹ and ⚓ Anchor Follies 2013-11-18 15:30:00 #RedRanger#Whitby42#Mistakes Some witty epigram. """ # Example 2: element Tree output # ############################## import xml.etree.ElementTree as XML from typing import cast class Blog_E(Blog_X): def xmlelt(self) -> XML.Element: blog = XML.Element("blog") title = XML.SubElement(blog, "title") title.text = self.title title.tail = "\n" entities = XML.SubElement(blog, "entries") entities.extend(cast('Post_E', c).xmlelt() for c in self.entries) blog.tail = "\n" return blog class Post_E(Post_X): def xmlelt(self) -> XML.Element: post = XML.Element("entry") title = XML.SubElement(post, "title") title.text = self.title date = XML.SubElement(post, "date") date.text = str(self.date) tags = XML.SubElement(post, "tags") for t in self.tags: tag = XML.SubElement(tags, "tag") tag.text = t text = XML.SubElement(post, "rst_text") text.text = self.rst_text post.tail = "\n" return post travel5 = Blog_E("Travel") travel5.append( Post_E( date=datetime.datetime(2013, 11, 14, 17, 25), title="Hard Aground", rst_text="""Some embarrassing revelation. Including ☹ and ⚓""", tags=["#RedRanger", "#Whitby42", "#ICW"], ) ) travel5.append( Post_E( date=datetime.datetime(2013, 11, 18, 15, 30), title="Anchor Follies", rst_text="""Some witty epigram. Including < & > characters.""", tags=["#RedRanger", "#Whitby42", "#Mistakes"], ) ) test_xml_out_2 = """ >>> tree = XML.ElementTree(travel5.xmlelt()) >>> text = XML.tostring(tree.getroot()) >>> print(text.decode('utf-8')) # doctest: +NORMALIZE_WHITESPACE Travel Hard Aground2013-11-14 17:25:00#RedRanger#Whitby42#ICWSome embarrassing revelation. Including ☹ and ⚓ Anchor Follies2013-11-18 15:30:00#RedRanger#Whitby42#MistakesSome witty epigram. Including < & > characters. """ def build_blog(document: XML.ElementTree) -> Blog_X: xml_blog = document.getroot() blog = Blog_X(xml_blog.findtext("title")) for xml_post in xml_blog.findall("entries/entry"): optional_tag_iter = ( t.text for t in xml_post.findall("tags/tag") ) tags = list( filter(None, optional_tag_iter) ) post = Post_X( date=datetime.datetime.strptime( xml_post.findtext("date"), "%Y-%m-%d %H:%M:%S" ), title=xml_post.findtext("title"), tags=tags, rst_text=xml_post.findtext("rst_text"), ) blog.append(post) return blog test_xml_in = """ >>> tree = XML.ElementTree(travel5.xmlelt()) >>> text = XML.tostring(tree.getroot()) >>> document = XML.parse(io.StringIO(text.decode("utf-8"))) >>> blog = build_blog(document) >>> rst_render(blog) # doctest: +NORMALIZE_WHITESPACE Travel ====== Hard Aground ------------ Some embarrassing revelation. Including ☹ and ⚓ :date: 2013-11-14 17:25:00 :tags: #RedRanger #Whitby42 #ICW Anchor Follies -------------- Some witty epigram. Including < & > characters. :date: 2013-11-18 15:30:00 :tags: #RedRanger #Whitby42 #Mistakes Tag Index ========= * #RedRanger - `Hard Aground`_ - `Anchor Follies`_ * #Whitby42 - `Hard Aground`_ - `Anchor Follies`_ * #ICW - `Hard Aground`_ * #Mistakes - `Anchor Follies`_ """ __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_11/__init__.py ================================================ ================================================ FILE: Chapter_11/ch11_ex1.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 11. Example 1. """ # Shelve Basics # ======================================== from typing import List, Dict, Any, Optional from collections import defaultdict import datetime from pathlib import Path from dataclasses import dataclass, asdict, field import shelve # Some Example Application Classes @dataclass class Post: date: datetime.datetime title: str rst_text: str tags: List[str] @dataclass class Blog: title: str entries: List[Post] = field(default_factory=list) underline: str = field(init=False) # Part of the persistence, not essential to the class. _id: str = field(default="", init=False, compare=False) def __post_init__(self) -> None: self.underline = "=" * len(self.title) def append(self, post: Post) -> None: self.entries.append(post) def by_tag(self) -> Dict[str, List[Dict[str, Any]]]: tag_index: Dict[str, List[Dict[str, Any]]] = defaultdict(list) for post in self.entries: for tag in post.tags: tag_index[tag].append(asdict(post)) return tag_index test_blog = """ >>> b1 = Blog(title="Travel Blog") >>> b1 Blog(title='Travel Blog', entries=[], underline='===========', _id='') >>> import shelve >>> from pathlib import Path >>> path = Path.cwd() / "data" / "ch11_blog1" >>> shelf = shelve.open(str(path), "n") >>> b1._id = 'Blog:1' >>> shelf[b1._id] = b1 >>> shelf['Blog:1'] Blog(title='Travel Blog', entries=[], underline='===========', _id='Blog:1') >>> shelf['Blog:1'].title 'Travel Blog' >>> shelf['Blog:1']._id 'Blog:1' >>> list(shelf.keys()) ['Blog:1'] >>> shelf.close() """ test_query = """ >>> path = Path.cwd() / "data" / "ch11_blog1" >>> shelf = shelve.open(str(path)) >>> results = (shelf[k] ... for k in shelf.keys() ... if k.startswith('Blog:') and shelf[k].title == 'Travel Blog' ... ) >>> list(results) [Blog(title='Travel Blog', entries=[], underline='===========', _id='Blog:1')] """ if __name__ == "__main__": # A Blog example b1 = Blog(title="Travel Blog") p1 = Post( date=datetime.datetime(2019, 1, 18), title="Some Post", rst_text="Details of the post", tags=["#sample", "#data"], ) b1.append(p1) # Some Manual access import shelve shelf = shelve.open(str(Path.cwd() / "data" / "ch11_blog")) db_id = 0 # Typical seqence for saving... db_id += 1 b1._id = f"Blog:{db_id}" shelf[b1._id] = b1 print(f"Create {shelf[b1._id]._id} {shelf[b1._id].title}") # Seaching through the shelf for a specific title... results = ( shelf[k] for k in shelf.keys() if k.startswith("Blog:") and shelf[k].title == "Travel Blog" ) for r0 in results: print(f"Retrieve {r0._id} {r0.title}") for p in r0.entries: print(f" {p}") print(f" {r0.by_tag()}") shelf.close() # Some more manual access if __name__ == "__main__": p2 = Post( date=datetime.datetime(2013, 11, 14, 17, 25), title="Hard Aground", rst_text="""Some embarrassing revelation. Including ☹ and ⚓︎""", tags=["#RedRanger", "#Whitby42", "#ICW"], ) p3 = Post( date=datetime.datetime(2013, 11, 18, 15, 30), title="Anchor Follies", rst_text="""Some witty epigram. Including ☺ and ☀︎︎""", tags=["#RedRanger", "#Whitby42", "#Mistakes"], ) shelf = shelve.open(str(Path.cwd() / "data" / "ch11_blog")) # Retrieve the blog by id blog_id = 1 key = f"Blog:{blog_id}" the_blog = shelf[key] # Update the blog the_blog.append(p2) the_blog.append(p3) # Persist the changes to the blog. shelf[key] = the_blog # What's in the database? print("Database has", list(shelf.keys())) shelf.close() __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_11/ch11_ex2.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 11. Example 2. """ from typing import List, Dict, Any, Optional, cast, Iterator, Union, TextIO import datetime from dataclasses import dataclass, field, asdict from pathlib import Path # Application Classes # ==================== # Designed to be used separately. @dataclass class Post: date: datetime.datetime title: str rst_text: str tags: List[str] underline: str = field(init=False) tag_text: str = field(init=False) # Will be set as part of saving to the shelf. # Part of the persistence, not essential to the class. _id: str = field(default='', init=False, repr=False, compare=False) _blog_id: str = field(default='', init=False, repr=False, compare=False) def __post_init__(self) -> None: self.underline = "-" * len(self.title) self.tag_text = " ".join(self.tags) @dataclass class Blog: title: str underline: str = field(init=False) # Will be set as part of saving to the shelf. # Part of the persistence, not essential to the class. _id: str = field(default="", init=False, compare=False) def __post_init__(self) -> None: self.underline = "=" * len(self.title) def by_tag(self, access: 'Access') -> Dict[str, List[Dict[str, Any]]]: tag_index: Dict[str, List[Dict[str, Any]]] = defaultdict(list) for post in access.post_iter(self): if post._blog_id == self._id: for tag in post.tags: tag_index[tag].append(asdict(post)) return tag_index test_relational_1 = """ >>> b1 = Blog(title="Travel Blog") >>> p2 = Post(date=datetime.datetime(2013,11,14,17,25), ... title="Hard Aground", ... rst_text="Some embarrassing revelation. Including ☹ and ⚓", ... tags=("#RedRanger", "#Whitby42", "#ICW"), ... ) >>> p3 = Post(date=datetime.datetime(2013,11,18,15,30), ... title="Anchor Follies", ... rst_text="Some witty epigram. Including < & > characters.", ... tags=("#RedRanger", "#Whitby42", "#Mistakes"), ... ) >>> import shelve >>> from pathlib import Path >>> path = Path.cwd() / "data" / "ch11_blog2" >>> shelf = shelve.open(str(path), 'n') >>> b1._id = 'Blog:1' >>> shelf[b1._id] = b1 >>> list(shelf.keys()) ['Blog:1'] >>> owner = shelf['Blog:1'] >>> owner Blog(title='Travel Blog', underline='===========', _id='Blog:1') >>> p2._parent = owner._id >>> p2._id = p2._parent + ':Post:2' >>> shelf[p2._id]= p2 >>> p3._parent = owner._id >>> p3._id = p3._parent + ':Post:3' >>> shelf[p3._id]= p3 >>> shelf.sync() >>> sorted(shelf.keys()) ['Blog:1', 'Blog:1:Post:2', 'Blog:1:Post:3'] """ # "Relational" Access Layer -- Separate Blogs from Posts # ====================================================== # We'll use hierarchical keys Post:id and Post:id:Child:id import shelve class OperationError(Exception): pass class Access: def __init__(self) -> None: self.database: shelve.Shelf = cast(shelve.Shelf, None) self.max: Dict[str, int] = {"Post": 0, "Blog": 0} def new(self, path: Path) -> None: self.database: shelve.Shelf = shelve.open(str(path), "n") self.max: Dict[str, int] = {"Post": 0, "Blog": 0} self.sync() def open(self, path: Path) -> None: self.database = shelve.open(str(path), "w") self.max = self.database["_DB:max"] def close(self) -> None: if self.database: self.database["_DB:max"] = self.max self.database.close() self.database = cast(shelve.Shelf, None) def sync(self) -> None: self.database["_DB:max"] = self.max self.database.sync() def create_blog(self, blog: Blog) -> Blog: self.max['Blog'] += 1 key = f"Blog:{self.max['Blog']}" blog._id = key self.database[blog._id] = blog return blog def retrieve_blog(self, key: str) -> Blog: return self.database[key] def create_post(self, blog: Blog, post: Post) -> Post: self.max['Post'] += 1 post_key = f"Post:{self.max['Post']}" post._id = post_key post._blog_id = blog._id self.database[post._id] = post return post def retrieve_post(self, key: str) -> Post: return self.database[key] def update_post(self, post: Post) -> Post: self.database[post._id] = post return post def delete_post(self, post: Post) -> None: del self.database[post._id] def __iter__(self) -> Iterator[Union[Blog, Post]]: for k in self.database: if k[0] == "_": # Skip the administrative objects continue yield self.database[k] def blog_iter(self) -> Iterator[Blog]: for k in self.database: if k.startswith('Blog:'): yield self.database[k] def post_iter(self, blog: Blog) -> Iterator[Post]: for k in self.database: if k.startswith('Post:'): if self.database[k]._blog_id == blog._id: yield self.database[k] def post_title_iter(self, blog: Blog, title: str) -> Iterator[Post]: return (p for p in self.post_iter(blog) if p.title == title) def blog_title_iter(self, title: str) -> Iterator[Blog]: return (b for b in self.blog_iter() if b.title == title) # Demonstration Script from contextlib import closing def database_script(access: Access) -> None: b1 = Blog(title="Travel Blog") p2 = Post( date=datetime.datetime(2013, 11, 14, 17, 25), title="Hard Aground", rst_text="""Some embarrassing revelation. Including ☹ and ⚓︎""", tags=["#RedRanger", "#Whitby42", "#ICW"], ) p3 = Post( date=datetime.datetime(2013, 11, 18, 15, 30), title="Anchor Follies", rst_text="""Some witty epigram. Including ☺ and ☀︎︎""", tags=["#RedRanger", "#Whitby42", "#Mistakes"], ) access.create_blog(b1) for post in p2, p3: access.create_post(b1, post) b = access.retrieve_blog(b1._id) print(b._id, b) for p in sorted(access.post_iter(b), key=lambda p: p._id): print(p._id, p) test_access = """ >>> with closing(Access()) as access: ... access.new(Path.cwd() / "data" / "ch11_blog") ... database_script(access) Blog:1 Blog(title='Travel Blog', underline='===========', _id='Blog:1') 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') 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') """ # Another Application # ============================== import string from collections import defaultdict from contextlib import redirect_stdout import sys class Render: def __init__(self, access: Access) -> None: self.access = access def emit_all(self, destination: TextIO=sys.stdout) -> None: for blog in self.access.blog_iter(): # Compute a filename for each blog. self.emit_blog(blog, destination) def emit_blog(self, blog: Blog, output: TextIO) -> None: with redirect_stdout(output): self.tag_index: Dict[str, List[str]] = defaultdict(list) print("{title}\n{underline}\n".format(**asdict(blog))) for post in self.access.post_iter(blog): self.emit_post(post) for tag in post.tags: self.tag_index[tag].append(post._id) self.emit_index() def emit_post(self, post: Post) -> None: template = string.Template( """ $title $underline $rst_text :date: $date :tags: $tag_text """ ) print(template.substitute(asdict(post))) def emit_index(self) -> None: print("Tag Index") print("=========") print() for tag in self.tag_index: print("* {0}".format(tag)) print() for b in self.tag_index[tag]: post = self.access.retrieve_post(b) print(" - `{title}`_".format(**asdict(post))) print() # Demo Script import shelve from contextlib import closing if __name__ == "__main__": with closing(Access()) as access: access.open(Path.cwd() / "data" / "ch11_blog") renderer = Render(access) renderer.emit_all() # Better Access Layer # ====================================== # Maintain a indexes associated with the Blog class Access2(Access): def create_post(self, blog: Blog, post: Post) -> Post: super().create_post(blog, post) # Update the index; append doesn't work. blog_index = f"_Index:{blog._id}" self.database.setdefault(blog_index, []) self.database[blog_index] = self.database[blog_index] + [post._id] return post def delete_post(self, post: Post) -> None: super().delete_post(post) # Update the index blog_index = f"_Index:{post._blog_id}" index_list = self.database[post._blog_id] index_list.remove(post._id) self.database[post._blog_id] = index_list def post_iter(self, blog: Blog) -> Iterator[Post]: blog_index = f"_Index:{blog._id}" for k in self.database[blog_index]: yield self.database[k] test_access_2 = """ >>> with closing(Access2()) as access: ... access.new(Path.cwd() / "data" / "ch11_blog2") ... database_script(access) Blog:1 Blog(title='Travel Blog', underline='===========', _id='Blog:1') 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') 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') >>> with closing(Access2()) as access: ... access.open(Path.cwd() / "data" / "ch11_blog2") ... print(sorted(access.database.keys())) ... print(access.database['_Index:Blog:1']) ['Blog:1', 'Post:1', 'Post:2', '_DB:max', '_Index:Blog:1'] ['Post:1', 'Post:2'] >>> with closing(Access2()) as access: ... access.open(Path.cwd() / "data" / "ch11_blog2") ... renderer = Render(access) ... renderer.emit_all() """ # Minor Index # ========================== # Another version of Access with slightly different blog add and search. # This a tiny help, because the iteration over the cached blog keys # is slightly faster. class Access3(Access2): def new(self, path: Path) -> None: super().new(path) self.database["_Index:Blog"] = list() def create_blog(self, blog: Blog) -> Blog: super().create_blog(blog) self.database["_Index:Blog"] += [blog._id] return blog def blog_iter(self) -> Iterator[Blog]: return (self.database[k] for k in self.database["_Index:Blog"]) test_access_3 = """ >>> with closing(Access3()) as access: ... access.new(Path.cwd() / "data" / "ch11_blog3") ... database_script(access) Blog:1 Blog(title='Travel Blog', underline='===========', _id='Blog:1') 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') 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') >>> with closing(Access3()) as access: ... access.open(Path.cwd() / "data" / "ch11_blog3") ... print(sorted(access.database.keys())) ... print(access.database['_Index:Blog:1']) ['Blog:1', 'Post:1', 'Post:2', '_DB:max', '_Index:Blog', '_Index:Blog:1'] ['Post:1', 'Post:2'] >>> with closing(Access3()) as access: ... access.open(Path.cwd() / "data" / "ch11_blog3") ... renderer = Render(access) ... renderer.emit_all() """ # Additional Indices # ================================ # A class with multiple indices. # Is this really worth the extra complexity? class Access4(Access3): def new(self, path: Path) -> None: super().new(path) self.database["_Index:Blog_Title"] = dict() def create_blog(self, blog): super().create_blog(blog) blog_title_dict = self.database["_Index:Blog_Title"] blog_title_dict.setdefault(blog.title, []) blog_title_dict[blog.title].append(blog._id) self.database["_Index:Blog_Title"] = blog_title_dict return blog def update_blog(self, blog: Blog) -> Blog: """Replace this Blog; update index.""" self.database[blog._id] = blog blog_title = self.database["_Index:Blog_Title"] # Remove key from index in old spot. empties = [] for k in blog_title: if blog._id in blog_title[k]: blog_title[k].remove(blog._id) if len(blog_title[k]) == 0: empties.append(k) # Cleanup zero-length lists from defaultdict. for k in empties: del blog_title[k] # Put key into index in new spot. blog_title[blog.title].append(blog._id) self.database["_Index:Blog_Title"] = blog_title return blog def blog_iter(self) -> Iterator[Blog]: return (self.database[k] for k in self.database["_Index:Blog"]) def blog_title_iter(self, title: str) -> Iterator[Blog]: blog_title = self.database["_Index:Blog_Title"] return (self.database[k] for k in blog_title[title]) test_access_4 = """ >>> with closing(Access4()) as access: ... access.new(Path.cwd() / "data" / "ch11_blog4") ... database_script(access) Blog:1 Blog(title='Travel Blog', underline='===========', _id='Blog:1') 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') 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') >>> with closing(Access4()) as access: ... access.open(Path.cwd() / "data" / "ch11_blog4") ... print(sorted(access.database.keys())) ... print(access.database['_Index:Blog:1']) ['Blog:1', 'Post:1', 'Post:2', '_DB:max', '_Index:Blog', '_Index:Blog:1', '_Index:Blog_Title'] ['Post:1', 'Post:2'] >>> with closing(Access4()) as access: ... access.open(Path.cwd() / "data" / "ch11_blog4") ... renderer = Render(access) ... renderer.emit_all() """ # Timing Comparison # ================================ # Larger Database Required import time import io def create(access, blogs=100, posts_per_blog=100) -> None: for b_n in range(blogs): b = Blog("Blog {0}".format(b_n)) access.create_blog(b) for p_n in range(posts_per_blog): p = Post( date=datetime.datetime.now(), title="Blog {0}; Post {1}".format(b_n, p_n), rst_text="Blog {0}; Post {1}\nText\n".format(b_n, p_n), tags=list("#tag{0}".format(p_n + i) for i in range(3)), ) access.create_post(b, p) def performance(cycles=3): import random result: Dict[str, float] = defaultdict(int) base_path = Path.cwd() / "data" for filename, class_ in ( (base_path / "ch11_blog_t", Access), (base_path / "ch11_blog_t2", Access2), (base_path / "ch11_blog_t3", Access3), (base_path / "ch11_blog_t4", Access4), ): buffer = io.StringIO() start = time.perf_counter() for _ in range(cycles): with closing(class_()) as access: access.new(filename) create(access, blogs=100, posts_per_blog=100) with closing(class_()) as access: access.open(filename) renderer = Render(access) renderer.emit_all(buffer) with closing(class_()) as access: access.open(filename) renderer = Render(access) titles = [] for i in range(10): choice = random.randint(1, 100) blog_by_id = access.retrieve_blog(f"Blog:{choice}") renderer.emit_blog(blog_by_id, buffer) titles.append(blog_by_id.title) with closing(class_()) as access: access.open(filename) renderer = Render(access) for t in titles: blogs = access.blog_title_iter(t) for b in blogs: renderer.emit_blog(b, buffer) finish = time.perf_counter() result[class_.__name__] = finish - start print("Time to create and render 10,000 posts") for r in sorted(result): print( f"Access Layer {r}: {result[r]/cycles:.1f} seconds " ) for path in base_path.glob("ch11_blog_t*.*"): path.unlink() __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) # performance() # Takes 45 seconds """ Time to create and render 10,000 posts Access Layer Access: 33.5 seconds Access Layer Access2: 4.0 seconds Access Layer Access3: 3.9 seconds Access Layer Access4: 4.0 seconds """ ================================================ FILE: Chapter_12/__init__.py ================================================ ================================================ FILE: Chapter_12/ch12_ex1.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 12. Example 1. """ from typing import Dict, List, Tuple # One issue here is that the microblog has no processing. # The classes tend to be rather anemic. # The upside is that it has all of the relevant relationships # So it shows SQL key handling nicely. # SQL Basics # ======================================== # Some Example Table Declarations for a simple microblog. sql_cleanup = """ DROP TABLE IF EXISTS blog; DROP TABLE IF EXISTS post; DROP TABLE IF EXISTS tag; DROP TABLE IF EXISTS assoc_post_tag; """ sql_ddl = """ CREATE TABLE blog( ID INTEGER PRIMARY KEY AUTOINCREMENT, TITLE TEXT ); CREATE TABLE post( id INTEGER PRIMARY KEY AUTOINCREMENT, date TIMESTAMP, title TEXT, rst_text TEXT, blog_id INTEGER REFERENCES blog(id) ); CREATE TABLE tag( id INTEGER PRIMARY KEY AUTOINCREMENT, phrase TEXT UNIQUE ON CONFLICT FAIL ); CREATE TABLE assoc_post_tag( post_id INTEGER REFERENCES post(id), tag_id INTEGER REFERENCES tag(id) ); """ import sqlite3 from pathlib import Path from contextlib import closing database = sqlite3.connect(Path.cwd() / "data" / "ch12_blog.db") # type: ignore # Note that sqlite3 really does use a Path. The declaration doesn't include it. # We have two choices. # 1. Use a ``# typing: ignore`` comment # 2. use ``str(Path.cwd()/"data"/"ch12_blog.db")`` # reveal_type(sqlite3.connect) database.executescript(sql_cleanup) with closing(database.cursor()) as cursor: for stmt in (stmt.rstrip() for stmt in sql_ddl.split(";")): print(stmt) cursor.execute(stmt) print(cursor) database.commit() database.close() # ACID # =============== database = sqlite3.connect( Path.cwd() / "data" / "ch12_blog.db", isolation_level="DEFERRED" ) # type: ignore try: with closing(database.cursor()) as cursor: cursor.execute("BEGIN") # cursor.execute("some statement") # cursor.execute("another statement") database.commit() except Exception as e: database.rollback() # Simple SQL # ====================== # Import import datetime # Connection database = sqlite3.connect(Path.cwd() / "data" / "ch12_blog.db") # type: ignore # Useful query to figuring out what PK was automatically assigned. get_last_id = """ SELECT last_insert_rowid() """ with closing(database.cursor()) as cursor: cursor.execute("BEGIN") # Build BLOG create_blog = """ INSERT INTO blog(title) VALUES(?) """ cursor.execute(create_blog, ("Travel Blog",)) row = cursor.execute(get_last_id).fetchone() blog_id = row[0] # Build POST create_post = """ INSERT INTO post(date, title, rst_text, blog_id) VALUES(?, ?, ?, ?) """ cursor.execute( create_post, ( datetime.datetime(2013, 11, 14, 17, 25), "Hard Aground", """Some embarrassing revelation. Including ☹ and ⚓︎""", blog_id, ), ) row = cursor.execute(get_last_id).fetchone() post_id = row[0] # Build TAGs for a Post create_tag = """ INSERT INTO tag(phrase) VALUES(?) """ retrieve_tag = """ SELECT id, phrase FROM tag WHERE phrase = ? """ create_tag_post_association = """ INSERT INTO assoc_post_tag(post_id, tag_id) VALUES (?, ?) """ for tag in ("#RedRanger", "#Whitby42", "#ICW"): row = cursor.execute(retrieve_tag, (tag,)).fetchone() if row: tag_id = row[0] else: cursor.execute(create_tag, (tag,)) row = cursor.execute(get_last_id).fetchone() tag_id = row[0] cursor.execute(create_tag_post_association, (post_id, tag_id)) database.commit() update_blog = """ UPDATE blog SET title=:new_title WHERE title=:old_title """ with closing(database.cursor()) as cursor: # Sample Update cursor.execute("BEGIN") cursor.execute( update_blog, dict( new_title="2013-2014 Travel", old_title="Travel Blog") ) database.commit() # Sample Delete delete_post_tag_by_blog_title = """ DELETE FROM assoc_post_tag WHERE post_id IN ( SELECT DISTINCT post_id FROM blog JOIN post ON blog.id = post.blog_id WHERE blog.title=:old_title) """ delete_post_by_blog_title = """ DELETE FROM post WHERE blog_id IN ( SELECT id FROM blog WHERE title=:old_title) """ delete_blog_by_title = """ DELETE FROM blog WHERE title=:old_title """ try: with closing(database.cursor()) as cursor: title = dict(old_title="2013-2014 Travel") cursor.execute("BEGIN") cursor.execute(delete_post_tag_by_blog_title, title) cursor.execute(delete_post_by_blog_title, title) cursor.execute(delete_blog_by_title, title) print("Post Delete, Pre Commit; should be no '2013-2014 Travel'") cursor.execute("SELECT * FROM blog") for row in cursor.fetchall(): print(row) cursor.execute("SELECT * FROM post") for row in cursor.fetchall(): print(row) cursor.execute("SELECT * FROM assoc_post_tag") for row in cursor.fetchall(): print(row) raise Exception("Demonstrating an Error") print("Should not get here to commit.") database.commit() except Exception as ex: print(f"Rollback due to {ex!r}") database.rollback() # Bulk examination of database to show simple queries with closing(database.cursor()) as cursor: print("Dumping whole database.") for row in cursor.execute("SELECT * FROM blog"): print("BLOG", row) for row in cursor.execute("SELECT * FROM post"): print("POST", row) for row in cursor.execute("SELECT * FROM tag"): print("TAG", row) for row in cursor.execute( """ SELECT assoc_post_tag.* FROM post JOIN assoc_post_tag ON post.id=assoc_post_tag.post_id JOIN tag ON tag.id=assoc_post_tag.tag_id """ ): print("ASSOC_POST_TAG", row) # Naked SQL Query # ========================== print("Dump a single blog by title.") # Three-step nested queries query_blog_by_title = """ SELECT * FROM blog WHERE title=? """ query_post_by_blog_id = """ SELECT * FROM post WHERE blog_id=? """ query_tag_by_post_id = """ SELECT tag.* FROM tag JOIN assoc_post_tag ON tag.id = assoc_post_tag.tag_id WHERE assoc_post_tag.post_id=? """ with closing(database.cursor()) as blog_cursor: blog_cursor.execute(query_blog_by_title, ("2013-2014 Travel",)) for blog in blog_cursor.fetchall(): print("Blog", blog) with closing(database.cursor()) as post_cursor: post_cursor.execute(query_post_by_blog_id, (blog[0],)) for post in post_cursor: print("Post", post) with closing(database.cursor()) as tag_cursor: tag_cursor.execute(query_tag_by_post_id, (post[0],)) for tag in tag_cursor.fetchall(): print("Tag", tag) # Tag index from collections import defaultdict query_by_tag = """ SELECT tag.phrase, post.title, post.id FROM tag JOIN assoc_post_tag ON tag.id = assoc_post_tag.tag_id JOIN post ON post.id = assoc_post_tag.post_id JOIN blog ON post.blog_id = blog.id WHERE blog.title=? """ tag_index: Dict[str, List[Tuple[str, int]]] = defaultdict(list) with closing(database.cursor()) as cursor: cursor.execute(query_by_tag, ("2013-2014 Travel",)) for tag, post_title, post_id in cursor.fetchall(): tag_index[tag].append((post_title, post_id)) print(tag_index) database.close() __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_12/ch12_ex2.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 12. Example 2. """ # BLOB Mapping # ========================= # Adding Decimal data to a SQLite database. import sqlite3 import decimal def adapt_currency(value): return str(value) sqlite3.register_adapter(decimal.Decimal, adapt_currency) def convert_currency(bytes): return decimal.Decimal(bytes.decode()) sqlite3.register_converter("DECIMAL", convert_currency) # When we define a table, we must use the type "decimal" # to get two-digit decimal values. decimal_cleanup = """ DROP TABLE IF EXISTS budget """ decimal_ddl = """ CREATE TABLE budget( year INTEGER, month INTEGER, category TEXT, amount DECIMAL ) """ insert_budget = """ INSERT INTO budget(year, month, category, amount) VALUES(:year, :month, :category, :amount) """ query_budget = """ SELECT * FROM budget """ test_decimal = """ >>> from pathlib import Path >>> database = sqlite3.connect( ... Path.cwd() / "data" / "ch12_blog.db", ... detect_types=sqlite3.PARSE_DECLTYPES # Required to include additional types ... ) # type: ignore >>> _ = database.execute(decimal_cleanup) >>> _ = database.execute(decimal_ddl) >>> _ = database.execute( ... insert_budget, ... dict(year=2013, month=1, category="fuel", amount=decimal.Decimal("256.78")), ... ) >>> _ = database.execute( ... insert_budget, ... dict(year=2013, month=2, category="fuel", amount=decimal.Decimal("287.65")), ... ) >>> for row in database.execute(query_budget): ... print(row) (2013, 1, 'fuel', Decimal('256.78')) (2013, 2, 'fuel', Decimal('287.65')) >>> database.close() """ __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_12/ch12_ex3.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 12. Example 3. """ # Manual ORM # ========================= from pathlib import Path from dataclasses import dataclass, asdict, field from typing import List, Dict, Any, DefaultDict, Optional, Iterator, cast import sqlite3 import datetime from contextlib import closing from collections import defaultdict from weakref import ref @dataclass class Blog: title: str underline: str = field(init=False) # Part of the persistence, not essential to the class. _id: str = field(default="", init=False, compare=False) _access: Optional[ref] = field(init=False, repr=False, default=None, compare=False) def __post_init__(self) -> None: self.underline = "=" * len(self.title) @property def entries(self) -> List['Post']: if self._access and self._access(): posts = cast('Access', self._access()).post_iter(self) return list(posts) raise RuntimeError("Can't work with Blog: no associated Access instance") def by_tag(self) -> Dict[str, List[Dict[str, Any]]]: if self._access and self._access(): return cast('Access', self._access()).post_by_tag(self) raise RuntimeError("Can't work with Blog: no associated Access instance") @dataclass class Post: date: datetime.datetime title: str rst_text: str tags: List[str] = field(default_factory=list) _id: str = field(default="", init=False, compare=False) def append(self, tag): self.tags.append(tag) # An access layer to map back and forth between Python objects and SQL rows. class Access: get_last_id = """ SELECT last_insert_rowid() """ def open(self, path: Path) -> None: self.database = sqlite3.connect(path) self.database.row_factory = sqlite3.Row def get_blog(self, id: str) -> Blog: query_blog = """ SELECT * FROM blog WHERE id=? """ row = self.database.execute(query_blog, (id,)).fetchone() blog = Blog(title=row["TITLE"]) blog._id = row["ID"] blog._access = ref(self) return blog def add_blog(self, blog: Blog) -> Blog: insert_blog = """ INSERT INTO blog(title) VALUES(:title) """ self.database.execute(insert_blog, dict(title=blog.title)) row = self.database.execute(self.get_last_id).fetchone() blog._id = str(row[0]) blog._access = ref(self) return blog def get_post(self, id: str) -> Post: query_post = """ SELECT * FROM post WHERE id=? """ row = self.database.execute(query_post, (id,)).fetchone() post = Post( title=row["TITLE"], date=row["DATE"], rst_text=row["RST_TEXT"] ) post._id = row["ID"] # Get tag text, too query_tags = """ SELECT tag.* FROM tag JOIN assoc_post_tag ON tag.id = assoc_post_tag.tag_id WHERE assoc_post_tag.post_id=? """ results = self.database.execute(query_tags, (id,)) for tag_id, phrase in results: post.append(phrase) return post def add_post(self, blog: Blog, post: Post) -> Post: insert_post = """ INSERT INTO post(title, date, rst_text, blog_id) VALUES(:title, :date, :rst_text, :blog_id) """ query_tag = """ SELECT * FROM tag WHERE phrase=? """ insert_tag = """ INSERT INTO tag(phrase) VALUES(?) """ insert_association = """ INSERT INTO assoc_post_tag(post_id, tag_id) VALUES(:post_id, :tag_id) """ try: with closing(self.database.cursor()) as cursor: cursor.execute( insert_post, dict( title=post.title, date=post.date, rst_text=post.rst_text, blog_id=blog._id, ), ) row = cursor.execute(self.get_last_id).fetchone() post._id = str(row[0]) for tag in post.tags: tag_row = cursor.execute(query_tag, (tag,)).fetchone() if tag_row is not None: tag_id = tag_row["ID"] else: cursor.execute(insert_tag, (tag,)) row = cursor.execute(self.get_last_id).fetchone() tag_id = str(row[0]) cursor.execute( insert_association, dict(tag_id=tag_id, post_id=post._id) ) self.database.commit() except Exception as ex: self.database.rollback() raise return post def blog_iter(self) -> Iterator[Blog]: query = """ SELECT * FROM blog """ results = self.database.execute(query) for row in results: blog = Blog(title=row["TITLE"]) blog._id = row["ID"] blog._access = ref(self) yield blog def post_iter(self, blog: Blog) -> Iterator[Post]: query = """ SELECT id FROM post WHERE blog_id=? """ results = self.database.execute(query, (blog._id,)) for row in results: yield self.get_post(row["ID"]) def post_by_tag(self, blog: Blog) -> Dict[str, List[Dict[str, Any]]]: """All posts of a blog, organized by tag, represented as dictionaries.""" query_by_tag = """ SELECT tag.phrase, post.id FROM tag JOIN assoc_post_tag ON tag.id = assoc_post_tag.tag_id JOIN post ON post.id = assoc_post_tag.post_id JOIN blog ON post.blog_id = blog.id WHERE blog.title=? """ results = self.database.execute(query_by_tag, (blog.title,)) tags: DefaultDict[str, List[Dict[str, Any]]] = defaultdict(list) for phrase, post_id in results.fetchall(): tags[phrase].append(asdict(self.get_post(post_id))) return tags if __name__ == "__main__": database_access = Access() database_access.open(Path.cwd() / "data" / "ch12_blog.db") b = Blog(title="2012 Travel") database_access.add_blog(b) print(b._id) p = Post( title="Some History", date=datetime.datetime(2012, 9, 16, 10, 00), rst_text="Some historyical notes.", tags=["#History", "#RedRanger"], ) database_access.add_post(b, p) d = b.by_tag() print(d) for b in database_access.blog_iter(): # print(f"b = {iter(b)}") print(asdict(b)) for p in database_access.post_iter(b): print(asdict(p)) __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_12/ch12_ex4.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 12. Example 4. .. important:: SQLAlchemy doesn't include any stubs or type hints. There's no point in running mypy on this module. You'll see the following errors (plus a few others):: Chapter_12/ch12_ex4.py:18: error: No library stub file for module 'sqlalchemy.ext.declarative' Chapter_12/ch12_ex4.py:18: note: (Stub files are from https://github.com/python/typeshed) Chapter_12/ch12_ex4.py:23: error: No library stub file for module 'sqlalchemy' Chapter_12/ch12_ex4.py:44: error: No library stub file for module 'sqlalchemy.orm' Chapter_12/ch12_ex4.py:117: error: No library stub file for module 'sqlalchemy.exc' """ import datetime # SQLAlchemy Mapping # ============================== # Some Classes that reflect our SQL data. from sqlalchemy.ext.declarative import declarative_base # Section 3.2.5 lists the column types from sqlalchemy import Column, Table from sqlalchemy import ( BigInteger, Boolean, Date, DateTime, Enum, Float, Integer, Interval, LargeBinary, Numeric, PickleType, SmallInteger, String, Text, Time, Unicode, UnicodeText, ForeignKey, ) from sqlalchemy.orm import relationship, backref # There are standard types and vendor types, also. # We'll stick with generic types. # The metaclass Base = declarative_base() # The application class/table declarations class Blog(Base): __tablename__ = "BLOG" id = Column(Integer, primary_key=True) title = Column(String) def as_dict(self): return dict( title=self.title, underline="=" * len(self.title), entries=[e.as_dict() for e in self.entries], ) assoc_post_tag = Table( "ASSOC_POST_TAG", Base.metadata, Column("POST_ID", Integer, ForeignKey("POST.id")), Column("TAG_ID", Integer, ForeignKey("TAG.id")), ) class Post(Base): __tablename__ = "POST" id = Column(Integer, primary_key=True) title = Column(String) date = Column(DateTime) rst_text = Column(UnicodeText) blog_id = Column(Integer, ForeignKey("BLOG.id")) blog = relationship("Blog", backref="entries") tags = relationship("Tag", secondary=assoc_post_tag, backref="posts") def as_dict(self): return dict( title=self.title, underline="-" * len(self.title), date=self.date, rst_text=self.rst_text, tags=[t.phrase for t in self.tags], ) class Tag(Base): __tablename__ = "TAG" id = Column(Integer, primary_key=True) phrase = Column(String, unique=True) __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) # Building a schema from sqlalchemy import create_engine engine = create_engine("sqlite:///./data/ch12_blog2.db", echo=True) Base.metadata.drop_all(engine) Base.metadata.create_all(engine) # Loading some data import sqlalchemy.exc from sqlalchemy.orm import sessionmaker Session = sessionmaker(bind=engine) session = Session() blog = Blog(title="Travel 2013") session.add(blog) tags = [] for phrase in "#RedRanger", "#Whitby42", "#ICW": try: tag = session.query(Tag).filter(Tag.phrase == phrase).one() except sqlalchemy.orm.exc.NoResultFound: tag = Tag(phrase=phrase) session.add(tag) tags.append(tag) p2 = Post( date=datetime.datetime(2013, 11, 14, 17, 25), title="Hard Aground", rst_text="""Some embarrassing revelation. Including ☹ and ⚓︎""", blog=blog, tags=tags, ) session.add(p2) tags = [] for phrase in "#RedRanger", "#Whitby42", "#Mistakes": try: tag = session.query(Tag).filter(Tag.phrase == phrase).one() except sqlalchemy.orm.exc.NoResultFound: tag = Tag(phrase=phrase) session.add(tag) tags.append(tag) p3 = Post( date=datetime.datetime(2013, 11, 18, 15, 30), title="Anchor Follies", rst_text="""Some witty epigram. Including ☺ and ☀︎︎""", blog=blog, tags=tags, ) session.add(p3) blog.posts = [p2, p3] session.commit() session = Session() for blog in session.query(Blog): print("{title}\n{underline}\n".format(**blog.as_dict())) for p in blog.entries: print(p.as_dict()) session2 = Session() results = ( session2.query(Post).join(assoc_post_tag).join(Tag).filter( Tag.phrase == "#Whitby42" ) ) for post in results: print(post.blog.title, post.date, post.title, [t.phrase for t in post.tags]) ================================================ FILE: Chapter_13/__init__.py ================================================ ================================================ FILE: Chapter_13/cards_openapi.json ================================================ { "openapi": "3.0.0", "info": { "description": "Deals simple hands of cards", "version": "2019.02", "title": "Chapter 13. Example 2" }, "components": { "schemas": { "cards": { "type": "array", "items": { "type": "object", "properties": { "rank": { "type": "number" }, "suit": { "type": "string" } } } } } }, "paths": { "/cards/{n}": { "get": { "summary": "Get a hand of cards", "parameters": [ { "in": "path", "name": "n", "description": "Number of Cards", "required": true, "schema": { "type": "number" } } ], "responses": { "200": { "description": "Hand of cards", "content": { "application/json": { "schema": { "type": "object", "properties": { "status": { "type": "string" }, "cards": { "$ref": "#/components/schemas/cards" } } } } } }, "400": { "description": "Invalid input" } } } }, "/hands/{h}/cards/{c}": { "get": { "summary": "Get several hands of cards", "parameters": [ { "in": "path", "name": "h", "description": "Number of Hands", "required": true, "schema": { "type": "number" } }, { "in": "path", "name": "c", "description": "Number of Cards", "required": true, "schema": { "type": "number" } } ], "responses": { "200": { "description": "List of hands of cards", "content": { "application/json": { "schema": { "type": "object", "properties": { "status": { "type": "string" }, "hands": { "type": "array", "items": { "$ref": "#/components/schemas/cards" } } } } } } }, "400": { "description": "Invalid input" } } } } } } ================================================ FILE: Chapter_13/ch13_e1_ex2.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 13. Example 2. """ # REST basics # ======================================== # Stateless. Roulette. Base class definitions. from typing import Optional, Iterable, TYPE_CHECKING if TYPE_CHECKING: from wsgiref.types import WSGIApplication, WSGIEnvironment, StartResponse import random from Chapter_13.ch13_ex1 import ( Wheel, Zero, DoubleZero, American, European, Response, json_get, ) import sys import wsgiref.util import json # REST Revised: Callable WSGI Applications # ========================================= class Wheel2(Wheel): def __call__( self, environ: "WSGIEnvironment", start_response: "StartResponse" ) -> Iterable[bytes]: winner = self.spin() status = "200 OK" headers = [("Content-type", "application/json; charset=utf-8")] start_response(status, headers) return [json.dumps(winner).encode("UTF-8")] class American2(DoubleZero, Wheel2): pass class European2(Zero, Wheel2): pass test_wheel = """ >>> am = American2(seed=2) >>> def mock_start(status, headers): ... print(status, headers) >>> am({}, mock_start) 200 OK [('Content-type', 'application/json; charset=utf-8')] [b'{"4": [35, 1], "Black": [1, 1], "Lo": [1, 1], "Even": [1, 1]}'] """ # A WSGI wrapper application. import sys class Wheel3: def __init__(self, seed: Optional[int] = None) -> None: self.am = American2(seed) self.eu = European2(seed) def __call__( self, environ: "WSGIEnvironment", start_response: "StartResponse" ) -> Iterable[bytes]: request = wsgiref.util.shift_path_info(environ) # 1. Parse. print("Wheel3", request, file=sys.stderr) # 2. Logging. if request and request.lower().startswith("eu"): # 3. Evaluate. response = self.eu(environ, start_response) else: response = self.am(environ, start_response) return response # 4. Respond. test_wheel3 = """ >>> wheel = Wheel3(seed=2) >>> def mock_start(status, headers): ... print(status, headers) >>> wheel({"PATH_INFO": "/am"}, mock_start) 200 OK [('Content-type', 'application/json; charset=utf-8')] [b'{"4": [35, 1], "Black": [1, 1], "Lo": [1, 1], "Even": [1, 1]}'] """ # Revised Server def roulette_server_3(count: int = 1) -> None: from wsgiref.simple_server import make_server httpd = make_server("localhost", 8080, Wheel3(2)) if count is None: httpd.serve_forever() else: for c in range(count): httpd.handle_request() # Wheel3 Demo # --------------- # When run as the main script, start a server and interact with it. def server_3() -> None: import concurrent.futures import time with concurrent.futures.ProcessPoolExecutor() as executor: srvr = executor.submit(roulette_server_3, 2) time.sleep(0.1) # Wait for the server to start r1 = json_get("/am") r2 = json_get("/eu") assert ( str(r1) == "200: {'4': [35, 1], 'Black': [1, 1], 'Lo': [1, 1], 'Even': [1, 1]}" ) assert ( str(r2) == "200: {'4': [35, 1], 'Black': [1, 1], 'Lo': [1, 1], 'Even': [1, 1]}" ) __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) server_3() ================================================ FILE: Chapter_13/ch13_e1_ex3.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 13. Example 3. """ # REST basics # ======================================== # Stateless. Roulette. Base class definitions. from typing import Dict, Tuple, List, Any, Optional, Iterable, TYPE_CHECKING if TYPE_CHECKING: from wsgiref.types import WSGIApplication, WSGIEnvironment, StartResponse import random from Chapter_13.ch13_ex1 import ( Wheel, Zero, DoubleZero, American, European, Response, json_get, ) import sys import wsgiref.util import json # REST Revised: Callable WSGI Applications # ========================================= class Wheel2(Wheel): def __call__( self, environ: "WSGIEnvironment", start_response: "StartResponse" ) -> Iterable[bytes]: winner = self.spin() status = "200 OK" headers = [("Content-type", "application/json; charset=utf-8")] start_response(status, headers) return [json.dumps(winner).encode("UTF-8")] class American2(DoubleZero, Wheel2): pass class European2(Zero, Wheel2): pass # A WSGI wrapper application. import sys class Wheel3: def __init__(self) -> None: self.am = American2() self.eu = European2() def __call__( self, environ: "WSGIEnvironment", start_response: "StartResponse" ) -> Iterable[bytes]: request = wsgiref.util.shift_path_info(environ) # 1. Parse. print("Wheel3", request, file=sys.stderr) # 2. Logging. if request and request.lower().startswith("eu"): # 3. Evaluate. response = self.eu(environ, start_response) else: response = self.am(environ, start_response) return response # 4. Respond. # REST with sessions and state # ======================================== # Player and Bet for Roulette. # CRUD design issues. # Player: # - GET to see stake and rounds played. # Bet: # - POST to create a series of bets or decline to bet. # - GET to see bets. # Wheel: # - GET to get spin and payout. # Stateful object from collections import defaultdict class Table: def __init__(self, stake: int = 100) -> None: self.bets: Dict[str, int] = defaultdict(int) self.stake = stake def place_bet(self, name: str, amount: int) -> None: self.bets[name] += amount def clear_bets(self, name: str) -> None: self.bets: Dict[str, int] = defaultdict(int) def resolve(self, spin: Dict[str, Tuple[int, int]]) -> List[Tuple[str, int, str]]: """spin is a dict with bet:(x:y).""" details = [] while self.bets: bet, amount = self.bets.popitem() if bet in spin: x, y = spin[bet] self.stake += int(amount * x / y) details.append((bet, amount, "win")) else: self.stake -= amount details.append((bet, amount, "lose")) return details # WSGI Applications class WSGI: def __call__( self, environ: "WSGIEnvironment", start_response: "StartResponse" ) -> Iterable[bytes]: raise NotImplementedError class RESTException(Exception): pass class Roulette(WSGI): def __init__(self, wheel: Wheel) -> None: self.table = Table(100) self.rounds = 0 self.wheel = wheel def __call__( self, environ: "WSGIEnvironment", start_response: "StartResponse" ) -> Iterable[bytes]: # print( environ, file=sys.stderr ) app = wsgiref.util.shift_path_info(environ) try: if app and app.lower() == "player": return self.player_app(environ, start_response) elif app and app.lower() == "bet": return self.bet_app(environ, start_response) elif app and app.lower() == "wheel": return self.wheel_app(environ, start_response) else: raise RESTException( "404 NOT_FOUND", "Unknown app in {SCRIPT_NAME}/{PATH_INFO}".format_map(environ), ) except RESTException as e: status = e.args[0] headers = [("Content-type", "text/plain; charset=utf-8")] start_response(status, headers, sys.exc_info()) return [repr(e.args).encode("UTF-8")] def player_app( self, environ: "WSGIEnvironment", start_response: "StartResponse" ) -> Iterable[bytes]: if environ["REQUEST_METHOD"] == "GET": details = dict(stake=self.table.stake, rounds=self.rounds) status = "200 OK" headers = [("Content-type", "application/json; charset=utf-8")] start_response(status, headers) return [json.dumps(details).encode("UTF-8")] else: raise RESTException( "405 METHOD_NOT_ALLOWED", "Method '{REQUEST_METHOD}' not allowed".format_map(environ), ) def bet_app( self, environ: "WSGIEnvironment", start_response: "StartResponse" ) -> Iterable[bytes]: if environ["REQUEST_METHOD"] == "GET": details = dict(self.table.bets) elif environ["REQUEST_METHOD"] == "POST": size = int(environ["CONTENT_LENGTH"]) raw = environ["wsgi.input"].read(size).decode("UTF-8") try: data = json.loads(raw) if isinstance(data, dict): data = [data] for detail in data: self.table.place_bet(detail["bet"], int(detail["amount"])) except Exception as e: # Must undo all bets. raise RESTException(f"403 FORBIDDEN", "Bet {raw!r}") details = dict(self.table.bets) else: raise RESTException( "405 METHOD_NOT_ALLOWED", "Method '{REQUEST_METHOD}' not allowed".format_map(environ), ) status = "200 OK" headers = [("Content-type", "application/json; charset=utf-8")] start_response(status, headers) return [json.dumps(details).encode("UTF-8")] def wheel_app( self, environ: "WSGIEnvironment", start_response: "StartResponse" ) -> Iterable[bytes]: if environ["REQUEST_METHOD"] == "POST": size = environ["CONTENT_LENGTH"] if size != "0": raw = environ["wsgi.input"].read(int(size)) raise RESTException( "403 FORBIDDEN", f"Data {raw!r} not allowed" ) spin = self.wheel.spin() payout = self.table.resolve(spin) self.rounds += 1 details = dict( spin=spin, payout=payout, stake=self.table.stake, rounds=self.rounds ) status = "200 OK" headers = [("Content-type", "application/json; charset=utf-8")] start_response(status, headers) return [json.dumps(details).encode("UTF-8")] else: raise RESTException( "405 METHOD_NOT_ALLOWED", "Method '{REQUEST_METHOD}' not allowed".format_map(environ), ) test_table = """ Spike to show that the essential features work. >>> wheel = American(seed=2) >>> roulette = Roulette(wheel) >>> data = {"bet": "Black", "amount": 2} >>> roulette.table.place_bet(data["bet"], int(data["amount"])) >>> print(roulette.table.bets) defaultdict(, {'Black': 2}) >>> spin = wheel.spin() >>> payout = roulette.table.resolve(spin) >>> print(spin, payout) {'4': (35, 1), 'Black': (1, 1), 'Lo': (1, 1), 'Even': (1, 1)} [('Black', 2, 'win')] """ # Server def roulette_server_3(count: int = 1) -> None: from wsgiref.simple_server import make_server from wsgiref.validate import validator wheel = American(seed=1) roulette = Roulette(wheel) debug = validator(roulette) httpd = make_server("", 8080, debug) if count is None: httpd.serve_forever() else: for c in range(count): httpd.handle_request() # Client import http.client import json def roulette_client( method: str = "GET", path: str = "/", data: Optional[Dict[str, str]] = None ) -> Response: rest = http.client.HTTPConnection("localhost", 8080) if data: header = {"Content-type": "application/json; charset=utf-8'"} params = json.dumps(data).encode("UTF-8") rest.request(method, path, params, header) else: rest.request(method, path) response = rest.getresponse() raw = response.read().decode("utf-8") try: document = json.loads(raw) except json.decoder.JSONDecodeError as ex: document = raw return Response(response.status, dict(response.getheaders()), document) def server_3() -> None: import concurrent.futures import time with concurrent.futures.ProcessPoolExecutor() as executor: executor.submit(roulette_server_3, 4) time.sleep(0.1) # Wait for the server to start r1 = roulette_client("GET", "/player/") print(r1) r2 = roulette_client("POST", "/bet/", {"bet": "Black", "amount": "2"}) print(r2) r3 = roulette_client("GET", "/bet/") print(r3) r4 = roulette_client("POST", "/wheel/") print(r4) assert r1.status == 200 and r1.content == {"stake": 100, "rounds": 0} assert r2.status == 200 and r2.content == {"Black": 2} assert r3.status == 200 and r3.content == {"Black": 2} assert ( r4.status == 200 and r4.content == {'spin': {'9': [35, 1], 'Red': [1, 1], 'Lo': [1, 1], 'Odd': [1, 1]}, 'payout': [['Black', 2, 'lose']], 'stake': 98, 'rounds': 1} ), f"{r4!r}" __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) server_3() ================================================ FILE: Chapter_13/ch13_e1_ex4.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 13. Example 4. """ # REST basics # ======================================== # Stateless. Roulette. Base class definitions. from typing import Dict, Tuple, Optional, List, Iterable, Any, TYPE_CHECKING, cast if TYPE_CHECKING: from wsgiref.types import WSGIApplication, WSGIEnvironment, StartResponse import random from Chapter_13.ch13_ex1 import ( Wheel, Zero, DoubleZero, American, European, Response, json_get, ) import sys import wsgiref.util import json # REST Revised: Callable WSGI Applications # ========================================= class Wheel2(Wheel): def __call__( self, environ: "WSGIEnvironment", start_response: "StartResponse" ) -> Iterable[bytes]: winner = self.spin() status = "200 OK" headers = [("Content-type", "application/json; charset=utf-8")] start_response(status, headers) return [json.dumps(winner).encode("UTF-8")] class American2(DoubleZero, Wheel2): pass class European2(Zero, Wheel2): pass # A WSGI wrapper application. import sys class Wheel3: def __init__(self): self.am = American2() self.eu = European2() def __call__( self, environ: "WSGIEnvironment", start_response: "StartResponse" ) -> Iterable[bytes]: request = wsgiref.util.shift_path_info(environ) # 1. Parse. print("Wheel3", request, file=sys.stderr) # 2. Logging. if request and request.lower().startswith("eu"): # 3. Evaluate. response = self.eu(environ, start_response) else: response = self.am(environ, start_response) return response # 4. Respond. # REST with sessions and state # ======================================== # Player and Bet for Roulette. # CRUD design issues. # Player: # - GET to see stake and rounds played. # Bet: # - POST to create a series of bets or decline to bet. # - GET to see bets. # Wheel: # - GET to get spin and payout. # Stateful object from collections import defaultdict class Table: def __init__(self, stake: int = 100) -> None: self.bets: Dict[str, int] = defaultdict(int) self.stake = stake def place_bet(self, name: str, amount: int) -> None: self.bets[name] += amount def clear_bets(self, name: str) -> None: self.bets: Dict[str, int] = defaultdict(int) def resolve(self, spin: Dict[str, Tuple[int, int]]) -> List[Tuple[str, int, str]]: """spin is a dict with bet:(x:y).""" details = [] while self.bets: bet, amount = self.bets.popitem() if bet in spin: x, y = spin[bet] self.stake += int(amount * x / y) details.append((bet, amount, "win")) else: self.stake -= amount details.append((bet, amount, "lose")) return details # WSGI Applications class WSGI: def __call__( self, environ: "WSGIEnvironment", start_response: "StartResponse" ) -> Iterable[bytes]: raise NotImplementedError class RESTException(Exception): pass class Roulette(WSGI): def __init__(self, wheel): self.table = Table(100) self.rounds = 0 self.wheel = wheel def __call__( self, environ: "WSGIEnvironment", start_response: "StartResponse" ) -> Iterable[bytes]: # print(environ, file=sys.stderr) app = wsgiref.util.shift_path_info(environ) try: if app and app.lower() == "player": return self.player_app(environ, start_response) elif app and app.lower() == "bet": return self.bet_app(environ, start_response) elif app and app.lower() == "wheel": return self.wheel_app(environ, start_response) else: raise RESTException( "404 NOT_FOUND", "Unknown app in {SCRIPT_NAME}/{PATH_INFO}".format_map(environ), ) except RESTException as e: status = e.args[0] headers = [("Content-type", "text/plain; charset=utf-8")] start_response(status, headers, sys.exc_info()) return [repr(e.args).encode("UTF-8")] def player_app( self, environ: "WSGIEnvironment", start_response: "StartResponse" ) -> Iterable[bytes]: if environ["REQUEST_METHOD"] == "GET": details = dict(stake=self.table.stake, rounds=self.rounds) status = "200 OK" headers = [("Content-type", "application/json; charset=utf-8")] start_response(status, headers) return [json.dumps(details).encode("UTF-8")] else: raise RESTException( "405 METHOD_NOT_ALLOWED", "Method '{REQUEST_METHOD}' not allowed".format_map(environ), ) def bet_app( self, environ: "WSGIEnvironment", start_response: "StartResponse" ) -> Iterable[bytes]: if environ["REQUEST_METHOD"] == "GET": details = dict(self.table.bets) elif environ["REQUEST_METHOD"] == "POST": size = int(environ["CONTENT_LENGTH"]) raw = environ["wsgi.input"].read(size).decode("UTF-8") try: data = json.loads(raw) if isinstance(data, dict): data = [data] for detail in data: self.table.place_bet(detail["bet"], int(detail["amount"])) except Exception as e: # TODO: Must undo all bets. raise RESTException(f"403 FORBIDDEN", "Bet {raw!r}") details = dict(self.table.bets) else: raise RESTException( "405 METHOD_NOT_ALLOWED", "Method '{REQUEST_METHOD}' not allowed".format_map(environ), ) status = "200 OK" headers = [("Content-type", "application/json; charset=utf-8")] start_response(status, headers) return [json.dumps(details).encode("UTF-8")] def wheel_app( self, environ: "WSGIEnvironment", start_response: "StartResponse" ) -> Iterable[bytes]: if environ["REQUEST_METHOD"] == "POST": size = environ["CONTENT_LENGTH"] if size != "": raw = environ["wsgi.input"].read(int(size)) raise RESTException( "403 FORBIDDEN", f"Data '{raw!r}' not allowed" ) spin = self.wheel.spin() payout = self.table.resolve(spin) self.rounds += 1 details = dict( spin=spin, payout=payout, stake=self.table.stake, rounds=self.rounds ) status = "200 OK" headers = [("Content-type", "application/json; charset=utf-8")] start_response(status, headers) return [json.dumps(details).encode("UTF-8")] else: raise RESTException( "405 METHOD_NOT_ALLOWED", "Method '{REQUEST_METHOD}' not allowed".format_map(environ), ) test_table = """ Spike to show that the essential features work. >>> wheel = American(seed=2) >>> roulette = Roulette(wheel) >>> data = {"bet": "Black", "amount": 2} >>> roulette.table.place_bet(data["bet"], int(data["amount"])) >>> print(roulette.table.bets) defaultdict(, {'Black': 2}) >>> spin = wheel.spin() >>> payout = roulette.table.resolve(spin) >>> print(spin, payout) {'4': (35, 1), 'Black': (1, 1), 'Lo': (1, 1), 'Even': (1, 1)} [('Black', 2, 'win')] """ # Server def roulette_server_4(count: int = 1): from wsgiref.simple_server import make_server from wsgiref.validate import validator wheel = American() roulette = Roulette(wheel) debug = validator(roulette) httpd = make_server("", 8080, debug) if count is None: httpd.serve_forever() else: for c in range(count): httpd.handle_request() # Client import http.client import json def roulette_client(method="GET", path="/", data=None): rest = http.client.HTTPConnection("localhost", 8080) if data: header = {"Content-type": "application/json; charset=utf-8'"} params = json.dumps(data).encode("UTF-8") rest.request(method, path, params, header) else: rest.request(method, path) response = rest.getresponse() raw = response.read().decode("utf-8") if 200 <= response.status < 300: document = json.loads(raw) return document else: print(response.status, response.reason) print(response.getheaders()) print(raw) # REST with authentication # ======================================== # Authentication class definition with password hashing. from hashlib import sha256 import os class Authentication: iterations = 1000 def __init__(self, username: bytes, password: bytes, salt: Optional[bytes]=None) -> None: """Works with bytes. Not Unicode strings.""" self.username = username self.salt = salt or os.urandom(24) self.hash = self._iter_hash(self.iterations, self.salt, username, password) @staticmethod def _iter_hash(iterations: int, salt: bytes, username: bytes, password: bytes): seed = salt + b":" + username + b":" + password for i in range(iterations): seed = sha256(seed).digest() return seed def __eq__(self, other: Any) -> bool: other = cast("Authentication", other) return self.username == other.username and self.hash == other.hash def __hash__(self) -> int: return hash(self.hash) def __repr__(self) -> str: salt_x = "".join("{0:x}".format(b) for b in self.salt) hash_x = "".join("{0:x}".format(b) for b in self.hash) return f"{self.username} {self.iterations:d}:{salt_x}:{hash_x}" def match(self, password: bytes) -> bool: test = self._iter_hash(self.iterations, self.salt, self.username, password) return self.hash == test # Constant Time is Best # Collection of users. class Users(dict): def __init__(self, *args, **kw) -> None: super().__init__(*args, **kw) # Can never be found -- dict key is invalid and isn't the username. self[""] = Authentication(b"__dummy__", b"Doesn't Matter") def add(self, authentication: Authentication) -> None: if authentication.username == "": raise KeyError("Invalid Authentication") self[authentication.username] = authentication def match(self, username: bytes, password: bytes) -> bool: if username in self and username != "": return self[username].match(password) else: # Time-wasting comparison return self[""].match(b"Something which doesn't match") # Global Objects users = Users() users.add(Authentication(b"Aladdin", b"open sesame")) test_matching = """ Spike to show user matching rule. >>> test_salt = bytes(range(24)) >>> al = Authentication(b"Aladdin", b"open sesame", test_salt) >>> al b'Aladdin' 1000:0123456789abcdef1011121314151617:a53bdcd6d16acc8fd33fc982c973147f15f6ce43cff4fb83a5f6b267de1 >>> users = Users() >>> users.add(Authentication(b"Aladdin", b"open sesame")) >>> users.match("", b"Doesn't Matter") False >>> users.match(b"__dummy__", b"Doesn't Matter") False """ # Authentication app import base64 class Authenticate(WSGI): def __init__(self, users, target_app): self.users = users self.target_app = target_app def __call__( self, environ: "WSGIEnvironment", start_response: "StartResponse" ) -> Iterable[bytes]: if "HTTP_AUTHORIZATION" in environ: scheme, credentials = environ["HTTP_AUTHORIZATION"].split() if scheme == "Basic": username, password = base64.b64decode(credentials).split(b":") if self.users.match(username, password): environ["Authenticate.username"] = username return self.target_app(environ, start_response) status = "401 UNAUTHORIZED" headers = [ ("Content-Type", "text/plain; charset=utf-8"), ("WWW-Authenticate", 'Basic realm="roulette@localhost"'), ] start_response(status, headers) return ["Not authorized".encode("utf-8")] # Some app which requires authentication class Some_App(WSGI): def __call__( self, environ: "WSGIEnvironment", start_response: "StartResponse" ) -> Iterable[bytes]: status = "200 OK" headers = [("Content-type", "text/plain; charset=utf-8")] start_response(status, headers) return ["Welcome".encode("UTF-8")] # Demo client import base64 def authenticated_client( method: str = "GET", path: str = "/", data: Optional[str] = None, username: str = "", password: str = "", ) -> Tuple[int, str, str]: rest = http.client.HTTPConnection("localhost", 8080) headers = {} if username and password: enc = base64.b64encode( username.encode("ascii") + b":" + password.encode("ascii") ) headers["Authorization"] = f"Basic {enc.decode('ascii')}" if data: headers["Content-type"] = "application/json; charset=utf-8" params = json.dumps(data).encode("utf-8") rest.request(method, path, params, headers=headers) else: rest.request(method, path, headers=headers) # print(f"*** CLIENT: {headers}") response = rest.getresponse() raw = response.read().decode("utf-8") if response.status == 401: print(response.getheaders()) return response.status, response.reason, raw # Server def auth_server(count: int = 1) -> None: from wsgiref.simple_server import make_server from wsgiref.validate import validator secure_app = Some_App() authenticated_app = Authenticate(users, secure_app) debug = validator(authenticated_app) httpd = make_server("", 8080, debug) if count is None: httpd.serve_forever() else: for c in range(count): httpd.handle_request() # Demo def server_5() -> None: import concurrent.futures import time with concurrent.futures.ProcessPoolExecutor() as executor: executor.submit(auth_server, 3) time.sleep(0.1) # Wait for the server to start print(authenticated_client("GET", "/player/")) print( authenticated_client( "GET", "/player/", username="Aladdin", password="open sesame" ) ) print( authenticated_client( "GET", "/player/", username="Aladdin", password="not right" ) ) __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) server_5() ================================================ FILE: Chapter_13/ch13_ex1.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 13. Example 1. """ # REST basics # ======================================== # Object and state test_example_1 = """ >>> from dataclasses import dataclass, asdict >>> import json >>> @dataclass ... class Greeting: ... message: str >>> g = Greeting("Hello World") >>> text = json.dumps(asdict(g)) >>> text '{"message": "Hello World"}' >>> text.encode('utf-8') b'{"message": "Hello World"}' """ # Stateless Roulette Server # ========================== # Base class definitions. from typing import ( Dict, Tuple, Optional, Callable, List, Union, Iterator, NamedTuple, Any, Type, Iterable, TYPE_CHECKING, ) from abc import abstractmethod import random class Wheel: """Abstract, zero bins omitted.""" def __init__(self, seed: Optional[int] = None) -> None: self.rng = random.Random() self.rng.seed(seed) self.bins = [ { str(n): (35, 1), self.redblack(n): (1, 1), self.hilo(n): (1, 1), self.evenodd(n): (1, 1), } for n in range(1, 37) ] self.bins.extend(self.zero()) @abstractmethod def zero(self) -> List[Dict[str, Tuple[int, int]]]: pass @staticmethod def redblack(n: int) -> str: return "Red" if n in ( 1, 3, 5, 7, 9, 12, 14, 16, 18, 19, 21, 23, 25, 27, 30, 32, 34, 36 ) else "Black" @staticmethod def hilo(n: int) -> str: return "Hi" if n >= 19 else "Lo" @staticmethod def evenodd(n: int) -> str: return "Even" if n % 2 == 0 else "Odd" def spin(self) -> Dict[str, Tuple[int, int]]: return self.rng.choice(self.bins) class Zero: def zero(self) -> List[Dict[str, Tuple[int, int]]]: return [{"0": (35, 1)}] class DoubleZero(Zero): def zero(self) -> List[Dict[str, Tuple[int, int]]]: z_bins = super().zero() z_bins += [{"00": (35, 1)}] return z_bins class American(DoubleZero, Wheel): pass class European(Zero, Wheel): pass # Some global objects used by a WSGI application function american = American(9973) european = European(9973) test_demonstrate_wheel = """ >>> american.bins[-2:] [{'0': (35, 1)}, {'00': (35, 1)}] >>> european.bins[-1] {'0': (35, 1)} >>> for i in range(7): ... print(american.spin()) {'25': (35, 1), 'Red': (1, 1), 'Hi': (1, 1), 'Odd': (1, 1)} {'18': (35, 1), 'Red': (1, 1), 'Lo': (1, 1), 'Even': (1, 1)} {'20': (35, 1), 'Black': (1, 1), 'Hi': (1, 1), 'Even': (1, 1)} {'21': (35, 1), 'Red': (1, 1), 'Hi': (1, 1), 'Odd': (1, 1)} {'32': (35, 1), 'Red': (1, 1), 'Hi': (1, 1), 'Even': (1, 1)} {'34': (35, 1), 'Red': (1, 1), 'Hi': (1, 1), 'Even': (1, 1)} {'21': (35, 1), 'Red': (1, 1), 'Hi': (1, 1), 'Odd': (1, 1)} >>> for i in range(7): ... print(european.spin()) {'25': (35, 1), 'Red': (1, 1), 'Hi': (1, 1), 'Odd': (1, 1)} {'18': (35, 1), 'Red': (1, 1), 'Lo': (1, 1), 'Even': (1, 1)} {'20': (35, 1), 'Black': (1, 1), 'Hi': (1, 1), 'Even': (1, 1)} {'21': (35, 1), 'Red': (1, 1), 'Hi': (1, 1), 'Odd': (1, 1)} {'32': (35, 1), 'Red': (1, 1), 'Hi': (1, 1), 'Even': (1, 1)} {'34': (35, 1), 'Red': (1, 1), 'Hi': (1, 1), 'Even': (1, 1)} {'21': (35, 1), 'Red': (1, 1), 'Hi': (1, 1), 'Odd': (1, 1)} """ import sys import wsgiref.util import json if TYPE_CHECKING: from wsgiref.types import WSGIApplication, WSGIEnvironment, StartResponse def wsgi_wheel( environ: "WSGIEnvironment", start_response: "StartResponse" ) -> Iterable[bytes]: request = wsgiref.util.shift_path_info(environ) # 1. Parse. print("wheel", repr(request), file=sys.stderr) # 2. Logging. if request and request.lower().startswith("eu"): # 3. Evaluate. winner = european.spin() else: winner = american.spin() status = "200 OK" # 4. Respond. headers = [("Content-Type", "text/plain; charset=utf-8")] start_response(status, headers) return [json.dumps(winner).encode("UTF-8")] # A function we can call to start a server # which handles a finite number of requests. # Handy for testing. from wsgiref.simple_server import make_server def roulette_server(count: int = 1) -> None: httpd = make_server("", 8080, wsgi_wheel) if count is None: httpd.serve_forever() else: for c in range(count): httpd.handle_request() # REST Client # ------------- # A REST client that simply loads a JSON document. import http.client import json from typing import NamedTuple class Response(NamedTuple): status: int headers: Dict[str, str] content: Optional[Any] def __str__(self) -> str: return f"{self.status}: {self.content}" def json_get(path: str = "/") -> Response: rest = http.client.HTTPConnection("localhost", 8080, timeout=5) rest.request("GET", path) response = rest.getresponse() # print(f"client: {response.status} {response.reason}") # print(f" {response.getheaders()}") raw = response.read().decode("utf-8") # print(f" {raw}") try: document = json.loads(raw) except json.decoder.JSONDecodeError as ex: document = None return Response(response.status, dict(response.getheaders()), document) # Roulette Demo # -------------- # When run as the main script, start a server and interact with it. # Note that the subprocess will inherit the state of the wheel from the parent # process; the results are therefore based on the seed, and deterministic. def server() -> None: import concurrent.futures import time with concurrent.futures.ProcessPoolExecutor() as executor: # We'll make four requests. This allows for a very clean termination of a test server. srvr = executor.submit(roulette_server, 4) time.sleep(0.1) # Wait for the server to start r1 = json_get() r2 = json_get() r3 = json_get("/european/") r4 = json_get("/european/") assert ( str(r1) == "200: {'22': [35, 1], 'Black': [1, 1], 'Hi': [1, 1], 'Even': [1, 1]}" ) assert ( str(r2) == "200: {'15': [35, 1], 'Black': [1, 1], 'Lo': [1, 1], 'Odd': [1, 1]}" ) assert ( str(r3) == "200: {'22': [35, 1], 'Black': [1, 1], 'Hi': [1, 1], 'Even': [1, 1]}" ) assert ( str(r4) == "200: {'15': [35, 1], 'Black': [1, 1], 'Lo': [1, 1], 'Odd': [1, 1]}" ) __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=True) server() ================================================ FILE: Chapter_13/ch13_ex2.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 13. Example 2. """ # Problem Domain # ============== from dataclasses import dataclass, asdict, astuple from typing import List, Dict, Any, Tuple, NamedTuple import random @dataclass(frozen=True) class Domino: v_0: int v_1: int @property def double(self): return self.v_0 == self.v_1 def __repr__(self): if self.double: return f"Double({self.v_0})" else: return f"Domino({self.v_0}, {self.v_1})" class Boneyard: """ >>> random.seed(2) >>> b = Boneyard(limit=6) >>> len(b._dominoes) 28 >>> b.deal(tiles=7, hands=2) [[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)]] """ def __init__(self, limit=6): self._dominoes = [ Domino(x, y) for x in range(0, limit + 1) for y in range(0, x + 1) ] random.shuffle(self._dominoes) def deal(self, tiles: int = 7, hands: int = 4) -> List[List[Tuple[int, int]]]: if tiles * hands > len(self._dominoes): raise ValueError(f"Invalid tiles={tiles}, hands={hands}") return [self._dominoes[h:h + tiles] for h in range(0, tiles * hands, tiles)] # FLASK Restful Web Service # ========================= from typing import Dict, Any, Tuple from flask import Flask, jsonify, abort from http import HTTPStatus # Application Server app = Flask(__name__) @app.route("/dominoes/") def dominoes(n: str) -> Tuple[Dict[str, Any], int]: try: hand_size = int(n) except ValueError: abort(HTTPStatus.BAD_REQUEST) if app.env == "development": random.seed(2) b = Boneyard(limit=6) hand_0 = b.deal(hand_size)[0] app.logger.info("Send %r", hand_0) return jsonify(status="OK", dominoes=[astuple(d) for d in hand_0]), HTTPStatus.OK @app.route("/hands//dominoes/") def hands(h: int, c: int) -> Tuple[Dict[str, Any], int]: if h == 0 or c == 0: return jsonify( status="Bad Request", error=[f"hands={h!r}, dominoes={c!r} is invalid"] ), HTTPStatus.BAD_REQUEST if app.env == "development": random.seed(2) b = Boneyard(limit=6) try: hand_list = b.deal(c, h) except ValueError as ex: return jsonify(status="Bad Request", error=ex.args), HTTPStatus.BAD_REQUEST app.logger.info("Send %r", hand_list) return jsonify( status="OK", dominoes=[[astuple(d) for d in hand] for hand in hand_list] ), HTTPStatus.OK OPENAPI_SPEC = { "openapi": "3.0.0", "info": { "description": "Deals simple hands of dominoes", "version": "2019.02", "title": "Chapter 13. Example 2", }, "paths": {}, } @app.route("/openapi.json") def openapi() -> Dict[str, Any]: """ >>> client = app.test_client() >>> response = client.get("/openapi.json") >>> response.get_json()['openapi'] '3.0.0' >>> response.get_json()['info']['title'] 'Chapter 13. Example 2' """ # See dominoes_openapi.json for full specification return jsonify(OPENAPI_SPEC) test_openapi_spec = """ >>> random.seed(2) >>> client = app.test_client() >>> response = client.get("/openapi.json") >>> response.get_json()['openapi'] '3.0.0' >>> response.get_json()['info']['title'] 'Chapter 13. Example 2' >>> response = client.get("/dominoes/5") >>> response.status '200 OK' >>> response.status_code 200 >>> response.get_json() {'dominoes': [[2, 0], [5, 5], [5, 2], [5, 0], [0, 0]], 'status': 'OK'} >>> document = response.get_json() >>> hand = list(Domino(*d) for d in document['dominoes']) >>> hand [Domino(2, 0), Double(5), Domino(5, 2), Domino(5, 0), Double(0)] >>> response = client.get("hands/2/dominoes/7") >>> response.status '200 OK' >>> response.status_code 200 >>> document = response.get_json() >>> document {'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'} >>> hands = list(list(Domino(*d) for d in h) for h in document['dominoes']) >>> for player, h in enumerate(hands): ... for d in h: ... if d.double: ... print(player, d) 0 Double(3) 1 Double(4) >>> response = client.get("hands/nope/dominoes/7") >>> response.status '404 NOT FOUND' >>> response = client.get("hands/0/dominoes/nope") >>> response.status '404 NOT FOUND' >>> response = client.get("hands/0/dominoes/7") >>> response.status '400 BAD REQUEST' >>> response.get_json() {'error': ['hands=0, dominoes=7 is invalid'], 'status': 'Bad Request'} >>> response = client.get("hands/4/dominoes/0") >>> response.status '400 BAD REQUEST' >>> response.get_json() {'error': ['hands=4, dominoes=0 is invalid'], 'status': 'Bad Request'} >>> response = client.get("hands/7/dominoes/7") >>> response.status '400 BAD REQUEST' >>> response.get_json() {'error': ['Invalid tiles=7, hands=7'], 'status': 'Bad Request'} """ __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_13/ch13_ex3.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 13. Example 3. """ # Problem Domain # ============== from typing import Dict, Any, Tuple, List from dataclasses import dataclass, asdict import random import secrets from enum import Enum class Status(str, Enum): UPDATED = "Updated" CREATED = "Created" @dataclass class Dice: roll: List[int] identifier: str status: str def reroll(self, keep_positions: List[int]) -> None: for i in range(len(self.roll)): if i not in keep_positions: self.roll[i] = random.randint(1, 6) self.status = Status.UPDATED def make_dice(n_dice: int) -> Dice: # Could also be a @classmethod return Dice( roll=[random.randint(1, 6) for _ in range(n_dice)], identifier=secrets.token_urlsafe(8), status=Status.CREATED, ) # FLASK Restful Web Service # ========================= from flask import Flask, jsonify, request, url_for, Blueprint, current_app, abort from typing import Dict, Any, Tuple, List from http import HTTPStatus OPENAPI_SPEC = { "openapi": "3.0.0", "info": { "title": "Chapter 13. Example 3", "version": "2019.02", "description": "Rolls dice", }, "paths": { "/rolls": { "post": { "description": "first roll", "responses": {201: {"description": "Success"}}, }, "get": { "description": "current state", "responses": {200: {"description": "Current state"}}, }, "patch": { "description": "subsequent roll", "responses": {200: {"description": "Modified"}}, } } } } SESSIONS: Dict[str, Dice] = {} rolls = Blueprint("rolls", __name__) @rolls.route("/openapi.json") def openapi() -> Dict[str, Any]: # See dice_openapi.json for full specification return jsonify(OPENAPI_SPEC) @rolls.route("/rolls", methods=["POST"]) def make_roll() -> Tuple[Dict[str, Any], HTTPStatus, Dict[str, str]]: body = request.get_json(force=True) if set(body.keys()) != {"dice"}: raise BadRequest(f"Extra fields in {body!r}") try: n_dice = int(body["dice"]) except ValueError as ex: raise BadRequest(f"Bad 'dice' value in {body!r}") dice = make_dice(n_dice) SESSIONS[dice.identifier] = dice current_app.logger.info(f"Rolled roll={dice!r}") headers = {"Location": url_for("rolls.get_roll", identifier=dice.identifier)} return jsonify(asdict(dice)), HTTPStatus.CREATED, headers @rolls.route("/rolls/", methods=["GET"]) def get_roll(identifier) -> Tuple[Dict[str, Any], HTTPStatus]: if identifier not in SESSIONS: abort(HTTPStatus.NOT_FOUND) return jsonify(asdict(SESSIONS[identifier])), HTTPStatus.OK @rolls.route("/rolls/", methods=["PATCH"]) def patch_roll(identifier) -> Tuple[Dict[str, Any], HTTPStatus]: if identifier not in SESSIONS: abort(HTTPStatus.NOT_FOUND) body = request.get_json(force=True) if set(body.keys()) != {"keep"}: raise BadRequest(f"Extra fields in {body!r}") try: keep_positions = [int(d) for d in body["keep"]] except ValueError as ex: raise BadRequest(f"Bad 'keep' value in {body!r}") dice = SESSIONS[identifier] dice.reroll(keep_positions) return jsonify(asdict(dice)), HTTPStatus.OK class BadRequest(Exception): pass def make_app() -> Flask: app = Flask(__name__) # Only used for HTML-based sessions... # app.secret_key = 'lt0oypOUT9Vu7cbyivfv9hdEzWLlEf_w' @app.errorhandler(BadRequest) def error_message(ex) -> Tuple[Dict[str, Any], HTTPStatus]: current_app.logger.error(f"{ex.args}") return jsonify(status="Bad Request", message=ex.args), HTTPStatus.BAD_REQUEST app.register_blueprint(rolls) return app test_not_found = """ >>> random.seed(2) >>> app = make_app() >>> client = app.test_client() >>> response = client.get("/rando_path") >>> response.status '404 NOT FOUND' >>> response.status_code 404 >>> response.data[:55] b'' """ test_post_get_patch = """ >>> random.seed(2) >>> app = make_app() >>> client = app.test_client() >>> response1 = client.post("/rolls", json={"dice": 5}) >>> response1.status '201 CREATED' >>> response1.status_code 201 >>> document1 = response1.get_json() >>> document1['roll'] [1, 1, 1, 3, 2] >>> document1['status'] 'Created' >>> document1['identifier'] in SESSIONS True >>> response1.headers['Location'].endswith(document1['identifier']) True >>> response1.headers['Location'].startswith('http://localhost/rolls/') True >>> response2 = client.get(f"/rolls/{document1['identifier']}") >>> response2.status '200 OK' >>> document2 = response2.get_json() >>> document2['roll'] [1, 1, 1, 3, 2] >>> document2['status'] 'Created' >>> document2['identifier'] == document1['identifier'] True >>> response3 = client.patch(f"/rolls/{document1['identifier']}", json={"keep": [0, 1, 2]}) >>> response3.status '200 OK' >>> document3 = response3.get_json() >>> document3['roll'] [1, 1, 1, 6, 6] >>> document3['status'] 'Updated' >>> document3['identifier'] == document1['identifier'] True """ test_post_bad_get = """ >>> random.seed(2) >>> app = make_app() >>> client = app.test_client() >>> response1 = client.post("/rolls", json={"dice": 5}) >>> response1.status '201 CREATED' >>> document1 = response1.get_json() >>> document1['roll'] [1, 1, 1, 3, 2] >>> document1['status'] 'Created' # Definitely NOT the identifier. >>> response2 = client.get(f"/rolls/xyzzy_{document1['identifier']}") >>> response2.status '404 NOT FOUND' >>> document2 = response2.data >>> document2[:55] b'' """ test_bad_post = """ >>> random.seed(2) >>> app = make_app() >>> client = app.test_client() >>> response1 = client.post("/rolls", json={"not_the_document": "you were looking for"}) >>> response1.status '400 BAD REQUEST' >>> document1 = response1.get_json() >>> document1['status'] 'Bad Request' >>> document1['message'] ["Extra fields in {'not_the_document': 'you were looking for'}"] """ __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_13/ch13_ex4.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 13. Example 4. .. note:: This example can't easily be tested by the automated test scripts. It requires a server to be started. """ import requests def demo(headers=None): headers = headers or {} get_openapi = requests.get("http://127.0.0.1:5000/openapi.json") if get_openapi.status_code == 200: document = get_openapi.json() if not document['openapi'].startswith('3.0'): raise Exception("Not a valid OpenAPI Version") if document['info']['version'] != '2019.02': raise Exception("Not a useful release") roll = requests.post("http://127.0.0.1:5000/roll", json={"dice": 5}, headers=headers) print(roll.status_code, roll.reason, roll.headers) body = roll.json() print(body) identifier = body["identifier"] roll_url = roll.headers["Location"] response = requests.get(f"http://127.0.0.1:5000/roll/{identifier}", headers=headers) print(response.status_code, response.reason) print(response.json()) response2 = requests.get(roll_url, headers=headers) print(response2.status_code, response2.reason) print(response2.json()) reroll = requests.patch(roll_url, json={"keep": [0, 1]}, headers=headers) print(reroll.status_code, reroll.reason) print(reroll.json()) import pytest from unittest.mock import Mock, call @pytest.fixture def mock_requests(monkeypatch): r0 = requests.Response() r0.status_code = 200 r0._content = b'{"openapi": "3.0.0", "info": {"version": "2019.02"}}' r1 = requests.Response() r1.status_code = 200 r1._content = b'{"valid": "json"}' r2 = requests.Response() r2.status_code = 201 r2.headers = {"Location": "http://mocked/roll/mockity-mock-mock"} r2._content = b'{"status": "OK", "roll": [1, 2, 3, 4, 5, 6], "identifier": "mockity-mock-mock"}' mock_module = Mock( get=Mock(side_effect=[r0, r1, r1]), post=Mock(return_value=r2), patch=Mock(return_value=r1), ) monkeypatch.setitem(globals(), "requests", mock_module) return mock_module def test_demo(mock_requests): demo() assert ( mock_requests.mock_calls == [ call.get("http://127.0.0.1:5000/openapi.json"), call.post("http://127.0.0.1:5000/roll", json={"dice": 5}, headers={}), call.get("http://127.0.0.1:5000/roll/mockity-mock-mock", headers={}), call.get("http://mocked/roll/mockity-mock-mock", headers={}), call.patch("http://mocked/roll/mockity-mock-mock", json={"keep": [0, 1]}, headers={}), ] ) if __name__ == "__main__": pytest.main([__file__]) demo() # demo({"Api-Key": "some_key"}) # demo({"Api-Key": "nope"}) ================================================ FILE: Chapter_13/ch13_ex5.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 13. Example 5. """ # FLASK Restful Web Service with State & Open SSL Cert # ==================================================== # Creating a certificate for common name will be 127.0.0.1 (since we're running locally) # # $ openssl req -newkey rsa:2048 -nodes -keyout key.pem -x509 -days 365 -out certificate.pem # Generating a RSA private key # ......................................+++++ # ....................+++++ # writing new private key to 'key.pem' # ----- # You are about to be asked to enter information that will be incorporated # into your certificate request. # What you are about to enter is what is called a Distinguished Name or a DN. # There are quite a few fields but you can leave some blank # For some fields there will be a default value, # If you enter '.', the field will be left blank. # ----- # Country Name (2 letter code) [AU]:US # State or Province Name (full name) [Some-State]:VA # Locality Name (eg, city) []:McLean # Organization Name (eg, company) [Internet Widgits Pty Ltd]:Mastering OO Python 2e # Organizational Unit Name (eg, section) []: # Common Name (e.g. server FQDN or YOUR name) []:127.0.0.1 # Email Address []: # Using the certificate # # $ FLASK_APP=ch13_ex5.py FLASK_ENV=development python -m flask run --cert certificate.pem --key key.pem # # Your browser will have questions about the authority, since this is self-signed. from flask import Flask, jsonify, request, url_for, Blueprint, current_app, json, abort from http import HTTPStatus from typing import Dict, Any, Tuple, Callable, Set from pathlib import Path import secrets import random from functools import wraps from typing import Callable, Set VALID_API_KEYS: Set[str] = set() def init_app(app): global VALID_API_KEYS if app.env == "development": VALID_API_KEYS = {"read-only", "admin", "write"} else: app.logger.info("Loading from {app.config['VALID_KEYS']}") raw_lines = (Path(app.config["VALID_KEYS"]).read_text().splitlines()) VALID_API_KEYS = set(filter(None, raw_lines)) def valid_api_key(view_function: Callable) -> Callable: @wraps(view_function) def confirming_view_function(*args, **kw): api_key = request.headers.get("Api-Key") if api_key not in VALID_API_KEYS: current_app.logger.error(f"Rejecting Api-Key:{api_key!r}") abort(HTTPStatus.UNAUTHORIZED) return view_function(*args, **kw) return confirming_view_function def api_key_in(valid_values: Set[str]): def concrete_decorator(view_function: Callable) -> Callable: @wraps(view_function) def confirming_view_function(*args, **kw): api_key = request.headers.get("Api-Key") if api_key not in valid_values: current_app.logger.error(f"Rejecting Api-Key:{api_key!r}") abort(HTTPStatus.UNAUTHORIZED) return view_function(*args, **kw) return confirming_view_function return concrete_decorator SESSIONS: Dict[str, Any] = {} roll = Blueprint("roll", __name__) @roll.route("/openapi.json") def openapi() -> Dict[str, Any]: source_path = next(Path.cwd().glob("**/dice_openapi.json")) return jsonify(json.loads(source_path.read_text())) @roll.route("/roll", methods=["POST"]) @valid_api_key def create_roll() -> Tuple[Any, HTTPStatus, Dict[str, Any]]: body = request.get_json(force=True) if set(body.keys()) != {"dice"}: raise BadRequest(f"Extra fields in {body!r}") try: n_dice = int(body["dice"]) except ValueError as ex: raise BadRequest(f"Bad 'dice' value in {body!r}") roll = [random.randint(1, 6) for _ in range(n_dice)] identifier = secrets.token_urlsafe(8) SESSIONS[identifier] = roll current_app.logger.info(f"Rolled roll={roll!r}, id={identifier!r}") headers = {"Location": url_for("roll.get_roll", identifier=identifier)} return jsonify( roll=roll, identifier=identifier, status="Created" ), HTTPStatus.CREATED, headers @roll.route("/roll/", methods=["GET"]) @valid_api_key def get_roll(identifier) -> Tuple[Dict[str, Any], HTTPStatus]: if identifier not in SESSIONS: abort(HTTPStatus.NOT_FOUND) return jsonify( roll=SESSIONS[identifier], identifier=identifier, status="OK" ), HTTPStatus.OK @roll.route("/roll/", methods=["PATCH"]) @valid_api_key def patch_roll(identifier) -> Tuple[Dict[str, Any], HTTPStatus]: if identifier not in SESSIONS: raise BadRequest(f"Unknown {identifier!r}") body = request.get_json(force=True) if set(body.keys()) != {"keep"}: raise BadRequest(f"Extra fields in {body!r}") try: keep_positions = [int(d) for d in body["keep"]] except ValueError as ex: raise BadRequest(f"Bad 'keep' value in {body!r}") roll = SESSIONS[identifier] for i in range(len(roll)): if i not in keep_positions: roll[i] = random.randint(1, 6) SESSIONS[identifier] = roll return jsonify( roll=SESSIONS[identifier], identifier=identifier, status="OK" ), HTTPStatus.OK class BadRequest(Exception): pass def make_app() -> Flask: app = Flask(__name__) app.config["VALID_KEYS"] = "valid_keys_file.txt" app.config["ENV"] = "development" @app.errorhandler(BadRequest) def error_message(ex) -> Tuple[Dict[str, Any], HTTPStatus]: current_app.logger.error(f"{ex.args}") return jsonify(status="Bad Request", message=ex.args), HTTPStatus.BAD_REQUEST init_app(app) app.register_blueprint(roll) return app test_get_openapi_spec = """ >>> random.seed(2) >>> app = make_app() >>> client = app.test_client() >>> response = client.get("/openapi.json") >>> response.status '200 OK' >>> response.status_code 200 >>> spec = response.get_json() >>> spec['info']['version'] '2019.02' >>> spec['info']['title'] 'Chapter 13. Examples 3 and 5' """ test_get_bad_post_roll = """ >>> random.seed(2) >>> app = make_app() >>> client = app.test_client() >>> response = client.post("/roll") >>> response.status '401 UNAUTHORIZED' """ test_get_good_post_roll = """ >>> random.seed(2) >>> app = make_app() >>> client = app.test_client() >>> response = client.post("/roll", json={"dice": 5}, headers=[("Api-Key", "admin")]) >>> response.status '201 CREATED' >>> document = response.get_json() >>> document['roll'] [1, 1, 1, 3, 2] """ __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_13/ch13_ex6.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 13. Example 6. """ # Multiprocessing Example # ========================= # We want a Simulation process to cough up some statistics # Import the simulation model... from Chapter_13.simulation_model import * import multiprocessing class Simulation(multiprocessing.Process): def __init__( self, setup_queue: multiprocessing.SimpleQueue, result_queue: multiprocessing.SimpleQueue, ) -> None: self.setup_queue = setup_queue self.result_queue = result_queue super().__init__() def run(self) -> None: """Waits for a termination""" print(f"{self.__class__.__name__} start") item = self.setup_queue.get() while item != (None, None): table, player = item self.sim = Simulate(table, player, samples=1) results = list(self.sim) self.result_queue.put((table, player, results[0])) item = self.setup_queue.get() print(f"{self.__class__.__name__} finish") # We want a Summarization process to gather and summarize all those stats. class Summarize(multiprocessing.Process): def __init__(self, queue: multiprocessing.SimpleQueue) -> None: self.queue = queue super().__init__() def run(self) -> None: """Waits for a termination""" print(f"{self.__class__.__name__} start") count = 0 item = self.queue.get() while item != (None, None, None): print(item) count += 1 item = self.queue.get() print(f"{self.__class__.__name__} finish {count}") # Create and run the simulation # ----------------------------- def server_6() -> None: # Two queues setup_q: multiprocessing.SimpleQueue = multiprocessing.SimpleQueue() results_q: multiprocessing.SimpleQueue = multiprocessing.SimpleQueue() # The summarization process: waiting for work result = Summarize(results_q) result.start() # The simulation process: also waiting for work. # We might want to create a Pool of these so that # we can get even more done at one time. simulators = [] for i in range(4): sim = Simulation(setup_q, results_q) sim.start() simulators.append(sim) # Queue up some objects to work on. table = Table(decks=6, limit=50, dealer=Hit17(), split=ReSplit(), payout=(3, 2)) for bet in Flat, Martingale, OneThreeTwoSix: player = Player(SomeStrategy(), bet(), 100, 25) for sample in range(5): setup_q.put((table, player)) # Queue a terminator for each simulator. for sim in simulators: setup_q.put((None, None)) # Wait for the simulations to all finish. for sim in simulators: sim.join() # Queue up a results terminator. # Results processing done? results_q.put((None, None, None)) result.join() del results_q del setup_q __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) server_6() ================================================ FILE: Chapter_13/dice_openapi.json ================================================ { "openapi": "3.0.0", "info": { "description": "Rolls dice", "version": "2019.02", "title": "Chapter 13. Examples 3 and 5" }, "components": { "schemas": { "roll": { "type": "object", "properties": { "status": { "type": "string" }, "roll": { "type": "array", "items": { "type": "integer" } }, "identifier": { "type": "string" } } } } }, "paths": { "/roll": { "post": { "summary": "Creates a roll", "responses": { "201": { "description": "The new roll", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/roll" } } }, "headers": { "Location": { "description": "URL to use", "schema": { "type": "string" } } } } } } }, "/roll/{id}": { "get": { "summary": "Repeats a roll's details", "parameters": [ { "in": "path", "name": "id", "description": "identifier", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "An existing roll", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/roll" } } } }, "400": { "description": "Invalid input" }, "404": { "description": "Unknown" } } }, "patch": { "summary": "Modifies a roll", "parameters": [ { "in": "path", "name": "id", "description": "identifier", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "The revised roll", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/roll" } } } }, "400": { "description": "Invalid input" }, "404": { "description": "Unknown" } } } } } } ================================================ FILE: Chapter_13/dominoes_openapi.json ================================================ { "openapi": "3.0.0", "info": { "description": "Deals simple hands of dominoes", "version": "2019.02", "title": "Chapter 13. Example 2" }, "components": { "schemas": { "dominoes": { "type": "array", "items": { "type": "array", "minLength": 2, "maxLength": 2, "items": { "type": "number" } } } } }, "paths": { "/cards/{n}": { "get": { "summary": "Get a hand of dominoes", "parameters": [ { "in": "path", "name": "n", "description": "Number of dominoes", "required": true, "schema": { "type": "number" } } ], "responses": { "200": { "description": "Hand of dominoes", "content": { "application/json": { "schema": { "type": "object", "properties": { "status": { "type": "string" }, "cards": { "$ref": "#/components/schemas/dominoes" } } } } } }, "400": { "description": "Invalid input" } } } }, "/hands/{h}/cards/{c}": { "get": { "summary": "Get several hands of dominoes", "parameters": [ { "in": "path", "name": "h", "description": "Number of Hands", "required": true, "schema": { "type": "number" } }, { "in": "path", "name": "c", "description": "Number of dominoes", "required": true, "schema": { "type": "number" } } ], "responses": { "200": { "description": "List of hands of dominoes", "content": { "application/json": { "schema": { "type": "object", "properties": { "status": { "type": "string" }, "hands": { "type": "array", "items": { "$ref": "#/components/schemas/dominoes" } } } } } } }, "400": { "description": "Invalid input" } } } } } } ================================================ FILE: Chapter_13/simulation_model.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 13. Example 5 -- simulation model. """ from typing import Tuple, Iterator # Mock Object Model # ===================== # A set of class hierarchies that we'll use for several examples. # The content is mostly mocks. class DealerRule: pass class Hit17(DealerRule): """Hits soft 17""" pass class Stand17(DealerRule): """Stands on soft 17""" pass class SplitRule: pass class ReSplit(SplitRule): """Simplistic resplit anything.""" pass class NoReSplit(SplitRule): """Simplistic no resplit.""" pass class NoReSplitAces(SplitRule): """One card only to aces; no resplit.""" pass class Table: def __init__(self, decks: int, limit: int, dealer: DealerRule, split: SplitRule, payout: Tuple[int, int]) -> None: self.decks = decks self.limit = limit self.dealer = dealer self.split = split self.payout = payout def as_tuple(self): return ( self.decks, self.limit, self.dealer.__class__.__name__, self.split.__class__.__name__, self.payout, ) class PlayerStrategy: pass class SomeStrategy(PlayerStrategy): pass class AnotherStrategy(PlayerStrategy): pass class BettingStrategy: def bet(self) -> int: raise NotImplementedError("No bet method") def record_win(self) -> None: pass def record_loss(self) -> None: pass class Flat(BettingStrategy): pass class Martingale(BettingStrategy): pass class OneThreeTwoSix(BettingStrategy): pass class Player: def __init__(self, play: PlayerStrategy, betting: BettingStrategy, rounds: int, stake: int) -> None: self.play = play self.betting = betting self.max_rounds = rounds self.init_stake = float(stake) def reset(self) -> None: self.rounds = self.max_rounds self.stake = self.init_stake def as_tuple(self) -> Tuple: return ( self.play.__class__.__name__, self.betting.__class__.__name__, self.max_rounds, self.init_stake, self.rounds, self.stake, ) # A mock simulation which is built from the above mock objects. import random class Simulate: def __init__( self, table: Table, player: Player, samples: int ) -> None: """Define table, player and number of samples.""" self.table = table self.player = player self.samples = samples def __iter__(self) -> Iterator[Tuple]: """Yield statistical samples.""" x, y = self.table.payout blackjack_payout = x / y for count in range(self.samples): self.player.reset() while self.player.stake > 0 and self.player.rounds > 0: self.player.rounds -= 1 outcome = random.random() if outcome < 0.579: self.player.stake -= 1 elif 0.579 <= outcome < 0.883: self.player.stake += 1 elif 0.883 <= outcome < 0.943: # a "push" pass else: # 0.943 <= outcome self.player.stake += blackjack_payout yield self.table.as_tuple() + self.player.as_tuple() ================================================ FILE: Chapter_14/__init__.py ================================================ ================================================ FILE: Chapter_14/ch14_ex1.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 14. Example 1. """ from Chapter_14.simulation_model import * # A typical main program using the above class definitions from pathlib import Path from typing import List, Any, TextIO, Iterator, Union import csv def simulate_blackjack() -> None: dealer_rule = Hit17() split_rule = NoReSplitAces() table = Table( decks=6, limit=50, dealer=dealer_rule, split=split_rule, payout=(3, 2) ) player_rule = SomeStrategy() betting_rule = Flat() player = Player( play=player_rule, betting=betting_rule, max_rounds=100, init_stake=50 ) simulator = Simulate( table, player, samples=100 ) result_path = Path.cwd() / "data" / "ch14_simulation.dat" with result_path.open("w", newline="") as results: wtr = csv.writer(results) wtr.writerows(simulator) if __name__ == "__main__": simulate_blackjack() check(Path.cwd() / "data" / "ch14_simulation.dat") # Locations # ============ # Tyical list of locations for config def location_list(config_name: str = "someapp.config") -> List[Path]: config_locations = ( Path(__file__), # Path("~someapp").expanduser(), if a special username Path("/opt") / "someapp", Path("/etc") / "someapp", Path.home(), Path.cwd(), ) candidates = (dir / config_name for dir in config_locations) config_paths = [path for path in candidates if path.exists()] return config_paths test_location_list = """ >>> import os >>> previous = os.getcwd() >>> os.chdir("Chapter_14") >>> location_list() # doctest: +ELLIPSIS [PosixPath('.../Chapter_14/someapp.config')] >>> os.chdir(previous) """ # INI files # ========= # Sample INI files import io ini_file = io.StringIO( """ ; Default casino rules [table] dealer= Hit17 split= NoResplitAces decks= 6 limit= 50 payout= (3,2) ; Player with SomeStrategy [player] play= SomeStrategy betting= Flat max_rounds= 100 init_stake= 50 [simulator] samples= 100 outputfile= data/ch14_simulation1.dat """ ) ini2_file = io.StringIO( """ ; Need to compare with OtherStrategy [player] play= OtherStrategy betting= Flat max_rounds= 100 init_stake= 50 [simulator] samples= 100 outputfile= data/ch14_simulation1a.dat """ ) import configparser # Using the config to build objects def main_ini(config: configparser.ConfigParser) -> None: dealer_nm = config.get("table", "dealer", fallback="Hit17") dealer_rule = { "Hit17": Hit17(), "Stand17": Stand17(), }.get(dealer_nm, Hit17()) split_nm = config.get("table", "split", fallback="ReSplit") split_rule = { "ReSplit": ReSplit(), "NoReSplit": NoReSplit(), "NoReSplitAces": NoReSplitAces(), }.get(split_nm, ReSplit()) decks = config.getint("table", "decks", fallback=6) limit = config.getint("table", "limit", fallback=100) payout = eval( config.get("table", "payout", fallback="(3,2)") ) table = Table( decks=decks, limit=limit, dealer=dealer_rule, split=split_rule, payout=payout ) player_nm = config.get( "player", "play", fallback="SomeStrategy") player_rule = { "SomeStrategy": SomeStrategy(), "AnotherStrategy": AnotherStrategy() }.get(player_nm, SomeStrategy()) bet_nm = config.get("player", "betting", fallback="Flat") betting_rule = { "Flat": Flat(), "Martingale": Martingale(), "OneThreeTwoSix": OneThreeTwoSix() }.get(bet_nm, Flat()) max_rounds = config.getint("player", "max_rounds", fallback=100) init_stake = config.getint("player", "init_stake", fallback=50) player = Player( play=player_rule, betting=betting_rule, max_rounds=max_rounds, init_stake=init_stake ) outputfile = config.get( "simulator", "outputfile", fallback="blackjack.csv") samples = config.getint("simulator", "samples", fallback=100) simulator = Simulate(table, player, samples=samples) with Path(outputfile).open("w", newline="") as results: wtr = csv.writer(results) wtr.writerows(simulator) # Sample Main Script to parse and start the application. if __name__ == "__main__": config = configparser.ConfigParser() config.read_file(ini_file) config.read_file(ini2_file) # Could use config.read_string(text), also # When there are multiple candidate locations, config.read(location_list("blackjack.ini")) for name, section in config.items(): print(name) for p in config.items(name): print(" ", p) main_ini(config) check(Path(config.get("simulator", "outputfile"))) __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_14/ch14_ex2.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 14. Example 2. """ from Chapter_14.simulation_model import * # A typical main program using the above class definitions from pathlib import Path from typing import List, Any, TextIO, Iterator, Union, Type import csv from dataclasses import dataclass from types import SimpleNamespace # PY files # ======== # Top-level -- v1 # ################ def simulate(table: Table, player: Player, outputpath: Path, samples: int) -> None: simulator = Simulate(table, player, samples=samples) with outputpath.open("w", newline="") as results: wtr = csv.writer(results) wtr.writerows(simulator) # Configuration in the main script # # ``from simulator import *`` # def simulate_SomeStrategy_Flat() -> None: dealer_rule = Hit17() split_rule = NoReSplitAces() table = Table( decks=6, limit=50, dealer=dealer_rule, split=split_rule, payout=(3, 2) ) player_rule = SomeStrategy() betting_rule = Flat() player = Player( play=player_rule, betting=betting_rule, max_rounds=100, init_stake=50 ) simulate(table, player, Path.cwd() / "data" / "ch14_simulation2a.dat", 100) if __name__ == "__main__": simulate_SomeStrategy_Flat() check(Path.cwd() / "data" / "ch14_simulation2a.dat") # Top-level -- v2b # ################ # Stuff imported from some application module # # ``from simulator import *`` # class AppConfig: """ These really are class-level ("static") values. This is *not* a dataclass-style definition of instance variables. """ table: Table player: Player samples: int outputfile: Path def simulate_c(config: Union[Type[AppConfig], SimpleNamespace]) -> None: simulator = Simulate(config.table, config.player, config.samples) with Path(config.outputfile).open("w", newline="") as results: wtr = csv.writer(results) wtr.writerows(simulator) # Configuration in the main script using a Python class definition class Example2(AppConfig): dealer_rule = Hit17() split_rule = NoReSplitAces() table = Table( decks=6, limit=50, dealer=dealer_rule, split=split_rule, payout=(3, 2) ) player_rule = SomeStrategy() betting_rule = Flat() player = Player( play=player_rule, betting=betting_rule, max_rounds=100, init_stake=50 ) outputfile = Path.cwd() / "data" / "ch14_simulation2b.dat" samples = 100 if __name__ == "__main__": simulate_c(Example2) check(Path.cwd() / "data" / "ch14_simulation2b.dat") # Top-level -- v2c # ################ # SimpleNamespace version c from types import SimpleNamespace config2c = SimpleNamespace( dealer_rule=Hit17(), split_rule=NoReSplitAces(), player_rule=SomeStrategy(), betting_rule=Flat(), outputfile=Path.cwd() / "data" / "ch14_simulation2c.dat", samples=100, ) config2c.table = Table( decks=6, limit=50, dealer=config2c.dealer_rule, split=config2c.split_rule, payout=(3, 2), ) config2c.player = Player( play=config2c.player_rule, betting=config2c.betting_rule, max_rounds=100, init_stake=50, ) if __name__ == "__main__": simulate_c(config2c) check(Path.cwd() / "data" / "ch14_simulation2c.dat") # SimpleNamespace version 2d # ########################## config2d = SimpleNamespace() config2d.dealer_rule = Hit17() config2d.split_rule = NoReSplitAces() config2d.table = Table( decks=6, limit=50, dealer=config2d.dealer_rule, split=config2d.split_rule, payout=(3, 2), ) config2d.player_rule = SomeStrategy() config2d.betting_rule = Flat() config2d.player = Player( play=config2d.player_rule, betting=config2d.betting_rule, max_rounds=100, init_stake=50, ) config2d.outputfile = Path.cwd() / "data" / "ch14_simulation2d.dat" config2d.samples = 100 if __name__ == "__main__": simulate_c(config2d) check(Path.cwd() / "data" / "ch14_simulation2d.dat") # SimpleNamespace version 2e # ########################## def make_config( dealer_rule: DealerRule = Hit17(), split_rule: SplitRule = NoReSplitAces(), decks: int = 6, limit: int = 50, payout: Tuple[int, int] = (3, 2), player_rule: PlayerStrategy = SomeStrategy(), betting_rule: BettingStrategy = Flat(), base_name: str = "ch14_simulation2e.dat", samples: int = 100, ) -> SimpleNamespace: return SimpleNamespace( dealer_rule=dealer_rule, split_rule=split_rule, table=Table( decks=decks, limit=limit, dealer=dealer_rule, split=split_rule, payout=payout, ), payer_rule=player_rule, betting_rule=betting_rule, player=Player( play=player_rule, betting=betting_rule, max_rounds=100, init_stake=50 ), outputfile=Path.cwd() / "data" / base_name, samples=samples, ) if __name__ == "__main__": simulate_c(make_config(base_name="ch14_simulation2e_1.dat")) check(Path.cwd() / "data" / "ch14_simulation2e_1.dat") simulate_c(make_config(dealer_rule=Stand17(), base_name="ch14_simulation2e_2.dat")) check(Path.cwd() / "data" / "ch14_simulation2e_2.dat") __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_14/ch14_ex3.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 14. Example 3. """ from Chapter_14.simulation_model import * from pprint import pprint # A typical main program using the above class definitions from pathlib import Path from typing import List, Any, TextIO, Iterator, Union, Dict import typing import csv from types import SimpleNamespace def simulate(table: Table, player: Player, outputpath: Path, samples: int) -> None: simulator = Simulate(table, player, samples=samples) with outputpath.open("w", newline="") as results: wtr = csv.writer(results) for gamestats in simulator: wtr.writerow(gamestats) # Exec Import # ################ import io py_file = io.StringIO( """ # SomeStrategy setup # Table dealer_rule = Hit17() split_rule = NoReSplitAces() table = Table(decks=6, limit=50, dealer=dealer_rule, split=split_rule, payout=(3,2)) # Player player_rule = SomeStrategy() betting_rule = Flat() player = Player(play=player_rule, betting=betting_rule, max_rounds=100, init_stake=50) # Simulation outputfile = Path.cwd()/"data"/"ch14_simulation3a.dat" samples = 100 """ ) if __name__ == "__main__": code = compile(py_file.read(), "stringio", "exec") assignments: Dict[str, Any] = dict() exec(code, globals(), assignments) config = SimpleNamespace(**assignments) print("Exec Import...") pprint(assignments) print("Table...") print(config.table) simulate(config.table, config.player, config.outputfile, config.samples) check(config.outputfile) # ChainMap and Import # ===================== # Essential Example from collections import ChainMap config_name = "config.py" config_locations = ( Path.cwd(), Path.home(), Path("/etc/thisapp"), # Optionally Path("~thisapp").expanduser(), when an app has a "home" directory Path(__file__), ) candidates = (dir / config_name for dir in config_locations) config_paths = (path for path in candidates if path.exists()) cm_config_1: typing.ChainMap[str, Any] = ChainMap() for path in config_paths: config_layer_1: Dict[str, Any] = {} source_code = path.read_text() exec(source_code, globals(), config_layer_1) cm_config_1.maps.append(config_layer_1) # Demo with Mock files import io py_text = """ # Default casino rules # Table dealer_rule = Hit17() split_rule = NoReSplitAces() table = Table(decks=6, limit=50, dealer=dealer_rule, split=split_rule, payout=(3,2)) # Player player_rule = SomeStrategy() betting_rule = Flat() player = Player(play=player_rule, betting=betting_rule, max_rounds=100, init_stake=50) # Simulation outputfile = Path.cwd()/"data"/"ch14_simulation3b.dat" samples = 100 """ py2_text = """ # Override values # Player player_rule = AnotherStrategy() betting_rule = Martingale() player = Player(play=player_rule, betting=betting_rule, max_rounds=100, init_stake=50) # Simulation outputfile = Path.cwd()/"data"/"ch14_simulation3b.dat" """ test_cm_config = """ >>> default_file = io.StringIO(py_text) >>> override_file = io.StringIO(py2_text) >>> cm_config: typing.ChainMap[str, Any] = ChainMap() >>> for path in override_file, default_file: ... config_layer: Dict[str, Any] = {} ... source_code = path.read() ... exec(source_code, globals(), config_layer) ... cm_config.maps.append(config_layer) >>> cm_config['player_rule'] # doctest: +ELLIPSIS AnotherStrategy() >>> cm_config['betting_rule'] # doctest: +ELLIPSIS Martingale() >>> cm_config['betting_rule'] = "final override" >>> pprint(cm_config) # doctest: +ELLIPSIS ChainMap({'betting_rule': 'final override'}, {'betting_rule': Martingale(), 'outputfile': PosixPath('.../data/ch14_simulation3b.dat'), 'player': Player(play=AnotherStrategy(), betting=Martingale(), max_rounds=100, init_stake=50, rounds=100, stake=50), 'player_rule': AnotherStrategy()}, {'betting_rule': Flat(), 'dealer_rule': Hit17(), 'outputfile': PosixPath('.../data/ch14_simulation3b.dat'), 'player': Player(play=SomeStrategy(), betting=Flat(), max_rounds=100, init_stake=50, rounds=100, stake=50), 'player_rule': SomeStrategy(), 'samples': 100, 'split_rule': NoReSplitAces(), 'table': Table(decks=6, limit=50, dealer=Hit17(), split=NoReSplitAces(), payout=(3, 2))}) """ if __name__ == "__main__": default_file = io.StringIO(py_text) override_file = io.StringIO(py2_text) cm_config: typing.ChainMap[str, Any] = ChainMap() for config_file in override_file, default_file: config_layer: Dict[str, Any] = {} source_code = config_file.read() exec(source_code, globals(), config_layer) cm_config.maps.append(config_layer) print() print("ChainMap") pprint(cm_config) class AttrChainMap(ChainMap): def __getattr__(self, name: str) -> Any: if name == "maps": return self.__dict__["maps"] return super().get(name, None) def __setattr__(self, name: str, value: Any) -> None: if name == "maps": self.__dict__["maps"] = value return self[name] = value test_acm_config = """ >>> default_file = io.StringIO(py_text) >>> override_file = io.StringIO(py2_text) >>> acm_config = AttrChainMap() >>> for path in override_file, default_file: ... config_layer: Dict[str, Any] = {} ... source_code = path.read() ... exec(source_code, globals(), config_layer) ... acm_config.maps.append(config_layer) >>> acm_config['player_rule'] # doctest: +ELLIPSIS AnotherStrategy() >>> acm_config.player_rule # doctest: +ELLIPSIS AnotherStrategy() >>> acm_config.betting_rule # doctest: +ELLIPSIS Martingale() """ if __name__ == "__main__": default_file = io.StringIO(py_text) override_file = io.StringIO(py2_text) config_acm = AttrChainMap() for file in override_file, default_file: config_layer = {} config_source = file.read() exec(config_source, globals(), config_layer) config_acm.maps.append(config_layer) print() print("AttrChainMap") pprint(config_acm) print(config_acm.table) print(config_acm["table"]) simulate(config_acm.table, config_acm.player, config_acm.outputfile, config_acm.samples) check(config_acm.outputfile) __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_14/ch14_ex4.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 14. Example 4. """ from Chapter_14.simulation_model import * # A typical main program using the above class definitions from pathlib import Path from typing import List, Any, TextIO, Iterator, Union, Dict import csv from collections import ChainMap import io # JSON or YAML files # =================== # JSON using dictionary-of-dictionaries nested structures. # This is inconvenient to handle multiple configuration files. import io json_file = io.StringIO( """ { "table":{ "dealer":"Hit17", "split":"NoResplitAces", "decks":6, "limit":50, "payout":[3,2] }, "player":{ "play":"SomeStrategy", "betting":"Flat", "rounds":100, "stake":50 }, "simulator":{ "samples":100, "outputfile":"data/ch14_simulation4a.dat" } } """ ) def main_nested_dict(config: Dict[str, Any]) -> None: dealer_nm = config.get("table", {}).get("dealer", "Hit17") dealer_rule = { "Hit17": Hit17(), "Stand17": Stand17() }.get(dealer_nm, Hit17()) split_nm = config.get("table", {}).get("split", "ReSplit") split_rule = { "ReSplit": ReSplit(), "NoReSplit": NoReSplit(), "NoReSplitAces": NoReSplitAces() }.get(split_nm, ReSplit()) decks = config.get("table", {}).get("decks", 6) limit = config.get("table", {}).get("limit", 100) payout = config.get("table", {}).get("payout", (3, 2)) table = Table( decks=decks, limit=limit, dealer=dealer_rule, split=split_rule, payout=payout ) player_nm = config.get("player", {}).get("play", "SomeStrategy") player_rule = { "SomeStrategy": SomeStrategy(), "AnotherStrategy": AnotherStrategy() }.get( player_nm, SomeStrategy() ) bet_nm = config.get("player", {}).get("betting", "Flat") betting_rule = { "Flat": Flat(), "Martingale": Martingale(), "OneThreeTwoSix": OneThreeTwoSix() }.get( bet_nm, Flat() ) rounds = config.get("player", {}).get("rounds", 100) stake = config.get("player", {}).get("stake", 50) player = Player(play=player_rule, betting=betting_rule, max_rounds=rounds, init_stake=stake) outputfile = config.get("simulator", {}).get("outputfile", "blackjack.csv") samples = config.get("simulator", {}).get("samples", 100) simulator = Simulate(table, player, samples) with Path(outputfile).open("w", newline="") as results: wtr = csv.writer(results) for gamestats in simulator: wtr.writerow(gamestats) if __name__ == "__main__": import json config = json.load(json_file) main_nested_dict(config) check(Path(config["simulator"]["outputfile"])) # Flat Version, allows multiple configuration files. json2_file = io.StringIO( """ { "player.betting": "Flat", "player.play": "SomeStrategy", "player.rounds": 100, "player.stake": 50, "table.dealer": "Hit17", "table.decks": 6, "table.limit": 50, "table.payout": [3, 2], "table.split": "NoResplitAces", "simulator.outputfile": "data/ch14_simulation4b.dat", "simulator.samples": 100 } """ ) json3_file = io.StringIO( """ { "player.betting": "Flat", "simulator.outputfile": "data/ch14_simulation4b.dat" } """ ) def simulate(table: Table, player: Player, outputpath: Path, samples: int) -> None: simulator = Simulate(table, player, samples=samples) with outputpath.open("w", newline="") as results: wtr = csv.writer(results) for gamestats in simulator: wtr.writerow(gamestats) # Using the config to build objects def main_cm(config: Dict[str, Any]) -> None: dealer_nm = config.get("table.dealer", "Hit17") dealer_rule = {"Hit17": Hit17(), "Stand17": Stand17()}.get(dealer_nm, Hit17()) split_nm = config.get("table.split", "ReSplit") split_rule = { "ReSplit": ReSplit(), "NoReSplit": NoReSplit(), "NoReSplitAces": NoReSplitAces() }.get( split_nm, ReSplit() ) decks = int(config.get("table.decks", 6)) limit = int(config.get("table.limit", 100)) payout = config.get("table.payout", (3, 2)) table = Table( decks=decks, limit=limit, dealer=dealer_rule, split=split_rule, payout=payout ) player_nm = config.get("player.play", "SomeStrategy") player_rule = { "SomeStrategy": SomeStrategy(), "AnotherStrategy": AnotherStrategy() }.get( player_nm, SomeStrategy() ) bet_nm = config.get("player.betting", "Flat") betting_rule = { "Flat": Flat(), "Martingale": Martingale(), "OneThreeTwoSix": OneThreeTwoSix() }.get( bet_nm, Flat() ) rounds = int(config.get("player.rounds", 100)) stake = int(config.get("player.stake", 50)) player = Player(play=player_rule, betting=betting_rule, max_rounds=rounds, init_stake=stake) # import yaml # print(yaml.dump(vars())) outputfile = Path(config.get("simulator.outputfile", "blackjack.csv")) samples = int(config.get("simulator.samples", 100)) simulate(table, player, outputfile, samples) # Sample Main Script to parse and start the application. if __name__ == "__main__": config_files = json2_file, json3_file, config = ChainMap(*[json.load(file) for file in reversed(config_files)]) print(config) main_cm(config) check(Path(config.get("simulator.outputfile"))) # YAML # ####### # Simple YAML yaml1_file = io.StringIO( """ player: betting: Flat play: SomeStrategy rounds: 100 stake: 50 table: dealer: Hit17 decks: 6 limit: 50 payout: [3, 2] split: NoResplitAces simulator: {outputfile: "data/ch14_simulation.dat", samples: 100} """ ) import yaml config = yaml.load(yaml1_file) if __name__ == "__main__": from pprint import pprint pprint(config) yaml1_file = io.StringIO( """ # Complete Simulation Settings table: !!python/object:Chapter_14.simulation_model.Table dealer: !!python/object:Chapter_14.simulation_model.Hit17 {} decks: 6 limit: 50 payout: !!python/tuple [3, 2] split: !!python/object:Chapter_14.simulation_model.NoReSplitAces {} player: !!python/object:Chapter_14.simulation_model.Player betting: !!python/object:Chapter_14.simulation_model.Flat {} init_stake: 50 max_rounds: 100 play: !!python/object:Chapter_14.simulation_model.SomeStrategy {} rounds: 0 stake: 63.0 samples: 100 outputfile: data/ch14_simulation4c.dat """ ) import yaml if __name__ == "__main__": config = yaml.load(yaml1_file) print(config) simulate(config["table"], config["player"], Path(config["outputfile"]), config["samples"]) check(Path(config["outputfile"])) __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_14/ch14_ex5.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 14. Example 5. """ from Chapter_14.simulation_model import * # A typical main program using the above class definitions from pathlib import Path from typing import List, Any, TextIO, Iterator, Union, IO, cast import csv from collections import ChainMap import io # Property files # =============== # - Lines have keys and values. # - Key ends with the first unescaped '=', ':', or white space character. # - Value is optional and defaults to "". # - Number sign (#) or the exclamation mark (!) as # the first non blank character in a line is a comment. # - The backwards slash is used to escape a character. # - Since #, !, =, and : have meaning, # when involved in a piece of key or element, use a preceding backslash # - Key with spaces is tolerated using '\ '. # - Key or value with newline is tolerated using '\\n'. # - Unicode escapes may be used: \uxxxx is the format. # - Everything is text, explicit conversions required # Example 1 # From http://en.wikipedia.org/wiki/.properties prop1 = """ # You are reading the ".properties" entry. ! The exclamation mark can also mark text as comments. # The key and element characters #, !, =, and : are written with a preceding backslash to ensure that they are properly loaded. website = http\://en.wikipedia.org/ language = English # The backslash below tells the application to continue reading # the value onto the next line. message = Welcome to \\ Wikipedia\! # Add spaces to the key key\ with\ spaces = This is the value that could be looked up with the key "key with spaces". # Unicode tab : \\u0009 """ # Example 2 # From http://docs.oracle.com/javase/7/docs/api/java/util/Properties.html prop2 = """ \:\= Truth = Beauty Truth:Beauty Truth :Beauty fruits apple, banana, pear, \\ cantaloupe, watermelon, \\ kiwi, mango cheeses """ # Property File Parsing Class import re class PropertyParser: def read_string(self, data: str) -> Iterator[Tuple[str, str]]: return self._parse(data) def read_file(self, file: IO[str]) -> Iterator[Tuple[str, str]]: data = file.read() return self.read_string(data) def read(self, path: Path) -> Iterator[Tuple[str, str]]: with path.open("r") as file: return self.read_file(file) key_element_pat = re.compile(r"(.*?)\s*(? Iterator[Tuple[str, str]]: logical_lines = ( line.strip() for line in re.sub(r"\\\n\s*", "", data).splitlines() ) non_empty = (line for line in logical_lines if len(line) != 0) non_comment = ( line for line in non_empty if not (line.startswith("#") or line.startswith("!")) ) for line in non_comment: ke_match = self.key_element_pat.match(line) if ke_match: key, element = ke_match.group(1), ke_match.group(2) else: key, element = line, "" key = self._escape(key) element = self._escape(element) yield key, element def load( self, file_name_or_path: Union[TextIO, str, Path] ) -> Iterator[Tuple[str, str]]: if isinstance(file_name_or_path, io.TextIOBase): return self.loads(file_name_or_path.read()) else: name_or_path = cast(Union[str, Path], file_name_or_path) with Path(name_or_path).open("r") as file: return self.loads(file.read()) def loads(self, data: str) -> Iterator[Tuple[str, str]]: return self._parse(data) def _escape(self, data: str) -> str: d1 = re.sub(r"\\([:#!=\s])", lambda x: x.group(1), data) d2 = re.sub(r"\\u([0-9A-Fa-f]+)", lambda x: chr(int(x.group(1), 16)), d1) return d2 def _escape2(self, data: str) -> str: d2 = re.sub( r"\\([:#!=\s])|\\u([0-9A-Fa-f]+)", lambda x: x.group(1) if x.group(1) else chr(int(x.group(2), 16)), data, ) return d2 test_should_parse_prop1 = """ A test for the prop1 example. We can create a dict since each key is unique. >>> parser = PropertyParser() >>> actual = dict(parser.read_string(prop1)) >>> expected = { ... "key with spaces": 'This is the value that could be looked up with the key "key with spaces".', ... "language": "English", ... "message": "Welcome to Wikipedia!", ... "tab": "\\t", ... "website": "http://en.wikipedia.org/", ... } >>> expected == actual True """ test_should_parse_prop2 = """ A test for the prop2 example. We create a list since each key is not unique. >>> parser = PropertyParser() >>> actual = list(parser.read_string(prop2)) >>> expected = [ ... (":=", ""), ... ("Truth", "Beauty"), ... ("Truth", "Beauty"), ... ("Truth", "Beauty"), ... ("fruits", "apple, banana, pear, cantaloupe, watermelon, kiwi, mango"), ... ("cheeses", ""), ... ] >>> expected == actual True >>> actual [(':=', ''), ('Truth', 'Beauty'), ('Truth', 'Beauty'), ('Truth', 'Beauty'), ('fruits', 'apple, banana, pear, cantaloupe, watermelon, kiwi, mango'), ('cheeses', '')] """ test_edge_case = """ >>> parser = PropertyParser() >>> prop = "a\:b: value" >>> actual = list(parser.read_string(prop)) >>> expected = [("a:b", "value")] >>> actual == expected True >>> actual [('a:b', 'value')] """ # Main Program to use property file input import ast def main_cm_prop(config): dealer_nm = config.get("table.dealer", "Hit17") dealer_rule = {"Hit17": Hit17(), "Stand17": Stand17()}.get(dealer_nm, Hit17()) split_nm = config.get("table.split", "ReSplit") split_rule = { "ReSplit": ReSplit(), "NoReSplit": NoReSplit(), "NoReSplitAces": NoReSplitAces() }.get( split_nm, ReSplit() ) decks = int(config.get("table.decks", 6)) limit = int(config.get("table.limit", 100)) payout = ast.literal_eval(config.get("table.payout", "(3,2)")) table = Table( decks=decks, limit=limit, dealer=dealer_rule, split=split_rule, payout=payout ) player_nm = config.get("player.play", "SomeStrategy") player_rule = { "SomeStrategy": SomeStrategy(), "AnotherStrategy": AnotherStrategy() }.get( player_nm, SomeStrategy() ) bet_nm = config.get("player.betting", "Flat") betting_rule = { "Flat": Flat(), "Martingale": Martingale(), "OneThreeTwoSix": OneThreeTwoSix() }.get( bet_nm, Flat() ) rounds = int(config.get("player.rounds", 100)) stake = int(config.get("player.stake", 50)) player = Player( play=player_rule, betting=betting_rule, max_rounds=rounds, init_stake=stake ) outputfile = config.get("simulator.outputfile", "blackjack.csv") samples = int(config.get("simulator.samples", 100)) simulator = Simulate(table, player, samples) with open(outputfile, "w", newline="") as results: wtr = csv.writer(results) for gamestats in simulator: wtr.writerow(gamestats) # Example property file. prop_file = io.StringIO( """ # Example Simulation Setup player.betting: Flat player.play: SomeStrategy player.rounds: 100 player.stake: 50 table.dealer: Hit17 table.decks: 6 table.limit: 50 table.payout: (3,2) table.split: NoResplitAces simulator.outputfile = data/ch14_simulation5.dat simulator.samples = 100 """ ) if __name__ == "__main__": from pprint import pprint pp = PropertyParser() candidate_list = [prop_file] properties = [dict(pp.read_file(file)) for file in reversed(candidate_list)] pprint(properties) config = ChainMap(*properties) main_cm_prop(config) check(Path(config["simulator.outputfile"])) __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_14/ch14_ex6.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 14. Example 6. """ from Chapter_14.simulation_model import * # A typical main program using the above class definitions from pathlib import Path from typing import List, Any, TextIO, Iterator, Union import csv from collections import ChainMap import ast # Exec Import # ################ import io # JSON or YAML files # =================== # JSON using dictionary-of-dictionaries nested structures. # This is inconvenient to handle multiple configuration files. import io # XML files # ========== # Plist # ####### # Sample PLIST Document. As bytes. import io plist_file = io.BytesIO( b""" player betting Flat play SomeStrategy rounds 100 stake 50 simulator outputfile ch14_simulation6a.dat samples 100 table dealer Hit17 decks 6 limit 50 payout 3 2 split NoResplitAces """ ) import plistlib print(plistlib.load(plist_file)) # Non-Plist # ########## # A completely customized XML document import io xml_file = io.BytesIO( b""" Hit17NoResplitAces650(3,2)
Flat SomeStrategy 100 50 data/ch14_simulation6b.dat 100
""" ) import xml.etree.ElementTree as XML class Configuration: def read_file(self, file): self.config = XML.parse(file) def read(self, filename): self.config = XML.parse(filename) def read_string(self, text): self.config = XML.fromstring(text) def get(self, qual_name, default): section, _, item = qual_name.partition(".") query = "./{0}/{1}".format(section, item) node = self.config.find(query) if node is None: return default return node.text def __getitem__(self, section): query = "./{0}".format(section) parent = self.config.find(query) return dict((item.tag, item.text) for item in parent) def main_cm_prop(config): dealer_nm = config.get("table.dealer", "Hit17") dealer_rule = {"Hit17": Hit17(), "Stand17": Stand17()}.get(dealer_nm, Hit17()) split_nm = config.get("table.split", "ReSplit") split_rule = { "ReSplit": ReSplit(), "NoReSplit": NoReSplit(), "NoReSplitAces": NoReSplitAces() }.get( split_nm, ReSplit() ) decks = int(config.get("table.decks", 6)) limit = int(config.get("table.limit", 100)) payout = ast.literal_eval(config.get("table.payout", "(3,2)")) table = Table( decks=decks, limit=limit, dealer=dealer_rule, split=split_rule, payout=payout ) player_nm = config.get("player.play", "SomeStrategy") player_rule = { "SomeStrategy": SomeStrategy(), "AnotherStrategy": AnotherStrategy() }.get( player_nm, SomeStrategy() ) bet_nm = config.get("player.betting", "Flat") betting_rule = { "Flat": Flat(), "Martingale": Martingale(), "OneThreeTwoSix": OneThreeTwoSix() }.get( bet_nm, Flat() ) rounds = int(config.get("player.rounds", 100)) stake = int(config.get("player.stake", 50)) player = Player(play=player_rule, betting=betting_rule, max_rounds=rounds, init_stake=stake) outputfile = config.get("simulator.outputfile", "blackjack.csv") samples = int(config.get("simulator.samples", 100)) simulator = Simulate(table, player, samples) with open(outputfile, "w", newline="") as results: wtr = csv.writer(results) for gamestats in simulator: wtr.writerow(gamestats) if __name__ == "__main__": config = Configuration() config.read_file(xml_file) main_cm_prop(config) check(Path(config["simulator"]["outputfile"])) __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_14/simulation_model.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 14. Example 1 -- simulation model. """ from dataclasses import dataclass, astuple, asdict, field from typing import Tuple, Iterator from pathlib import Path import csv # Mock Object Model # ===================== # A set of class hierarchies that we'll use for several examples. # The content is mostly mocks. class DealerRule: def __repr__(self) -> str: return f"{self.__class__.__name__}()" class Hit17(DealerRule): """Hits soft 17""" pass class Stand17(DealerRule): """Stands on soft 17""" pass class SplitRule: def __repr__(self) -> str: return f"{self.__class__.__name__}()" class ReSplit(SplitRule): """Simplistic resplit anything.""" pass class NoReSplit(SplitRule): """Simplistic no resplit.""" pass class NoReSplitAces(SplitRule): """One card only to aces; no resplit.""" pass @dataclass class Table: decks: int limit: int dealer: DealerRule split: SplitRule payout: Tuple[int, int] class PlayerStrategy: def __repr__(self) -> str: return f"{self.__class__.__name__}()" class SomeStrategy(PlayerStrategy): pass class AnotherStrategy(PlayerStrategy): pass class BettingStrategy: def __repr__(self) -> str: return f"{self.__class__.__name__}()" def bet(self) -> int: raise NotImplementedError("No bet method") def record_win(self) -> None: pass def record_loss(self) -> None: pass class Flat(BettingStrategy): pass class Martingale(BettingStrategy): pass class OneThreeTwoSix(BettingStrategy): pass @dataclass class Player: play: PlayerStrategy betting: BettingStrategy max_rounds: int init_stake: int rounds: int = field(init=False) stake: float = field(init=False) def __post_init__(self): self.reset() def reset(self) -> None: self.rounds = self.max_rounds self.stake = self.init_stake # A mock simulation which is built from the above mock objects. import random @dataclass class Simulate: """Mock simulation.""" table: Table player: Player samples: int def __iter__(self) -> Iterator[Tuple]: """Yield statistical samples.""" x, y = self.table.payout blackjack_payout = x / y for count in range(self.samples): self.player.reset() while self.player.stake > 0 and self.player.rounds > 0: self.player.rounds -= 1 outcome = random.random() if outcome < 0.579: self.player.stake -= 1 elif 0.579 <= outcome < 0.883: self.player.stake += 1 elif 0.883 <= outcome < 0.943: # a "push" pass else: # 0.943 <= outcome self.player.stake += blackjack_payout yield astuple(self.table) + astuple(self.player) def check(path: Path) -> None: """ Validate unit test result file can be read. :param path: Path to the example output """ with path.open("r") as results: rdr = csv.reader(results) outcomes = (float(row[10]) for row in rdr) first = next(outcomes) sum_0, sum_1 = 1, first value_min = value_max = first for value in outcomes: sum_0 += 1 # value**0 sum_1 += value # value**1 value_min = min(value_min, value) value_max = max(value_max, value) mean = sum_1 / sum_0 print( f"{path}\nMean = {mean:.1f}\n" f"House Edge = { 1 - mean / 50:.1%}\n" f"Range = {value_min:.1f} {value_max:.1f}" ) ================================================ FILE: Chapter_14/someapp.config ================================================ ================================================ FILE: Chapter_15/__init__.py ================================================ ================================================ FILE: Chapter_15/ch15_ex1.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 15. Example 1. """ import random from typing import Tuple, List, Iterator, Optional, Type # A poor design class DominoBoneYard: """ A relatively poor design. A number of unrelated things all jumbled together >>> random.seed(42) >>> dby = DominoBoneYard() >>> len(dby._dominoes) 28 >>> hands = list(dby.hand_iter(4)) >>> hands[0] [(5, 3), (5, 1), (4, 0), (6, 0), (6, 6), (3, 0), (2, 2)] >>> dby.score_hand(hands[0]) 43 >>> hands[1] [(4, 1), (4, 4), (3, 3), (6, 3), (4, 2), (5, 4), (5, 0)] >>> dby.rank_hand(hands[1]) >>> dby.score_hand(hands[1][-2:]) 10 >>> hands[2] [(6, 4), (1, 0), (4, 3), (1, 1), (5, 2), (6, 5), (2, 1)] >>> dby.doubles_indices(hands[0]) [4, 6] >>> for d in dby.doubles_indices(hands[0]): ... print(hands[0][d]) (6, 6) (2, 2) >>> dby.can_play_first(hands[0]) True """ def __init__(self, limit: int = 6) -> None: self._dominoes = [(x, y) for x in range(limit + 1) for y in range(x + 1)] random.shuffle(self._dominoes) def double(self, domino: Tuple[int, int]) -> bool: x, y = domino return x == y def score(self, domino: Tuple[int, int]) -> int: return domino[0] + domino[1] def hand_iter(self, players: int = 4) -> Iterator[List[Tuple[int, int]]]: for p in range(players): yield self._dominoes[p * 7:p * 7 + 7] def can_play_first(self, hand: List[Tuple[int, int]]) -> bool: for d in hand: if self.double(d) and d[0] == 6: return True return False def score_hand(self, hand: List[Tuple[int, int]]) -> int: return sum(d[0] + d[1] for d in hand) def rank_hand(self, hand: List[Tuple[int, int]]) -> None: hand.sort(key=self.score, reverse=True) def doubles_indices(self, hand: List[Tuple[int, int]]) -> List[int]: return [i for i in range(len(hand)) if self.double(hand[i])] # Revised and Decomposed based on ISP from typing import NamedTuple class Domino(NamedTuple): v1: int v2: int def double(self) -> bool: return self.v1 == self.v2 def score(self) -> int: return self.v1 + self.v2 class Hand(list): def score(self) -> int: return sum(d.score() for d in self) def rank(self) -> None: self.sort(key=lambda d: d.score(), reverse=True) def doubles_indices(self) -> List[int]: return [i for i in range(len(self)) if self[i].double()] class DominoBoneYard2: def __init__(self, limit: int = 6) -> None: self._dominoes = [Domino(x, y) for x in range(limit + 1) for y in range(x + 1)] random.shuffle(self._dominoes) def hand_iter(self, players: int = 4) -> Iterator[Hand]: for p in range(players): hand, self._dominoes = Hand(self._dominoes[:7]), self._dominoes[7:] yield hand test_dby2 = """ >>> random.seed(42) >>> dby = DominoBoneYard2() >>> len(dby._dominoes) 28 >>> hands = list(dby.hand_iter(4)) >>> hands[0] [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)] >>> hands[0].score() 43 >>> hands[1] [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)] >>> hands[1].rank() >>> hands[1].pop(0) Domino(v1=6, v2=3) >>> hands[1].pop(0) Domino(v1=5, v2=4) >>> hands[1].pop(0) Domino(v1=4, v2=4) >>> hands[1].pop(0) Domino(v1=3, v2=3) >>> hands[1].pop(0) Domino(v1=4, v2=2) >>> hands[1].score() 10 >>> hands[2] [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)] >>> hands[0].doubles_indices() [4, 6] >>> for d in hands[0].doubles_indices(): ... print(hands[0][d]) Domino(v1=6, v2=6) Domino(v1=2, v2=2) """ class Hand3(Hand): def highest_double_index(self) -> Optional[int]: descending = sorted( self.doubles_indices(), key=lambda double_index: self[double_index].v1, reverse=True, ) if descending: return descending[0] return None class DominoBoneYard3(DominoBoneYard2): def hand_iter(self, players: int = 4) -> Iterator[Hand3]: for p in range(players): hand, self._dominoes = Hand3(self._dominoes[:7]), self._dominoes[7:] yield hand test_dby3 = """ >>> random.seed(42) >>> dby = DominoBoneYard3() >>> len(dby._dominoes) 28 >>> hands = list(dby.hand_iter(4)) >>> hands[0] [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)] >>> hands[0].score() 43 >>> hdi = hands[0].highest_double_index() >>> hdi 4 >>> hands[0][hdi] Domino(v1=6, v2=6) >>> hands[1] [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)] """ class FancyDealer4: def __init__(self): self.boneyard = DominoBoneYard3() def hand_iter( self, players: int = 4, tiles: int = 7 ) -> Iterator[Hand3]: if players * tiles > len(self.boneyard._dominoes): raise ValueError(f"Can't deal players={players} tiles={tiles}") for p in range(players): hand = Hand3(self.boneyard._dominoes[:tiles]) self.boneyard._dominoes = self.boneyard._dominoes[tiles:] yield hand test_fancy4 = """ >>> random.seed(42) >>> dby1 = FancyDealer4() >>> hands = list(dby1.hand_iter(4)) >>> hands[0] [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)] >>> random.seed(42) >>> dby2 = FancyDealer4() >>> hands5 = list(dby2.hand_iter(players=2, tiles=5)) >>> hands5[0] [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)] """ class DominoBoneYard3b: hand_size: int = 7 def __init__(self, limit: int = 6) -> None: self._dominoes = [Domino(x, y) for x in range(limit + 1) for y in range(x + 1)] random.shuffle(self._dominoes) def hand_iter(self, players: int = 4) -> Iterator[Hand3]: for p in range(players): hand = Hand3(self._dominoes[:self.hand_size]) self._dominoes = self._dominoes[self.hand_size:] yield hand test_dby5 = """ >>> random.seed(42) >>> dby = DominoBoneYard3b() >>> len(dby._dominoes) 28 >>> hands = list(dby.hand_iter(4)) >>> hands[0] [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)] >>> hands[0].score() 43 >>> hdi = hands[0].highest_double_index() >>> hdi 4 >>> hands[0][hdi] Domino(v1=6, v2=6) >>> hands[1] [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)] """ class DominoBoneYard3c: domino_class: Type[Domino] = Domino hand_class: Type[Hand] = Hand3 hand_size: int = 7 def __init__(self, limit: int = 6) -> None: self._dominoes = [ self.domino_class(x, y) for x in range(limit + 1) for y in range(x + 1) ] random.shuffle(self._dominoes) def hand_iter(self, players: int = 4) -> Iterator[Hand]: for p in range(players): hand = self.hand_class(self._dominoes[:self.hand_size]) self._dominoes = self._dominoes[self.hand_size:] yield hand class Hand4(Hand3): def __init__(self, *args) -> None: super().__init__(*args) self.doubles = [d for d in self if d.double()] self.doubles.sort(key=lambda d: d.score()) def doubles_indices(self) -> List[int]: return [self.index(d) for d in self.doubles] test_dby6 = """ >>> random.seed(42) >>> dby = DominoBoneYard3c() >>> len(dby._dominoes) 28 >>> hands = list(dby.hand_iter(4)) >>> hands[0] [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)] >>> hands[0].score() 43 >>> hdi = hands[0].highest_double_index() >>> hdi 4 >>> hands[0][hdi] Domino(v1=6, v2=6) >>> hands[1] [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)] >>> random.seed(42) >>> DominoBoneYard3c.hand_class = Hand4 >>> dby = DominoBoneYard3c() >>> len(dby._dominoes) 28 >>> hands = list(dby.hand_iter(4)) >>> hands[0] [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)] >>> hands[0].score() 43 >>> hdi = hands[0].highest_double_index() >>> hdi 4 """ __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_15/ch15_ex2.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 15. Example 2. """ from typing import ( NamedTuple, List, Type, Optional, Iterator, Tuple, DefaultDict, Union, cast, Any, ) import random from collections import defaultdict # Duck Typing from typing import NamedTuple class Domino_1(NamedTuple): v1: int v2: int @property def double(self) -> bool: return self.v1 == self.v2 @property def score(self) -> int: return self.v1 + self.v2 from dataclasses import dataclass @dataclass(frozen=True, eq=True, order=True) class Domino_2: v1: int v2: int @property def double(self) -> bool: return self.v1 == self.v2 @property def score(self) -> int: return self.v1 + self.v2 Domino = Union[Domino_1, Domino_2] def builder(v1: int, v2: int) -> Domino: return Domino_2(v1, v2) test_dominoe_classes = """ >>> d_1a = Domino_1(6, 5) >>> d_1b = Domino_1(6, 5) >>> d_1a == d_1b True >>> d_1a.double False >>> d_1a.score 11 >>> d_2a = Domino_2(5, 3) >>> d_2b = Domino_2(5, 3) >>> d_2a == d_2b True >>> d_2a.double False >>> d_2a.score 8 """ # More Complex Example class Hand(list): def __init__(self, *args: Domino) -> None: super().__init__(cast(Tuple[Any], args)) def score(self) -> int: return sum(d.score for d in self) def rank(self) -> None: self.sort(key=lambda d: d.score, reverse=True) def doubles(self) -> List[Domino_1]: return [d for d in self if d.double] def highest_double(self) -> Optional[Domino_1]: descending = sorted(self.doubles(), key=lambda d: d.v1, reverse=True) if descending: return descending[0] return None class DominoBoneYard: domino_class: Type[Domino] = Domino_1 hand_class: Type[Hand] = Hand hand_size: int = 7 def __init__(self, limit: int = 6) -> None: self._dominoes: List[Domino] = [ self.domino_class(x, y) for x in range(limit + 1) for y in range(x + 1) ] random.shuffle(self._dominoes) def draw(self, n: int = 1) -> Optional[List[Domino]]: deal, remainder = self._dominoes[:n], self._dominoes[n:] if len(deal) != n: return None self._dominoes = remainder return deal def hand_iter(self, players: int = 4) -> Iterator[Hand]: hands: List[Optional[List[Domino]]] = [ self.draw(self.hand_size) for _ in range(players) ] if not all(hands): raise ValueError(f"Can't deal {self.hand_size} tiles to {players} players") yield from (self.hand_class(*h) for h in hands if h is not None) test_dby = """ >>> random.seed(42) >>> DominoBoneYard.hand_class = Hand >>> dby = DominoBoneYard() >>> len(dby._dominoes) 28 >>> hands = list(dby.hand_iter(4)) >>> hands[0] [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)] >>> hands[0].score() 43 >>> hd = hands[0].highest_double() >>> hd Domino_1(v1=6, v2=6) >>> hands[1] [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)] """ test_dby_exception = """ >>> random.seed(42) >>> DominoBoneYard.hand_class = Hand >>> dby = DominoBoneYard() >>> hands = list(dby.hand_iter(5)) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): File "/Users/slott/miniconda3/envs/mastering/lib/python3.7/doctest.py", line 1329, in __run compileflags, 1), test.globs) File "", line 1, in hands = list(dby.hand_iter(5)) File "/Users/slott/Documents/.../mastering-oo-python-2e/Chapter_15/ch15_ex2.py", line 119, in hand_iter raise ValueError(f"Can't deal {self.hand_size} tiles to {players} players") ValueError: Can't deal 7 tiles to 5 players """ class Hand_X1(Hand): def __init__(self, *args) -> None: super().__init__(*args) self.end: DefaultDict[int, List[Domino_1]] = defaultdict(list) for d in self: self.end[d.v1].append(d) self.end[d.v2].append(d) def matches(self, spots: int) -> List[Domino_1]: return self.end.get(spots, []) test_dby_3 = """ >>> random.seed(42) >>> DominoBoneYard.hand_class = Hand_X1 >>> DominoBoneYard.domino_class = Domino_2 >>> dby = DominoBoneYard() >>> len(dby._dominoes) 28 >>> hands = list(dby.hand_iter(4)) >>> h_0 = hands[0] >>> h_0 [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)] >>> h_0.score() 43 >>> h_0.matches(3) [Domino_2(v1=5, v2=3), Domino_2(v1=3, v2=0)] """ __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_16/__init__.py ================================================ ================================================ FILE: Chapter_16/ch16_ex1.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 16. Example 1. """ from typing import Type # Simple Logging # ============== class Player: def __init__(self, bet: str, strategy: str, stake: int) -> None: self.logger = logging.getLogger(self.__class__.__qualname__) self.logger.debug("init bet %r, strategy %r, stake %r", bet, strategy, stake) # Decorator for Logging # ======================== # Define a decorator for a class. # This is confusing to mypy because it's not clear the decorator adds an attribute # It's not optimal def logged(cls: Type) -> Type: cls.logger = logging.getLogger(cls.__qualname__) return cls import logging import sys # Add a level # ============ logging.addLevelName(15, "VERBOSE") VERBOSE = 15 # Manual Logging # =============== # Mypy is happier. But. We're repeated the class name. class Player_2: logger = logging.getLogger("Player_2") def __init__(self, bet: str, strategy: str, stake: int) -> None: self.logger.debug("init bet %s, strategy %s, stake %d", bet, strategy, stake) # Using a metaclass for consistent logger definition # ================================================== class LoggedClassMeta(type): def __new__(cls, name, bases, namespace, **kwds): result = type.__new__(cls, name, bases, dict(namespace)) result.logger = logging.getLogger(result.__qualname__) return result class LoggedClass(metaclass=LoggedClassMeta): logger: logging.Logger # Sample Class class Player_3(LoggedClass): def __init__(self, bet: str, strategy: str, stake: int) -> None: self.logger.debug("init bet %s, strategy %s, stake %d", bet, strategy, stake) __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) # No configuration -- no output logger = logging.getLogger("no_config") logger.info("Create Player 2") p2 = Player_2("Bet1", "Strategy1", 1) logger.info("Create Player 3") p3 = Player_3("Bet1", "Strategy1", 1) # Configuration changed -- now there's output logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) loggerc = logging.getLogger("config") loggerc.info("Create Player") pc = Player("Bet", "Strategy", 10) loggerc.info("Create Player 2") pc2 = Player_2("Bet2", "Strategy2", 2) loggerc.info("Create Player 3") pc3 = Player_3("Bet3", "Strategy3", 3) logging.shutdown() ================================================ FILE: Chapter_16/ch16_ex10.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 16. Example 9. """ from Chapter_16.ch16_ex9 import Log_Producer, Log_Consumer_1 import logging import logging.handlers import yaml import queue # Modified Queue Handler # ================================== # Extended QueueHandler class class WaitQueueHandler(logging.handlers.QueueHandler): def enqueue(self, record): self.queue.put(record) # Revised Producer class Log_Producer_2(Log_Producer): handler_class = WaitQueueHandler # The Queue import multiprocessing queue2: multiprocessing.Queue = multiprocessing.Queue(100) # Waaayyyy too small # The consumer process consumer2 = Log_Consumer_1(queue2) consumer2.start() # The producers producers = [] for i in range(10): proc = Log_Producer_2(i, queue2) proc.start() producers.append(proc) # Normal termination for p in producers: p.join() queue2.put(None) consumer2.join() logging.shutdown() __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_16/ch16_ex2.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 16. Example 2. """ from typing import Type import logging import sys # Multiple Loggers # =========================== # This is confusing to mypy because it's not clear the decorator adds attributes. def log_to(*names: str): if len(names) == 0: names = ('logger',) def concrete_log_to(cls: Type) -> Type: for log_name in names: setattr(cls, log_name, logging.getLogger( f"{log_name}.{cls.__qualname__}")) return cls return concrete_log_to # Sample Class # Chapter_16/ch16_ex2.py:41: error: "Player" has no attribute "audit" # Chapter_16/ch16_ex2.py:42: error: "Player" has no attribute "verbose" @log_to("audit", "verbose") class Player: def __init__(self, bet: str, strategy: str, stake: int) -> None: self.audit.info(f"Initial {stake:d}") self.verbose.info(f"Init bet={bet:s} strategy={strategy:s} stake={stake:d}") # Chapter_16/ch16_ex2.py:50: error: "Table" has no attribute "security" @log_to("security") class Table: def add_player(self, player: Player) -> None: self.security.info(f"Adding {player}") # Demo Output logging.basicConfig(stream=sys.stderr, level=logging.DEBUG, style="{") print("Create Player 2") p3 = Player("Bet3", "Strategy3", 3) t = Table() t.add_player(p3) logging.shutdown() __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_16/ch16_ex3.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 16. Example 3. """ # Multiple Loggers with YAML Config # ============================================= # Sample configuration file config3 = """ version: 1 handlers: console: class: logging.StreamHandler stream: ext://sys.stderr formatter: basic audit_file: class: logging.FileHandler filename: data/ch16_audit.log encoding: utf-8 formatter: basic formatters: basic: style: "{" format: "{levelname:s}:{name:s}:{message:s}" loggers: verbose: handlers: [console] level: INFO propagate: False # Added audit: handlers: [console,audit_file] level: INFO propagate: False # Added root: # Added handlers: [console] level: INFO """ import logging.config import yaml config_dict = yaml.load(config3) print(config_dict) logging.config.dictConfig(config_dict) # Logging verbose = logging.getLogger("verbose.example.SomeClass") audit = logging.getLogger("audit.example.SomeClass") verbose.info("Verbose information") audit.info("Audit record with before and after") print("Root Handlers:", logging.getLogger().handlers) print("Verbose Handlers:", logging.getLogger('verbose').handlers) print("Audit Handlers:", logging.getLogger('audit').handlers) __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_16/ch16_ex4.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 16. Example 4. """ # Startup/Shutdown # ============================================= # Some main function from typing import Dict, Counter import logging import collections from Chapter_16.ch16_ex1 import LoggedClass class Main(LoggedClass): def __init__(self) -> None: self.counts: Counter[str] = collections.Counter() def run(self) -> int: self.logger.info("Start") # Some processing in and around the counter increments self.counts["input"] += 2000 self.counts["reject"] += 500 self.counts["output"] += 1500 self.logger.info("Counts %s", self.counts) for k in self.counts: self.logger.info(f"{k:.<16s} {self.counts[k]:>6,d}") return 0 config3 = """ version: 1 handlers: console: class: logging.StreamHandler stream: ext://sys.stderr formatter: control audit_file: class: logging.FileHandler filename: data/ch16_audit.log encoding: utf-8 formatter: basic formatters: control: style: "{" format: "{levelname:s}:{message:s}" basic: style: "{" format: "{levelname:s}:{name:s}:{message:s}" loggers: verbose: handlers: [console] level: INFO propagate: False # Added audit: handlers: [console,audit_file] level: INFO propagate: False # Added root: # Added handlers: [console] level: INFO disable_existing_loggers: False """ # Main program def demo4a() -> None: import sys import logging import logging.config import yaml logging.config.dictConfig(yaml.load(config3)) try: application = Main() status = application.run() except Exception as e: logging.exception(e) status = 2 finally: logging.shutdown() # sys.exit(status) # Atexit def demo4b() -> None: import atexit import logging import logging.config import yaml import sys logging.config.dictConfig(yaml.load(config3)) atexit.register(logging.shutdown) try: application = Main() status = application.run() except Exception as e: logging.exception(e) status = 2 # sys.exit(status) __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) demo4a() demo4b() ================================================ FILE: Chapter_16/ch16_ex5.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 16. Example 5. """ from typing import Type, Dict import logging import logging.config import yaml # A context manager can be used, also. # Note that there are profound limitations when using dictConfig. # Any loggers created prior to running dictConfig wind up disconnected. # Be sure to include ``disable_existing_loggers: False`` in the dictionary. # Debugging # ================== # New Config config5 = """ version: 1 disable_existing_loggers: False handlers: console: class: logging.StreamHandler stream: ext://sys.stderr formatter: basic audit_file: class: logging.FileHandler filename: data/ch16_audit.log encoding: utf-8 formatter: detailed formatters: basic: style: "{" format: "{levelname:s}:{name:s}:{message:s}" detailed: style: "{" format: "{levelname:s}:{name:s}:{asctime:s}:{message:s}" datefmt: "%Y-%m-%d %H:%M:%S" loggers: audit: handlers: [console,audit_file] level: INFO propagate: False root: handlers: [console] level: INFO disable_existing_loggers: False """ # Some classes from Chapter_16.ch16_ex1 import LoggedClass class BettingStrategy(LoggedClass): def bet(self) -> int: raise NotImplementedError("No bet method") def record_win(self) -> None: pass def record_loss(self) -> None: pass class OneThreeTwoSix(BettingStrategy): def __init__(self) -> None: self.wins = 0 def _state(self) -> Dict[str, int]: return dict(wins=self.wins) def bet(self) -> int: bet = {0: 1, 1: 3, 2: 2, 3: 6}[self.wins % 4] self.logger.debug(f"Bet {self._state()}; based on {bet}") return bet def record_win(self) -> None: self.wins += 1 self.logger.debug(f"Win: {self._state()}") def record_loss(self) -> None: self.wins = 0 self.logger.debug(f"Loss: {self._state()}") # A Decorator -- This confuses mypy def audited(cls: Type) -> Type: cls.logger = logging.getLogger(cls.__qualname__) cls.audit = logging.getLogger(f"audit.{cls.__qualname__}") return cls # A metaclass -- Much easier on mypy # Extending the basic logged class meta to add yet more features from Chapter_16.ch16_ex1 import LoggedClassMeta class AuditedClassMeta(LoggedClassMeta): def __new__(cls, name, bases, namespace, **kwds): result = LoggedClassMeta.__new__(cls, name, bases, dict(namespace)) for item, type_ref in result.__annotations__.items(): if issubclass(type_ref, logging.Logger): prefix = "" if item == "logger" else f"{item}." logger = logging.getLogger(f"{prefix}{result.__qualname__}") setattr(result, item, logger) return result class AuditedClass(LoggedClass, metaclass=AuditedClassMeta): audit: logging.Logger pass class Table(AuditedClass): def bet(self, bet: str, amount: int) -> None: self.logger.info("Betting %d on %s", amount, bet) self.audit.info("Bet:%r, Amount:%r", bet, amount) # A Main Program demo import atexit logging.config.dictConfig(yaml.load(config5)) atexit.register(logging.shutdown) log = logging.getLogger("main") log.info("Starting") strategy = OneThreeTwoSix() application = Table() application.bet("Black", strategy.bet()) strategy.record_win() application.bet("Black", strategy.bet()) strategy.record_win() application.bet("Black", strategy.bet()) strategy.record_loss() application.bet("Black", strategy.bet()) log.info("Finish") logging.shutdown() __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_16/ch16_ex6.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 16. Example 6. """ from typing import Optional import logging import logging.config import yaml import getpass config5 = """ version: 1 disable_existing_loggers: False handlers: console: class: logging.StreamHandler stream: ext://sys.stderr formatter: basic audit_file: class: logging.FileHandler filename: data/ch16_audit.log encoding: utf-8 formatter: detailed formatters: basic: style: "{" format: "{levelname:s}:{name:s}:{message:s}" detailed: style: "{" format: "{levelname:s}:{name:s}:{asctime:s}:{message:s}" datefmt: "%Y-%m-%d %H:%M:%S" loggers: audit: handlers: [console,audit_file] level: INFO propagate: False root: handlers: [console] level: INFO """ # Extending # ==================== # Doesn't seem to work as expected. # Note that the factory is somehow bypassed by a LoggerAdapter # Also. Thus mystifies mypy because we're adding attributes to the base class. class UserLogRecordFactory: def __init__(self) -> None: self.user: Optional[str] = None self.previous = logging.getLogRecordFactory() def __call__(self, *args, **kwargs) -> logging.LogRecord: print("Building log with ", args, kwargs) user = getpass.getuser() record = self.previous(*args, **kwargs) record.user = user # type: ignore return record # Adapter. This kind of extension may not be needed. # The "extra" is set as the default behavior. # However, the processing is obscure. It behaves as if it bypassed the factory. # Yet. The code looks like it won't bypass the factory. class UserLogAdapter(logging.LoggerAdapter): def process(self, msg, kwargs): kwargs['user'] = self.extra.get('user', None) return msg, kwargs # Installation logging.config.dictConfig(yaml.load(config5)) logging.setLogRecordFactory(UserLogRecordFactory()) # Use log = logging.getLogger("test.demo6") for h in logging.getLogger().handlers: h.setFormatter(logging.Formatter(fmt="{user}:{name}:{levelname}:{message}", style="{")) import threading data = threading.local() data.user = "Some User" data.ip_address = "127.0.0.1" log.info("message without User") log.info("message with user") log.info("message with extra", extra={"more": "More Data"}) # auth_log = logging.LoggerAdapter( log, data.__dict__ ) # "Attempt to overwrite 'user' in LogRecord" # auth_log = UserLogAdapter( log, data.__dict__ ) # _log() got an unexpected keyword argument 'user' # auth_log.info( "message with User" ) __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_16/ch16_ex7.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 16. Example 7. """ # Warnings # ==================== # Deprecation import warnings class Player: """version 2.1""" def bet(self) -> None: warnings.warn( "bet is deprecated, use place_bet", DeprecationWarning, stacklevel=2) pass warnings.simplefilter("always", category=DeprecationWarning) p2 = Player() p2.bet() # Configuration import warnings try: import simulation_model_1 as model except ImportError as e: warnings.warn(repr(e)) if 'model' not in globals(): try: import simulation_model_2 as model except ImportError as e: warnings.warn(repr(e)) if 'model' not in globals(): # raise ImportError("Missing simulation_model_1 and simulation_model_2") pass __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_16/ch16_ex8.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 16. Example 8. """ # Tail Buffer # ======================== # Class Definition # Note. Logging has no type hints. So. mypy fails here. import logging import logging.config import logging.handlers import yaml class TailHandler(logging.handlers.MemoryHandler): def shouldFlush(self, record: logging.LogRecord) -> bool: """ Check for buffer full or a record at the flushLevel or higher. """ if record.levelno >= self.flushLevel: return True while len(self.buffer) > self.capacity: self.acquire() try: del self.buffer[0] finally: self.release() return False # Configuration config8 = """ version: 1 disable_existing_loggers: False handlers: console: class: logging.StreamHandler stream: ext://sys.stderr formatter: basic tail: (): __main__.TailHandler target: cfg://handlers.console capacity: 5 formatters: basic: style: "{" format: "{levelname:s}:{name:s}:{message:s}" loggers: test: handlers: [tail] level: DEBUG propagate: False root: handlers: [console] level: INFO """ # Installation if __name__ == "__main__": logging.config.dictConfig(yaml.load(config8)) log = logging.getLogger("test.demo8") # Use Case 1 -- last 5 before ERROR. log.info("Last 5 before error") for i in range(20): log.debug(f"Message {i:d}") log.error("Error causes dump of last 5") # Use Case 2 -- last 5 before shutdown. log.info("Last 5 before shutdown") for i in range(20, 40): log.debug(f"Message {i:d}") log.info("Shutdown causes dump of last 5") logging.shutdown() __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_16/ch16_ex9.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 16. Example 9. """ import logging import logging.config import logging.handlers import yaml import time # Producer/Consumer # ========================== # The Consumer consumer_config = """ version: 1 disable_existing_loggers: False handlers: console: class: logging.StreamHandler stream: ext://sys.stderr formatter: basic formatters: basic: style: "{" format: "{levelname:s}:{name:s}:{message:s}" loggers: combined: handlers: [console] formatter: detail level: INFO propagate: False root: handlers: [console] level: INFO """ import collections import logging import multiprocessing class Log_Consumer_1(multiprocessing.Process): """In effect, an instance of QueueListener.""" def __init__(self, queue): self.source = queue super().__init__() logging.config.dictConfig(yaml.load(consumer_config)) self.combined = logging.getLogger(f"combined.{self.__class__.__qualname__}") self.log = logging.getLogger(self.__class__.__qualname__) self.counts = collections.Counter() def run(self): self.log.info("Consumer Started") while True: log_record = self.source.get() if log_record == None: break self.combined.handle(log_record) self.counts[log_record.getMessage()] += 1 self.log.info("Consumer Finished") self.log.info(self.counts) # The Producers class Log_Producer(multiprocessing.Process): handler_class = logging.handlers.QueueHandler def __init__(self, proc_id, queue): self.proc_id = proc_id self.destination = queue super().__init__() self.log = logging.getLogger( f"{self.__class__.__qualname__}.{self.proc_id}") self.log.handlers = [self.handler_class(self.destination)] self.log.setLevel(logging.INFO) def run(self): self.log.info(f"Started") for i in range(100): self.log.info(f"Message {i:d}") time.sleep(0.001) self.log.info(f"Finished") def demo(): # The Queue import multiprocessing # size = 10 # Too small. size = 30 # Better queue1: multiprocessing.Queue = multiprocessing.Queue(size) # The consumer process consumer = Log_Consumer_1(queue1) consumer.start() # The producers producers = [] for i in range(10): proc = Log_Producer(i, queue1) proc.start() producers.append(proc) # Normal termination for p in producers: p.join() queue1.put(None) consumer.join() logging.shutdown() __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) demo() ================================================ FILE: Chapter_17/__init__.py ================================================ ================================================ FILE: Chapter_17/ch17_data.csv ================================================ rate_in,time_in,distance_in,rate_out,time_out,distance_out 2,3,,2,3,6 5,,7,5,1.4,7 ,11,13,1.18,11,13 ================================================ FILE: Chapter_17/ch17_ex1.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 17. Example 1. """ # Card and Deck # ======================== from typing import Type, cast, Callable import enum class Suit(enum.Enum): CLUB = "♣" DIAMOND = "♦" HEART = "♥" SPADE = "♠" class Card: def __init__( self, rank: int, suit: Suit, hard: int = None, soft: int = None ) -> None: self.rank = rank self.suit = suit self.hard = hard or int(rank) self.soft = soft or int(rank) def __str__(self) -> str: return f"{self.rank!s}{self.suit.value!s}" class AceCard(Card): def __init__(self, rank: int, suit: Suit) -> None: super().__init__(rank, suit, 1, 11) class FaceCard(Card): def __init__(self, rank: int, suit: Suit) -> None: super().__init__(rank, suit, 10, 10) class LogicError(Exception): pass def card(rank: int, suit: Suit) -> Card: if rank == 1: return AceCard(rank, suit) elif 2 <= rank < 11: return Card(rank, suit) elif 11 <= rank < 14: return FaceCard(rank, suit) else: raise LogicError(f"Rank {rank} invalid") import random class Deck1(list): def __init__(self, size: int = 1) -> None: super().__init__() self.rng = random.Random() for d in range(size): for s in iter(Suit): cards: List[Card] = ( [cast(Card, AceCard(1, s))] + [Card(r, s) for r in range(2, 12)] + [FaceCard(r, s) for r in range(12, 14)] ) super().extend(cards) self.rng.shuffle(self) class Deck2(list): def __init__( self, size: int = 1, random: random.Random = random.Random(), ace_class: Type[Card] = AceCard, card_class: Type[Card] = Card, face_class: Type[Card] = FaceCard, ) -> None: super().__init__() self.rng = random for d in range(size): for s in iter(Suit): cards = ( [ace_class(1, s)] + [card_class(r, s) for r in range(2, 12)] + [face_class(r, s) for r in range(12, 14)] ) super().extend(cards) self.rng.shuffle(self) # Card Test # ======================== # Some Test Cases import unittest class TestCard(unittest.TestCase): def setUp(self) -> None: self.three_clubs = Card(3, Suit.CLUB) def test_should_returnStr(self) -> None: self.assertEqual("3♣", str(self.three_clubs)) def test_should_getAttrValues(self) -> None: self.assertEqual(3, self.three_clubs.rank) self.assertEqual(Suit.CLUB, self.three_clubs.suit) self.assertEqual(3, self.three_clubs.hard) self.assertEqual(3, self.three_clubs.soft) class TestAceCard(unittest.TestCase): def setUp(self) -> None: self.ace_spades = AceCard(1, Suit.SPADE) @unittest.expectedFailure def test_should_returnStr(self) -> None: self.assertEqual("A♠", str(self.ace_spades)) def test_should_getAttrValues(self) -> None: self.assertEqual(1, self.ace_spades.rank) self.assertEqual(Suit.SPADE, self.ace_spades.suit) self.assertEqual(1, self.ace_spades.hard) self.assertEqual(11, self.ace_spades.soft) class TestFaceCard(unittest.TestCase): def setUp(self) -> None: self.queen_hearts = FaceCard(12, Suit.HEART) @unittest.expectedFailure def test_should_returnStr(self) -> None: self.assertEqual("Q♥", str(self.queen_hearts)) def test_should_getAttrValues(self) -> None: self.assertEqual(12, self.queen_hearts.rank) self.assertEqual(Suit.HEART, self.queen_hearts.suit) self.assertEqual(10, self.queen_hearts.hard) self.assertEqual(10, self.queen_hearts.soft) # Suite def suite2() -> unittest.TestSuite: s = unittest.TestSuite() load_from = unittest.defaultTestLoader.loadTestsFromTestCase s.addTests(load_from(TestCard)) s.addTests(load_from(TestAceCard)) s.addTests(load_from(TestFaceCard)) return s if __name__ == "__main__": t = unittest.TextTestRunner() t.run(suite2()) # Card Factory Test # ============================= # Another Test Case class TestCardFactory(unittest.TestCase): def test_rank1_should_createAceCard(self) -> None: c = card(1, Suit.CLUB) self.assertIsInstance(c, AceCard) def test_rank2_should_createCard(self) -> None: c = card(2, Suit.DIAMOND) self.assertIsInstance(c, Card) def test_rank10_should_createCard(self) -> None: c = card(10, Suit.HEART) self.assertIsInstance(c, Card) def test_rank10_should_createFaceCard(self) -> None: c = card(11, Suit.SPADE) self.assertIsInstance(c, Card) def test_rank13_should_createFaceCard(self) -> None: c = card(13, Suit.CLUB) self.assertIsInstance(c, Card) def test_otherRank_should_exception(self) -> None: with self.assertRaises(LogicError): c = card(14, Suit.DIAMOND) with self.assertRaises(LogicError): c = card(0, Suit.DIAMOND) # Another Suite def suite3() -> unittest.TestSuite: s = unittest.TestSuite() s.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestCardFactory)) return s if __name__ == "__main__": t = unittest.TextTestRunner() t.run(suite3()) # Deck with Mock Card # ============================== # Class Definitions class DeckEmpty(Exception): pass class Deck3(list): def __init__( self, size: int = 1, random: random.Random = random.Random(), card_factory: Callable[[int, Suit], Card] = card, ) -> None: super().__init__() self.rng = random for d in range(size): super().extend( [card_factory(r, s) for r in range(1, 14) for s in iter(Suit)] ) self.rng.shuffle(self) def deal(self) -> Card: try: return self.pop(0) except IndexError: raise DeckEmpty() # Test Cases import unittest import unittest.mock class TestDeckBuild(unittest.TestCase): def setUp(self) -> None: self.mock_card = unittest.mock.Mock(return_value=unittest.mock.sentinel.card) self.mock_rng = unittest.mock.Mock(wraps=random.Random()) self.mock_rng.shuffle = unittest.mock.Mock() def test_Deck3_should_build(self) -> None: d = Deck3(size=1, random=self.mock_rng, card_factory=self.mock_card) self.assertEqual(52 * [unittest.mock.sentinel.card], d) self.mock_rng.shuffle.assert_called_with(d) self.assertEqual(52, len(self.mock_card.mock_calls)) expected = [ unittest.mock.call(r, s) for r in range(1, 14) for s in (Suit.CLUB, Suit.DIAMOND, Suit.HEART, Suit.SPADE) ] self.assertEqual(expected, self.mock_card.mock_calls) class TestDeckDeal(unittest.TestCase): def setUp(self) -> None: self.mock_deck = [getattr(unittest.mock.sentinel, str(x)) for x in range(52)] self.mock_card = unittest.mock.Mock(side_effect=self.mock_deck) self.mock_rng = unittest.mock.Mock(wraps=random.Random()) self.mock_rng.shuffle = unittest.mock.Mock() def test_Deck3_should_deal(self) -> None: d = Deck3(size=1, random=self.mock_rng, card_factory=self.mock_card) dealt = [] for i in range(52): card = d.deal() dealt.append(card) self.assertEqual(dealt, self.mock_deck) def test_empty_deck_should_exception(self) -> None: d = Deck3(size=1, random=self.mock_rng, card_factory=self.mock_card) for i in range(52): card = d.deal() self.assertRaises(DeckEmpty, d.deal) # Suite def suite4(): s = unittest.TestSuite() s.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestDeckBuild)) s.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestDeckDeal)) return s if __name__ == "__main__": t = unittest.TextTestRunner() t.run(suite4()) # Doctest # =============== # Sample Function with doctest string def ackermann(m: int, n: int) -> int: """Ackermann's Function ackermann(m, n) = $2 \\uparrow^{m-2} (n+3)-3$ See http://en.wikipedia.org/wiki/Ackermann_function and http://en.wikipedia.org/wiki/Knuth%27s_up-arrow_notation. >>> from Chapter_17.ch17_ex1 import ackermann >>> ackermann(2,4) 11 >>> ackermann(0,4) 5 >>> ackermann(1,0) 2 >>> ackermann(1,1) 3 """ if m == 0: return n + 1 elif m > 0 and n == 0: return ackermann(m - 1, 1) elif m > 0 and n > 0: return ackermann(m - 1, ackermann(m, n - 1)) else: raise LogicError() if __name__ == "__main__": import doctest suite5 = doctest.DocTestSuite() t = unittest.TextTestRunner(verbosity=2) t.run(suite5) # Combined Testing # ========================= # Main Program to combine suites if __name__ == "__main__": all_tests = unittest.TestSuite() all_tests.addTests(suite2()) all_tests.addTests(suite3()) all_tests.addTests(suite4()) all_tests.addTests(suite5) t = unittest.TextTestRunner() t.run(all_tests) # OS testing # ====================== # Functions to test from collections import defaultdict from typing import NamedTuple, Dict, List class GameStat(NamedTuple): player: str bet: str rounds: int final: int import csv from pathlib import Path from typing import Iterable, Iterator, Dict, DefaultDict, List def gamestat_iter(source: Iterable[Dict[str, str]]) -> Iterator[GameStat]: for row in source: yield GameStat(row["player"], row["bet"], int(row["rounds"]), int(row["final"])) def rounds_final(path: Path) -> DefaultDict[int, List[int]]: stats: DefaultDict[int, List[int]] = defaultdict(list) with path.open() as source: reader = csv.DictReader(source) assert set(reader.fieldnames) == set(GameStat._fields) for gs in gamestat_iter(reader): stats[gs.rounds].append(gs.final) return stats # Two approaches: # # - io.StringIO() # # - create a file # We might want to test missing or damaged file features, in which # case StringIO doesn't work as well as creating a file. # Test Cases import os class Test_Missing(unittest.TestCase): def setUp(self) -> None: try: (Path.cwd() / "data" / "ch17_sample.csv").unlink() # print(f"setUp removed {(Path.cwd()/"data"/"ch17_sample.csv")}") except OSError as e: pass # print("setUp expected", e) def test_missingFile_should_returnDefault(self) -> None: self.assertRaises( FileNotFoundError, rounds_final, (Path.cwd() / "data" / "ch17_sample.csv") ) class Test_Damaged(unittest.TestCase): def setUp(self) -> None: with (Path.cwd() / "data" / "ch17_sample.csv").open("w") as target: print("not_player,bet,rounds,final", file=target) print("data,1,1,1", file=target) def test_damagedFile_should_raiseException(self) -> None: self.assertRaises( AssertionError, rounds_final, (Path.cwd() / "data" / "ch17_sample.csv") ) def suite7(): s = unittest.TestSuite() s.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(Test_Missing)) s.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(Test_Damaged)) return s if __name__ == "__main__": t = unittest.TextTestRunner() t.run(suite7()) # External CSV Examples # ====================== # Unit Under Test from Chapter_4.ch04_ex3 import RateTimeDistance, RTD_Dynamic # Sample data sample_data = """\ rate_in,time_in,distance_in,rate_out,time_out,distance_out 2,3,,2,3,6 5,,7,5,1.4,7 ,11,13,1.18,11,13 """ # Parse the sample data from typing import Optional import csv def float_or_none(text: str) -> Optional[float]: if len(text) == 0: return None return float(text) # TestCase with only one test method class Test_RTD(unittest.TestCase): def runTest(self) -> None: with (Path.cwd() / "data" / "ch17_data.csv").open() as source: rdr = csv.DictReader(source) for row in rdr: self.example(**row) def example( self, rate_in: str, time_in: str, distance_in: str, rate_out: str, time_out: str, distance_out: str, ) -> None: args = dict( rate=float_or_none(rate_in), time=float_or_none(time_in), distance=float_or_none(distance_in), ) expected = dict( rate=float(rate_out), time=float(time_out), distance=float(distance_out) ) rtd = RateTimeDistance(**args) assert rtd.distance and rtd.rate and rtd.time self.assertAlmostEqual(rtd.distance, rtd.rate * rtd.time, places=2) self.assertAlmostEqual(rtd.rate, expected["rate"], places=2) self.assertAlmostEqual(rtd.time, expected["time"], places=2) self.assertAlmostEqual(rtd.distance, expected["distance"], places=2) # Build Suite from user-supplied sample data with (Path.cwd() / "data" / "ch17_data.csv").open("w", newline="") as target: target.write(sample_data) def suite9(): suite = unittest.TestSuite() suite.addTest(Test_RTD()) return suite if __name__ == "__main__": t = unittest.TextTestRunner() t.run(suite9()) # Performance Testing # ====================== # Using unittest for this is a bit "forced." We don't really need unittest framework # for this. import unittest import timeit class Test_Performance(unittest.TestCase): def test_simpleCalc_shouldbe_fastEnough(self): t = timeit.timeit( stmt="""RateTimeDistance(rate=1, time=2)""", setup="""from Chapter_4.ch04_ex3 import RateTimeDistance""", ) print("Run time", t) self.assertLess(t, 10, f"run time {t} >= 10") # Make a suite of the testcases def suite10(): s = unittest.TestSuite() s.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(Test_Performance)) return s if __name__ == "__main__": t = unittest.TextTestRunner() t.run(suite10()) ================================================ FILE: Chapter_17/ch17_ex2.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 17. Example 2. """ import unittest # SQLite testing # ========================= # This is integration testing, not unit testing. # Integration means we use the database # instead of isolating our code from the database. # A more formal unit test would mock the database layer. # SQLAlchemy ORM classes from typing import Any from Chapter_12.ch12_ex4 import Base, Blog, Post, Tag, assoc_post_tag import datetime import sqlalchemy.exc from sqlalchemy import create_engine def build_test_db(name="sqlite:///./data/ch17_blog.db"): """ Create Test Database and Schema """ engine = create_engine(name, echo=True) Base.metadata.drop_all(engine) Base.metadata.create_all(engine) return engine # Unittest Case from sqlalchemy.orm import sessionmaker, Session class Test_Blog_Queries(unittest.TestCase): Session: Any session: Session @staticmethod def setUpClass() -> None: engine = build_test_db() Test_Blog_Queries.Session = sessionmaker(bind=engine) session = Test_Blog_Queries.Session() tag_rr = Tag(phrase="#RedRanger") session.add(tag_rr) tag_w42 = Tag(phrase="#Whitby42") session.add(tag_w42) tag_icw = Tag(phrase="#ICW") session.add(tag_icw) tag_mis = Tag(phrase="#Mistakes") session.add(tag_mis) blog1 = Blog(title="Travel 2013") session.add(blog1) b1p1 = Post( date=datetime.datetime(2013, 11, 14, 17, 25), title="Hard Aground", rst_text="""Some embarrassing revelation. Including ☹ and ⚓︎""", blog=blog1, tags=[tag_rr, tag_w42, tag_icw], ) session.add(b1p1) b1p2 = Post( date=datetime.datetime(2013, 11, 18, 15, 30), title="Anchor Follies", rst_text="""Some witty epigram. Including ☺ and ☀︎︎""", blog=blog1, tags=[tag_rr, tag_w42, tag_mis], ) session.add(b1p2) blog2 = Blog(title="Travel 2014") session.add(blog2) session.commit() def setUp(self) -> None: self.session = Test_Blog_Queries.Session() def test_query_eqTitle_should_return1Blog(self) -> None: """Tests schema definition""" results = self.session.query(Blog).filter(Blog.title == "Travel 2013").all() self.assertEqual(1, len(results)) self.assertEqual(2, len(results[0].entries)) def test_query_likeTitle_should_return2Blog(self) -> None: """Tests SQLAlchemy, and test data""" results = self.session.query(Blog).filter(Blog.title.like("Travel %")).all() self.assertEqual(2, len(results)) def test_query_eqW42_tag_should_return2Post(self) -> None: results = self.session.query(Post).join(assoc_post_tag).join(Tag).filter( Tag.phrase == "#Whitby42" ).all() self.assertEqual(2, len(results)) def test_query_eqICW_tag_should_return1Post(self) -> None: results = self.session.query(Post).join(assoc_post_tag).join(Tag).filter( Tag.phrase == "#ICW" ).all() # print( [r.title for r in results] ) self.assertEqual(1, len(results)) self.assertEqual("Hard Aground", results[0].title) self.assertEqual("Travel 2013", results[0].blog.title) self.assertEqual( set(["#RedRanger", "#Whitby42", "#ICW"]), set(t.phrase for t in results[0].tags), ) # Make a suite of the testcases def suite8() -> unittest.TestSuite: s = unittest.TestSuite() s.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(Test_Blog_Queries)) return s if __name__ == "__main__": t = unittest.TextTestRunner() t.run(suite8()) __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_17/test_ch17.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 17. Example 2. .. note:: This example's name, ``test_ch17.py`` is chosen to help pytest do test discovery. """ # Card and Deck # ======================== import enum from typing import cast, Type import random class Suit(enum.Enum): CLUB = "♣" DIAMOND = "♦" HEART = "♥" SPADE = "♠" class Card: def __init__( self, rank: int, suit: Suit, hard: int = None, soft: int = None ) -> None: self.rank = rank self.suit = suit self.hard = hard or int(rank) self.soft = soft or int(rank) def __str__(self) -> str: return f"{self.rank!s}{self.suit.value!s}" class AceCard(Card): def __init__(self, rank: int, suit: Suit) -> None: super().__init__(rank, suit, 1, 11) class FaceCard(Card): def __init__(self, rank: int, suit: Suit) -> None: super().__init__(rank, suit, 10, 10) class LogicError(Exception): pass def card(rank: int, suit: Suit) -> Card: if rank == 1: return AceCard(rank, suit) elif 2 <= rank < 11: return Card(rank, suit) elif 11 <= rank < 14: return FaceCard(rank, suit) else: raise LogicError(f"Rank {rank} invalid") class Deck1(list): def __init__(self, size: int = 1) -> None: super().__init__() self.rng = random.Random() for d in range(size): for s in iter(Suit): cards: List[Card] = ( [cast(Card, AceCard(1, s))] + [Card(r, s) for r in range(2, 12)] + [FaceCard(r, s) for r in range(12, 14)] ) super().extend(cards) self.rng.shuffle(self) class Deck2(list): def __init__( self, size: int = 1, random: random.Random = random.Random(), ace_class: Type[Card] = AceCard, card_class: Type[Card] = Card, face_class: Type[Card] = FaceCard, ) -> None: super().__init__() self.rng = random for d in range(size): for s in iter(Suit): cards = ( [ace_class(1, s)] + [card_class(r, s) for r in range(2, 12)] + [face_class(r, s) for r in range(12, 14)] ) super().extend(cards) self.rng.shuffle(self) # Card Test # ======================== # Some Test Cases from pytest import mark def test_card(): three_clubs = Card(3, Suit.CLUB) assert "3♣" == str(three_clubs) assert 3 == three_clubs.rank assert Suit.CLUB == three_clubs.suit assert 3 == three_clubs.hard assert 3 == three_clubs.soft @mark.xfail def test_ace_card(): ace_spades = AceCard(1, Suit.SPADE) assert "A♠" == str(ace_spades), "This is expected to fail" assert 1 == ace_spades.rank assert Suit.SPADE == ace_spades.suit assert 1 == ace_spades.hard assert 11 == ace_spades.soft @mark.xfail def test_face_card(): queen_hearts = FaceCard(12, Suit.HEART), "This is expected to fail" assert "Q♥" == str(queen_hearts) assert 12 == queen_hearts.rank assert Suit.HEART == queen_hearts.suit assert 10 == queen_hearts.hard assert 10 == queen_hearts.soft # Suites -- not relevant for pytest -- the test discovery handles this. # Card Factory Test # ============================= # Another Test Case from pytest import raises def test_card_factory(): c1 = card(1, Suit.CLUB) assert isinstance(c1, AceCard) c2 = card(2, Suit.DIAMOND) assert isinstance(c1, Card) c10 = card(10, Suit.HEART) assert isinstance(c10, Card) cj = card(11, Suit.SPADE) assert isinstance(cj, FaceCard) ck = card(13, Suit.CLUB) assert isinstance(ck, FaceCard) with raises(LogicError): c14 = card(14, Suit.DIAMOND) with raises(LogicError): c0 = card(0, Suit.DIAMOND) # Deck with Mock Card # ============================== # Class Definitios class DeckEmpty(Exception): pass class Deck3(list): def __init__(self, size=1, random=random.Random(), card_factory=card): super().__init__() self.rng = random for d in range(size): super().extend([card_factory(r, s) for r in range(1, 14) for s in iter(Suit)]) self.rng.shuffle(self) def deal(self): try: return self.pop(0) except IndexError: raise DeckEmpty() # Test Cases import unittest.mock from types import SimpleNamespace from pytest import fixture @fixture def deck_context(): mock_deck = [ getattr(unittest.mock.sentinel, str(x)) for x in range(52) ] mock_card = unittest.mock.Mock(side_effect=mock_deck) mock_rng = unittest.mock.Mock( wraps=random.Random, shuffle=unittest.mock.Mock(return_value=None) ) return SimpleNamespace(**locals()) def test_deck_build(deck_context): d = Deck3( size=1, random=deck_context.mock_rng, card_factory=deck_context.mock_card ) deck_context.mock_rng.shuffle.assert_called_once_with(d) assert 52 == len(deck_context.mock_card.mock_calls) expected = [ unittest.mock.call(r, s) for r in range(1, 14) for s in iter(Suit) ] assert expected == deck_context.mock_card.mock_calls def test_deck_deal(deck_context): d = Deck3( size=1, random=deck_context.mock_rng, card_factory=deck_context.mock_card ) dealt = [] for c in range(52): c = d.deal() dealt.append(c) assert deck_context.mock_deck == dealt with raises(DeckEmpty): extra = d.deal() # Doctest # =============== # Sample Function with doctest string. pytest finds these, too. def ackermann(m, n): """Ackermann's Function ackermann(m, n) = $2 \\uparrow^{m-2} (n+3)-3$ See http://en.wikipedia.org/wiki/Ackermann_function and http://en.wikipedia.org/wiki/Knuth%27s_up-arrow_notation. >>> ackermann(2,4) 11 >>> ackermann(0,4) 5 >>> ackermann(1,0) 2 >>> ackermann(1,1) 3 """ if m == 0: return n + 1 elif m > 0 and n == 0: return ackermann(m - 1, 1) elif m > 0 and n > 0: return ackermann(m - 1, ackermann(m, n - 1)) # OS testing # ====================== # Functions to test from collections import defaultdict from typing import NamedTuple, Dict, List class GameStat(NamedTuple): player: str bet: str rounds: int final: int import csv from pathlib import Path def gamestat_iter(iterator): for row in iterator: yield GameStat(row["player"], row["bet"], int(row["rounds"]), int(row["final"])) def rounds_final(path: Path): stats: Dict[int, List[int]] = defaultdict(list) with path.open() as source: reader = csv.DictReader(source) assert set(reader.fieldnames) == set(GameStat._fields) for gs in gamestat_iter(reader): stats[gs.rounds].append(gs.final) return stats # Two approaches: # # - io.StringIO() # # - create a file # We might want to test missing or damaged file features, in which # case StringIO doesn't work as well as creating a file. # Test Cases from pytest import fixture, mark @fixture def no_file_path(): file_path = Path.cwd() / "data" / "ch17_sample.csv" try: file_path.unlink() # print(f"no_file_path fixture removed {file_path}") except FileNotFoundError as e: pass yield file_path # Cleanup can go here def test_missing(no_file_path): with raises(FileNotFoundError): rounds_final(no_file_path) @fixture def damaged_file_path(): file_path = Path.cwd() / "data" / "ch17_sample.csv" with file_path.open("w", newline="") as target: print("not_player,bet,rounds,final", file=target) print("data,1,1,1", file=target) yield file_path file_path.unlink() def test_damaged(damaged_file_path): with raises(AssertionError): stats = rounds_final(Path.cwd()/"data"/"ch17_sample.csv") # SQLite testing # ========================= # This is integration testing, not unit testing. # Integration means we use the database # instead of isolating our code from the database. # A more formal unit test would mock the database layer. # SQLAlchemy ORM classes from Chapter_12.ch12_ex4 import Base, Blog, Post, Tag, assoc_post_tag import datetime # Create Test Database and Schema import sqlalchemy.exc from sqlalchemy import create_engine def built_test_db(name="sqlite:///./data/ch17_blog.db"): engine = create_engine(name, echo=True) Base.metadata.drop_all(engine) Base.metadata.create_all(engine) return engine # Unittest Case from sqlalchemy.orm import sessionmaker @fixture(scope="module") def db_session_maker(): engine = built_test_db() session_maker = sessionmaker(bind=engine) session = session_maker() tag_rr = Tag(phrase="#RedRanger") session.add(tag_rr) tag_w42 = Tag(phrase="#Whitby42") session.add(tag_w42) tag_icw = Tag(phrase="#ICW") session.add(tag_icw) tag_mis = Tag(phrase="#Mistakes") session.add(tag_mis) blog1 = Blog(title="Travel 2013") session.add(blog1) b1p1 = Post( date=datetime.datetime(2013, 11, 14, 17, 25), title="Hard Aground", rst_text="""Some embarrassing revelation. Including ☹ and ⚓︎""", blog=blog1, tags=[tag_rr, tag_w42, tag_icw], ) session.add(b1p1) b1p2 = Post( date=datetime.datetime(2013, 11, 18, 15, 30), title="Anchor Follies", rst_text="""Some witty epigram. Including ☺ and ☀︎︎""", blog=blog1, tags=[tag_rr, tag_w42, tag_mis], ) session.add(b1p2) blog2 = Blog(title="Travel 2014") session.add(blog2) session.commit() return session_maker def test_database(db_session_maker): db_session = db_session_maker() # Tests schema definition results = db_session.query(Blog).filter(Blog.title == "Travel 2013").all() assert 1 == len(results) assert 2 == len(results[0].entries) # Tests SQLAlchemy, and test data results = db_session.query(Blog).filter(Blog.title.like("Travel %")).all() assert 2 == len(results) results = db_session.query(Post).join(assoc_post_tag).join(Tag).filter( Tag.phrase == "#Whitby42" ).all() assert 2 == len(results) results = db_session.query(Post).join(assoc_post_tag).join(Tag).filter( Tag.phrase == "#ICW" ).all() # print( [r.title for r in results] ) assert 1 == len(results) assert "Hard Aground" == results[0].title assert "Travel 2013" == results[0].blog.title assert set(["#RedRanger", "#Whitby42", "#ICW"]) == set(t.phrase for t in results[0].tags) # External CSV Examples # ====================== # Unit Under Test from Chapter_4.ch04_ex3 import RateTimeDistance from pytest import approx # Parsing the sample data def float_or_none(text): if len(text) == 0: return None return float(text) # Build Suite from user-supplied sample data import csv with (Path.cwd() / "Chapter_17" / "ch17_data.csv").open() as source: rdr = csv.DictReader(source) rtd_cases = list(rdr) @fixture(params=rtd_cases) def rtd_example(request): """Each request will include a param. This will be a row from the source cases.""" yield request.param def test_rtd(rtd_example): args = dict( rate=float_or_none(rtd_example['rate_in']), time=float_or_none(rtd_example['time_in']), distance=float_or_none(rtd_example['distance_in']), ) result = dict( rate=float_or_none(rtd_example['rate_out']), time=float_or_none(rtd_example['time_out']), distance=float_or_none(rtd_example['distance_out']), ) # print(f"***{args}***") rtd = RateTimeDistance(**args) assert rtd.distance == approx(rtd.rate * rtd.time) assert rtd.rate == approx(result["rate"], abs=1E-2) assert rtd.time == approx(result["time"]) assert rtd.distance == approx(result["distance"]) # Performance Testing # ====================== # This can be used stand-alone, or with pytest. import timeit def test_performance(): t = timeit.timeit( stmt="""RateTimeDistance( rate=1, time=2 )""", setup="""from Chapter_4.ch04_ex3 import RateTimeDistance""", ) print("Run time", t) assert t < 10, f"run time {t} >= 10" ================================================ FILE: Chapter_18/__init__.py ================================================ ================================================ FILE: Chapter_18/ch18_demo.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 18. demo. """ import sys print(sys.argv) ================================================ FILE: Chapter_18/ch18_ex1.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 18. Example 1. """ import os # Command-line parsing # ======================= import argparse import sys import logging from typing import List, Optional __version__ = "2e" def get_options_1( argv: List[str] = sys.argv[1:], defaults: Optional[argparse.Namespace] = None ) -> argparse.Namespace: """ Parse command-line arguments and options :param argv: Command line, default ``sys.argv[1:]`` :param defaults: an ``argparse.Namespace`` with defaults :return: argparse.Namespace with parameters """ # Step 1: Parser. parser = argparse.ArgumentParser( prog="ch18_ex1.py", description="Simulate Blackjack", add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) # Step 2: Arguments. # Simple on-off options parser.add_argument("-v", "--verbose", action="store_true", default=False) parser.add_argument( "--debug", action="store_const", const=logging.DEBUG, default=logging.INFO, dest="logging_level", ) # Options with values parser.add_argument( "--dealerhit", action="store", default="Hit17", choices=["Hit17", "Stand17"], dest="dealer_rule", ) parser.add_argument( "--resplit", action="store", default="ReSplit", choices=["ReSplit", "NoReSplit", "NoReSplitAces"], dest="split_rule", ) parser.add_argument( "--decks", action="store", default=6, type=int, help="Decks to deal" ) parser.add_argument("--limit", action="store", default=50, type=int) parser.add_argument("--payout", action="store", default="(3,2)") parser.add_argument( "-p", "--playerstrategy", action="store", default="SomeStrategy", choices=["SomeStrategy", "AnotherStrategy"], dest="player_rule", ) parser.add_argument( "-b", "--bet", action="store", default="Flat", choices=["Flat", "Martingale", "OneThreeTwoSix"], dest="betting_rule", ) parser.add_argument("-r", "--rounds", action="store", default=100, type=int) parser.add_argument("-s", "--stake", action="store", default=50, type=int) parser.add_argument( "--samples", action="store", default=int(os.environ.get("SIM_SAMPLES", 100)), type=int, help="Samples to generate", ) # Arguments parser.add_argument("outputfile", action="store", metavar="output") # required # Version and help parser.add_argument("-V", "--version", action="version", version=__version__) parser.add_argument("-?", "--help", action="help") # Step 3: Parse return parser.parse_args(argv, namespace=defaults) # Examples test_parsing_1 = """ >>> config1 = get_options_1( ... ["data/ch18_simulation1.dat"] ... ) >>> config1 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) >>> config2 = get_options_1( ... ["-v", "--samples", "2", "data/ch18_simulation2.dat"] ... ) >>> config2 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) >>> config3 = get_options_1( ... ["-b", "Martingale", "--samples", "3", "data/ch18_simulation3.dat"] ... ) >>> config3 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) >>> import shlex >>> config4 = get_options_1( ... shlex.split("-b Martingale --samples 3 data/ch18_simulation3.dat") ... ) >>> config4 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) """ import pytest def test_get_config_1(capsys): with pytest.raises(SystemExit) as exception: get_options_1( ["-b", "Doesn't Work", "--samples", "x", "data/ch18_simulation3.dat"] ) assert exception.value.args == (2,) out, err = capsys.readouterr() expected_err = """\ usage: ch18_ex1.py [-v] [--debug] [--dealerhit {Hit17,Stand17}] [--resplit {ReSplit,NoReSplit,NoReSplitAces}] [--decks DECKS] [--limit LIMIT] [--payout PAYOUT] [-p {SomeStrategy,AnotherStrategy}] [-b {Flat,Martingale,OneThreeTwoSix}] [-r ROUNDS] [-s STAKE] [--samples SAMPLES] [-V] [-?] output ch18_ex1.py: error: argument -b/--bet: invalid choice: "Doesn't Work" (choose from 'Flat', 'Martingale', 'OneThreeTwoSix') """ assert expected_err == err # Supplying Defaults # ===================== # Simple config4 = argparse.Namespace() config4.dealer_rule = "Hit17" config4.split_rule = "NoReSplitAces" config4.limit = 50 config4.decks = 6 config4.payout = "(3,2)" config4.player_rule = "SomeStrategy" config4.betting_rule = "Flat" config4.rounds = 100 config4.stake = 50 config4.outputfile = "data/ch18_simulation4.dat" config4.samples = int(os.environ.get("SIM_SAMPLES", 200)) test_parsing_2 = """ >>> config5 = get_options_1( ... ["-b", "OneThreeTwoSix", "data/ch18_simulation4.dat"], defaults=config4 ... ) >>> config5 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) """ # Path manipulations # =================== from pathlib import Path test_path_examples = """ >>> p = Path.cwd() / "data" / "simulation.csv" >>> p.name 'simulation.csv' >>> p.suffix '.csv' >>> p.exists() False """ test_path_example2 = """ >>> source_path = Path("data")/"ch14_simulation.dat" >>> with source_path.open() as source_file: ... count = 0 ... for line in source_file: ... if len(line) > 0: ... count += 1 >>> count 100 """ test_path_example3 = """ >>> if (Path("data")/"ch18_directory").exists(): ... (Path("data")/"ch18_directory").rmdir() >>> target = Path("data")/"ch18_directory" >>> target.mkdir(exist_ok=True, parents=True) >>> (Path("data")/"ch18_directory").exists() True >>> import datetime >>> today = datetime.datetime.today() >>> today = datetime.datetime(2019, 3, 18) >>> target = Path("data")/today.strftime("%Y%m%d") >>> target.mkdir(exist_ok=True, parents=True) >>> target.exists() True """ __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) import pytest pytest.main(["Chapter_18/ch18_ex1.py"]) get_options_1(['--help']) ================================================ FILE: Chapter_18/ch18_ex2.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 18. Example 2. """ # Command-line parsing # ======================= # Supply Defaults Via ChainMap built from configuration files and environment variables. from collections import ChainMap from typing import Optional, cast, Dict, Any, List, Type from pathlib import Path import yaml import sys import os import argparse from Chapter_18.ch18_ex1 import get_options_1 def nint(x: Optional[str]) -> Optional[int]: if x is None: return x return int(x) def get_options_2(argv: List[str] = sys.argv[1:]) -> argparse.Namespace: """ Get arguments and options :param argv: default sys.argv[1:] :return: argparse.Namespace """ # 1. Get files config_locations = ( Path.cwd(), Path.home(), Path.cwd() / "opt", # A stand-in for Path("/etc") or Path("/opt") Path(__file__) / "config", # Other common places... # Path("~someapp").expanduser(), ) candidate_paths = (dir / "ch18app.yaml" for dir in config_locations) config_paths = (path for path in candidate_paths if path.exists()) files_values = [yaml.load(str(path)) for path in config_paths] # 2. Get potential overrides from the run-time environment env_settings = [ ("samples", nint(os.environ.get("SIM_SAMPLES", None))), ("stake", nint(os.environ.get("SIM_STAKE", None))), ("rounds", nint(os.environ.get("SIM_ROUNDS", None))), ] env_values = {k: v for k, v in env_settings if v is not None} # 3. Build defaults defaults = argparse.Namespace( **ChainMap( env_values, # check here first *files_values # All of the files, in order ) ) # 4. Use the previously-defined argument parser. return get_options_1(argv, defaults) test_env_override = """ >>> config5a = get_options_2( ... ["-b", "OneThreeTwoSix", "data/ch18_simulation5.dat"], ... ) >>> config5a 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) >>> os.environ["SIM_STAKE"] = "100" # Mock the user's environment >>> config5b = get_options_2( ... ["-b", "OneThreeTwoSix", "data/ch18_simulation5.dat"], ... ) >>> config5b 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) """ __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_18/ch18_ex3.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 18. Example 3. """ # Preliminary Definitions # ======================== # Import the simulation model we've been using. # Plus a handy validation function that assures the output is sensible. # And logging, since we'll use it for some examples. from Chapter_14.simulation_model import * import logging from pprint import pprint from pathlib import Path import os # Top-level Function # =================== # Here's the main feature of a program as a top-level function. import ast import csv import argparse def simulate_blackjack(config: argparse.Namespace) -> None: dealer_classes = {"Hit17": Hit17, "Stand17": Stand17} dealer_rule = dealer_classes[config.dealer_rule]() split_classes = { "ReSplit": ReSplit, "NoReSplit": NoReSplit, "NoReSplitAces": NoReSplitAces } split_rule = split_classes[config.split_rule]() try: payout = ast.literal_eval(config.payout) assert len(payout) == 2 except Exception as ex: raise ValueError(f"Invalid payout {config.payout}") from ex table = Table( decks=config.decks, limit=config.limit, dealer=dealer_rule, split=split_rule, payout=payout, ) player_classes = {"SomeStrategy": SomeStrategy, "AnotherStrategy": AnotherStrategy} player_rule = player_classes[config.player_rule]() betting_classes = { "Flat": Flat, "Martingale": Martingale, "OneThreeTwoSix": OneThreeTwoSix } betting_rule = betting_classes[config.betting_rule]() player = Player( play=player_rule, betting=betting_rule, max_rounds=config.rounds, init_stake=config.stake, ) simulate = Simulate(table, player, config.samples) with Path(config.outputfile).open("w", newline="") as target: wtr = csv.writer(target) wtr.writerows(simulate) # Using the top-level function from Chapter_18.ch18_ex2 import get_options_2 if __name__ == "__main__": arguments = ["-b", "OneThreeTwoSix", "data/ch18_simulation5.dat"] config_1 = get_options_2(arguments) simulate_blackjack(config_1) check(Path.cwd() / "data" / "ch18_simulation5.dat") # Here's how we can build the configuration as a context manager. # It's consistent with using a context manager for logging setup. from typing import List class Build_Config: def __init__(self, argv: List[str]) -> None: self.options = get_options_2(argv) def __enter__(self) -> argparse.Namespace: return self.options def __exit__(self, *exc) -> None: return # Using the top-level function with a context manager that collects # the configuration. if __name__ == "__main__": arguments = ["-b", "OneThreeTwoSix", "data/ch18_simulation5.dat"] with Build_Config(arguments) as config_2: simulate_blackjack(config_2) check(Path.cwd() / "data" / "ch18_simulation5.dat") # Logging and config as context # =============================== import logging.config import sys # Here's logging setup as a context manager. class Setup_Logging: def __init__(self, stream=sys.stderr, disable_existing_loggers=False) -> None: """ Preserves existing loggers. """ self.config = dict( version=1, handlers={ "console": { "class": "logging.StreamHandler", "stream": stream, "formatter": "basic", } }, formatters={ "basic": {"format": "{name} ({levelname}) {message}", "style": "{"} }, root={"handlers": ["console"], "level": logging.INFO}, disable_existing_loggers=disable_existing_loggers, ) def __enter__(self) -> "Setup_Logging": logging.config.dictConfig(self.config) return self def __exit__(self, *exc) -> None: logging.shutdown() return # The downside of using a dictConfig as a context manager is that # logging objects created before logging is configured don't connect. # properly to the root logger with usable handlers. class ClassLogger: log = logging.getLogger("ClassLogger") def work(self) -> None: self.log.info("Some Info") self.log.warning("A Warning") class InstanceLogger: def __init__(self, name: str) -> None: self.log = logging.getLogger(f"InstanceLogger.{name}") def work(self) -> None: self.log.info("Some Info") self.log.warning("A Warning") # Here's a main function that uses two nested contexts. test_nested_contexts_1 = """ Loggers created outside the context *will* be ignored because disable_existing_loggers is True. This includes loggers in class definitions. >>> il_early = InstanceLogger("ignored") >>> with Setup_Logging(disable_existing_loggers=True, stream=sys.stdout): ... cl = ClassLogger() ... il = InstanceLogger("good") ... cl.work() ... il.work() ... il_early.work() InstanceLogger.good (INFO) Some Info InstanceLogger.good (WARNING) A Warning """ test_nested_contexts_2 = """ All loggers *will* be used because disable_existing_loggers is False >>> il_early = InstanceLogger("retained") >>> with Setup_Logging(disable_existing_loggers=False, stream=sys.stdout): ... cl = ClassLogger() ... il = InstanceLogger("good") ... cl.work() ... il.work() ... il_early.work() ClassLogger (INFO) Some Info ClassLogger (WARNING) A Warning InstanceLogger.good (INFO) Some Info InstanceLogger.good (WARNING) A Warning InstanceLogger.retained (INFO) Some Info InstanceLogger.retained (WARNING) A Warning """ # PITL via Function Composition # ======================================= # Example of adding features through more # top-level functions. def simulate_blackjack_betting(config: argparse.Namespace) -> None: for bet_class in "Flat", "Martingale", "OneThreeTwoSix": config.betting_rule = bet_class config.outputfile = Path("data")/f"ch18_simulation6_{bet_class}.dat" simulate_blackjack(config) # This works reasonably well. We can do a bit more with object # composition. # Top script if __name__ == "__main__": arguments = ["-b", "OneThreeTwoSix", "data/ch18_simulation5.dat"] with Setup_Logging(): with Build_Config(arguments) as config_3: simulate_blackjack_betting(config_3) check(Path.cwd() / "data" / "ch18_simulation6_Flat.dat") check(Path.cwd() / "data" / "ch18_simulation6_Martingale.dat") check(Path.cwd() / "data" / "ch18_simulation6_OneThreeTwoSix.dat") # PITL via Object Composition # ======================================= # Proper **Command** design pattern from typing import Dict, Any, Type class Command: """ Typical use >>> c = Command() >>> c.configure(argparse.Namespace(item="value")) >>> c.run() """ def __init__(self) -> None: self.config: Dict[str, Any] = {} def configure(self, namespace: argparse.Namespace) -> None: self.config.update(vars(namespace)) def run(self) -> None: """Overridden by a subclass""" pass class Simulate_Command(Command): dealer_rule_map = {"Hit17": Hit17, "Stand17": Stand17} split_rule_map = { "ReSplit": ReSplit, "NoReSplit": NoReSplit, "NoReSplitAces": NoReSplitAces } player_rule_map = {"SomeStrategy": SomeStrategy, "AnotherStrategy": AnotherStrategy} betting_rule_map = { "Flat": Flat, "Martingale": Martingale, "OneThreeTwoSix": OneThreeTwoSix } def run(self) -> None: dealer_rule = self.dealer_rule_map[self.config["dealer_rule"]]() split_rule = self.split_rule_map[self.config["split_rule"]]() payout: Tuple[int, int] try: payout = ast.literal_eval(self.config["payout"]) assert len(payout) == 2 except Exception as e: raise Exception(f"Invalid payout {self.config['payout']!r}") from e table = Table( decks=self.config["decks"], limit=self.config["limit"], dealer=dealer_rule, split=split_rule, payout=payout, ) player_rule = self.player_rule_map[self.config["player_rule"]]() betting_rule = self.betting_rule_map[self.config["betting_rule"]]() player = Player( play=player_rule, betting=betting_rule, max_rounds=self.config["rounds"], init_stake=self.config["stake"], ) simulate = Simulate(table, player, self.config["samples"]) with Path(self.config["outputfile"]).open("w", newline="") as target: wtr = csv.writer(target) wtr.writerows(simulate) if __name__ == "__main__": arguments = ["-b", "OneThreeTwoSix", "data/ch18_simulation5.dat"] with Setup_Logging(): with Build_Config(arguments) as config_4: main = Simulate_Command() main.configure(config_4) main.run() # Composition class Analyze_Command(Command): def run(self) -> None: with Path(self.config["outputfile"]).open() as target: rdr = csv.reader(target) outcomes = (float(row[10]) for row in rdr) first = next(outcomes) sum_0, sum_1 = 1, first value_min = value_max = first for value in outcomes: sum_0 += 1 # value**0 sum_1 += value # value**1 value_min = min(value_min, value) value_max = max(value_max, value) mean = sum_1 / sum_0 print( f"{self.config['outputfile']}\n" f"Mean = {mean:.1f}\n" f"House Edge = {1 - mean / 50:.1%}\n" f"Range = {value_min:.1f} {value_max:.1f}" ) class Command_Sequence(Command): """ Subclass provides a sequence of classes. These will be expanded into instance and configured. """ steps: List[Type[Command]] = [] def __init__(self) -> None: self._sequence = [class_() for class_ in self.steps] def configure(self, config: argparse.Namespace) -> None: for step in self._sequence: step.configure(config) def run(self) -> None: for step in self._sequence: step.run() class Simulate_and_Analyze(Command_Sequence): steps = [Simulate_Command, Analyze_Command] if __name__ == "__main__": with Build_Config(arguments) as config_5: both = Simulate_and_Analyze() both.configure(config_5) both.run() check(Path.cwd() / "data" / "ch18_simulation6_Flat.dat") check(Path.cwd() / "data" / "ch18_simulation6_Martingale.dat") check(Path.cwd() / "data" / "ch18_simulation6_OneThreeTwoSix.dat") # Wrapping another class in a For-All loop. class ForAllBets_Simulate(Command): def run(self) -> None: for bet_class in "Flat", "Martingale", "OneThreeTwoSix": self.config["betting_rule"] = bet_class self.config["outputfile"] = Path("data")/f"ch18_simulation7_{bet_class}.dat" sim = Simulate_Command() # sim.config = self.config # Push the configuration directly. sim.configure(argparse.Namespace(**self.config)) sim.run() if __name__ == "__main__": with Build_Config(arguments) as config_6: msc = ForAllBets_Simulate() msc.configure(config_6) msc.run() check(Path.cwd() / "data" / "ch18_simulation7_Flat.dat") check(Path.cwd() / "data" / "ch18_simulation7_Martingale.dat") check(Path.cwd() / "data" / "ch18_simulation7_OneThreeTwoSix.dat") # Overall doctest __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_18/ch18app.yaml ================================================ # User configuration file: Chapter_18/ch18app.yaml dealer_rule: Hit17 split_rule: NoReSplitAces ================================================ FILE: Chapter_18/opt/ch18app.yaml ================================================ # Installation-wide configuration file: Chapter_18/opt/ch18app.yaml rounds: 100 samples: 100 stake: 50 ================================================ FILE: Chapter_19/__init__.py ================================================ ================================================ FILE: Chapter_19/ch19_ex1.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 19. Example 1. """ import unittest # Import the test suite. from Chapter_19.tests import test_all if __name__ == "__main__": # Execute all tests which can be discovered in the suite. unittest.main(test_all, verbosity=2) ================================================ FILE: Chapter_19/ch19_ex2.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 19. Example 2. """ import pytest if __name__ == "__main__": # Execute all tests which can be discovered in the suite. pytest.main(["-v", "tests"]) ================================================ FILE: Chapter_19/some_algorithm/__init__.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 19. Example 3. """ # Package with variant implementations # ------------------------------------- # A complex import import sys from typing import Type from Chapter_19.some_algorithm.abstraction import AbstractSomeAlgorithm SomeAlgorithm: Type[AbstractSomeAlgorithm] if sys.platform.endswith("32"): # print(f"{sys.platform}: SHORT") from Chapter_19.some_algorithm.short_version import * SomeAlgorithm = Implementation_Short else: # print(f"{sys.platform}: LONG") from Chapter_19.some_algorithm.long_version import * SomeAlgorithm = Implementation_Long # Some additional debugging to display the import behavior print(f"{__name__}: {SomeAlgorithm.__module__}\n{SomeAlgorithm.__doc__}") ================================================ FILE: Chapter_19/some_algorithm/abstraction.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 19. Example 3. """ class AbstractSomeAlgorithm: pass ================================================ FILE: Chapter_19/some_algorithm/long_version.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 19. Example 3. """ # Long-version Implementation # ------------------------------------- from .abstraction import AbstractSomeAlgorithm class Implementation_Long(AbstractSomeAlgorithm): """ The Long Version """ def value(self) -> int: return 2 ** 42 ================================================ FILE: Chapter_19/some_algorithm/short_version.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 19. Example 3. """ # Short-version Implementation # ------------------------------------- from .abstraction import AbstractSomeAlgorithm class Implementation_Short(AbstractSomeAlgorithm): """ The Short Version """ def value(self) -> int: return 42 ================================================ FILE: Chapter_19/tests/__init__.py ================================================ ================================================ FILE: Chapter_19/tests/test_all.py ================================================ # A Test Module # ---------------- """Test all features of some_algorithm. Requires some_algorithm package be on the PYTHONPATH. """ # Imports import unittest from Chapter_19 import some_algorithm # Test Case class TestSomeAlgorithm(unittest.TestCase): def test_import_should_see_value(self): x = some_algorithm.SomeAlgorithm() assert 2 ** 42 == x.value() # Run the implicit test suite if this is used as a main program. if __name__ == "__main__": unittest.main(exit=False) ================================================ FILE: Chapter_2/__init__.py ================================================ ================================================ FILE: Chapter_2/ch02_ex1.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 2. Example 1. """ from typing import Tuple class Card: def __init__(self, rank: str, suit: str) -> None: self.suit = suit self.rank = rank self.hard, self.soft = self._points() def _points(self) -> Tuple[int, int]: return int(self.rank), int(self.rank) class AceCard(Card): def _points(self) -> Tuple[int, int]: return 1, 11 class FaceCard(Card): def _points(self) -> Tuple[int, int]: return 10, 10 test_card = """ >>> x = Card('2','♠') >>> str(x) # doctest: +ELLIPSIS '<....Card object at ...>' >>> repr(x) # doctest: +ELLIPSIS '<....Card object at ...>' >>> print(x) # doctest: +ELLIPSIS <....Card object at ...> >>> cards = [AceCard('A', '♠'), Card('2','♠'), FaceCard('J','♠'),] >>> cards # doctest: +ELLIPSIS [<...AceCard ...>, <...Card ...>, <...FaceCard ...>] """ __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_2/ch02_ex2.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 2. Example 2. """ from typing import Tuple, Any, Union, cast # Definition of a simple class hierarchy. # Note the overlap with a dataclass if we use properties. class Card: insure = False def __init__(self, rank: str, suit: Any) -> None: self.suit = suit self.rank = rank self.hard, self.soft = self._points() def __eq__(self, other: Any) -> bool: return ( self.suit == cast("Card", other).suit and self.rank == cast("Card", other).rank and self.hard == cast("Card", other).hard and self.soft == cast("Card", other).soft ) def __repr__(self) -> str: return f"{self.__class__.__name__}(suit={self.suit!r}, rank={self.rank!r})" def __str__(self) -> str: return f"{self.rank}{self.suit}" def _points(self) -> Tuple[int, int]: return int(self.rank), int(self.rank) class AceCard(Card): insure = True def _points(self) -> Tuple[int, int]: return 1, 11 class FaceCard(Card): def _points(self) -> Tuple[int, int]: return 10, 10 # We can create cards like this test_card = """ >>> Suit.Club >>> d1 = [AceCard('A', '♠'), Card('2', '♠'), FaceCard('Q', '♠'), ] >>> d1 [AceCard(suit='♠', rank='A'), Card(suit='♠', rank='2'), FaceCard(suit='♠', rank='Q')] >>> Card('2', '♠') Card(suit='♠', rank='2') >>> str(Card('2', '♠')) '2♠' """ # Instead of strings, we can use an enum from enum import Enum class Suit(str, Enum): Club = "♣" Diamond = "♦" Heart = "♥" Spade = "♠" # We can create cards like this test_card_suit = """ >>> cards = [AceCard('A', Suit.Spade), Card('2', Suit.Spade), FaceCard('Q', Suit.Spade),] >>> cards [AceCard(suit=, rank='A'), Card(suit=, rank='2'), FaceCard(suit=, rank='Q')] """ test_suit_value = """ >>> Suit.Heart.value '♥' >>> Suit.Heart.value = 'H' # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): File "/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py", line 1329, in __run compileflags, 1), test.globs) File "", line 1, in Suit.Heart.value = 'H' File "/Users/slott/miniconda3/envs/py37/lib/python3.7/types.py", line 175, in __set__ raise AttributeError("can't set attribute") AttributeError: can't set attribute """ __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_2/ch02_ex3.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 2. Example 3. """ from Chapter_2.ch02_ex2 import * from typing import cast, Iterable, Iterator # Factory Function def card(rank: int, suit: Suit) -> Card: if rank == 1: return AceCard("A", suit) elif 2 <= rank < 11: return Card(str(rank), suit) elif 11 <= rank < 14: name = {11: "J", 12: "Q", 13: "K"}[rank] return FaceCard(name, suit) raise Exception("Design Failure") # This function builds a Card from a numeric rank and a Suit object. We can now # build cards very simply. test_card = """ >>> deck = [card(rank, suit) for rank in range(1, 14) for suit in (Suit.Club, Suit.Diamond, Suit.Heart, Suit.Spade)] >>> len(deck) 52 >>> sorted(set(c.suit for c in deck)) [, , , ] >>> sorted(set(c.rank for c in deck)) ['10', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'J', 'K', 'Q'] """ deck = [ card(rank, suit) for rank in range(1, 14) for suit in cast(Iterable[Suit], Suit) ] deck_l = [ card(rank, suit) for rank in range(1, 14) for suit in iter(Suit) ] # Here's a less desirable form of the factory function. # It harbors a hidden bug because the else assumes too much. def card2(rank: int, suit: Suit) -> Card: if rank == 1: return AceCard("A", suit) elif 2 <= rank < 11: return Card(str(rank), suit) else: name = {11: "J", 12: "Q", 13: "K"}[rank] return FaceCard(name, suit) test_card2 = """ >>> deck2 = [card2(rank, suit) for rank in range(13) for suit in Suit] # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): KeyError: 0 """ # Here's a more consistent factory function that doesn't mix elif and a mapping. def card3(rank: int, suit: Suit) -> Card: if rank == 1: return AceCard("A", suit) elif 2 <= rank < 11: return Card(str(rank), suit) elif rank == 11: return FaceCard("J", suit) elif rank == 12: return FaceCard("Q", suit) elif rank == 13: return FaceCard("K", suit) else: raise Exception("Rank out of range") # Note... This works, but mypy doesn't completely understand the simple form. # To help mypy, we use this: cast(Iterable[Suit], Suit) # This makes it clear an Enum subclass is an iterable over the enumerated values. test_card3 = """ >>> deck3 = [card3(rank, suit) for rank in range(1, 14) for suit in Suit] >>> len(deck3) 52 >>> sorted(set(c.suit for c in deck3)) [, , , ] >>> sorted(set(c.rank for c in deck3)) ['10', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'J', 'K', 'Q'] """ deck3 = [ card3(rank, suit) for rank in range(1, 14) for suit in cast(Iterable[Suit], Suit) ] # Here's an incomplete, but more consistent factory that uses just a mapping. # This doesn't properly translate rank to a string. def card4(rank: int, suit: Suit) -> Card: class_ = {1: AceCard, 11: FaceCard, 12: FaceCard, 13: FaceCard}.get(rank, Card) return class_(str(rank), suit) test_card4 = """ >>> deck4 = [card4(rank, suit) for rank in range(1, 14) for suit in Suit] >>> len(deck4) 52 >>> sorted(set(c.suit for c in deck4)) [, , , ] >>> sorted(set(c.rank for c in deck4)) ['1', '10', '11', '12', '13', '2', '3', '4', '5', '6', '7', '8', '9'] """ # Here's the two-parallel mapping version. def card5(rank: int, suit: Suit) -> Card: class_ = {1: AceCard, 11: FaceCard, 12: FaceCard, 13: FaceCard}.get(rank, Card) rank_str = {1: "A", 11: "J", 12: "Q", 13: "K"}.get(rank, str(rank)) return class_(rank_str, suit) test_card5 = """ >>> deck5 = [card5(rank, suit) for rank in range(1, 14) for suit in Suit] >>> len(deck5) 52 >>> sorted(set(c.suit for c in deck5)) [, , , ] >>> sorted(set(c.rank for c in deck5)) ['10', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'J', 'K', 'Q'] """ # Here's the mapping two a 2-tuple version. def card6(rank: int, suit: Suit) -> Card: class_, rank_str = { 1: (AceCard, "A"), 11: (FaceCard, "J"), 12: (FaceCard, "Q"), 13: (FaceCard, "K") }.get( rank, (Card, str(rank)) ) return class_(rank_str, suit) test_card6 = """ >>> deck6 = [card6(rank, suit) for rank in range(1, 14) for suit in Suit] >>> len(deck6) 52 >>> sorted(set(c.suit for c in deck6)) [, , , ] >>> sorted(set(c.rank for c in deck6)) ['10', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'J', 'K', 'Q'] """ # Here's the mapping to a partial version. # While this is appealing, it doesn't work well for class instance creation. # We get TypeError: 'AceCard' object is not callable # from functools import partial # part_class = partial(AceCard, 'A') # card = part_class(Suit.Heart) # Instead, we'll use lambdas as our partials. def card7(rank: int, suit: Suit) -> Card: class_rank = { 1: lambda suit: AceCard("A", suit), 11: lambda suit: FaceCard("J", suit), 12: lambda suit: FaceCard("Q", suit), 13: lambda suit: FaceCard("K", suit), }.get( rank, lambda suit: Card(str(rank), suit) ) return class_rank(suit) test_card7 = """ >>> deck7 = [card7(rank, suit) for rank in range(1, 14) for suit in Suit] >>> len(deck7) 52 >>> sorted(set(c.suit for c in deck7)) [, , , ] >>> sorted(set(c.rank for c in deck7)) ['10', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'J', 'K', 'Q'] """ # Here's a stateful card factory that uses a fluent interface # to build cards. Note the methods **must** be called in the right # order, a common limitation of fluent interfaces. class CardFactory: def rank(self, rank: int) -> "CardFactory": self.class_, self.rank_str = { 1: (AceCard, "A"), 11: (FaceCard, "J"), 12: (FaceCard, "Q"), 13: (FaceCard, "K"), }.get( rank, (Card, str(rank)) ) return self def suit(self, suit: Suit) -> Card: return self.class_(self.rank_str, suit) test_card8 = """ >>> card8 = CardFactory() >>> deck8 = [card8.rank(r + 1).suit(s) for r in range(13) for s in Suit] >>> len(deck8) 52 >>> sorted(set(c.suit for c in deck8)) [, , , ] >>> sorted(set(c.rank for c in deck8)) ['10', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'J', 'K', 'Q'] """ __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) deck = [ card(rank, suit) for rank in range(1, 14) for suit in cast(Iterable[Suit], Suit) ] deck3 = [ card3(rank, suit) for rank in range(1, 14) for suit in cast(Iterable[Suit], Suit) ] deck4 = [ card4(rank, suit) for rank in range(1, 14) for suit in cast(Iterable[Suit], Suit) ] deck5 = [ card5(rank, suit) for rank in range(1, 14) for suit in cast(Iterable[Suit], Suit) ] deck6 = [ card6(rank, suit) for rank in range(1, 14) for suit in cast(Iterable[Suit], Suit) ] deck7 = [ card7(rank, suit) for rank in range(1, 14) for suit in cast(Iterable[Suit], Suit) ] card8 = CardFactory() deck8 = [ card8.rank(r + 1).suit(s) for r in range(13) for s in cast(Iterable[Suit], Suit) ] ================================================ FILE: Chapter_2/ch02_ex4.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 2. Example 4. """ # Alternative Designs for the Initialization from Chapter_2.ch02_ex3 import card, Suit from typing import Any, cast, Iterator from abc import abstractmethod # Subclass only. Omitting the superclass __init__, while legal, baffles mypy badly. # Omitting the __init__ is not suggested. class Card2: @abstractmethod def __init__(self, rank: int, suit: Suit) -> None: self.rank: str self.suit: Suit self.hard: int self.soft: int def __eq__(self, other: Any) -> bool: return ( self.suit == cast("Card2", other).suit and self.rank == cast("Card2", other).rank and self.hard == cast("Card2", other).hard and self.soft == cast("Card2", other).soft ) def __repr__(self) -> str: return f"suit={self.suit!r}, rank={self.rank!r}, hard={self.hard!r}, soft={self.soft!r}" class NumberCard2(Card2): def __init__(self, rank: int, suit: Suit) -> None: self.suit = suit self.rank = str(rank) self.hard = self.soft = rank class AceCard2(Card2): def __init__(self, rank: int, suit: Suit) -> None: self.suit = suit self.rank = "A" self.hard, self.soft = 1, 11 class FaceCard2(Card2): def __init__(self, rank: int, suit: Suit) -> None: self.suit = suit self.rank = {11: "J", 12: "Q", 13: "K"}[rank] self.hard = self.soft = 10 def card9(rank: int, suit: Suit) -> Card2: if rank == 1: return AceCard2(rank, suit) elif 2 <= rank < 11: return NumberCard2(rank, suit) elif 11 <= rank < 14: return FaceCard2(rank, suit) else: raise Exception("Rank out of range") test_compare_card9_with_card = """ >>> # Compare with an example 2 deck >>> deck = [card(rank, suit) for rank in range(1, 14) for suit in (Suit.Club, Suit.Diamond, Suit.Heart, Suit.Spade)] >>> deck9 = [card9(rank, suit) for rank in range(1, 14) for suit in Suit] >>> for c9, c in zip(deck9, deck): ... assert c9 == c, f"{c9!r} != {c!r}" >>> assert deck9 == deck """ # Mixed subclass and superclass. # It's abstract in principle, but technically concrete. # This parallels a dataclass. class Card3: def __init__(self, rank: str, suit: Suit, hard: int, soft: int) -> None: self.rank = rank self.suit = suit self.hard = hard self.soft = soft def __eq__(self, other: Any) -> bool: return ( self.suit == cast("Card3", other).suit and self.rank == cast("Card3", other).rank and self.hard == cast("Card3", other).hard and self.soft == cast("Card3", other).soft ) class NumberCard3(Card3): def __init__(self, rank: int, suit: Suit) -> None: super().__init__(str(rank), suit, rank, rank) class AceCard3(Card3): def __init__(self, rank: int, suit: Suit) -> None: super().__init__("A", suit, 1, 11) class FaceCard3(Card3): def __init__(self, rank: int, suit: Suit) -> None: rank_str = {11: "J", 12: "Q", 13: "K"}[rank] super().__init__(rank_str, suit, 10, 10) def card10(rank: int, suit: Suit) -> Card3: if rank == 1: return AceCard3(rank, suit) elif 2 <= rank < 11: return NumberCard3(rank, suit) elif 11 <= rank < 14: return FaceCard3(rank, suit) else: raise Exception("Rank out of range") test_compare_card10_with_card = """ >>> # Compare with an example 2 deck >>> deck = [card(rank, suit) for rank in range(1, 14) for suit in (Suit.Club, Suit.Diamond, Suit.Heart, Suit.Spade)] >>> deck10 = [card10(rank, suit) for rank in range(1, 14) for suit in Suit] >>> assert deck10 == deck """ __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) deck9 = [ card9(rank, suit) for rank in range(1, 14) for suit in cast(Iterator[Suit], Suit) ] deck10 = [ card10(rank, suit) for rank in range(1, 14) for suit in cast(Iterator[Suit], Suit) ] ================================================ FILE: Chapter_2/ch02_ex5.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 2. Example 5. """ # Alternative Designs for the Initialization from Chapter_2.ch02_ex3 import Card, card, Suit from typing import List, Iterable, cast, Union, NamedTuple, Tuple, Optional, overload import random # While Card should be immutable, that's a topic for the next chapter. # Composite Objects: Deck # ==================================== # A simple Deck definition test_no_deck = """ >>> random.seed(42) >>> d = [card(r + 1, s) for r in range(13) for s in iter(Suit)] >>> random.shuffle(d) >>> hand = [d.pop(), d.pop()] >>> hand [FaceCard(suit=, rank='J'), Card(suit=, rank='2')] """ class Deck: def __init__(self) -> None: self._cards = [card(r + 1, s) for r in range(13) for s in iter(Suit)] random.shuffle(self._cards) def pop(self) -> Card: return self._cards.pop() test_deck = """ >>> random.seed(42) >>> d = Deck() >>> hand = [d.pop(), d.pop()] >>> hand [FaceCard(suit=, rank='J'), Card(suit=, rank='2')] """ # A subclass of list definition class Deck2(list): def __init__(self) -> None: super().__init__( card(r + 1, s) for r in range(13) for s in cast(Iterable[Suit], Suit) ) random.shuffle(self) test_deck2 = """ >>> random.seed(42) >>> d = Deck2() >>> hand = [d.pop(), d.pop()] >>> hand [FaceCard(suit=, rank='J'), Card(suit=, rank='2')] """ # A better subclass of list which has the necessary additional features of # multiple sets of cards plus not dealing the entire deck. class Deck3(list): def __init__(self, decks: int = 1) -> None: super().__init__() for i in range(decks): self.extend(card(r + 1, s) for r in range(13) for s in iter(Suit)) random.shuffle(self) burn = random.randint(1, 52) for i in range(burn): self.pop() test_deck3 = """ >>> random.seed(42) >>> d = Deck3() >>> hand = [d.pop(), d.pop()] >>> hand [Card(suit=, rank='9'), FaceCard(suit=, rank='K')] """ class Deck3a(list): def __init__(self, decks: int = 1) -> None: super().__init__( card(r + 1, s) for r in range(13) for s in iter(Suit) for d in range(decks) ) random.shuffle(self) burn = random.randint(1, 52) for i in range(burn): self.pop() test_deck3a = """ >>> random.seed(42) >>> d = Deck3a() >>> hand = [d.pop(), d.pop()] >>> hand [Card(suit=, rank='9'), FaceCard(suit=, rank='K')] """ # Composite Objects: Hand # =================================== # A simplistic Hand without a proper initialization of the cards. class Hand: def __init__(self, dealer_card: Card) -> None: self.dealer_card: Card = dealer_card self.cards: List[Card] = [] def hard_total(self) -> int: return sum(c.hard for c in self.cards) def soft_total(self) -> int: return sum(c.soft for c in self.cards) def __repr__(self) -> str: return f"{self.__class__.__name__} {self.dealer_card} {self.cards}" test_hand = """ >>> random.seed(42) >>> d = Deck() >>> h = Hand(d.pop()) >>> h.cards.append(d.pop()) >>> h.cards.append(d.pop()) >>> h Hand J♣ [Card(suit=, rank='2'), AceCard(suit=, rank='A')] """ # A Better Hand with a complete initialization of the cards. # This works better with serialization. class Hand2: def __init__(self, dealer_card: Card, *cards: Card) -> None: self.dealer_card = dealer_card self.cards = list(cards) def card_append(self, card: Card) -> None: self.cards.append(card) def hard_total(self) -> int: return sum(c.hard for c in self.cards) def soft_total(self) -> int: return sum(c.soft for c in self.cards) def __repr__(self) -> str: return f"{self.__class__.__name__}({self.dealer_card!r}, *{self.cards})" test_hand2 = """ >>> random.seed(42) >>> d = Deck() >>> h = Hand2(d.pop(), d.pop(), d.pop()) >>> h Hand2(FaceCard(suit=, rank='J'), *[Card(suit=, rank='2'), AceCard(suit=, rank='A')])""" # A Hand which can be built from another Hand or a collection of Cards. # This allows us to freeze the hand or build a memento version of the hand. class Hand3: @overload def __init__(self, arg1: "Hand3") -> None: ... @overload def __init__(self, arg1: Card, arg2: Card, arg3: Card) -> None: ... def __init__( self, arg1: Union[Card, "Hand3"], arg2: Optional[Card] = None, arg3: Optional[Card] = None, ) -> None: self.dealer_card: Card self.cards: List[Card] if isinstance(arg1, Hand3) and not arg2 and not arg3: # Clone an existing hand self.dealer_card = arg1.dealer_card self.cards = arg1.cards elif ( isinstance(arg1, Card) and isinstance(arg2, Card) and isinstance(arg3, Card) ): # Build a fresh, new hand. self.dealer_card = cast(Card, arg1) self.cards = [arg2, arg3] def __repr__(self) -> str: return f"{self.__class__.__name__}({self.dealer_card!r}, *{self.cards})" test_hand3 = """ >>> random.seed(42) >>> d = Deck() >>> h = Hand3(d.pop(), d.pop(), d.pop()) >>> memento = Hand3(h) >>> memento Hand3(FaceCard(suit=, rank='J'), *[Card(suit=, rank='2'), AceCard(suit=, rank='A')]) """ # A Hand which can be built from another Hand. # Or a split from another hand. # Or individual cards. # Note the complexity of the initialization is nearly impossible # to specify clearly. # - __init__(self, arg1 : Hand4) -> None: ... # - __init__(self, arg1 : Hand4, arg2: Card, *, split: int) -> None: ... # - __init__(self, arg1 : Card, arg2: Card, arg3: Card) -> None: ... # - __init__(self, arg1 : Union[Hand4, Card], arg2: Optional[Card]=None, arg3: Optional[Card] = None, split: Optional[int] = None) -> None: ... # This is an indication of a need to have staticmethods for creating instances. # See the next example for this. class Hand4: @overload def __init__(self, arg1: "Hand4") -> None: ... @overload def __init__(self, arg1: "Hand4", arg2: Card, *, split: int) -> None: ... @overload def __init__(self, arg1: Card, arg2: Card, arg3: Card) -> None: ... def __init__( self, arg1: Union["Hand4", Card], arg2: Optional[Card] = None, arg3: Optional[Card] = None, split: Optional[int] = None, ) -> None: self.dealer_card: Card self.cards: List[Card] if isinstance(arg1, Hand4): # Clone an existing hand self.dealer_card = arg1.dealer_card self.cards = arg1.cards elif isinstance(arg1, Hand4) and isinstance(arg2, Card) and "split" is not None: # Split an existing hand self.dealer_card = arg1.dealer_card self.cards = [arg1.cards[split], arg2] elif isinstance(arg1, Card) and isinstance(arg2, Card) and isinstance( arg3, Card ): # Build a fresh, new hand from three cards self.dealer_card = arg1 self.cards = [arg2, arg3] else: raise TypeError("Invalid constructor {arg1!r} {arg2!r} {arg3!r}") def __str__(self) -> str: return ", ".join(map(str, self.cards)) test_hand4 = """ >>> import random >>> random.seed(42) >>> d = Deck() >>> h = Hand4(d.pop(), d.pop(), d.pop()) >>> s1 = Hand4(h, d.pop(), split=0) >>> s2 = Hand4(h, d.pop(), split=1) >>> print("start", h, "split1", s1, "split2", s2) start 2♠, A♦ split1 2♠, A♦ split2 2♠, A♦ """ # A Hand with static methods to split or frozen as a memento. class Hand5: def __init__(self, dealer_card: Card, *cards: Card) -> None: self.dealer_card = dealer_card self.cards = list(cards) @staticmethod def freeze(other) -> "Hand5": hand = Hand5(other.dealer_card, *other.cards) return hand @staticmethod def split(other, card0, card1) -> Tuple["Hand5", "Hand5"]: hand0 = Hand5(other.dealer_card, other.cards[0], card0) hand1 = Hand5(other.dealer_card, other.cards[1], card1) return hand0, hand1 def __str__(self) -> str: return ", ".join(map(str, self.cards)) test_hand_5 = """ >>> import random >>> random.seed(42) >>> d = Deck() >>> h = Hand5(d.pop(), d.pop(), d.pop()) >>> s1, s2 = Hand5.split(h, d.pop(), d.pop()) >>> print("start", h, "split1", s1, "split2", s2) start 2♠, A♦ split1 2♠, Q♠ split2 A♦, 5♦ """ # Composite Objects: Betting Strategy # ============================================== # A strategy class hierarchy for Betting. class BettingStrategy: def bet(self) -> int: raise NotImplementedError("No bet method") def record_win(self) -> None: pass def record_loss(self) -> None: pass class Flat(BettingStrategy): def bet(self) -> int: return 1 test_flat = """ >>> flat_bet = Flat() >>> flat_bet.bet() 1 """ import abc from abc import abstractmethod class BettingStrategy2(metaclass=abc.ABCMeta): @abstractmethod def bet(self) -> int: return 1 def record_win(self): pass def record_loss(self): pass # A strategy class hierarchy for Play. class GameStrategy: def insurance(self, hand: Hand) -> bool: return False def split(self, hand: Hand) -> bool: return False def double(self, hand: Hand) -> bool: return False def hit(self, hand: Hand) -> bool: return sum(c.hard for c in hand.cards) <= 17 test_game = """ >>> dumb = GameStrategy() >>> dumb.insurance(Hand2(card(1, Suit.Heart), card(1, Suit.Spade), card(13, Suit.Spade))) False >>> h17 = Hand2(card(1, Suit.Heart), card(10, Suit.Heart), card(7, Suit.Club)) >>> [f"{c}: {c.hard}" for c in h17.cards] ['10♥: 10', '7♣: 7'] >>> [f"{c}: {c.soft}" for c in h17.cards] ['10♥: 10', '7♣: 7'] >>> dumb.hit(Hand2(card(1, Suit.Heart), card(10, Suit.Heart), card(7, Suit.Club))) True >>> dumb.hit(Hand2(card(1, Suit.Heart), card(10, Suit.Heart), card(8, Suit.Club))) False >>> s18 = Hand2(card(1, Suit.Heart), card(1, Suit.Heart), card(7, Suit.Club)) >>> [f"{c}: {c.hard}" for c in s18.cards] ['A♥: 1', '7♣: 7'] >>> [f"{c}: {c.soft}" for c in s18.cards] ['A♥: 11', '7♣: 7'] """ # A simple outline for the Table. class Table: def __init__(self) -> None: self.deck = Deck() def place_bet(self, amount: int) -> None: print("Bet", amount) def get_hand(self) -> Hand2: try: self.hand = Hand2(self.deck.pop(), self.deck.pop(), self.deck.pop()) self.hole_card = self.deck.pop() except IndexError: # Out of cards: need to shuffle. # This is not technically correct: cards currently in play should not appear in the next deck. self.deck = Deck() return self.get_hand() print("Deal", self.hand) return self.hand def can_insure(self, hand: Hand) -> bool: return hand.dealer_card.insure # A Player definition class Player: def __init__( self, table: Table, bet_strategy: BettingStrategy, game_strategy: GameStrategy ) -> None: self.bet_strategy = bet_strategy self.game_strategy = game_strategy self.table = table def game(self): self.table.place_bet(self.bet_strategy.bet()) self.hand = self.table.get_hand() if self.table.can_insure(self.hand): if self.game_strategy.insurance(self.hand): self.table.insure(self.bet_strategy.bet()) # etc. # Typical Use Case test_table_player = """ >>> random.seed(42) >>> table = Table() >>> flat_bet = Flat() >>> dumb = GameStrategy() >>> p = Player(table, flat_bet, dumb) >>> p.game() Bet 1 Deal Hand2(FaceCard(suit=, rank='J'), *[Card(suit=, rank='2'), AceCard(suit=, rank='A')]) """ # A Player definition using wide-open keyword definitions. # While the following is *technically* possible, a Very Bad Idea for type checking. # self.__dict__.update(kw) class Player2(Player): def __init__(self, **kw) -> None: """Must provide table, bet_strategy, game_strategy.""" self.bet_strategy: BettingStrategy = kw["bet_strategy"] self.game_strategy: GameStrategy = kw["game_strategy"] self.table: Table = kw["table"] self.log_name: Optional[str] = kw.get("log_name") def game(self) -> None: self.table.place_bet(self.bet_strategy.bet()) self.hand = self.table.get_hand() # Typical Use Case. test_table_player2 = """ >>> random.seed(42) >>> table = Table() >>> flat_bet = Flat() >>> dumb = GameStrategy() >>> p2 = Player2(table=table, bet_strategy=flat_bet, game_strategy=dumb) >>> p2.game() Bet 1 Deal Hand2(FaceCard(suit=, rank='J'), *[Card(suit=, rank='2'), AceCard(suit=, rank='A')]) """ class Player2x(Player): def __init__(self, **kw) -> None: """Must provide table, bet_strategy, game_strategy.""" self.bet_strategy: BettingStrategy = kw["bet_strategy"] self.game_strategy: GameStrategy = kw["game_strategy"] self.table: Table = kw["table"] self.log_name: Optional[str] = kw.get("log_name") def game(self) -> None: self.table.place_bet(self.bet_strategy.bet()) self.hand = self.table.get_hand() # Bonus Use Case. Set an additional attribute. test_table_player2_extra = """ >>> random.seed(42) >>> table = Table() >>> flat_bet = Flat() >>> dumb = GameStrategy() >>> p2 = Player2x(table=table, bet_strategy=flat_bet, game_strategy=dumb, log_name="Flat/Dumb") >>> p2.game() Bet 1 Deal Hand2(FaceCard(suit=, rank='J'), *[Card(suit=, rank='2'), AceCard(suit=, rank='A')]) >>> print(p2.log_name, p2.hand) Flat/Dumb Hand2(FaceCard(suit=, rank='J'), *[Card(suit=, rank='2'), AceCard(suit=, rank='A')]) """ # A Player definition using wide-open keyword definitions. # While ``self.__dict__.update(extras)`` is *technically* possible, it's a bad idea. class Player3(Player): def __init__( self, table: Table, bet_strategy: BettingStrategy, game_strategy: GameStrategy, **extras, ) -> None: self.bet_strategy = bet_strategy self.game_strategy = game_strategy self.table = table # Bad: self.__dict__.update(extras) # Slightly better? for name in extras: setattr(self, name, extras[name]) # Much Better self.log_name: str = extras.pop("log_name", self.__class__.__name__) if extras: raise TypeError(f"Extra **kw arguments: {extras!r}") test_table_player3 = """ >>> random.seed(42) >>> table = Table() >>> flat_bet = Flat() >>> dumb = GameStrategy() >>> p3 = Player3(table, flat_bet, dumb, log_name="Flat/Dumb") >>> p3.game() Bet 1 Deal Hand2(FaceCard(suit=, rank='J'), *[Card(suit=, rank='2'), AceCard(suit=, rank='A')]) >>> print(p3.log_name, p3.hand) Flat/Dumb Hand2(FaceCard(suit=, rank='J'), *[Card(suit=, rank='2'), AceCard(suit=, rank='A')]) """ # Bad Ideas # ==================== # class-level Validation # # Run-time complexity for little real value. These are design issues. Use mypy and do it only once. class ValidPlayer: def __init__(self, table, bet_strategy, game_strategy): assert isinstance(table, Table) assert isinstance(bet_strategy, BettingStrategy) assert isinstance(game_strategy, GameStrategy) self.bet_strategy = bet_strategy self.game_strategy = game_strategy self.table = table test_table_valid_player = """ >>> import random >>> random.seed(42) >>> table = Table() >>> flat_bet = Flat() >>> dumb = GameStrategy() >>> p4 = ValidPlayer(table, flat_bet, dumb) """ class Player4: def __init__( self, table: Table, bet_strategy: BettingStrategy, game_strategy: GameStrategy ) -> None: """Creates a new player associated with a table, and configured with proper betting and play strategies :param table: an instance of :class:`Table` :param bet_strategy: an instance of :class:`BettingStrategy` :param game_strategy: an instance of :class:`GameStrategy` """ self.bet_strategy = bet_strategy self.game_strategy = game_strategy self.table = table __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_20/README.rst ================================================ A Mini Python Project ===================== Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 20. Example 1. This example shows some of the directory structure of a larger Python project. The tests can be run as follows:: PYTHONPATH=Chapter_20/src pytest Chapter_20 The documentation can be built as follows:: cd docs PYTHONPATH=../src make html ================================================ FILE: Chapter_20/__init__.py ================================================ ================================================ FILE: Chapter_20/combo.py ================================================ # ############# # Combinations # ############# # # .. contents:: # # Definition # ========== # # For some deeper statistical calculations, # we need the number of combinations of *n* things # taken *k* at a time, :math:`\binom{n}{k}`. # # .. math:: # # \binom{n}{k} = \dfrac{n!}{k!(n-k)!} # # The function will use an internal ``fact()`` function because # we don't need factorial anywhere else in the application. # # We'll rely on a simplistic factorial function without memoization. # # Test Case # ========= # # Here are two simple unit tests for this function provided # as doctest examples. # # >>> from combo import combinations # >>> combinations(4,2) # 6 # >>> combinations(8,4) # 70 # # Implementation # =============== # # Here's the essential function definition, with docstring: # :: def combinations(n: int, k: int) -> int: """Compute :math:`\binom{n}{k}`, the number of combinations of *n* things taken *k* at a time. :param n: integer size of population :param k: groups within the population :returns: :math:`\binom{n}{k}` """ # An important consideration here is that someone hasn't confused # the two argument values. # :: assert k <= n # Here's the embedded factorial function. It's recursive. The Python # stack limit is a limitation on the size of numbers we can use. # :: def fact(a: int) -> int: if a == 0: return 1 return a*fact(a-1) # Here's the final calculation. Note that we're using integer division. # Otherwise, we'd get an unexpected conversion to float. # :: return fact(n)//(fact(k)*fact(n-k)) # We can make this more efficient by treating the factorial product # as a *Multiset* of individual integer values. The product updates the # multiset. The division removes items from the multiset. Once the final # set of factors is available, the final product can be computed more efficiently. ================================================ FILE: Chapter_20/combo.py.html ================================================ Combinations

Combinations

Definition

For some deeper statistical calculations, we need the number of combinations of n things taken k at a time, (nk).

(nk) = (n!)/(k!(n − k)!)

The function will use an internal fact() function because we don't need factorial anywhere else in the application.

We'll rely on a simplistic factorial function without memoization.

Test Case

Here are two simple unit tests for this function provided as doctest examples.

>>> from combo import combinations
>>> combinations(4,2)
6
>>> combinations(8,4)
70

Implementation

Here's the essential function definition, with docstring:

def combinations(n: int, k: int) -> int:
    """Compute :math:`\binom{n}{k}`, the number of
    combinations of *n* things taken *k* at a time.

    :param n: integer size of population
    :param k: groups within the population
    :returns: :math:`\binom{n}{k}`
    """

An important consideration here is that someone hasn't confused the two argument values.

assert k <= n

Here's the embedded factorial function. It's recursive. The Python stack limit is a limitation on the size of numbers we can use.

def fact(a: int) -> int:
    if a == 0: return 1
    return a*fact(a-1)

Here's the final calculation. Note that we're using integer division. Otherwise, we'd get an unexpected conversion to float.

return fact(n)//(fact(k)*fact(n-k))

We can make this more efficient by treating the factorial product as a Multiset of individual integer values. The product updates the multiset. The division removes items from the multiset. Once the final set of factors is available, the final product can be computed more efficiently.

================================================ FILE: Chapter_20/combo.py.txt ================================================ ############# Combinations ############# .. contents:: Definition ========== For some deeper statistical calculations, we need the number of combinations of *n* things taken *k* at a time, :math:`\binom{n}{k}`. .. math:: \binom{n}{k} = \dfrac{n!}{k!(n-k)!} The function will use an internal ``fact()`` function because we don't need factorial anywhere else in the application. We'll rely on a simplistic factorial function without memoization. Test Case ========= Here are two simple unit tests for this function provided as doctest examples. >>> from combo import combinations >>> combinations(4,2) 6 >>> combinations(8,4) 70 Implementation =============== Here's the essential function definition, with docstring: :: def combinations(n: int, k: int) -> int: """Compute :math:`\binom{n}{k}`, the number of combinations of *n* things taken *k* at a time. :param n: integer size of population :param k: groups within the population :returns: :math:`\binom{n}{k}` """ An important consideration here is that someone hasn't confused the two argument values. :: assert k <= n Here's the embedded factorial function. It's recursive. The Python stack limit is a limitation on the size of numbers we can use. :: def fact(a: int) -> int: if a == 0: return 1 return a*fact(a-1) Here's the final calculation. Note that we're using integer division. Otherwise, we'd get an unexpected conversion to float. :: return fact(n)//(fact(k)*fact(n-k)) We can make this more efficient by treating the factorial product as a *Multiset* of individual integer values. The product updates the multiset. The division removes items from the multiset. Once the final set of factors is available, the final product can be computed more efficiently. ================================================ FILE: Chapter_20/docs/Makefile ================================================ # Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) ================================================ FILE: Chapter_20/docs/conf.py ================================================ # -*- coding: utf-8 -*- # # Configuration file for the Sphinx documentation builder. # # This file does only contain a selection of the most common options. For a # full list see the documentation: # http://www.sphinx-doc.org/en/master/config # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # # import os # import sys # sys.path.insert(0, os.path.abspath('.')) # -- Project information ----------------------------------------------------- project = 'Chapter 20' copyright = '2019, S.Lott' author = 'S.Lott' # The short X.Y version version = '' # The full version, including alpha/beta/rc tags release = '2.0' # -- General configuration --------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.todo', 'sphinx.ext.mathjax', 'sphinx.ext.viewcode', 'sphinx.ext.githubpages', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The master toctree document. master_doc = 'index' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = None # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = 'alabaster' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # # html_theme_options = {} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Custom sidebar templates, must be a dictionary that maps document names # to template names. # # The default sidebars (for documents that don't match any pattern) are # defined by theme itself. Builtin themes are using these templates by # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', # 'searchbox.html']``. # # html_sidebars = {} # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. htmlhelp_basename = 'Chapter20doc' # -- Options for LaTeX output ------------------------------------------------ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'Chapter20.tex', 'Chapter 20 Documentation', 'S.Lott', 'manual'), ] # -- Options for manual page output ------------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'chapter20', 'Chapter 20 Documentation', [author], 1) ] # -- Options for Texinfo output ---------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'Chapter20', 'Chapter 20 Documentation', author, 'Chapter20', 'One line description of project.', 'Miscellaneous'), ] # -- Options for Epub output ------------------------------------------------- # Bibliographic Dublin Core info. epub_title = project # The unique identifier of the text. This can be a ISBN number # or the project homepage. # # epub_identifier = '' # A unique identification for the text. # # epub_uid = '' # A list of files that should not be packed into the epub file. epub_exclude_files = ['search.html'] # -- Extension configuration ------------------------------------------------- # -- Options for todo extension ---------------------------------------------- # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True ================================================ FILE: Chapter_20/docs/implementation.rst ================================================ Implementation ============== Here's a reference to the `inception document <_static/inception_doc/index.html>`_ Here's a reference to the :ref:`user_story` The ch20_ex1 module ------------------- .. automodule:: ch20_ex1 :members: :undoc-members: :special-members: Some Other Module ----------------- We'd have an ``.. automodule::`` directive here, for ``some_other_module``. ================================================ FILE: Chapter_20/docs/index.rst ================================================ .. Chapter 20 documentation master file, created by sphinx-quickstart on Wed Apr 3 16:18:57 2019. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to Chapter 20's documentation! ====================================== .. toctree:: :maxdepth: 2 :caption: Contents: user_story implementation Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` ================================================ FILE: Chapter_20/docs/user_story.rst ================================================ .. _user_story: User Stories ============ The user generally has three tasks: customize the simulation's parameters, run a simulation, and analyze the results of a simulation. ================================================ FILE: Chapter_20/src/__init__.py ================================================ ================================================ FILE: Chapter_20/src/ch20_ex1.py ================================================ #!/usr/bin/env python3.7 # Mastering Object-Oriented Python 2e # # Code Examples for Mastering Object-Oriented Python 2nd Edition # # Chapter 20. Example 1. # """ Blackjack Cards and Decks ========================= This module contains a definition of :class:`Card`, :class:`Deck` and :class:`Shoe` suitable for Blackjack. The :class:`Card` class hierarchy --------------------------------- The :class:`Card` class hierarchy includes the following class definitions. :class:`Card` is the superclass as well as being the class for number cards. :class:`FaceCard` defines face cards: J, Q and K. :class:`AceCard` defines the Ace. This is special in Blackjack because it creates a soft total for a hand. We create cards using the :func:`card` factory function to create the proper :class:`Card` subclass instances from a rank and suit. The :class:`Suit` enumeration has all of the Suit instances. :: >>> from ch20_ex1 import cards >>> ace_clubs= cards.card( 1, cards.suits[0] ) >>> ace_clubs 'A♣' >>> ace_diamonds= cards.card( 1, cards.suits[1] ) >>> ace_clubs.rank == ace_diamonds.rank True The :class:`Deck` and :class:`Shoe` class hierarchy --------------------------------------------------- The basic :class:`Deck` creates a single 52-card deck. The :class:`Shoe` subclass creates a given number of decks. A :class:`Deck` can be shuffled before the cards can be extracted with the :meth:`pop` method. A :class:`Shoe` must be shuffled and *burned*. The burn operation sequesters a random number of cards based on a mean and standard deviation. The mean is a number of cards (52 is the default.) The standard deviation for the burn is also given as a number of cards (2 is the default.) """ # Example Sphinx-style Documentation # ------------------------------------- # Imports from enum import Enum from typing import Optional class Suit(str, Enum): """ Enumeration of all possible values for a card's suit. """ Club = "♣" Diamond = "♦" Heart = "♥" Spade = "♠" class Card: """ Definition of a numeric rank playing card. Subclasses will define :py:class:`FaceCard` and :py:class:`AceCard`. :ivar rank: int rank of the card :ivar suit: Suit suit of the card :ivar hard: int Hard point total for a card :ivar soft: int Soft total; same as hard for all cards except Aces. """ def __init__( self, rank: int, suit: Suit, hard: int, soft: Optional[int] = None ) -> None: """Define the values for this card. :param rank: Numeric rank in the range 1-13. :param suit: Suit object (often a character from '♣♡♢♠') :param hard: Hard point total (or 10 for FaceCard or 1 for AceCard) :param soft: The soft total for AceCard, otherwise defaults to hard. """ self.rank = rank self.suit = suit self.hard = hard self.soft = soft if soft is not None else hard def __str__(self) -> str: return f"{self.rank}{self.suit}" def __repr__(self) -> str: return f"{self.__class__.__name__}(rank={self.rank}, suit={self.suit})" class FaceCard(Card): """ Subclass of :py:class:`Card` with Ranks 11-13 represented by J, Q, and K. """ rank_str = {11: "J", 12: "Q", 13: "K"} def __str__(self) -> str: return f"{self.rank_str[self.rank]}{self.suit}" class AceCard(Card): """ Subclass of :py:class:`Card` with rank of 1 represented by A. """ def __str__(self) -> str: return f"A{self.suit}" def card(rank: int, suit: Suit) -> Card: """ Create a :py:class:`Card` instance from rank and suit. Can raise :py:exc:`TypeError` for ranks out of the range 1 to 13, inclusive. :param suit: Suit object :param rank: Numeric rank in the range 1-13 :returns: :py:class:`Card` instance :raises TypeError: rank out of range c >>> from Chapter_20.ch20_ex1 import card >>> str(card(3, Suit.Heart)) '3♥' >>> str(card(1, Suit.Heart)) 'A♥' """ if rank == 1: return AceCard(rank, suit, 1, 11) elif 2 <= rank < 11: return Card(rank, suit, rank) elif 11 <= rank < 14: return FaceCard(rank, suit, 10) else: raise TypeError ================================================ FILE: Chapter_20/tests/test_ch20.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 20. Example 2. """ # NOTE. This expects Chapter_20/src to be on the ``PYTHONPATH`` from pytest import * from ch20_ex1 import * def test_card_factory(): c_3h = card(3, Suit.Heart) assert str(c_3h) == '3♥' c_1h = card(1, Suit.Heart) 'assert str(c_1h) ==A♥' ================================================ FILE: Chapter_3/__init__.py ================================================ ================================================ FILE: Chapter_3/ch03_ex1.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 3. Example 1. """ from typing import Any, cast, Callable from enum import Enum import sys # Mutable and Immutable Objects # ============================== # Card Class with anomalies # ############################ # Definition of a simple class hierarchy for immutable objects # without a formal equality test or hash function. This will # somewhat work as expected. However, there will also be anomalies. class Card: insure = False def __init__(self, rank: str, suit: "Suit", hard: int, soft: int) -> None: self.rank = rank self.suit = suit self.hard = hard self.soft = soft def __repr__(self) -> str: return f"{self.__class__.__name__}(suit={self.suit!r}, rank={self.rank!r})" def __str__(self) -> str: return f"{self.rank}{self.suit}" class NumberCard(Card): def __init__(self, rank: int, suit: "Suit") -> None: super().__init__(str(rank), suit, rank, rank) class AceCard(Card): insure = True def __init__(self, rank: int, suit: "Suit") -> None: super().__init__("A", suit, 1, 11) class FaceCard(Card): def __init__(self, rank: int, suit: "Suit") -> None: rank_str = {11: "J", 12: "Q", 13: "K"}[rank] super().__init__(rank_str, suit, 10, 10) class Suit(str, Enum): Club = "\N{BLACK CLUB SUIT}" Diamond = "\N{BLACK DIAMOND SUIT}" Heart = "\N{BLACK HEART SUIT}" Spade = "\N{BLACK SPADE SUIT}" # Some Use cases for two seemingly equal cards. test_card = """ >>> c1 = AceCard(1, Suit.Club) >>> c2 = AceCard(1, Suit.Club) >>> id(c1) == id(c2) False >>> c1 is c2 False >>> hash(c1) == hash(c2) False >>> hash(c1), hash(c2) # doctest: +ELLIPSIS (..., ...) >>> c1 == c2 False >>> set([c1, c2]) {AceCard(suit=, rank='A'), AceCard(suit=, rank='A')} """ # Better Card Class # ############################ # Definition of a more sophisticated class hierarchy # with a formal equality and hash test. This will create # properly immutable objects. There will be no anomalies with # seemingly equal objects. # We'll look at a far better solution using typing.NamedTuple below. class Card2: insure = False def __init__(self, rank: str, suit: "Suit", hard: int, soft: int) -> None: self.rank = rank self.suit = suit self.hard = hard self.soft = soft def __repr__(self) -> str: return f"{self.__class__.__name__}(suit={self.suit!r}, rank={self.rank!r})" def __str__(self) -> str: return f"{self.rank}{self.suit}" def __eq__(self, other: Any) -> bool: return ( self.suit == cast(Card2, other).suit and self.rank == cast(Card2, other).rank ) def __hash__(self) -> int: return (hash(self.suit) + 4*hash(self.rank)) % sys.hash_info.modulus def __format__(self, format_spec: str) -> str: if format_spec == "": return str(self) rs = ( format_spec.replace("%r", self.rank) .replace("%s", self.suit) .replace("%%", "%") ) return rs def __bytes__(self) -> bytes: class_code = self.__class__.__name__[0] rank_number_str = {"A": "1", "J": "11", "Q": "12", "K": "13"}.get( self.rank, self.rank ) string = f"({' '.join([class_code, rank_number_str, self.suit])})" return bytes(string, encoding="utf-8") class NumberCard2(Card2): def __init__(self, rank: int, suit: "Suit") -> None: super().__init__(str(rank), suit, rank, rank) class AceCard2(Card2): insure = True def __init__(self, rank: int, suit: "Suit") -> None: super().__init__("A", suit, 1, 11) class FaceCard2(Card2): def __init__(self, rank: int, suit: "Suit") -> None: rank_str = {11: "J", 12: "Q", 13: "K"}[rank] super().__init__(rank_str, suit, 10, 10) def card2(rank: int, suit: Suit) -> Card2: class_ = {1: AceCard2, 11: FaceCard2, 12: FaceCard2, 13: FaceCard2}.get( rank, NumberCard2 ) return class_(rank, suit) # Some Use cases for two seemingly equal cards. test_card2 = """ >>> c1 = AceCard2(1, Suit.Club) >>> c2 = AceCard2(1, Suit.Club) >>> id(c1), id(c2) # doctest: +ELLIPSIS (..., ...) >>> id(c1) == id(c2) False >>> c1 is c2 False >>> hash(c1) == hash(c2) True >>> c1 == c2 True >>> set([c1, c2]) {AceCard2(suit=, rank='A')} >>> bytes(c1) b'(A 1 \xe2\x99\xa3)' """ # Another Poorly-Designed Card Class # ################################## # Definition of a weird class hierarchy # with a formal equality but no hash test. This will create # properly mutable objects that can't be put into sets. class Card3: insure = False def __init__(self, rank: str, suit: "Suit", hard: int, soft: int) -> None: self.rank = rank self.suit = suit self.hard = hard self.soft = soft def __repr__(self) -> str: return f"{self.__class__.__name__}(suit={self.suit!r}, rank={self.rank!r})" def __str__(self) -> str: return f"{self.rank}{self.suit}" def __eq__(self, other: Any) -> bool: return ( self.suit == cast(Card3, other).suit and self.rank == cast(Card3, other).rank ) # __hash__ = None # mypy balks at this. class AceCard3(Card3): insure = True def __init__(self, rank: int, suit: "Suit") -> None: super().__init__("A", suit, 1, 11) class NumberCard3(Card3): def __init__(self, rank: int, suit: "Suit") -> None: super().__init__(str(rank), suit, rank, rank) class FaceCard3(Card3): def __init__(self, rank: int, suit: "Suit") -> None: rank_str = {11: "J", 12: "Q", 13: "K"}[rank] super().__init__(rank_str, suit, 10, 10) # Some Use cases for two seemingly equal cards that cannot be hashed. test_card3 = """ >>> c1 = AceCard3(1, Suit.Club) >>> c2 = AceCard3(1, Suit.Club) >>> id(c1) == id(c2) False >>> c1 is c2 False >>> hash(c1) == hash(c2) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): TypeError: unhashable type: 'AceCard3' >>> c1 == c2 True >>> set([c1, c2]) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): TypeError: unhashable type: 'AceCard3' """ __test__ = { name: value for name, value in locals().items() if name.startswith("test_") } if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_3/ch03_ex2.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 3. Example 2. """ from typing import NamedTuple, Tuple, cast, Iterable from enum import Enum # Properly Immutable Card # ======================= class Suit(str, Enum): Club = "\N{BLACK CLUB SUIT}" Diamond = "\N{BLACK DIAMOND SUIT}" Heart = "\N{BLACK HEART SUIT}" Spade = "\N{BLACK SPADE SUIT}" class Card(NamedTuple): rank: str suit: Suit def __str__(self) -> str: return f"{self.rank}{self.suit.value}" @property def insure(self) -> bool: return False @property def hard(self) -> int: hard, soft = self._points() return hard @property def soft(self) -> int: hard, soft = self._points() return soft def _points(self) -> Tuple[int, int]: pass class NumberCard(Card): def _points(self) -> Tuple[int, int]: return int(self.rank), int(self.rank) class AceCard(Card): @property def insure(self) -> bool: return True def _points(self) -> Tuple[int, int]: return 1, 11 class FaceCard(Card): def _points(self) -> Tuple[int, int]: return 10, 10 def card(rank: int, suit: Suit) -> Card: class_, rank_str = { 1: (AceCard, "A"), 11: (FaceCard, "J"), 12: (FaceCard, "Q"), 13: (FaceCard, "K") }.get( rank, (NumberCard, str(rank)) ) return class_(rank_str, suit) test_card = """ >>> deck = [card(r, s) for r in range(1, 14) for s in cast(Iterable[Suit], Suit)] >>> len(deck) 52 >>> s_1 = card(1, Suit.Spade) >>> s_1 AceCard(rank='A', suit=) >>> s_1.insure True >>> s_1.hard 1 >>> s_1.soft 11 >>> s_j = card(11, Suit.Spade) >>> s_j FaceCard(rank='J', suit=) >>> s_j.insure False >>> s_j.hard 10 >>> s_j.soft 10 """ # Some Use cases for two seemingly equal cards. test_card_equality = """ >>> c1 = card(1, Suit.Club) >>> c2 = card(1, Suit.Club) >>> id(c1) == id(c2) False >>> c1 is c2 False >>> hash(c1) == hash(c2) True >>> c1 == c2 True >>> set([c1, c2]) {AceCard(rank='A', suit=)} """ __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_3/ch03_ex3.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 3. Example 3. """ import sys import random from typing import cast, Iterable, Any, Callable from Chapter_3.ch03_ex1 import Suit, Card2, card2 # Complex Mutable Object Example # ============================== # Definition of a simple class hierarchy # with a formal equality and hash test. class Hand: def __init__(self, dealer_card: Card2, *cards: Card2) -> None: self.dealer_card = dealer_card self.cards = list(cards) def __str__(self) -> str: return ", ".join(map(str, self.cards)) def __repr__(self) -> str: cards_text = ", ".join(map(repr, self.cards)) return f"{self.__class__.__name__}({self.dealer_card!r}, {cards_text})" def __format__(self, spec: str) -> str: if spec == "": return str(self) return ", ".join(f"{c:{spec}}" for c in self.cards) def __eq__(self, other: Any) -> bool: if isinstance(other, int): return self.total() == other try: return ( self.cards == cast(Hand, other).cards and self.dealer_card == cast(Hand, other).dealer_card ) except AttributeError: return NotImplemented def __lt__(self, other: Any) -> bool: if isinstance(other, int): return self.total() < other try: return self.total() < cast(Hand, other).total() except AttributeError: return NotImplemented def __le__(self, other: Any) -> bool: if isinstance(other, int): return self.total() <= other try: return self.total() <= cast(Hand, other).total() except AttributeError: return NotImplemented # __hash__: Callable[[], int] = None def total(self) -> int: delta_soft = max(c.soft - c.hard for c in self.cards) hard = sum(c.hard for c in self.cards) if hard + delta_soft <= 21: return hard + delta_soft return hard class FrozenHand(Hand): def __init__(self, *args, **kw) -> None: if len(args) == 1 and isinstance(args[0], Hand): # Clone a hand other = cast(Hand, args[0]) self.dealer_card = other.dealer_card self.cards = other.cards else: # Build a fresh Hand from Card instances. super().__init__(*args, **kw) def __hash__(self) -> int: return sum(hash(c) for c in self.cards) % sys.hash_info.modulus class Deck(list): def __init__(self) -> None: super().__init__( card2(r + 1, s) for r in range(13) for s in cast(Iterable[Suit], Suit) ) random.shuffle(self) test_frozen_hand = """ >>> from collections import defaultdict >>> random.seed(1138) >>> d = Deck() >>> h = Hand(d.pop(), d.pop(), d.pop()) >>> print("Player: {hand:%r%s}".format(hand=h)) Player: K♦, 9♥ >>> stats = defaultdict(int) >>> stats[h] += 1 # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): TypeError: unhashable type: 'Hand' >>> h_f = FrozenHand(h) >>> stats = defaultdict(int) >>> stats[h_f] += 1 >>> print(stats) defaultdict(, {FrozenHand(NumberCard2(suit=, rank='5'), FaceCard2(suit=, rank='K'), NumberCard2(suit=, rank='9')): 1}) """ __test__ = { name: value for name, value in locals().items() if name.startswith("test_") } if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_3/ch03_ex4.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 3. Example 4. """ from typing import Iterable, cast, Any, Union, Type, Tuple import random from collections import defaultdict from Chapter_3.ch03_ex1 import card2, Suit, Card2, AceCard2, FaceCard2, NumberCard2 from Chapter_3.ch03_ex3 import Hand, FrozenHand class Deck(list): def __init__(self) -> None: super().__init__( card2(r + 1, s) for r in range(13) for s in cast(Iterable[Suit], Suit) ) random.shuffle(self) # __hash__ # ========= test_hash = """ >>> v1 = 123_456_789 >>> v2 = 2_305_843_009_337_150_740 >>> hash(v1) 123456789 >>> hash(v2) 123456789 >>> v2 == v1 False """ # __bool__ # ==================== # Not much to show here. The default works as expected. # __format__ # ===================== # # Really, this belongs with __str__() and __repr__() # # Examples of how __format__ gets invoked test_format = """ >>> c = card2(2, Suit.Club) >>> print("function", format(c)) function 2♣ >>> print(f"Card plain {c}") Card plain 2♣ >>> print(f"Card !r {c!r}") Card !r NumberCard2(suit=, rank='2') >>> print(f"Card !s {c!s}") Card !s 2♣ # Our own unique formatting language uses "r" and "s" for rank and suit. >>> print("Card :%s {0:%s}".format(c)) Card :%s ♣ >>> print("Card :%r {0:%r}".format(c)) Card :%r 2 >>> print("Card :%r of %s {0:%r of %s}".format(c)) Card :%r of %s 2 of ♣ >>> print("Card :%s%r {0:%s%r}".format(c)) Card :%s%r ♣2 # Extra literals we leave alone. >>> print("Card nested {0:{fill}{align}16s}".format(c, fill="*", align="<")) Card nested *<16s """ # RE to parse the specification. import re spec_pat = re.compile( r"(?P.?[\<\>=\^])?" "(?P[-+ ])?" "(?P#)?" "(?P0)?" "(?P\d*)" "(?P,)?" "(?P\.\d*)?" "(?P[bcdeEfFgGnosxX%])?" ) test_spec_pat = """ >>> for spec in ( ... "<30", ... ">30", ... "^30", ... "*^30", ... "+f", ... "-f", ... " f", ... "d", ... "x", ... "o", ... "b", ... "#x", ... "#o", ... "#b", ... ",", ... ".2%", ... "06.4f", ... ): ... print(spec, spec_pat.match(spec).groupdict()) <30 {'fill_align': '<', 'sign': None, 'alt': None, 'padding': None, 'width': '30', 'comma': None, 'precision': None, 'type': None} >30 {'fill_align': '>', 'sign': None, 'alt': None, 'padding': None, 'width': '30', 'comma': None, 'precision': None, 'type': None} ^30 {'fill_align': '^', 'sign': None, 'alt': None, 'padding': None, 'width': '30', 'comma': None, 'precision': None, 'type': None} *^30 {'fill_align': '*^', 'sign': None, 'alt': None, 'padding': None, 'width': '30', 'comma': None, 'precision': None, 'type': None} +f {'fill_align': None, 'sign': '+', 'alt': None, 'padding': None, 'width': '', 'comma': None, 'precision': None, 'type': 'f'} -f {'fill_align': None, 'sign': '-', 'alt': None, 'padding': None, 'width': '', 'comma': None, 'precision': None, 'type': 'f'} f {'fill_align': None, 'sign': ' ', 'alt': None, 'padding': None, 'width': '', 'comma': None, 'precision': None, 'type': 'f'} d {'fill_align': None, 'sign': None, 'alt': None, 'padding': None, 'width': '', 'comma': None, 'precision': None, 'type': 'd'} x {'fill_align': None, 'sign': None, 'alt': None, 'padding': None, 'width': '', 'comma': None, 'precision': None, 'type': 'x'} o {'fill_align': None, 'sign': None, 'alt': None, 'padding': None, 'width': '', 'comma': None, 'precision': None, 'type': 'o'} b {'fill_align': None, 'sign': None, 'alt': None, 'padding': None, 'width': '', 'comma': None, 'precision': None, 'type': 'b'} #x {'fill_align': None, 'sign': None, 'alt': '#', 'padding': None, 'width': '', 'comma': None, 'precision': None, 'type': 'x'} #o {'fill_align': None, 'sign': None, 'alt': '#', 'padding': None, 'width': '', 'comma': None, 'precision': None, 'type': 'o'} #b {'fill_align': None, 'sign': None, 'alt': '#', 'padding': None, 'width': '', 'comma': None, 'precision': None, 'type': 'b'} , {'fill_align': None, 'sign': None, 'alt': None, 'padding': None, 'width': '', 'comma': ',', 'precision': None, 'type': None} .2% {'fill_align': None, 'sign': None, 'alt': None, 'padding': None, 'width': '', 'comma': None, 'precision': '.2', 'type': '%'} 06.4f {'fill_align': None, 'sign': None, 'alt': None, 'padding': '0', 'width': '6', 'comma': None, 'precision': '.4', 'type': 'f'} """ # Nested {}'s test_nested_curlies = """ >>> random.seed(9973) >>> stats = defaultdict(int) >>> d = Deck() >>> h1 = FrozenHand(d.pop(), d.pop(), d.pop()) >>> stats[h1] += 1 >>> h2 = FrozenHand(d.pop(), d.pop(), d.pop()) >>> stats[h2] += 1 >>> width = 6 >>> for hand, count in stats.items(): ... print("{hand:%r%s} {count:{width}d}".format(hand=hand, count=count, width=width)) 5♦, 5♠ 1 8♠, 9♦ 1 """ # __bytes__ # ===================== # # Export Card2 instance as a bytes. Recover a Card2 instance from bytes. def card_from_bytes(buffer: bytes) -> Card2: """Parses bytes to rebuild the original Card2 instance.""" string = buffer.decode("utf8") try: if not (string[0] == "(" and string[-1] == ")"): raise ValueError code, rank_number, suit_value = string[1:-1].split() if int(rank_number) not in range(1, 14): raise ValueError class_ = {"A": AceCard2, "N": NumberCard2, "F": FaceCard2}[code] return class_(int(rank_number), Suit(suit_value)) except (IndexError, KeyError, ValueError) as ex: raise ValueError(f"{buffer!r} isn't a Card2 instance") test_bytes = """ >>> random.seed(1138) >>> d = Deck() >>> c = d.pop() >>> c NumberCard2(suit=, rank='5') >>> b = bytes(c) >>> print(b) b'(N 5 \\xe2\\x99\\xa5)' >>> data = b'(N 5 \\xe2\\x99\\xa5)' >>> c2 = card_from_bytes(data) >>> c2 NumberCard2(suit=, rank='5') >>> card_from_bytes(b'random') # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ValueError: b'random' isn't a Card2 instance >>> card_from_bytes(b'(less random)') Traceback (most recent call last): ValueError: b'(less random)' isn't a Card2 instance >>> card_from_bytes(b'(X 5 \\xe2\\x99\\xa5)') Traceback (most recent call last): ValueError: b'(X 5 \\xe2\\x99\\xa5)' isn't a Card2 instance >>> card_from_bytes(b'(N 25 \\xe2\\x99\\xa5)') Traceback (most recent call last): ValueError: b'(N 25 \\xe2\\x99\\xa5)' isn't a Card2 instance >>> card_from_bytes(b'(N 5 nope)') Traceback (most recent call last): ValueError: b'(N 5 nope)' isn't a Card2 instance """ # Comparison # ==================== # The object resolution for comparison special methods. # A partial class to see what happens. class BlackJackCard_p: def __init__(self, rank: int, suit: Suit) -> None: self.rank = rank self.suit = suit def __lt__(self, other: Any) -> bool: print(f"Compare {self} < {other}") return self.rank < cast(BlackJackCard_p, other).rank def __str__(self) -> str: return f"{self.rank}{self.suit}" test_blackjackcard_partial = """ >>> two = BlackJackCard_p(2, Suit.Spade) >>> three = BlackJackCard_p(3, Suit.Spade) >>> two < three Compare 2♠ < 3♠ True >>> two > three Compare 3♠ < 2♠ False >>> two == three False >>> two <= three # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): File "/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py", line 1329, in __run compileflags, 1), test.globs) File "", line 1, in print("{0} <= {1} :: {2!r}".format(two, three, two <= three)) # doctest: +IGNORE_EXCEPTION_DETAIL TypeError: '<=' not supported between instances of 'BlackJackCard_p' and 'BlackJackCard_p' >>> two_c = BlackJackCard_p(2, Suit.Club) >>> two_c == BlackJackCard_p(2, Suit.Club) False """ # A more complete class to show same-class comparisons. class BlackJackCard: def __init__(self, rank: int, suit: Suit, hard: int, soft: int) -> None: self.rank = rank self.suit = suit self.hard = hard self.soft = soft def __lt__(self, other: Any) -> bool: if not isinstance(other, BlackJackCard): return NotImplemented return self.rank < other.rank def __le__(self, other: Any) -> bool: try: return self.rank <= cast(BlackJackCard, other).rank except AttributeError: return NotImplemented def __gt__(self, other: Any) -> bool: if not isinstance(other, BlackJackCard): return NotImplemented return self.rank > other.rank def __ge__(self, other: Any) -> bool: try: return self.rank >= cast(BlackJackCard, other).rank except AttributeError: return NotImplemented def __eq__(self, other: Any) -> bool: if not isinstance(other, BlackJackCard): return NotImplemented return (self.rank == other.rank and self.suit == other.suit) def __ne__(self, other: Any) -> bool: if not isinstance(other, BlackJackCard): return NotImplemented return (self.rank != other.rank or self.suit != other.suit) def __str__(self) -> str: return f"{self.rank}{self.suit}" def __repr__(self) -> str: return (f"{self.__class__.__name__}" f"(rank={self.rank!r}, suit={self.suit!r}, " f"hard={self.hard!r}, soft={self.soft!r})") class Ace21Card(BlackJackCard): def __init__(self, rank: int, suit: Suit) -> None: super().__init__(rank, suit, 1, 11) def __str__(self) -> str: return f"A{self.suit}" def __repr__(self) -> str: return f"{self.__class__.__name__}(rank={self.rank!r}, suit={self.suit!r}, hard={self.hard!r}, soft={self.soft!r})" class Face21Card(BlackJackCard): FACE_MAP = {11: "J", 12: "Q", 13: "K"} def __init__(self, rank: int, suit: Suit) -> None: super().__init__(rank, suit, 10, 10) def __str__(self) -> str: return f"{self.FACE_MAP[self.rank]}{self.suit}" def __repr__(self) -> str: return f"{self.__class__.__name__}(rank={self.rank!r}, suit={self.suit!r}, hard={self.hard!r}, soft={self.soft!r})" class Number21Card(BlackJackCard): def __init__(self, rank: int, suit: Suit) -> None: super().__init__(rank, suit, rank, rank) def card21(rank: int, suit: Suit) -> BlackJackCard: if rank == 1: return Ace21Card(rank, suit) elif 2 <= rank < 11: return Number21Card(rank, suit) elif 11 <= rank < 14: return Face21Card(rank, suit) else: raise TypeError test_blackjack_full = """ >>> two = card21(2, "♠") >>> three = card21(3, "♠") >>> f"{two} < {three} is {two < three}" '2♠ < 3♠ is True' >>> f"{two} > {three} is {two > three}" '2♠ > 3♠ is False' >>> f"{two} == {three} is {two == three}" '2♠ == 3♠ is False' >>> f"{two} <= {three} is {two <= three}" '2♠ <= 3♠ is True' >>> two_c = card21(2, "♣") >>> f"{two} == {two_c} is {two == two_c}" '2♠ == 2♣ is False' >>> two.rank == two_c.rank True >>> # A mixed class comparison with int >>> f"2 < {three} is {2 < three}" # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): File "/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py", line 1329, in __run compileflags, 1), test.globs) File "", line 1, in print("{0} < {1} :: {2!r}".format(2, three, 2 < three)) # doctest: +IGNORE_EXCEPTION_DETAIL TypeError: '<' not supported between instances of 'int' and 'Number21Card' >>> two < 2 # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): File "/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py", line 1329, in __run compileflags, 1), test.globs) File "", line 1, in two < 2 TypeError: '<' not supported between instances of 'Number21Card' and 'int' >>> two > 2 # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): File "/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py", line 1329, in __run compileflags, 1), test.globs) File "", line 1, in two > 2 TypeError: '>' not supported between instances of 'Number21Card' and 'int' >>> two == 2 False >>> 2 == two False """ test_hand = """ >>> two = card21(2, Suit.Spade) >>> three = card21(3, Suit.Spade) >>> two_c = card21(2, Suit.Club) >>> ace = card21(1, Suit.Club) >>> cards = [ace, two, two_c, three] >>> h = Hand( card21(10,'♠'), *cards ) >>> print(h) A♣, 2♠, 2♣, 3♠ >>> h.total() 18 """ # Destruction and __del__() # ==================================== # Noisy Exit class Noisy: def __del__(self) -> None: print(f"Removing {id(self)}") # Simple create and delete. test_noisy_1 = """ >>> x = Noisy() >>> del x # doctest: +ELLIPSIS Removing ... """ # Shallow copy and multiple references. test_noisy_2 = """ >>> ln = [Noisy(), Noisy()] >>> ln2 = ln[:] >>> del ln # doctest: +ELLIPSIS >>> del ln2 # doctest: +ELLIPSIS Removing ... Removing ... """ # Circularity class Parent: def __init__(self, *children: 'Child') -> None: for child in children: child.parent = self self.children = {c.id: c for c in children} def __del__(self) -> None: print( f"Removing {self.__class__.__name__} {id(self):d}" ) class Child: def __init__(self, id: str) -> None: self.id = id self.parent: Parent = cast(Parent, None) def __del__(self) -> None: print( f"Removing {self.__class__.__name__} {id(self):d}" ) test_circularity_fail = """ >>> p = Parent(Child('a'), Child('b')) >>> del p # doctest: +ELLIPSIS >>> p_0 = Parent() >>> del p_0 # doctest: +ELLIPSIS Removing Parent ... >>> import gc >>> gc.collect() # doctest: +ELLIPSIS Removing Child ... Removing Child ... Removing Parent ... ... >>> print(gc.garbage) [] """ # No circularity via weak references. from weakref import ref class Parent2: def __init__(self, *children: 'Child2') -> None: for child in children: child.parent = ref(self) self.children = {c.id: c for c in children} def __del__(self) -> None: print( f"Removing {self.__class__.__name__} {id(self):d}" ) class Child2: def __init__(self, id: str) -> None: self.id = id self.parent: ref[Parent2] = cast(ref[Parent2], None) def __del__(self) -> None: print( f"Removing {self.__class__.__name__} {id(self):d}" ) test_circularity_pass = """ >>> p = Parent2(Child('a'), Child('b')) >>> del p # doctest: +ELLIPSIS Removing Parent2 ... Removing Child ... Removing Child ... """ # Immutable init and __new__() # ======================================== # Doesn't work. Can't use this form of __init__ with immutable classes. class Float_Fail(float): def __init__(self, value: float, unit: str) -> None: super().__init__(value) self.unit = unit test_float_fail = """ >>> x = Float_Fail(6.8, "knots") # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): File "/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py", line 1329, in __run compileflags, 1), test.globs) File "", line 1, in x = Float_Fail(6.8, "knots") TypeError: float expected at most 1 arguments, got 2 """ # This is how we can tweak an immutable object before the __init__ is invoked. # See https://github.com/python/mypy/issues/1053 # This *should* work since v0.3.1 # Adding type hints will report mypy errors. # float is (implicitly) a subclass of object. # object.__new__() takes no arguments. # float.__new__() is *really* more like type.__new__ class Float_Units(float): def __new__(cls, value, unit): obj = super().__new__(cls, float(value)) obj.unit = unit return obj from typing import overload, Optional, SupportsFloat class Float_Units_Ugly(float): unit: str def __new__(cls: Type, value: SupportsFloat, unit: str) -> 'Float_Units_Ugly': obj = cast('Float_Units_Ugly', cast(type, super()).__new__(cls, float(value))) obj.unit = unit return obj test_float_pass = """ >>> speed = Float_Units(6.8, "knots") >>> speed*2 13.6 >>> speed.unit 'knots' """ __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_3/ch03_ex5.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 3. Example 5. """ from typing import Iterable, cast, Any, Union, Type, Tuple import random from collections import defaultdict from Chapter_3.ch03_ex1 import card2, Suit, Card2, AceCard2, FaceCard2, NumberCard2 from Chapter_3.ch03_ex3 import Hand, FrozenHand # Immutable init and __new__() # ======================================== # Doesn't work. Can't use this form of __init__ with immutable classes. class Float_Fail(float): def __init__(self, value: float, unit: str) -> None: super().__init__(value) self.unit = unit test_float_fail = """ >>> x = Float_Fail(6.8, "knots") # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): File "/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py", line 1329, in __run compileflags, 1), test.globs) File "", line 1, in x = Float_Fail(6.8, "knots") TypeError: float expected at most 1 arguments, got 2 """ # This is how we can tweak an immutable object before the __init__ is invoked. # See https://github.com/python/mypy/issues/1053 # This *should* work since v0.3.1 # Adding type hints will report mypy errors. # float is (implicitly) a subclass of object. # object.__new__() takes no arguments. # float.__new__() is *really* more like type.__new__ class Float_Units(float): def __new__(cls, value, unit): obj = super().__new__(cls, float(value)) obj.unit = unit return obj test_float_units = """ >>> speed = Float_Units(6.8, "knots") >>> speed*2 13.6 >>> speed.unit 'knots' """ # Option 2... # A number of casts to work with mypy. # While this "works" the cast(type, super() doesn't make sense. from typing import overload, Optional, SupportsFloat, Dict class Float_Units_Ugly(float): unit: str def __new__(cls: Type, value: SupportsFloat, unit: str) -> "Float_Units_Ugly": # print(f"Float_Units_Ugly {cls}") obj = cast("Float_Units_Ugly", cast(type, super()).__new__(cls, float(value))) obj.unit = unit return obj test_float_units = """ >>> speed = Float_Units_Ugly(6.8, "knots") >>> speed*2 13.6 >>> speed.unit 'knots' """ # Option 3... # Metaclass to adjust structure. # Also relevant to the more complex example that follows. class AddUnitMeta(type): def __new__( cls: Type, name: str, bases: Tuple[Type, ...], namespace: Dict[str, Any], **kwds ) -> "Float_Units2": namespace["unit"] = None result = cast("Float_Units2", super().__new__(cls, name, bases, namespace)) return result class Float_Units2(float, metaclass=AddUnitMeta): def withUnit(self, unit): self.unit = unit return self test_float_units_2 = """ >>> speed = Float_Units2(6.8).withUnit("knots") >>> speed*2 13.6 >>> speed.unit 'knots' """ # Metaclass and __new__() # =================================== # Example 1. Classes with pre-built loggers. import logging class LoggedMeta(type): def __new__( cls: Type, name: str, bases: Tuple[Type, ...], namespace: Dict[str, Any] ) -> "Logged": result = cast("Logged", super().__new__(cls, name, bases, namespace)) result.logger = logging.getLogger(name) return result class Logged(metaclass=LoggedMeta): logger: logging.Logger class SomeApplicationClass(Logged): def __init__(self, v1: int, v2: int) -> None: self.logger.info("v1=%r, v2=%r", v1, v2) self.v1 = v1 self.v2 = v2 self.v3 = v1 * v2 self.logger.info("product=%r", self.v3) test_meta = """ >>> import sys >>> logging.basicConfig(stream=sys.stdout, level=logging.INFO) >>> sa = SomeApplicationClass(6, 7) INFO:SomeApplicationClass:v1=6, v2=7 INFO:SomeApplicationClass:product=42 >>> logging.shutdown() """ __test__ = { name: value for name, value in locals().items() if name.startswith("test_") } if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_4/__init__.py ================================================ ================================================ FILE: Chapter_4/ch04_ex1.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 4. Example 1. """ from enum import Enum import random from typing import Any, cast, NamedTuple, Callable, Iterable, Union from dataclasses import dataclass # Immutability # ====================== # Yes, this is out of order from the book. # It's presented first to make the code below work out nicely. # A simple-looking card class with comparisons. # Uses __slots__ to constrain the definition. class Suit(str, Enum): Club = "\N{BLACK CLUB SUIT}" Diamond = "\N{BLACK DIAMOND SUIT}" Heart = "\N{BLACK HEART SUIT}" Spade = "\N{BLACK SPADE SUIT}" class BlackJackCard: """Abstract Superclass.""" # Note: __slots__ isn't inherited and must be repeated. # THe alternative is kind of hideous. __slots__ = ("rank", "suit", "hard", "soft") def __init__(self, rank: str, suit: "Suit", hard: int, soft: int) -> None: self.rank = rank self.suit = suit self.hard = hard self.soft = soft def __repr__(self) -> str: return f"{self.__class__.__name__}(rank={self.rank}, suit={self.suit!r}, hard={self.hard}, soft={self.soft}" def __str__(self) -> str: return f"{self.rank}{self.suit}" # if we don't want to repeat __slots__ we can use this to prevent attributes being set. # def __setattr__(self, name: str, value: Any) -> NoReturn: # raise AttributeError( # f"{self.__class__.__name__} has no attribute {name!r}" # ) def __lt__(self, other: Any) -> bool: # Bad idea if not issubclass(other.__class__, BlackJackCard): return NotImplemented return self.rank < cast(BlackJackCard, other).rank def __le__(self, other: Any) -> bool: # Better idea try: return self.rank <= cast(BlackJackCard, other).rank except AttributeError: return NotImplemented def __eq__(self, other: Any) -> bool: try: return self.rank == cast(BlackJackCard, other).rank and self.suit == cast( BlackJackCard, other ).suit except AttributeError: return NotImplemented class Ace21Card(BlackJackCard): __slots__ = ("rank", "suit", "hard", "soft") def __init__(self, rank: int, suit: Suit) -> None: super().__init__("A", suit, 1, 11) def __repr__(self) -> str: return f"{self.__class__.__name__}(rank=1, suit={self.suit!r})" class Face21Card(BlackJackCard): __slots__ = ("rank", "suit", "hard", "soft") def __init__(self, rank: int, suit: Suit) -> None: rank_str = {11: "J", 12: "Q", 13: "K"}[rank] super().__init__(rank_str, suit, 10, 10) def __repr__(self) -> str: rank_num = {"J": 11, "Q": 12, "K": 13}[self.rank] return f"{self.__class__.__name__}(rank={rank_num}, suit={self.suit!r})" class Number21Card(BlackJackCard): __slots__ = ("rank", "suit", "hard", "soft") def __init__(self, rank: int, suit: Suit) -> None: super().__init__(str(rank), suit, rank, rank) def __repr__(self) -> str: return f"{self.__class__.__name__}(rank={self.rank}, suit={self.suit!r})" def card21(rank: int, suit: Suit) -> BlackJackCard: if rank == 1: return Ace21Card(rank, suit) elif 2 <= rank < 11: return Number21Card(rank, suit) elif 11 <= rank < 14: return Face21Card(rank, suit) else: raise TypeError def compare(a: Any, b: Any) -> None: print(f"{a} == {b} {a==b}, {a} < {b} {a {b} {a>b}, {a} >= {b} {a >= b}") test_comparisons_21 = """ >>> c = Ace21Card("A", Suit.Spade) >>> c.label = "no slot named label" # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): File "/Users/slott/Documents/Writing/Python/Mastering OO Python 2e/mastering-oo-python-2e/Chapter_4/ch04_ex1.py", line 173, in card2d.label = "no slot named label" AttributeError: 'Number21Card' object has no attribute 'label' >>> print(c.label) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): File "/Users/slott/Documents/Writing/Python/Mastering OO Python 2e/mastering-oo-python-2e/Chapter_4/ch04_ex1.py", line 173, in card2d.label = "no slot named label" AttributeError: 'Number21Card' object has no attribute 'label' >>> c Ace21Card(rank=1, suit=) >>> c.rank = 2 >>> card2d = card21(2, Suit.Diamond) >>> card2s = card21(2, Suit.Spade) >>> cardkd = card21(13, Suit.Diamond) >>> card2d Number21Card(rank=2, suit=) >>> compare(card2d, card2s) 2♦ == 2♠ False, 2♦ < 2♠ False, 2♦ <= 2♠ True 2♦ != 2♠ True, 2♦ > 2♠ False, 2♦ >= 2♠ True >>> compare(card2s, cardkd) 2♠ == K♦ False, 2♠ < K♦ True, 2♠ <= K♦ True 2♠ != K♦ True, 2♠ > K♦ False, 2♠ >= K♦ False >>> compare(card2d, 2) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): File "/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py", line 1329, in __run compileflags, 1), test.globs) File "", line 1, in compare(card2d, 2) # doctest: +IGNORE_EXCEPTION_DETAIL File "/Users/slott/Documents/Writing/Python/Mastering OO Python 2e/mastering-oo-python-2e/Chapter_4/ch04_ex1.py", line 117, in compare print(f"{a} == {b} {a==b}, {a} < {b} {a None: super().__init__() for i in range(decks): self.extend(factory(r + 1, s) for r in range(13) for s in cast(Iterable[Suit], Suit)) random.shuffle(self) burn = random.randint(1, 52) for i in range(burn): self.pop() class AceCard2(NamedTuple): rank: str suit: Suit hard: int = 1 soft: int = 11 def __str__(self) -> str: return f"{self.rank}{self.suit}" class FaceCard2(NamedTuple): rank: str suit: Suit hard: int = 10 soft: int = 10 def __str__(self) -> str: return f"{self.rank}{self.suit}" class NumberCard2(NamedTuple): rank: str suit: Suit hard: int soft: int def __str__(self) -> str: return f"{self.rank}{self.suit}" def card2(rank: int, suit: Suit) -> Union[AceCard2, FaceCard2, NumberCard2]: """No parent class... """ if rank == 1: return AceCard2("A", suit) elif 2 <= rank < 11: return NumberCard2(str(rank), suit, rank, rank) elif 11 <= rank < 14: rank_str = {11: "J", 12: "Q", 13: "K"}[rank] return FaceCard2(rank_str, suit) else: raise TypeError test_comparisons_2 = """ >>> c = AceCard2("A", Suit.Spade) >>> c.rank 'A' >>> c.suit >>> c.hard 1 >>> c.not_allowed = 2 # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): File "/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py", line 1329, in __run compileflags, 1), test.globs) File "", line 1, in c.not_allowed = 2 AttributeError: 'AceCard2' object has no attribute 'not_allowed' >>> c.rank = 3 # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): File "/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py", line 1329, in __run compileflags, 1), test.globs) File "", line 1, in c.rank = 3 AttributeError: can't set attribute >>> c.label = "no slot named label" # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): File "/Users/slott/Documents/Writing/Python/Mastering OO Python 2e/mastering-oo-python-2e/Chapter_4/ch04_ex1.py", line 173, in card2d.label = "no slot named label" AttributeError: 'AceCard2' object has no attribute 'label' >>> print(c.label) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): File "/Users/slott/Documents/Writing/Python/Mastering OO Python 2e/mastering-oo-python-2e/Chapter_4/ch04_ex1.py", line 173, in card2d.label = "no slot named label" AttributeError: 'AceCard2' object has no attribute 'label' >>> c AceCard2(rank='A', suit=, hard=1, soft=11) >>> card2d = card2(2, Suit.Diamond) >>> card2s = card2(2, Suit.Spade) >>> cardkd = card2(13, Suit.Diamond) >>> card2d NumberCard2(rank='2', suit=, hard=2, soft=2) >>> compare(card2d, card2s) 2♦ == 2♠ False, 2♦ < 2♠ False, 2♦ <= 2♠ False 2♦ != 2♠ True, 2♦ > 2♠ True, 2♦ >= 2♠ True >>> compare(card2s, cardkd) 2♠ == K♦ False, 2♠ < K♦ True, 2♠ <= K♦ True 2♠ != K♦ True, 2♠ > K♦ False, 2♠ >= K♦ False >>> compare(card2d, 2) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): TypeError: '<' not supported between instances of 'NumberCard2' and 'int' """ @dataclass(eq=True, order=True, frozen=True) class AceCard3: rank: str suit: Suit hard: int = 1 soft: int = 11 @dataclass(eq=True, order=True, frozen=True) class FaceCard3: rank: str suit: Suit hard: int = 10 soft: int = 10 @dataclass(eq=True, order=True, frozen=True) class NumberCard3: rank: str suit: Suit hard: int soft: int def card3(rank, suit) -> Union[AceCard3, FaceCard3, NumberCard3]: if rank == 1: return AceCard3("A", suit) elif 2 <= rank < 11: return NumberCard3(str(rank), suit, rank, rank) elif 11 <= rank < 14: rank_str = {11: "J", 12: "Q", 13: "K"}[rank] return FaceCard3(rank_str, suit) else: raise TypeError test_comparisons_3 = """ >>> c = AceCard3("A", Suit.Spade) >>> c.label = "no slot named label" Traceback (most recent call last): dataclasses.FrozenInstanceError: cannot assign to field 'label' >>> print(c.label) Traceback (most recent call last): AttributeError: 'AceCard3' object has no attribute 'label' >>> c AceCard3(rank='A', suit=, hard=1, soft=11) >>> card2d = card3(2, Suit.Diamond) >>> card2s = card3(2, Suit.Spade) >>> cardkd = card3(13, Suit.Diamond) >>> card2d NumberCard3(rank='2', suit=, hard=2, soft=2) >>> compare(card2d, card2s) NumberCard3(rank='2', suit=, hard=2, soft=2) == NumberCard3(rank='2', suit=, hard=2, soft=2) False, NumberCard3(rank='2', suit=, hard=2, soft=2) < NumberCard3(rank='2', suit=, hard=2, soft=2) False, NumberCard3(rank='2', suit=, hard=2, soft=2) <= NumberCard3(rank='2', suit=, hard=2, soft=2) False NumberCard3(rank='2', suit=, hard=2, soft=2) != NumberCard3(rank='2', suit=, hard=2, soft=2) True, NumberCard3(rank='2', suit=, hard=2, soft=2) > NumberCard3(rank='2', suit=, hard=2, soft=2) True, NumberCard3(rank='2', suit=, hard=2, soft=2) >= NumberCard3(rank='2', suit=, hard=2, soft=2) True >>> compare(card2s, cardkd) Traceback (most recent call last): File "/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py", line 1329, in __run compileflags, 1), test.globs) File "", line 1, in compare(card2s, cardkd) File "/Users/slott/Documents/Writing/Python/Mastering OO Python 2e/mastering-oo-python-2e/Chapter_4/ch04_ex1.py", line 117, in compare print(f"{a} == {b} {a==b}, {a} < {b} {a>> compare(card2d, 2) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): TypeError: '<' not supported between instances of 'NumberCard3' and 'int' """ __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_4/ch04_ex2.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 4. Example 2. """ import random from typing import List from Chapter_4.ch04_ex1 import Deck, BlackJackCard # Property Decorator # ============================== # # Definition of Hand using a property for the total. class Hand: def __init__( self, dealer_card: BlackJackCard, *cards: BlackJackCard ) -> None: self.dealer_card: BlackJackCard = dealer_card self._cards: List[BlackJackCard] = list(cards) def __str__(self) -> str: return ", ".join(map(str, self.card)) def __repr__(self) -> str: return ( f"{self.__class__.__name__}" f"({self.dealer_card!r}, " f"{', '.join(map(repr, self.card))})" ) @property def card(self) -> List[BlackJackCard]: return self._cards @card.setter def card(self, aCard: BlackJackCard) -> None: raise NotImplementedError @card.deleter def card(self) -> None: raise NotImplementedError def split(self, deck: Deck) -> "Hand": """Updates this hand and also returns the new hand.""" assert self._cards[0].rank == self._cards[1].rank c1 = self._cards[-1] del self.card self.card = deck.pop() h_new = self.__class__(self.dealer_card, c1, deck.pop()) return h_new class Hand_Lazy(Hand): @property def total(self) -> int: delta_soft = max(c.soft - c.hard for c in self._cards) hard_total = sum(c.hard for c in self._cards) if hard_total + delta_soft <= 21: return hard_total + delta_soft return hard_total @property def card(self) -> List[BlackJackCard]: return self._cards @card.setter def card(self, aCard: BlackJackCard) -> None: self._cards.append(aCard) @card.deleter def card(self) -> None: self._cards.pop(-1) # We can now work with the total value of a hand using Hand.total # instead of hand.total(). # test_hand_lazy = """ >>> random.seed(9973) >>> d = Deck() >>> h = Hand_Lazy(d.pop(), d.pop(), d.pop()) >>> print(h.total) 14 >>> h.card = d.pop() >>> print(h.total) 18 """ # What's the advantage? # Simpler syntax. We can still have lazy vs. eager calculation of # the total value of the hand. class Hand_Eager(Hand): def __init__( self, dealer_card: BlackJackCard, *cards: BlackJackCard ) -> None: self.dealer_card = dealer_card self.total = 0 self._delta_soft = 0 self._hard_total = 0 self._cards: List[BlackJackCard] = list() for c in cards: # Mypy cannot discern the actual type of the setter. # https://github.com/python/mypy/issues/4167 self.card = c # type: ignore @property def card(self) -> List[BlackJackCard]: return self._cards @card.setter def card(self, aCard: BlackJackCard) -> None: self._cards.append(aCard) self._delta_soft = max(aCard.soft - aCard.hard, self._delta_soft) self._hard_total = self._hard_total + aCard.hard self._set_total() @card.deleter def card(self) -> None: removed = self._cards.pop(-1) self._hard_total -= removed.hard # Issue: was this the only ace? self._delta_soft = max(c.soft - c.hard for c in self._cards) self._set_total() def _set_total(self) -> None: if self._hard_total + self._delta_soft <= 21: self.total = self._hard_total + self._delta_soft else: self.total = self._hard_total test_hand_eager_and_lazy = """ >>> random.seed(9973) >>> d = Deck() >>> h = Hand_Eager(d.pop(), d.pop(), d.pop()) >>> print(h.total) 14 >>> h.card = d.pop() >>> print(h.total) 18 >>> random.seed(9973) >>> d = Deck() >>> c = d.pop() >>> h = Hand_Lazy(d.pop(), c, c) # Force splittable hand >>> h2 = h.split(d) >>> print(h) 6♦, A♦ >>> print(h2) 6♦, 4♠ """ __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_4/ch04_ex3.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 4. Example 3. """ from typing import Any, Optional from dataclasses import dataclass from Chapter_4.ch04_ex1 import Deck, BlackJackCard # Eagerly Computed Attributes # ============================ # Compute early and often. This is rather complex because it # derives any one from the other two. The ``Optional[float]`` is misleading. @dataclass class RateTimeDistance: rate: Optional[float] = None time: Optional[float] = None distance: Optional[float] = None def __post_init__(self) -> None: if self.rate is not None and self.time is not None: self.distance = self.rate * self.time elif self.rate is not None and self.distance is not None: self.time = self.distance / self.rate elif self.time is not None and self.distance is not None: self.rate = self.distance / self.time test_rtd = """ >>> rtd = RateTimeDistance(rate=6.3, time=8.25, distance=None) >>> print(f"Rate={rtd.rate}, Time={rtd.time}, Distance={rtd.distance}") Rate=6.3, Time=8.25, Distance=51.975 >>> RateTimeDistance(rate=5.2, time=9.5) RateTimeDistance(rate=5.2, time=9.5, distance=49.4) >>> RateTimeDistance(distance=48.5, rate=6.1) RateTimeDistance(rate=6.1, time=7.950819672131148, distance=48.5) >>> r1 = RateTimeDistance(time=1, rate=0) >>> r1.distance = -99 >>> r1 RateTimeDistance(rate=0, time=1, distance=-99) """ # Dynamic Attributes # ================== class RTD_Dynamic: def __init__(self) -> None: self.rate: float self.time: float self.distance: float super().__setattr__("rate", None) super().__setattr__("time", None) super().__setattr__("distance", None) def __repr__(self) -> str: clauses = [] if self.rate: clauses.append(f"rate={self.rate}") if self.time: clauses.append(f"time={self.time}") if self.distance: clauses.append(f"distance={self.distance}") return (f"{self.__class__.__name__}" f"({', '.join(clauses)})") def __setattr__(self, name: str, value: float) -> None: if name == "rate": super().__setattr__("rate", value) elif name == "time": super().__setattr__("time", value) elif name == "distance": super().__setattr__("distance", value) if self.rate and self.time: super().__setattr__("distance", self.rate * self.time) elif self.rate and self.distance: super().__setattr__("time", self.distance / self.rate) elif self.time and self.distance: super().__setattr__("rate", self.distance / self.time) test_rtd_dynamic = """ >>> rtd = RTD_Dynamic() >>> rtd.time = 9.5 >>> rtd RTD_Dynamic(time=9.5) >>> rtd.rate = 6.25 >>> rtd RTD_Dynamic(rate=6.25, time=9.5, distance=59.375) >>> rtd.distance 59.375 >>> rtd.time = None >>> rtd.rate = 6.125 >>> rtd RTD_Dynamic(rate=6.125, time=9.5, distance=58.1875) """ # Descriptors # ===================== # A Non-Data descriptor example where the descriptor object # reads a local cache file to set class-level parameters from pathlib import Path from typing import Type class PersistentState: """Abstract superclass to use a StateManager object""" _saved: Path class StateManager: """May create a directory. Sets _saved in the instance.""" def __init__(self, base: Path) -> None: self.base = base def __get__(self, instance: PersistentState, owner: Type) -> Path: if not hasattr(instance, "_saved"): class_path = self.base / owner.__name__ class_path.mkdir(exist_ok=True, parents=True) instance._saved = class_path / str(id(instance)) return instance._saved class PersistentClass(PersistentState): state_path = StateManager(Path.cwd() / "data" / "state") def __init__(self, a: int, b: float) -> None: self.a = a self.b = b self.c: Optional[float] = None self.state_path.write_text(repr(vars(self))) def calculate(self, c: float) -> float: self.c = c self.state_path.write_text(repr(vars(self))) return self.a * self.b + self.c def __str__(self) -> str: return self.state_path.read_text() test_persist = """ >>> x = PersistentClass(1, 2) >>> str(x) # doctest: +ELLIPSIS "{'a': 1, 'b': 2, 'c': None, '_saved': ...)}" >>> x.calculate(3) 5 >>> str(x) # doctest: +ELLIPSIS "{'a': 1, 'b': 2, 'c': 3, '_saved': ...)}" """ # A data descriptor example with data in the containing instance. class Conversion: """Depends on a standard value.""" conversion: float standard: str def __get__(self, instance: Any, owner: type) -> float: return getattr(instance, self.standard) * self.conversion def __set__(self, instance: Any, value: float) -> None: setattr(instance, self.standard, value / self.conversion) class Standard(Conversion): """Defines a standard value.""" conversion = 1.0 class Speed(Conversion): standard = "standard_speed" # KPH class KPH(Standard, Speed): pass class Knots(Speed): conversion = 0.5399568 class MPH(Speed): conversion = 0.62137119 class Trip: kph = KPH() knots = Knots() mph = MPH() def __init__( self, distance: float, kph: Optional[float] = None, mph: Optional[float] = None, knots: Optional[float] = None, ) -> None: self.distance = distance # Nautical Miles if kph: self.kph = kph elif mph: self.mph = mph elif knots: self.knots = knots else: raise TypeError("Impossible pattern of None values") self.time = self.distance / self.knots def __str__(self) -> str: return ( f"distance: {self.distance} nm, " f"rate: {self.kph} " f"kph = {self.mph} " f"mph = {self.knots} knots, " f"time = {self.time} hrs" ) test_trip = """ >>> m2 = Trip(distance=13.2, knots=5.9) >>> print(m2) distance: 13.2 nm, rate: 10.92680006993152 kph = 6.789598762345432 mph = 5.9 knots, time = 2.23728813559322 hrs >>> print(f"Speed: {m2.mph:.3f} mph") Speed: 6.790 mph >>> m2.standard_speed 10.92680006993152 """ __test__ = { name: value for name, value in locals().items() if name.startswith("test_") } if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_4/ch04_ex4.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 4. Example 4. """ from typing import Any, Optional from Chapter_4.ch04_ex1 import Suit class Card: """A poor replacement for polymorphism.""" def __init__(cls, rank: int, suit: Suit) -> None: super().__setattr__("_cache", {"suit": suit, "rank": rank}) def __setattr__(self, name: str, value: Any) -> None: raise TypeError("Can't Touch That") def __getattr__(self, name: str) -> Any: if name in self._cache: return self._cache[name] elif name == "hard": if self.rank in (11, 12, 13): return 10 elif self.rank == 1: return 1 elif self.rank in range(2, 10): return self.rank else: raise ValueError("Invalid Rank") elif name == "soft": if self.rank in (11, 12, 13): return 10 elif self.rank == 1: return 11 elif self.rank in range(2, 10): return self.rank else: raise ValueError("Invalid Rank") else: raise AttributeError(name) test_card = """ >>> c = Card(2, Suit.Club) >>> c.rank = 3 # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): File "/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py", line 1329, in __run compileflags, 1), test.globs) File "", line 1, in c.rank = 3 File "/Users/slott/Documents/Writing/Python/Mastering OO Python 2e/mastering-oo-python-2e/Chapter_4/ch04_ex4.py", line 17, in __setattr__ raise TypeError("Can't Touch That") TypeError: Can't Touch That >>> c.extra = 3 # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): File "/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py", line 1329, in __run compileflags, 1), test.globs) File "", line 1, in c.rank = 3 File "/Users/slott/Documents/Writing/Python/Mastering OO Python 2e/mastering-oo-python-2e/Chapter_4/ch04_ex4.py", line 17, in __setattr__ raise TypeError("Can't Touch That") TypeError: Can't Touch That >>> c.extra # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): File "/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py", line 1329, in __run compileflags, 1), test.globs) File "", line 1, in c.extra File "/Users/slott/Documents/Writing/Python/Mastering OO Python 2e/mastering-oo-python-2e/Chapter_4/ch04_ex4.py", line 44, in __getattr__ raise AttributeError(name) AttributeError: extra >>> c.rank 2 >>> c.hard 2 >>> c.soft 2 """ class RTD_Solver: def __init__( self, *, rate: float = None, time: float = None, distance: float = None ) -> None: if rate: self.rate = rate if time: self.time = time if distance: self.distance = distance def __getattr__(self, name: str) -> float: if name == "rate": print("Computing Rate") return self.distance / self.time elif name == "time": return self.distance / self.rate elif name == "distance": return self.rate * self.time else: raise AttributeError(f"Can't compute {name}") test_rtd = """ >>> r1 = RTD_Solver(rate=6.25, distance=10.25) >>> r1.time 1.64 >>> r1.rate 6.25 """ class SuperSecret: def __init__(self, hidden: Any, exposed: Any) -> None: self._hidden = hidden self.exposed = exposed def __getattribute__(self, item: str): if (len(item) >= 2 and item[0] == "_" and item[1] != "_"): raise AttributeError(item) return super().__getattribute__(item) test_secret = """ >>> x = SuperSecret('onething', 'another') >>> x.exposed 'another' >>> x._hidden # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): File "/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py", line 1329, in __run compileflags, 1), test.globs) File "", line 1, in x._hidden # File "/Users/slott/Documents/Writing/Python/Mastering OO Python 2e/mastering-oo-python-2e/Chapter_4/ch04_ex4.py", line 132, in __getattribute__ raise AttributeError(item) AttributeError: _hidden >>> dir(x) # doctest: +ELLIPSIS [..., '_hidden', 'exposed'] >>> vars(x) {'_hidden': 'onething', 'exposed': 'another'} """ __test__ = { name: value for name, value in locals().items() if name.startswith("test_") } if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_4/ch04_ex5.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 4. Example 5. """ from dataclasses import dataclass from enum import Enum from typing import Iterator, cast, Iterable, Optional @dataclass class RTD: rate: Optional[float] time: Optional[float] distance: Optional[float] def compute(self) -> "RTD": if ( self.distance is None and self.rate is not None and self.time is not None ): self.distance = self.rate * self.time elif ( self.rate is None and self.distance is not None and self.time is not None ): self.rate = self.distance / self.time elif ( self.time is None and self.distance is not None and self.rate is not None ): self.time = self.distance / self.rate return self test_rtd = """ >>> r = RTD(distance=13.5, rate=6.1, time=None) >>> r.compute() RTD(rate=6.1, time=2.2131147540983607, distance=13.5) """ class Suit(str, Enum): Club = "\N{BLACK CLUB SUIT}" Diamond = "\N{BLACK DIAMOND SUIT}" Heart = "\N{BLACK HEART SUIT}" Spade = "\N{BLACK SPADE SUIT}" @dataclass(frozen=True, order=True) class Card: rank: int suit: str @property def points(self) -> int: return self.rank class Ace(Card): @property def points(self) -> int: return 1 class Face(Card): @property def points(self) -> int: return 10 def deck() -> Iterator[Card]: for rank in range(1, 14): for suit in cast(Iterable[Suit], Suit): if rank == 1: yield Ace(rank, suit) elif rank >= 11: yield Face(rank, suit) else: yield Card(rank, suit) test_dataclass = """ >>> a = Card(7, Suit.Heart) >>> a.rank 7 >>> a.suit >>> b = Card(7, Suit.Heart) >>> a == b True >>> a < Card(8, Suit.Spade) True """ test_hand = """ >>> import random >>> random.seed(16) >>> cards = list(deck()) >>> random.shuffle(cards) >>> hand = cards[:5] >>> any(c.rank == 1 for c in hand) True >>> any(c.points == 10 for c in hand) True >>> sum(c.points for c in hand) 34 >>> for c in hand: ... print(f"{c!r}: {c.points}") Card(rank=3, suit=): 3 Ace(rank=1, suit=): 1 Face(rank=11, suit=): 10 Face(rank=13, suit=): 10 Face(rank=12, suit=): 10 >>> Ace(1, Suit.Spade) in set(hand) True """ __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_5/__init__.py ================================================ ================================================ FILE: Chapter_5/ch05_ex1.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 5. Example 1. """ from typing import Any, Union # Metaclass # ====================== # Abstract Base Class Example. from abc import ABCMeta, abstractmethod, ABC class Card: pass class Hand(list): def __init__(self, *cards: Card) -> None: super().__init__(cards) class AbstractBettingStrategy(metaclass=ABCMeta): @abstractmethod def bet(self, hand: Hand) -> int: return 1 @abstractmethod def record_win(self, hand: Hand) -> None: pass @abstractmethod def record_loss(self, hand: Hand) -> None: pass class AbstractBettingStrategy2(ABC): @abstractmethod def bet(self, hand: Hand) -> int: return 1 @abstractmethod def record_win(self, hand: Hand) -> None: pass @abstractmethod def record_loss(self, hand: Hand) -> None: pass @classmethod def __subclasshook__(cls, subclass: type) -> bool: """Validate the class definition is complete.""" if cls is AbstractBettingStrategy2: has_bet = any(hasattr(B, "bet") for B in subclass.__mro__) has_record_win = any(hasattr(B, "record_win") for B in subclass.__mro__) has_record_loss = any(hasattr(B, "record_loss") for B in subclass.__mro__) if has_bet and has_record_win and has_record_loss: return True # print(f"has_bet {has_bet}, has_record_win {has_record_win}, has_record_loss {has_record_loss}") return False test_broken = """ >>> class Simple_Broken(AbstractBettingStrategy): ... def bet(self, hand: Hand) -> int: ... return 1 >>> simple = Simple_Broken() # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): File "/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py", line 1329, in __run compileflags, 1), test.globs) File "", line 1, in simple = Simple_Broken() # doctest: +IGNORE_EXCEPTION_DETAIL TypeError: Can't instantiate abstract class Simple_Broken with abstract methods record_loss, record_win """ test_broken_2 = """ >>> class Simple_Broken2(AbstractBettingStrategy2): ... def bet(self, hand: Hand) -> int: ... return 1 ... >>> simple2 = Simple_Broken2() # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): File "/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py", line 1329, in __run compileflags, 1), test.globs) File "", line 1, in simple2 = Simple_Broken2() TypeError: Can't instantiate abstract class Simple_Broken2 with abstract methods record_loss, record_win """ class Simple(AbstractBettingStrategy): def bet(self, hand: Hand) -> int: return 1 def record_win(self, hand: Hand) -> None: pass def record_loss(self, hand: Hand) -> None: pass test_proper = """ >>> simple = Simple() """ from typing import Tuple, Iterator class LikeAbstract: def aMethod(self, arg: int) -> int: raise NotImplementedError # The following will raise a mypy error. # Chapter_5/ch05_ex1.py:114: error: Signature of "aMethod" incompatible with supertype "LikeAbstract" class LikeConcrete(LikeAbstract): def aMethod(self, arg1: str, arg2: Tuple[int, int]) -> Iterator[Any]: pass __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_5/ch05_ex2.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 5. Example 2. """ import numbers import decimal import collections.abc test_membership = """ >>> isinstance(42, numbers.Number) True >>> 355/113 3.1415929203539825 >>> isinstance(355/113, numbers.Number) True >>> issubclass(decimal.Decimal, numbers.Number) True >>> issubclass(decimal.Decimal, numbers.Integral) False >>> issubclass(decimal.Decimal, numbers.Real) False >>> issubclass(decimal.Decimal, numbers.Complex) False >>> issubclass(decimal.Decimal, numbers.Rational) False """ test_iterator = """ >>> x = [1, 2, 3] >>> iter(x) # doctest: +ELLIPSIS >>> x_iter = iter(x) >>> next(x_iter) 1 >>> next(x_iter) 2 >>> next(x_iter) 3 >>> next(x_iter) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): File "/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py", line 1329, in __run compileflags, 1), test.globs) File "", line 1, in next(x_iter) StopIteration >>> isinstance(x_iter, collections.abc.Iterator) True """ __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_6/__init__.py ================================================ ================================================ FILE: Chapter_6/ch06_ex1.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 6. Example 1. """ from typing import Callable, Dict # Callable # ====================== # Callable Example #1. Inefficient. But. It does work. IntExp = Callable[[int, int], int] class Power1: def __call__(self, x: int, n: int) -> int: p = 1 for i in range(n): p *= x return p pow1: IntExp = Power1() test_power1 = """ >>> pow1 = Power1() >>> pow1(2, 1024) 179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137216 """ # Example 2. Subtle error, can be detected by the class definition # Disliked by mypy as an 'Invalid base class' error: we're forced to ignore it. from collections.abc import Callable as CallableClass class Power2(CallableClass): # type: ignore def __call_(self, x: int, n: int) -> int: p = 1 for i in range(n): p *= x return p test_power2 = """ >>> pow2 = Power2() # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): File "/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py", line 1329, in __run compileflags, 1), test.globs) File "", line 1, in pow2 = Power2() File "/Users/slott/miniconda3/envs/py37/lib/python3.7/typing.py", line 813, in __new__ obj = super().__new__(cls, *args, **kwds) TypeError: Can't instantiate abstract class Power2 with abstract methods __call__ """ # Example 3. Subtle error, detectable by mypy. class Power3: def __call_(self, x: int, n: int) -> int: p = 1 for i in range(n): p *= x return p # mypy will detect this problem. # Chapter_6/ch06_ex1.py:68: error: Incompatible types in assignment (expression has type "Power3", variable has type "Callable[[int, int], int]") pow3: IntExp = Power3() test_power3 = """ >>> pow3 = Power3() >>> pow3(2, 1024) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): File "/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py", line 1329, in __run compileflags, 1), test.globs) File "", line 1, in pow3(2, 1024) TypeError: 'Power3' object is not callable """ class Power4: def __call__(self, x: int, n: int) -> int: if n == 0: return 1 elif n % 2 == 1: return self.__call__(x, n - 1) * x else: # n % 2 == 0: t = self.__call__(x, n // 2) return t * t pow4: IntExp = Power4() test_power4 = """ >>> pow4(2, 1024) 179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137216 """ # Example 4, iterative, also super efficient. class Power4i: def __call__(self, x: int, n: int) -> int: p = 1 while n != 0: if n % 2 == 1: p *= x n -= 1 else: # n % 2 == 0: t = self.__call__(x, n // 2) p *= t p *= t n = 0 return p pow4i: IntExp = Power4i() test_power4i = """ >>> pow4i(2, 1024) 179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137216 """ # Example 5, memoization class Power5: def __init__(self) -> None: self.memo: Dict[int, int] = {} def __call__(self, x: int, n: int) -> int: if (x, n) not in self.memo: if n == 0: self.memo[x, n] = 1 elif n % 2 == 1: self.memo[x, n] = self.__call__(x, n - 1) * x elif n % 2 == 0: t = self.__call__(x, n // 2) self.memo[x, n] = t * t else: raise Exception("Logic Error") return self.memo[x, n] pow5: IntExp = Power5() test_power5 = """ >>> pow5(2, 1024) 179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137216 """ # Example 6, functools memoization from functools import lru_cache @lru_cache() def pow6(x: int, n: int) -> int: if n == 0: return 1 elif n % 2 == 1: return pow6(x, n - 1) * x else: # n % 2 == 0: t = pow6(x, n // 2) return t * t test_power6 = """ >>> pow6(2, 1024) 179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137216 """ def performance() -> None: """Timeit results""" import timeit iterative = timeit.timeit( "pow1(2, 1024)", """ class Power1: def __call__(self, x: int, n: int) -> int: p = 1 for i in range(n): p *= x return p pow1= Power1() """, number=100_000, ) # otherwise it takes 2 minutes print("Iterative", iterative) recursive = timeit.timeit( "pow4(2,1024)", """ class Power4: def __call__(self, x: int, n: int) -> int: if n == 0: return 1 elif n % 2 == 1: return self.__call__(x, n-1) * x else: # n % 2 == 0: t= self.__call__(x, n//2) return t*t pow4= Power4() """, number=100_000, ) print("Recursive", recursive) memoized = timeit.timeit( "pow5(2,1024)", """ class Power5: def __init__( self ) -> None: self.memo = {} def __call__(self, x: int, n: int) -> int: if (x,n) not in self.memo: if n == 0: self.memo[x,n]= 1 elif n % 2 == 1: self.memo[x,n]= self.__call__(x, n-1) * x elif n % 2 == 0: t= self.__call__(x, n//2) self.memo[x,n]= t*t else: raise Exception("Logic Error") return self.memo[x,n] pow5 = Power5() """, number=100_000, ) print("Memoized", memoized) # Some additional Callable Examples # --------------------------------- # The BetingStrategy superclass. class BettingStrategy: def __init__(self) -> None: self._win = 0 self._loss = 0 @property def win(self) -> int: return self._win @win.setter def win(self, value: int) -> None: self._win = value self.stage = 1 @property def loss(self) -> int: return self._loss @loss.setter def loss(self, value: int) -> None: self._loss = value def __call__(self) -> int: return 1 test_flat_betting_strategy = """ >>> bet = BettingStrategy() >>> bet() 1 >>> bet.win += 1 >>> bet() 1 >>> bet.loss += 1 >>> bet() 1 """ # A stateful betting strategy. Property-based class BettingMartingale(BettingStrategy): def __init__(self) -> None: self._win = 0 self._loss = 0 self.stage = 1 @property def win(self) -> int: return self._win @win.setter def win(self, value: int) -> None: self._win = value self.stage = 1 @property def loss(self) -> int: return self._loss @loss.setter def loss(self, value: int) -> None: self._loss = value self.stage *= 2 def __call__(self) -> int: return self.stage test_martingale_betting_strategy = """ >>> bet = BettingMartingale() >>> bet() 1 >>> bet.win += 1 >>> bet() 1 >>> bet.loss += 1 >>> bet() 2 >>> bet.loss += 1 >>> bet() 4 >>> bet.win += 1 >>> bet() 1 """ # Another stateful betting strategy, using ``__setattr__()`` instead # if properties. class BettingMartingale2(BettingStrategy): def __init__(self) -> None: self.win = 0 self.loss = 0 self.stage = 1 def __setattr__(self, name: str, value: int) -> None: if name == "win": self.stage = 1 elif name == "loss": self.stage *= 2 super().__setattr__(name, value) def __call__(self) -> int: return self.stage test_martingale2_betting_strategy = """ >>> bet = BettingMartingale2() >>> bet() 1 >>> bet.win += 1 >>> bet() 1 >>> bet.loss += 1 >>> bet() 2 >>> bet.loss += 1 >>> bet() 4 >>> bet.win += 1 >>> bet() 1 """ __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) # Takes 12 seconds. # performance() ================================================ FILE: Chapter_6/ch06_ex2.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 6. Example 2. """ from typing import Callable, TypeVar, Iterable, Iterator, cast, Dict, Match from pathlib import Path # Contexts # ================= # With statement example 1 -- file processing def slow(source="itmaybeahack.com.bkup-Feb-2012.gz") -> int: from pathlib import Path source_path = Path.cwd()/"data"/source target_path = Path.cwd()/"data"/"subset.csv" import re format_1_pat = re.compile( r"([\d\.]+)\s+" # digits and .'s: host r"(\S+)\s+" # non-space: logname r"(\S+)\s+" # non-space: user r"\[(.+?)\]\s+" # Everything in []: time r'"(.+?)"\s+' # Everything in "": request r"(\d+)\s+" # digits: status r"(\S+)\s+" # non-space: bytes r'"(.*?)"\s+' # Everything in "": referrer r'"(.*?)"\s*' # Everything in "": user agent ) import gzip import csv T = Optional[Match[str]] class Counter: def __init__(self, source: Iterable[T]) -> None: self.source_iter = source self.count = 0 def __iter__(self) -> Iterator[T]: for item in self.source_iter: yield item self.count += 1 with target_path.open('w', newline='') as target: wtr = csv.writer(target) with gzip.open(source_path, "r") as source: line_iter = (b.decode() for b in source) row_iter = Counter(format_1_pat.match(line) for line in line_iter) non_empty_rows: Iterator[Match] = filter(None, row_iter) wtr.writerows(m.groups() for m in non_empty_rows) return row_iter.count test_file_proc = """ >>> import time >>> start = time.perf_counter() >>> rows = slow() >>> end = time.perf_counter() >>> print(f"Wrote {rows:,} summary rows in {end-start:.3f} seconds") # doctest: +ELLIPSIS Wrote 380,517 summary rows in ... seconds """ # With statement example 2 -- decimal contexts test_decimal = """ >>> import decimal >>> PENNY = decimal.Decimal("0.00") >>> price = decimal.Decimal("15.99") >>> rate = decimal.Decimal("0.0075") >>> print(f"Tax={(price * rate).quantize(PENNY)}, Fully={price * rate}") Tax=0.12, Fully=0.119925 >>> with decimal.localcontext() as ctx: ... ctx.rounding = decimal.ROUND_DOWN ... tax = (price * rate).quantize(PENNY) >>> print(f"Tax={tax}") Tax=0.11 """ # With statement example 3 -- logging level change -- perhaps not ideal # There are better ways to accomplish this. import logging, sys class Debugging: def __init__(self, aName=None): self.logname = aName def __enter__(self): self.default = logging.getLogger(self.logname).getEffectiveLevel() logging.getLogger().setLevel(logging.DEBUG) def __exit__(self, exc_type, exc_value, traceback): logging.getLogger(self.logname).setLevel(self.default) test_debugging = """ >>> import io >>> log_file = io.StringIO() >>> logging.basicConfig(stream=log_file, level=logging.INFO) >>> logging.info("Before") >>> logging.debug("Silenced before") >>> with Debugging(): ... logging.info("During") ... logging.debug("Enabled during") >>> logging.info("Between") >>> logging.debug("Silenced between") >>> with Debugging(): ... logging.info("Again") ... logging.debug("Enabled Again") >>> logging.info("Done") >>> logging.debug("Silenced at the end") >>> print(log_file.getvalue()) INFO:root:Before INFO:root:During DEBUG:root:Enabled during INFO:root:Between INFO:root:Again DEBUG:root:Enabled Again INFO:root:Done """ # With statement example 4 -- sets the random seed value. import random from typing import Optional, Type from types import TracebackType class KnownSequence: def __init__(self, seed: int = 0) -> None: self.seed = 0 def __enter__(self) -> 'KnownSequence': self.was = random.getstate() random.seed(self.seed, version=1) return self def __exit__( self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType] ) -> Optional[bool]: random.setstate(self.was) return False test_known_sequence = """ >>> print(tuple(random.randint(-1, 36) for i in range(5))) # doctest: +ELLIPSIS (...) >>> with KnownSequence(): ... print(tuple(random.randint(-1, 36) for i in range(5))) (23, 25, 1, 15, 31) >>> print(tuple(random.randint(-1, 36) for i in range(5))) # doctest: +ELLIPSIS (...) >>> with KnownSequence(): ... print(tuple(random.randint(-1, 36) for i in range(5))) (23, 25, 1, 15, 31) >>> print(tuple(random.randint(-1, 36) for i in range(5))) # doctest: +ELLIPSIS (...) """ # Some classes for example 5 from typing import NamedTuple from enum import Enum class Suit(Enum): Clubs = "♣" Diamonds = "♦" Hearts = "♥" Spades = "♠" class Card(NamedTuple): rank: int suit: Suit class Deck(list): def __init__(self, size: int = 1) -> None: super().__init__() for d in range(size): cards = [Card(r, s) for r in range(13) for s in cast(Iterable[Suit], Suit)] super().extend(cards) random.shuffle(self) # Exam[le 5 -- A Context Manager as Factory example class Deterministic_Deck: def __init__(self, *args, **kw) -> None: self.args = args self.kw = kw def __enter__(self) -> Deck: self.was = random.getstate() random.seed(0, version=1) return Deck(*self.args, **self.kw) def __exit__( self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType] ) -> Optional[bool]: random.setstate(self.was) return False test_deterministic_deck = """ Random >>> for i in range(3): ... d1 = Deck() ... print(d1.pop(), d1.pop(), d1.pop()) # doctest: +ELLIPSIS Card(rank=..., suit=...) Card(rank=..., suit=...) Card(rank=..., suit=...) Card(rank=..., suit=...) Card(rank=..., suit=...) Card(rank=..., suit=...) Card(rank=..., suit=...) Card(rank=..., suit=...) Card(rank=..., suit=...) Known >>> for i in range(3): ... with Deterministic_Deck(1) as dd1: ... print(dd1.pop(), dd1.pop(), dd1.pop()) Card(rank=6, suit=) Card(rank=12, suit=) Card(rank=6, suit=) Card(rank=6, suit=) Card(rank=12, suit=) Card(rank=6, suit=) Card(rank=6, suit=) Card(rank=12, suit=) Card(rank=6, suit=) """ # Example 6 -- A Context Manager as Mixin class Deck2(list, KnownSequence): def __init__(self, size: int = 1) -> None: super().__init__() for d in range(size): cards = [Card(r, s) for r in range(13) for s in cast(Iterable[Suit], Suit)] super().extend(cards) self.raw = True KnownSequence.__init__(self) def pop(self, *args, **kw) -> Card: if self.raw: random.shuffle(self) self.raw = False return super().pop(*args, **kw) test_context_mixin = """ Random >>> for i in range(3): ... dd2r = Deck2() ... print(dd2r.pop(), dd2r.pop(), dd2r.pop()) # doctest: +ELLIPSIS Card(rank=..., suit=...) Card(rank=..., suit=...) Card(rank=..., suit=...) Card(rank=..., suit=...) Card(rank=..., suit=...) Card(rank=..., suit=...) Card(rank=..., suit=...) Card(rank=..., suit=...) Card(rank=..., suit=...) Known >>> for i in range(3): ... with Deck2(1) as dd2k: ... print(dd2k.pop(), dd2k.pop(), dd2k.pop()) Card(rank=6, suit=) Card(rank=12, suit=) Card(rank=6, suit=) Card(rank=6, suit=) Card(rank=12, suit=) Card(rank=6, suit=) Card(rank=6, suit=) Card(rank=12, suit=) Card(rank=6, suit=) """ # Example 7 -- A Context Manager for a File Copy from pathlib import Path from typing import Optional class Updating: def __init__(self, target: Path) -> None: self.target: Path = target self.previous: Optional[Path] = None def __enter__(self) -> None: try: self.previous = ( self.target.parent / (self.target.stem + " backup") ).with_suffix(self.target.suffix) self.target.rename(self.previous) except FileNotFoundError: # Target doesn't exist. That's okay. self.previous = None def __exit__( self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType] ) -> Optional[bool]: if exc_type is not None: # An Exception Occurred: Preserve the erroneous file, if possible. try: self.failure = ( self.target.parent / (self.target.stem + " error") ).with_suffix(self.target.suffix) self.target.rename(self.failure) except FileNotFoundError: pass # Never even got created. # If there was a previous file, put the old file back in place. if self.previous: self.previous.rename(self.target) return False def some_update(important_path): with Updating(important_file): with important_file.open('w') as revision: revision.write("Attempted Update\\n") raise Exception("oops") test_updating_context = """ Our file. Make sure it's gone. >>> important_file = Path.cwd()/"data"/"some_file.txt" >>> try: ... important_file.unlink() ... except IOError as e: ... pass First. Create the data. >>> with important_file.open('w') as original: ... _ = original.write("Original data\\n") Second. Try the update. >>> try: ... with Updating(important_file): ... with important_file.open('w') as revision: ... _ = revision.write("Attempted Update\\n") ... raise Exception("oops") ... except Exception as ex: ... print(ex) oops # ``some_file error.txt`` left for us to examine. # ``some_file.txt`` left intact >>> important_file.read_text() 'Original data\\n' >>> (Path.cwd()/"data"/"some_file error.txt").read_text() 'Attempted Update\\n' """ __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_7/__init__.py ================================================ ================================================ FILE: Chapter_7/ch07_defaults.json ================================================ { "decks": 6, "table_limit": 50, "playerclass": "Passive" } ================================================ FILE: Chapter_7/ch07_ex1.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 7. Example 1. """ # Existing Classes # ############################## from pathlib import Path from enum import Enum from typing import ( cast, Iterable, List, TypeVar, Dict, Optional, Iterator, Union, overload, ) from collections.abc import MutableSequence class Suit(str, Enum): Clubs = "♣" Diamonds = "♦" Hearts = "♥" Spades = "♠" # collections.namedtuple and typing.NamedTuple # ============================================ from collections import namedtuple BlackjackCard = namedtuple("BlackjackCard", "rank,suit,hard,soft") from typing import NamedTuple class BlackjackCard_T(NamedTuple): rank: str suit: Suit hard: int soft: int def is_ace(self) -> bool: return False def card(rank: int, suit: Suit) -> BlackjackCard: if rank == 1: return BlackjackCard("A", suit, 1, 11) elif 2 <= rank < 11: return BlackjackCard(str(rank), suit, rank, rank) elif rank == 11: return BlackjackCard("J", suit, 10, 10) elif rank == 12: return BlackjackCard("Q", suit, 10, 10) elif rank == 13: return BlackjackCard("K", suit, 10, 10) else: raise ValueError(f"Invalid Rank {rank}") def card_t(rank: int, suit: Suit) -> BlackjackCard_T: if rank == 1: return BlackjackCard_T("A", suit, 1, 11) elif 2 <= rank < 11: return BlackjackCard_T(str(rank), suit, rank, rank) elif rank == 11: return BlackjackCard_T("J", suit, 10, 10) elif rank == 12: return BlackjackCard_T("Q", suit, 10, 10) elif rank == 13: return BlackjackCard_T("K", suit, 10, 10) else: raise ValueError(f"Invalid Rank {rank}") test_namedtuple = """ >>> c = card(10, Suit.Spades) >>> print(c) BlackjackCard(rank='10', suit=, hard=10, soft=10) >>> c_t = card_t(10, Suit.Spades) >>> print(c_t) BlackjackCard_T(rank='10', suit=, hard=10, soft=10) >>> c_t.is_ace() False """ # This doesn't work out well. The parent class cannot be defined # with a method. class AceCard(BlackjackCard): def is_ace(self) -> bool: return True class AceCard_T(BlackjackCard_T): def is_ace(self) -> bool: return True test_subclass = """ >>> c_1 = AceCard("A", Suit.Spades, 1, 11) >>> print(c_1) AceCard(rank='A', suit=, hard=1, soft=11) >>> c_1t = AceCard_T("A", Suit.Spades, 1, 11) >>> print(c_1t) AceCard_T(rank='A', suit=, hard=1, soft=11) >>> c_1t.is_ace() True """ test_immutable = """ >>> c_1 = AceCard(1, Suit.Spades, 1, 11) >>> c_1.rank = 12 # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): File "/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py", line 1329, in __run compileflags, 1), test.globs) File "", line 1, in c_1.rank = 12 # doctest: +IGNORE_EXCEPTION_DETAIL AttributeError: can't set attribute >>> c_1t = AceCard_T(1, Suit.Spades, 1, 11) >>> c_1t.rank = 12 # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): File "/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py", line 1329, in __run compileflags, 1), test.globs) File "", line 1, in c_1t.rank = 12 # doctest: +IGNORE_EXCEPTION_DETAIL AttributeError: can't set attribute """ # deque # ================================ # Example of Deck built from deque. class Card(NamedTuple): rank: int suit: Suit import random from collections import deque class MultiDeck(list): """A sequence of decks. Each shuffled separately. """ def __init__(self, size: int = 5) -> None: super().__init__() for d in range(size): deck = list( card(r, s) for r in range(1, 14) for s in cast(Iterable[Suit], Suit) ) random.shuffle(deck) while deck: super().append(deck.pop()) test_multideck = """ >>> random.seed(9973) >>> d = MultiDeck() >>> print(d.pop(), d.pop(), d.pop()) BlackjackCard(rank='4', suit=, hard=4, soft=4) BlackjackCard(rank='A', suit=, hard=1, soft=11) BlackjackCard(rank='J', suit=, hard=10, soft=10) >>> more_cards = [d.pop() for _ in range(49)] >>> print(d.pop(), d.pop(), d.pop()) BlackjackCard(rank='10', suit=, hard=10, soft=10) BlackjackCard(rank='3', suit=, hard=3, soft=3) BlackjackCard(rank='6', suit=, hard=6, soft=6) """ # ChainMap # ===================== import argparse import json import os import sys from collections import ChainMap from typing import Dict, Any def get_options(argv: List[str] = sys.argv[1:]) -> ChainMap: """Four Sources: comand line, file, OS environ, defaults.""" parser = argparse.ArgumentParser( description="Process some integers.") parser.add_argument( "-c", "--configuration", type=open, nargs="?") parser.add_argument( "-p", "--playerclass", type=str, nargs="?", default="Simple") cmdline = parser.parse_args(argv) if cmdline.configuration: config_file = json.load(cmdline.configuration) cmdline.configuration.close() else: config_file = {} default_path = (Path.cwd() / "Chapter_7" / "ch07_defaults.json") with default_path.open() as default_file: defaults = json.load(default_file) combined = ChainMap( vars(cmdline), config_file, os.environ, defaults) return combined test_options = """ >>> options = get_options(['-p', 'Aggressive']) >>> print("combined", options['playerclass']) combined Aggressive >>> print("cmdline playerclass", options.maps[0].get('playerclass', None)) cmdline playerclass Aggressive >>> print("config_file playerclass", options.maps[1].get('playerclass', None)) config_file playerclass None >>> print("os environ playerclass", options.maps[2].get('playerclass', None)) os environ playerclass None >>> print("default playerclass", options.maps[3].get('playerclass', None)) default playerclass Passive """ # OrderedDict # ====================== # No longer necessary. A ``dict`` does this, also. # Some Sample XML source = """ firstmore words secondmore words thirdmore words """ # Parsing from collections import OrderedDict import xml.etree.ElementTree as etree test_ordered_dict = """ >>> doc = etree.XML(source) # Parse >>> >>> topics = OrderedDict() # Gather tags within >>> for topic in doc.findall("topics/entry"): ... topics[topic.attrib['ID']] = topic >>> # Order of entry is preserved. Always. >>> for topic in topics: # Display tags within each <topic> ... print(topic, topics[topic].find("title").text) UUID98765 first UUID87654 second UUID65432 third >>> # We can also lookup by a key. >>> for tag in doc.findall("indices/bytag/tag"): ... print(tag.attrib['text']) ... for e in tag.findall("entry"): ... print(' ', e.attrib['IDREF'], topics[e.attrib['IDREF']].find("title").text) #sometag UUID87654 second UUID98765 first #anothertag UUID98765 first UUID65432 third """ # The point is to keep the topics in an ordereddict by their original positions # in the document and also reference them by ID. # We can reference them from other places without scrambling # the original order. test_dict_ordering = """ >>> some_dict = {'zzz': 1, 'aaa': 2} >>> some_dict['mmm'] = 3 >>> some_dict {'zzz': 1, 'aaa': 2, 'mmm': 3} >>> sorted(some_dict) ['aaa', 'mmm', 'zzz'] """ # Defaultdict # ===================== from collections import defaultdict from typing import DefaultDict messages: Dict[str, str] = defaultdict(lambda: "N/A") messages["error1"] = "Full Error Text" messages["other"] messages["error2"] = "Another Error Text" test_default_dict = """ >>> messages_with_default = [k for k in messages if messages[k] == "N/A"] >>> messages_with_default ['other'] >>> messages['error1'] 'Full Error Text' >>> messages['weird'] 'N/A' """ from typing import Dict, List, Tuple def dice_examples(n: int=12, seed: Any=None) -> DefaultDict[int, List]: if seed: random.seed(seed) Roll = Tuple[int, int] outcomes: DefaultDict[int, List[Roll]] = defaultdict(list) for _ in range(n): d1, d2 = random.randint(1, 6), random.randint(1, 6) outcomes[d1+d2].append((d1, d2)) return outcomes test_default_dict_2 = """ >>> d = dice_examples(12, seed=42) >>> d 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)]}) """ # Counter # ================== # Extension of defaultdict(int) # A Data Source import random def value_iterator(count=100, seed=4000) -> Iterable[str]: random.seed(seed, version=1) for i in range(count): yield str(random.randint(1, 6) + random.randint(1, 6)) from collections import defaultdict T = TypeVar("T") def freq_ordered(values: Iterable[T]) -> Dict[int, List[T]]: """ Shows ties as list of pair values with the same frequency. """ frequency: Dict[T, int] = defaultdict(int) for p in values: frequency[p] += 1 rank_by_value: Dict[int, List[T]] = defaultdict(list) for pair, freq in frequency.items(): rank_by_value[freq].append(pair) return rank_by_value from collections import Counter freq_2: Dict[str, int] = Counter(value_iterator()) test_counter = """ >>> freq_1 = freq_ordered(value_iterator()) >>> for freq in sorted(freq_1, reverse=True): ... for v in freq_1[freq]: ... print(repr(v), freq) '7' 19 '6' 17 '8' 16 '10' 10 '4' 9 '11' 8 '5' 8 '9' 5 '3' 4 '12' 2 '2' 2 >>> freq_2 = Counter(value_iterator()) >>> for k, freq in freq_2.most_common(): ... print(repr(k), freq) '7' 19 '6' 17 '8' 16 '10' 10 '4' 9 '11' 8 '5' 8 '9' 5 '3' 4 '12' 2 '2' 2 """ def bag_demo() -> None: """ >>> bag_demo() Counter({'a': 2, 'r': 1, 'd': 1, 'w': 1, 'o': 1, 'l': 1, 'v': 1, 'e': 1, 's': 1}) Counter({'o': 2, 'z': 1, 'y': 1, 'm': 1, 'l': 1, 'g': 1, 'i': 1, 'e': 1, 's': 1}) 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}) Counter({'a': 2, 'r': 1, 'd': 1, 'w': 1, 'v': 1}) Counter({'z': 1, 'y': 1, 'm': 1, 'o': 1, 'g': 1, 'i': 1}) """ bag1 = Counter("aardwolves") bag2 = Counter("zymologies") print(bag1) print(bag2) print(bag1+bag2) print(bag1-bag2) print(bag2-bag1) __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) # performance() ================================================ FILE: Chapter_7/ch07_ex2.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 7. Example 2. """ from typing import List, cast, Any, Optional, Iterable, overload, Union, Iterator # Extending Classes # ############################## # Basic Stats formulae import math def mean(outcomes: List[float]) -> float: return sum(outcomes) / len(outcomes) def stdev(outcomes: List[float]) -> float: n = float(len(outcomes)) return math.sqrt(n * sum(x ** 2 for x in outcomes) - sum(outcomes) ** 2) / n test_stats = """ >>> sample_data = [2, 4, 4, 4, 5, 5, 7, 9] >>> mean(sample_data) 5.0 >>> stdev(sample_data) 2.0 """ # A simple (lazy) stats list class. # Note the difficulty in expressing a type constraint: List[float]. class StatsList(list): def __init__(self, iterable: Optional[Iterable[float]]) -> None: super().__init__(cast(Iterable[Any], iterable)) @property def mean(self) -> float: return sum(self) / len(self) @property def stdev(self) -> float: n = len(self) return math.sqrt(n * sum(x ** 2 for x in self) - sum(self) ** 2) / n test_lazy_stats_list = """ >>> sl = StatsList([2, 4, 4, 4, 5, 5, 7, 9]) >>> sl.mean 5.0 >>> sl.stdev 2.0 >>> sl[2] = 10 >>> round(sl.mean, 2) 5.75 >>> round(sl.stdev, 2) 2.54 """ import random def data_gen() -> int: return random.randint(1, 6) + random.randint(1, 6) def demo_statslist() -> None: """ >>> random.seed(42) >>> demo_statslist() mean = 7.000000 stdev= 2.328 """ random.seed(42) data = [data_gen() for _ in range(100)] stats = StatsList(data) print(f"mean = {stats.mean:f}") print(f"stdev= {stats.stdev:.3f}") class Explore(list): # There are two overloaded definitions, the type hints tend to be complex for this case def __getitem__(self, index): print(index, index.indices(len(self))) return super().__getitem__(index) test_explore = """ >>> x= Explore('abcdefg') >>> x[:] slice(None, None, None) (0, 7, 1) ['a', 'b', 'c', 'd', 'e', 'f', 'g'] >>> x[:-1] slice(None, -1, None) (0, 6, 1) ['a', 'b', 'c', 'd', 'e', 'f'] >>> x[1:] slice(1, None, None) (1, 7, 1) ['b', 'c', 'd', 'e', 'f', 'g'] >>> x[::2] slice(None, None, 2) (0, 7, 2) ['a', 'c', 'e', 'g'] """ # Eager Stats List class # Note the difficulty in expressing a type constraint: List[float]. class StatsList2(list): """Eager Stats.""" def __init__(self, iterable: Optional[Iterable[float]]) -> None: self.sum0 = 0 # len(self), sometimes called "N" self.sum1 = 0.0 # sum(self) self.sum2 = 0.0 # sum(x**2 for x in self) super().__init__(cast(Iterable[Any], iterable)) for x in self: self._new(x) def _new(self, value: float) -> None: self.sum0 += 1 self.sum1 += value self.sum2 += value * value def _rmv(self, value: float) -> None: self.sum0 -= 1 self.sum1 -= value self.sum2 -= value * value def insert(self, index: int, value: float) -> None: super().insert(index, value) self._new(value) def pop(self, index: int = 0) -> None: value = super().pop(index) self._rmv(value) return value def append(self, value: float) -> None: super().append(value) self._new(value) def extend(self, sequence: Iterable[float]) -> None: super().extend(sequence) for value in sequence: self._new(value) def remove(self, value: float) -> None: super().remove(value) self._rmv(value) def __iadd__(self, sequence: Iterable[float]) -> "StatsList2": for v in sequence: self.append(v) return self def __add__(self, sequence: Iterable[float]) -> "StatsList2": generic = super().__add__(cast(StatsList2, sequence)) result = StatsList2(generic) return result # reveal_type(list.__iadd__) # reveal_type(list.__add__) # reveal_type(StatsList2.__iadd__) # reveal_type(StatsList2.__add__) @property def mean(self) -> float: return self.sum1 / self.sum0 @property def stdev(self) -> float: return math.sqrt(self.sum0 * self.sum2 - self.sum1 * self.sum1) / self.sum0 @overload def __setitem__(self, index: int, value: float) -> None: ... @overload def __setitem__(self, index: slice, value: Iterable[float]) -> None: ... def __setitem__(self, index, value) -> None: if isinstance(index, slice): start, stop, step = index.indices(len(self)) olds = [self[i] for i in range(start, stop, step)] super().__setitem__(index, value) for x in olds: self._rmv(x) for x in value: self._new(x) else: old = self[index] super().__setitem__(index, value) self._rmv(old) self._new(value) def __delitem__(self, index: Union[int, slice]) -> None: # Index may be a single integer, or a slice if isinstance(index, slice): start, stop, step = index.indices(len(self)) olds = [self[i] for i in range(start, stop, step)] super().__delitem__(index) for x in olds: self._rmv(x) else: old = self[index] super().__delitem__(index) self._rmv(old) # reveal_type(list.__setitem__) # reveal_type(MutableSequence.__setitem__) # reveal_type(StatsList2.__setitem__) # reveal_type(list.__delitem__) # reveal_type(StatsList2.__delitem__) test_eager_stats_list = """ >>> sl2 = StatsList2([2, 4, 3, 4, 5, 5, 7, 9, 10]) >>> print("start", sl2, sl2.sum0, sl2.sum1, sl2.sum2) start [2, 4, 3, 4, 5, 5, 7, 9, 10] 9 49.0 325.0 >>> sl2[2] = 4 >>> print("replace", sl2, sl2.sum0, sl2.sum1, sl2.sum2) replace [2, 4, 4, 4, 5, 5, 7, 9, 10] 9 50.0 332.0 >>> del sl2[-1] >>> print("remove", sl2, sl2.sum0, sl2.sum1, sl2.sum2) remove [2, 4, 4, 4, 5, 5, 7, 9] 8 40.0 232.0 >>> sl2.insert(0, -1) >>> print("insert", sl2, sl2.sum0, sl2.sum1, sl2.sum2) insert [-1, 2, 4, 4, 4, 5, 5, 7, 9] 9 39.0 233.0 >>> r = sl2.pop() >>> print("pop", sl2, sl2.sum0, sl2.sum1, sl2.sum2) pop [2, 4, 4, 4, 5, 5, 7, 9] 8 40.0 232.0 >>> sl2.append(1) >>> print("append", sl2, sl2.sum0, sl2.sum1, sl2.sum2) append [2, 4, 4, 4, 5, 5, 7, 9, 1] 9 41.0 233.0 >>> sl2.extend([10, 11, 12]) >>> print("extend", sl2, sl2.sum0, sl2.sum1, sl2.sum2) extend [2, 4, 4, 4, 5, 5, 7, 9, 1, 10, 11, 12] 12 74.0 598.0 >>> sl2.remove(-2) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): File "/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py", line 1329, in __run compileflags, 1), test.globs) File "<doctest __main__.__test__.test_eager_stats_list[14]>", line 1, in <module> sl2.remove(-2) # doctest: +IGNORE_EXCEPTION_DETAIL File "/Users/slott/Documents/Writing/Python/Mastering OO Python 2e/mastering-oo-python-2e/Chapter_7/ch07_ex1.py", line 438, in remove super().remove(value) ValueError: list.remove(x): x not in list >>> print("failed remove", sl2, sl2.sum0, sl2.sum1, sl2.sum2) failed remove [2, 4, 4, 4, 5, 5, 7, 9, 1, 10, 11, 12] 12 74.0 598.0 >>> sl2 += [21, 22, 23] >>> print("+=", sl2, sl2.sum0, sl2.sum1, sl2.sum2) += [2, 4, 4, 4, 5, 5, 7, 9, 1, 10, 11, 12, 21, 22, 23] 15 140.0 2052.0 >>> sl = StatsList([2, 4, 4, 4, 5, 5, 7, 9, 1, 10, 11, 12, 21, 22, 23]) >>> print("expected", len(sl), "actual", sl2.sum0) expected 15 actual 15 >>> print("expected", sum(sl), "actual", sl2.sum1) expected 140 actual 140.0 >>> print("expected", sum(x * x for x in sl), "actual", sl2.sum2) expected 2052 actual 2052.0 >>> sl.mean == sl2.mean True >>> sl.stdev == sl2.stdev True >>> sl2a = StatsList2([2, 4, 3, 4, 5, 5, 7, 9, 10]) >>> del sl2a[1:3] >>> print('slice del', sl2a, sl2a.sum0, sl2a.sum1, sl2a.sum2) slice del [2, 4, 5, 5, 7, 9, 10] 7 42.0 300.0 """ # Wrapping Classes # ############################## # Stats List Wrapper class StatsList3: def __init__(self) -> None: self._list: List[float] = list() self.sum0 = 0 # len(self), sometimes called "N" self.sum1 = 0. # sum(self) self.sum2 = 0. # sum(x**2 for x in self) def append(self, value: float) -> None: self._list.append(value) self.sum0 += 1 self.sum1 += value self.sum2 += value * value # etc. def __getitem__(self, index: int) -> float: return self._list.__getitem__(index) @property def mean(self) -> float: return self.sum1 / self.sum0 @property def stdev(self) -> float: return math.sqrt(self.sum0 * self.sum2 - self.sum1 * self.sum1) / self.sum0 test_wrapper_stats_list = """ >>> sl3 = StatsList3() >>> for data in 2, 4, 4, 4, 5, 5, 7, 9: ... sl3.append(data) >>> print(f"Mean {sl3.mean:.1f}, Standard Deviation {sl3.stdev:.1f}") Mean 5.0, Standard Deviation 2.0 """ # Heading 4 -- Extending Classes # ############################## # Stats Counter import math from collections import Counter class StatsCounter(Counter): @property def mean(self) -> float: sum0 = sum(v for k, v in self.items()) sum1 = sum(k * v for k, v in self.items()) return sum1 / sum0 @property def stdev(self) -> float: sum0 = sum(v for k, v in self.items()) sum1 = sum(k * v for k, v in self.items()) sum2 = sum(k * k * v for k, v in self.items()) return math.sqrt(sum0 * sum2 - sum1 * sum1) / sum0 @property def median(self) -> Any: all = list(sorted(self.elements())) return all[len(all) // 2] @property def median2(self) -> Optional[float]: mid = sum(self.values()) // 2 low = 0 for k, v in sorted(self.items()): if low <= mid < low + v: return k low += v return None test_stats_counter = """ >>> sc = StatsCounter([2, 4, 4, 4, 5, 5, 7, 9]) >>> print(sc.mean, sc.stdev, sc.most_common(), sc.median, sc.median2) 5.0 2.0 [(4, 3), (5, 2), (2, 1), (7, 1), (9, 1)] 5 5 """ __test__ = { name: value for name, value in locals().items() if name.startswith("test_") } if __name__ == "__main__": import doctest doctest.testmod(verbose=False) # performance() ================================================ FILE: Chapter_7/ch07_ex3.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 7. Example 3. """ from typing import List, cast, Any, Optional, Iterable, overload, Union, Iterator # Extending Classes # ############################## # Basic Stats formulae import math # New Sequence from Scratch. # ====================================== # A Binary Search Tree. # # http://en.wikipedia.org/wiki/Binary_search_tree # import collections.abc import weakref from abc import ABCMeta, abstractmethod from typing import TypeVar, Any class Comparable(metaclass=ABCMeta): @abstractmethod def __lt__(self, other: Any) -> bool: ... def __ge__(self, other: Any) -> bool: ... # In case we need a type variable that maps to Comparable NodeItem = TypeVar("NodeItem", bound=Comparable) class TreeNode: """ Ideally, there's weakref to the tree; tree has the key() function. """ def __init__( self, item: Optional[Comparable], less: Optional["TreeNode"] = None, more: Optional["TreeNode"] = None, parent: Optional["TreeNode"] = None, ) -> None: self.item = item self.less = less self.more = more if parent: # Can't create a weakref to a None value. Only set if there's a value self.parent = parent @property def parent(self) -> Optional["TreeNode"]: return self.parent_ref() @parent.setter def parent(self, value: "TreeNode") -> None: self.parent_ref = weakref.ref(value) def __repr__(self) -> str: return f"TreeNode({self.item!r}, {self.less!r}, {self.more!r})" def find(self, item: Comparable) -> "TreeNode": if self.item is None: # Root if self.more: return self.more.find(item) elif self.item == item: return self elif self.item > item and self.less: return self.less.find(item) elif self.item < item and self.more: return self.more.find(item) raise KeyError def __iter__(self) -> Iterator[Comparable]: if self.less: yield from self.less if self.item: yield self.item if self.more: yield from self.more def add(self, item: Comparable) -> None: if self.item is None: # Root Special Case if self.more: self.more.add(item) else: self.more = TreeNode(item, parent=self) elif self.item >= item: if self.less: self.less.add(item) else: self.less = TreeNode(item, parent=self) elif self.item < item: if self.more: self.more.add(item) else: self.more = TreeNode(item, parent=self) def remove(self, item: Comparable) -> None: # Recursive search for node if self.item is None or item > self.item: if self.more: self.more.remove(item) else: raise KeyError elif item < self.item: if self.less: self.less.remove(item) else: raise KeyError else: # self.item == item if self.less and self.more: # Two children are present successor = self.more._least() self.item = successor.item if successor.item: successor.remove(successor.item) elif self.less: # One child on less self._replace(self.less) elif self.more: # One child on more self._replace(self.more) else: # Zero children self._replace(None) def _least(self) -> "TreeNode": if self.less is None: return self return self.less._least() def _replace(self, new: Optional["TreeNode"] = None) -> None: if self.parent: if self == self.parent.less: self.parent.less = new else: self.parent.more = new if new is not None: new.parent = self.parent class Tree(collections.abc.MutableSet): def __init__(self, source: Iterable[Comparable] = None) -> None: self.root = TreeNode(None) self.size = 0 if source: for item in source: self.root.add(item) self.size += 1 def add(self, item: Comparable) -> None: self.root.add(item) self.size += 1 def discard(self, item: Comparable) -> None: if self.root.more: try: self.root.more.remove(item) self.size -= 1 except KeyError: pass else: pass def __contains__(self, item: Any) -> bool: if self.root.more: self.root.more.find(cast(Comparable, item)) return True else: return False def __iter__(self) -> Iterator[Comparable]: if self.root.more: for item in iter(self.root.more): yield item # Otherwise, the tree is empty. def __len__(self) -> int: return self.size test_tree = """ >>> bt = Tree() >>> bt.add("Number 1") >>> print(list(iter(bt))) ['Number 1'] >>> bt.add("Number 3") >>> print(list(iter(bt))) ['Number 1', 'Number 3'] >>> bt.add("Number 2") >>> print(list(iter(bt))) ['Number 1', 'Number 2', 'Number 3'] >>> print(repr(bt.root)) TreeNode(None, None, TreeNode('Number 1', None, TreeNode('Number 3', TreeNode('Number 2', None, None), None))) >>> print("Number 2" in bt) True >>> print(len(bt)) 3 >>> bt.remove("Number 3") >>> print(list(iter(bt))) ['Number 1', 'Number 2'] >>> bt.discard("Number 3") # Should be silent >>> bt.remove("Number 3") # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): File "/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py", line 1329, in __run compileflags, 1), test.globs) File "<doctest __main__.__test__.test_tree[13]>", line 1, in <module> bt.remove("Number 3") # doctest: +IGNORE_EXCEPTION_DETAIL File "/Users/slott/miniconda3/envs/py37/lib/python3.7/_collections_abc.py", line 583, in remove raise KeyError(value) KeyError: 'Number 3' >>> bt.add("Number 1") >>> print(list(iter(bt))) ['Number 1', 'Number 1', 'Number 2'] """ test_tree_256_randomized_insert_delete = """ >>> import random >>> for i in range(256): ... values = [random.random() for _ in range(i)] ... random.shuffle(values) ... bt = Tree() ... for i in values: ... bt.add(i) ... assert list(bt) == list(sorted(values)), f"IN: {values}, OUT: {list(bt)}" ... random.shuffle(values) ... for i in values: ... bt.remove(i) ... values.remove(i) ... assert list(bt) == list(sorted(values)), f"IN: {values}, OUT: {list(bt)}" """ test_tree_merge = """ >>> s1 = Tree(["Item 1", "Another", "Middle"]) >>> s2 = Tree(["Another", "More", "Yet More"]) >>> print(list(s1)) ['Another', 'Item 1', 'Middle'] >>> print(list(s2)) ['Another', 'More', 'Yet More'] >>> print(list(iter(s1 | s2))) ['Another', 'Another', 'Item 1', 'Middle', 'More', 'Yet More'] >>> union = s1 | s2 >>> list(union) ['Another', 'Another', 'Item 1', 'Middle', 'More', 'Yet More'] >>> len(union) 6 >>> union.remove('Another') >>> list(union) ['Another', 'Item 1', 'Middle', 'More', 'Yet More'] """ __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) # performance() ================================================ FILE: Chapter_7/ch07_ex4.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 7. Example 4. """ # Comparisons # ====================================== # Using a list vs. a set import timeit def performance() -> None: list_time = timeit.timeit("l.remove(10); l.append(10)", "l = list(range(20))") set_time = timeit.timeit("l.remove(10); l.add(10)", "l = set(range(20))") print(f"append; remove: list {list_time:.3f}, set {set_time:.3f}") # Using two parallel lists vs. a mapping list_2_time = timeit.timeit( "i= k.index(10); v[i]= 0", "k=list(range(20)); v=list(range(20))" ) dict_time = timeit.timeit("m[10]= 0", "m=dict(zip(list(range(20)),list(range(20))))") print(f"setitem: two lists {list_2_time:.3f}, one dict {dict_time:.3f}") __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) performance() ================================================ FILE: Chapter_8/__init__.py ================================================ ================================================ FILE: Chapter_8/ch08_ex1.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 8. Example 1. """ # noisyfloat # ================================ import sys def trace(frame, event, arg): if frame.f_code.co_name.startswith("__"): print(frame.f_code.co_name, frame.f_code.co_filename, event) # sys.settrace(trace) class NoisyFloat(float): def __add__(self, other: float) -> 'NoisyFloat': print(self, "+", other) return NoisyFloat(super().__add__(other)) def __radd__(self, other: float) -> 'NoisyFloat': print(self, "r+", other) return NoisyFloat(super().__radd__(other)) test_noisy_float = """ >>> x = NoisyFloat(2) >>> y = NoisyFloat(3) >>> x + y + 2.5 2.0 + 3.0 5.0 + 2.5 7.5 """ # Fixed Point # ================================= import numbers import math from typing import Union, Optional, Any class FixedPoint(numbers.Rational): __slots__ = ("value", "scale", "default_format") def __init__(self, value: Union['FixedPoint', int, float], scale: int = 100) -> None: self.value: int self.scale: int if isinstance(value, FixedPoint): self.value = value.value self.scale = value.scale elif isinstance(value, int): self.value = value self.scale = scale elif isinstance(value, float): self.value = int(scale * value + .5) # Round half up self.scale = scale else: raise TypeError(f"Can't build FixedPoint from {value!r} of {type(value)}") digits = int(math.log10(scale)) self.default_format = "{{0:.{digits}f}}".format(digits=digits) def __str__(self) -> str: return self.__format__(self.default_format) def __repr__(self) -> str: return f"{self.__class__.__name__:s}({self.value:d},scale={self.scale:d})" def __format__(self, specification: str) -> str: if specification == "": specification = self.default_format return specification.format(self.value / self.scale) # no rounding def numerator(self) -> int: return self.value def denominator(self) -> int: return self.scale def __add__(self, other: Union['FixedPoint', int]) -> 'FixedPoint': if not isinstance(other, FixedPoint): new_scale = self.scale new_value = self.value + other * self.scale else: new_scale = max(self.scale, other.scale) new_value = self.value * (new_scale // self.scale) + other.value * ( new_scale // other.scale ) return FixedPoint(int(new_value), scale=new_scale) def __sub__(self, other: Union['FixedPoint', int]) -> 'FixedPoint': if not isinstance(other, FixedPoint): new_scale = self.scale new_value = self.value - other * self.scale else: new_scale = max(self.scale, other.scale) new_value = self.value * (new_scale // self.scale) - other.value * ( new_scale // other.scale ) return FixedPoint(int(new_value), scale=new_scale) def __mul__(self, other: Union['FixedPoint', int]) -> 'FixedPoint': if not isinstance(other, FixedPoint): new_scale = self.scale new_value = self.value * other else: new_scale = self.scale * other.scale new_value = self.value * other.value return FixedPoint(int(new_value), scale=new_scale) def __truediv__(self, other: Union['FixedPoint', int]) -> 'FixedPoint': if not isinstance(other, FixedPoint): new_value = int(self.value / other) else: new_value = int(self.value / (other.value / other.scale)) return FixedPoint(new_value, scale=self.scale) def __floordiv__(self, other: Union['FixedPoint', int]) -> 'FixedPoint': if not isinstance(other, FixedPoint): new_value = int(self.value // other) else: new_value = int(self.value // (other.value / other.scale)) return FixedPoint(new_value, scale=self.scale) def __mod__(self, other: Union['FixedPoint', int]) -> 'FixedPoint': if not isinstance(other, FixedPoint): new_value = (self.value / self.scale) % other else: new_value = self.value % (other.value / other.scale) return FixedPoint(new_value, scale=self.scale) def __pow__(self, other: Union['FixedPoint', int]) -> 'FixedPoint': if not isinstance(other, FixedPoint): new_value = (self.value / self.scale) ** other else: new_value = (self.value / self.scale) ** (other.value / other.scale) return FixedPoint(int(new_value) * self.scale, scale=self.scale) def __abs__(self) -> 'FixedPoint': return FixedPoint(abs(self.value), self.scale) def __float__(self) -> float: return self.value / self.scale def __int__(self) -> int: return int(self.value / self.scale) def __trunc__(self) -> int: return int(math.trunc(self.value / self.scale)) def __ceil__(self) -> int: return int(math.ceil(self.value / self.scale)) def __floor__(self) -> int: return int(math.floor(self.value / self.scale)) # reveal_type(numbers.Rational.__round__) def __round__(self, ndigits: Optional[int] = 0) -> Any: return FixedPoint(round(self.value / self.scale, ndigits=ndigits), self.scale) def __neg__(self) -> 'FixedPoint': return FixedPoint(-self.value, self.scale) def __pos__(self) -> 'FixedPoint': return self # Note equality among floats isn't a good idea. # Also, should FixedPoint(123, 100) equal FixedPoint(1230, 1000)? def __eq__(self, other: Any) -> bool: if isinstance(other, FixedPoint): if self.scale == other.scale: return self.value == other.value else: return self.value * other.scale // self.scale == other.value else: return abs(self.value / self.scale - float(other)) < .5 / self.scale def __ne__(self, other: Any) -> bool: return not (self == other) def __le__(self, other: 'FixedPoint') -> bool: return self.value / self.scale <= float(other) def __lt__(self, other: 'FixedPoint') -> bool: return self.value / self.scale < float(other) def __ge__(self, other: 'FixedPoint') -> bool: return self.value / self.scale >= float(other) def __gt__(self, other: 'FixedPoint') -> bool: return self.value / self.scale > float(other) def __hash__(self) -> int: P = sys.hash_info.modulus m, n = self.value, self.scale # Remove common factors of P. (Unnecessary if m and n already coprime.) while m % P == n % P == 0: m, n = m // P, n // P if n % P == 0: hash_ = sys.hash_info.inf else: # Fermat's Little Theorem: pow(n, P-1, P) is 1, so # pow(n, P-2, P) gives the inverse of n modulo P. hash_ = (abs(m) % P) * pow(n, P - 2, P) % P if m < 0: hash_ = -hash_ if hash_ == -1: hash_ = -2 return hash_ def __radd__(self, other: Union['FixedPoint', int]) -> 'FixedPoint': if not isinstance(other, FixedPoint): new_scale = self.scale new_value = other * self.scale + self.value else: new_scale = max(self.scale, other.scale) new_value = other.value * (new_scale // other.scale) + self.value * ( new_scale // self.scale ) return FixedPoint(int(new_value), scale=new_scale) def __rsub__(self, other: Union['FixedPoint', int]) -> 'FixedPoint': if not isinstance(other, FixedPoint): new_scale = self.scale new_value = other * self.scale - self.value else: new_scale = max(self.scale, other.scale) new_value = other.value * (new_scale // other.scale) - self.value * ( new_scale // self.scale ) return FixedPoint(int(new_value), scale=new_scale) def __rmul__(self, other: Union['FixedPoint', int]) -> 'FixedPoint': if not isinstance(other, FixedPoint): new_scale = self.scale new_value = other * self.value else: new_scale = self.scale * other.scale new_value = other.value * self.value return FixedPoint(int(new_value), scale=new_scale) def __rtruediv__(self, other: Union['FixedPoint', int]) -> 'FixedPoint': if not isinstance(other, FixedPoint): new_value = self.scale * int(other / (self.value / self.scale)) else: new_value = int((other.value / other.scale) / self.value) return FixedPoint(new_value, scale=self.scale) def __rfloordiv__(self, other: Union['FixedPoint', int]) -> 'FixedPoint': if not isinstance(other, FixedPoint): new_value = self.scale * int(other // (self.value / self.scale)) else: new_value = int((other.value / other.scale) // self.value) return FixedPoint(new_value, scale=self.scale) def __rmod__(self, other: Union['FixedPoint', int]) -> 'FixedPoint': if not isinstance(other, FixedPoint): new_value = other % (self.value / self.scale) else: new_value = (other.value / other.scale) % (self.value / self.scale) return FixedPoint(new_value, scale=self.scale) def __rpow__(self, other: Union['FixedPoint', int]) -> 'FixedPoint': if not isinstance(other, FixedPoint): new_value = other ** (self.value / self.scale) else: new_value = (other.value / other.scale) ** self.value / self.scale return FixedPoint(int(new_value) * self.scale, scale=self.scale) def round_to(self, new_scale: int) -> 'FixedPoint': f = new_scale / self.scale return FixedPoint(int(self.value * f + .5), scale=new_scale) # test cases to show that ``FixedPoint`` numbers work properly. test_fp = """ >>> f1 = FixedPoint(12.34, 100) >>> f2 = FixedPoint(1234, 100) >>> print(f1, repr(f1)) 12.34 FixedPoint(1234,scale=100) >>> print(f2, repr(f2)) 12.34 FixedPoint(1234,scale=100) >>> print(f1 * f2, f1 + f2, f1 - f2, f1 / f2) 152.2756 24.68 0.00 1.00 >>> print(f1 + 101, f1 * 2, f1 - 101, f1 / 2, f1 % 1, f1 // 2) 113.34 24.68 -88.66 6.17 0.34 6.17 >>> print(101 + f2, 2 * f2, 101 - f1, 25 / f1, 1334 % f1, 25 // f1) 113.34 24.68 88.66 2.00 1.28 2.00 >>> print("round", round(f1)) round 12.00 >>> print("ceil", math.ceil(f1)) ceil 13 >>> print("floor", math.floor(f1)) floor 12 >>> print("trunc", math.trunc(f1)) trunc 12 >>> print("==", f1 == f2, f1 == 12.34, f1 == 1234 / 100, f1 == FixedPoint(12340, 1000)) == True True True True >>> print(hash(f1), hash(f2), hash(FixedPoint(12340, 1000))) 1521856386081038020 1521856386081038020 1521856386081038020 >>> f3 = FixedPoint(200, 100) >>> print(f3 * f3 * f3, f3 ** 3, 3 ** f3) 8.000000 8.00 9.00 >>> price = FixedPoint(1299, 100) >>> tax_rate = FixedPoint(725, 1000) >>> tax = price * tax_rate >>> print(tax, tax.round_to(100)) 9.41775 9.42 """ __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_9/__init__.py ================================================ ================================================ FILE: Chapter_9/ch09_ex1.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 9. Example 1. """ # Decorator Example 1 # ================================ # Use of builtin decorators from typing import Any, cast, Optional, Type from types import TracebackType import math import random from Chapter_6.ch06_ex2 import KnownSequence class Angle(float): __slots__ = ("_degrees",) @staticmethod def from_radians(value: float) -> "Angle": return Angle(180 * value / math.pi) def __init__(self, degrees: float) -> None: self._degrees = degrees @property def radians(self) -> float: return math.pi * self._degrees / 180 @property def degrees(self) -> float: return self._degrees test_angle = """ >>> a = Angle(22.5) >>> round(a.radians/math.pi, 3) 0.125 >>> b = Angle.from_radians(.227) >>> round(b.degrees, 1) 13.0 >>> b.radians 0.227 """ # Decorator Example 2 # ================================ # Use of library decorators. # Some preliminary definitions from enum import Enum class Suit(Enum): Clubs = "♣" Diamonds = "♦" Hearts = "♥" Spades = "♠" # Using functools.total_ordering # Not a good idea. Use dataclasses instead. import functools @functools.total_ordering class CardTO: __slots__ = ("rank", "suit") def __init__(self, rank: int, suit: Suit) -> None: self.rank = rank self.suit = suit def __eq__(self, other: Any) -> bool: return self.rank == cast(CardTO, other).rank def __lt__(self, other: Any) -> bool: return self.rank < cast(CardTO, other).rank def __str__(self) -> str: return f"{self.rank:d}{self.suit:s}" test_total_ordering = """ >>> c1 = CardTO(3, Suit.Clubs) >>> c2 = CardTO(3, Suit.Hearts) >>> c1 == c2 True >>> c1 < c2 False >>> c1 <= c2 True >>> c1 >= c2 True >>> c1 > c2 False >>> c1 != c2 False """ from dataclasses import dataclass @dataclass(frozen=True) class CardDC: rank: int suit: Suit def __eq__(self, other: Any) -> bool: return self.rank == cast(CardTO, other).rank def __lt__(self, other: Any) -> bool: return self.rank < cast(CardTO, other).rank def __le__(self, other: Any) -> bool: return self.rank <= cast(CardTO, other).rank def __str__(self) -> str: return f"{self.rank:d}{self.suit:s}" test_dc_ordering = """ >>> c1 = CardDC(3, Suit.Clubs) >>> c2 = CardDC(3, Suit.Hearts) >>> c1 == c2 True >>> c1 < c2 False >>> c1 <= c2 True >>> c1 >= c2 True >>> c1 > c2 False >>> c1 != c2 False """ # For later examples class Deck(list): def __init__(self, size: int = 1) -> None: for d in range(size): cards = [CardDC(r, s) for r in range(1, 14) for s in Suit] super().extend(cards) random.shuffle(self) # Mixin example 1 # ======================= # Mixin using enums from typing import Type, List from enum import Enum class EnumDomain: @classmethod def domain(cls: Type) -> List[str]: return [m.value for m in cls] class SuitD(str, EnumDomain, Enum): Clubs = "♣" Diamonds = "♦" Hearts = "♥" Spades = "♠" test_enum = """ >>> SuitD.domain() ['♣', '♦', '♥', '♠'] >>> SuitD.Clubs.center(5) ' ♣ ' >>> Suit.Clubs.center(5) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): File "/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py", line 1329, in __run compileflags, 1), test.globs) File "<doctest __main__.__test__.test_enum[2]>", line 1, in <module> Suit.Clubs.center(5) AttributeError: 'Suit' object has no attribute 'center' >>> Suit.Clubs.value.center(5) ' ♣ ' """ # Decorator Example 1 # ============================== # Simple function decorator import logging, sys import functools from typing import Callable, TypeVar, List FuncType = Callable[..., Any] F = TypeVar("F", bound=FuncType) def debug(function: F) -> F: @functools.wraps(function) def logged_function(*args, **kw): logging.debug("%s(%r, %r)", function.__name__, args, kw) result = function(*args, **kw) logging.debug("%s = %r", function.__name__, result) return result return cast(F, logged_function) @debug def ackermann(m: int, n: int) -> int: if m == 0: return n + 1 elif m > 0 and n == 0: return ackermann(m - 1, 1) elif m > 0 and n > 0: return ackermann(m - 1, ackermann(m, n - 1)) else: raise Exception(f"Design Error: {vars()}") test_debug_1 = """ >>> logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) >>> ackermann(2, 4) 11 >>> logging.shutdown() """ # Decorator Example 2 # ============================== def debug2(function: F) -> F: log = logging.getLogger(function.__name__) @functools.wraps(function) def logged_function(*args, **kw): log.debug("call(%r, %r)", args, kw) result = function(*args, **kw) log.debug("result = %r", result) return result return cast(F, logged_function) @debug2 def ackermann2(m: int, n: int) -> int: if m == 0: return n + 1 elif m > 0 and n == 0: return ackermann2(m - 1, 1) elif m > 0 and n > 0: return ackermann2(m - 1, ackermann2(m, n - 1)) else: raise Exception(f"Design Error: {vars()}") @debug2 def simpler(x: int, y: int) -> int: return 2 * x + y test_debug_2 = """ >>> logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) >>> ackermann2(2, 4) 11 >>> simpler(20, 2) 42 >>> logging.shutdown() """ # Decorator Example 3 # ============================== # Parameterized decorator def decorator(config) -> Callable[[F], F]: def concrete_decorator(function: F) -> F: def wrapped(*args, **kw): return function(*args, **kw) return cast(F, wrapped) return concrete_decorator def debug_named(log_name: str) -> Callable[[F], F]: log = logging.getLogger(log_name) def concrete_decorator(function: F) -> F: @functools.wraps(function) def wrapped(*args, **kw): log.debug("%s(%r, %r)", function.__name__, args, kw) result = function(*args, **kw) log.debug("%s = %r", function.__name__, result) return result return cast(F, wrapped) return concrete_decorator @debug_named("recursion") def ackermann3(m: int, n: int) -> int: if m == 0: return n + 1 elif m > 0 and n == 0: return ackermann3(m - 1, 1) elif m > 0 and n > 0: return ackermann3(m - 1, ackermann3(m, n - 1)) else: raise Exception(f"Design Error: {vars()}") test_debug_3 = """ >>> logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) >>> ackermann3(2, 4) 11 >>> logging.shutdown() """ # Class Decorator 1 # ============================== # Unit and Standard Unit def standard(class_: Type) -> Type: class_.standard = class_ return class_ def nonstandard(based_on: Type) -> Callable[[Type], Type]: def concrete_decorator(class_: Type) -> Type: class_.standard = based_on return class_ return concrete_decorator class Unit: factor = 1.0 @classmethod def value(class_, value: float) -> float: if value is None: return None return value / class_.factor @classmethod def convert(class_, value: float) -> float: if value is None: return None return value * class_.factor @standard class INCH(Unit): """inch""" name = "in" @nonstandard(INCH) class FOOT(Unit): """foot""" name = "ft" factor = 1 / 12 test_class_decorator = """ >>> length = INCH.value(18) >>> print(FOOT.convert(length), FOOT.name, "=", INCH.convert(length), INCH.name) 1.5 ft = 18.0 in """ # Method Decorator # ============================= def audit(method: F) -> F: @functools.wraps(method) def wrapper(self, *args, **kw): template = "%s\n before %s\n after %s" audit_log = logging.getLogger("audit") before = repr(self) # a kind of deep copy to preserve state try: result = method(self, *args, **kw) except Exception as e: after = repr(self) audit_log.exception(template, method.__qualname__, before, after) raise after = repr(self) audit_log.info(template, method.__qualname__, before, after) return result return cast(F, wrapper) class Hand: def __init__(self, *cards: CardDC) -> None: self._cards = list(cards) @audit def __iadd__(self, card: CardDC) -> "Hand": self._cards.append(card) self._cards.sort(key=lambda c: c.rank) return self def __repr__(self) -> str: cards = ", ".join(map(str, self._cards)) return f"{self.__class__.__name__}({cards})" test_audit = """ >>> logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) >>> with KnownSequence(): ... d = Deck() ... h = Hand(d.pop(), d.pop()) ... h += d.pop() ... print(h) Hand(7Suit.Clubs, 7Suit.Hearts, 13Suit.Clubs) >>> with KnownSequence(): ... d = Deck() ... h = Hand(d.pop(), d.pop()) ... h += "Not A Card!" # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): File "/Users/slott/Documents/Writing/Python/Mastering OO Python 2e/mastering-oo-python-2e/Chapter_9/ch09_ex1.py", line 390, in wrapper result = method(self, *args, **kw) File "/Users/slott/Documents/Writing/Python/Mastering OO Python 2e/mastering-oo-python-2e/Chapter_9/ch09_ex1.py", line 390, in wrapper File "/Users/slott/Documents/Writing/Python/Mastering OO Python 2e/mastering-oo-python-2e/Chapter_9/ch09_ex1.py", line 410, in __iadd__ result = method(self, *args, **kw) self._cards.sort(key=lambda c: c.rank) File "/Users/slott/Documents/Writing/Python/Mastering OO Python 2e/mastering-oo-python-2e/Chapter_9/ch09_ex1.py", line 410, in __iadd__ File "/Users/slott/Documents/Writing/Python/Mastering OO Python 2e/mastering-oo-python-2e/Chapter_9/ch09_ex1.py", line 410, in <lambda> self._cards.sort(key=lambda c: c.rank) self._cards.sort(key=lambda c: c.rank) AttributeError: 'str' object has no attribute 'rank' >>> logging.shutdown() """ # More Complex Decoration # ================================== # This decorator's effect is effectively hidden from mypy. # The new method is injected dynamically. def memento(class_: Type) -> Type: def memento_method(self): return ( f"{self.__class__.__qualname__}" f"(**{vars(self)!r})" ) class_.memento = memento_method return class_ @memento class StatefulClass: def __init__(self, value: Any) -> None: self.value = value def __repr__(self) -> str: return f"{self.value}" test_memento_1 = """ >>> st = StatefulClass(2.7) >>> print(st.memento()) StatefulClass(**{'value': 2.7}) """ class Memento: def memento(self) -> str: return f"{self.__class__.__qualname__}(**{vars(self)!r})" class StatefulClass2(Memento): def __init__(self, value): self.value = value def __repr__(self): return f"{self.value}" test_memento_2 = """ >>> st2 = StatefulClass2(2.7) >>> print(st2.memento()) StatefulClass2(**{'value': 2.7}) """ __test__ = { name: value for name, value in locals().items() if name.startswith("test_") } if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: Chapter_9/ch09_ex2.py ================================================ #!/usr/bin/env python3.7 """ Mastering Object-Oriented Python 2e Code Examples for Mastering Object-Oriented Python 2nd Edition Chapter 9. Example 2. """ from typing import Any, Type # Class Decorator 2 -- Logger # ============================== import logging import sys # Wordy - but visible to mypy class UglyClass1: def __init__(self) -> None: self.logger = logging.getLogger(self.__class__.__qualname__) self.logger.info("New thing") def method(self, *args: Any) -> int: self.logger.info("method %r", args) return 42 # Non-DRY -- class name is repeated class UglyClass2: logger = logging.getLogger("UglyClass2") def __init__(self) -> None: self.logger.info("New thing") def method(self, *args: Any) -> int: self.logger.info("method %r", args) return 42 # Less Ugly, more DRY # However... mypy can't see this attribute -- not a solution # Chapter_9/ch09_ex2.py:54: error: "SomeClass" has no attribute "logger" # Chapter_9/ch09_ex2.py:57: error: "SomeClass" has no attribute "logger" def logged(class_: Type) -> Type: class_.logger = logging.getLogger(class_.__qualname__) return class_ @logged class SomeClass: def __init__(self) -> None: self.logger.info("New thing") # mypy error def method(self, *args: Any) -> int: self.logger.info("method %r", args) # mypy error return 42 # More DRY. And visible to mypy. class LoggedInstance: logger: logging.Logger def __new__(cls): instance = super().__new__(cls) instance.logger = logging.getLogger(cls.__qualname__) return instance class SomeClass2(LoggedInstance): def __init__(self) -> None: self.logger.info("New thing") def method(self, *args: Any) -> int: self.logger.info("method %r", args) return 42 # And a class-level logger, just to be complete. class LoggedClassMeta(type): def __new__(cls, name, bases, namespace, **kwds): result = type.__new__(cls, name, bases, dict(namespace)) result.logger = logging.getLogger(result.__qualname__) return result class LoggedClass(metaclass=LoggedClassMeta): logger: logging.Logger pass class SomeClass3(LoggedClass): def __init__(self) -> None: self.logger.info("New thing") def method(self, *args: Any) -> int: self.logger.info("method %r", args) return 42 class LoggedWithHook: def __init_subclass__(cls, name=None): cls.logger = logging.getLogger(name or cls.__qualname__) class SomeClass4(LoggedWithHook): def __init__(self) -> None: self.logger.info("New thing") def method(self, *args: Any) -> int: self.logger.info("method %r", args) return 42 class SomeClass4s(LoggedWithHook, name='special'): def __init__(self) -> None: self.logger.info("New thing") def method(self, *args: Any) -> int: self.logger.info("method %r", args) return 42 test_logged_class = """ >>> logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) >>> uc1 = UglyClass1() >>> uc1.method(355 / 113) 42 >>> uc2 = UglyClass2() >>> uc2.method(355 / 113) 42 >>> sc = SomeClass() >>> sc.method(355 / 113) 42 >>> sc2 = SomeClass2() >>> sc2.method(355 / 113) 42 >>> sc3 = SomeClass3() >>> sc3.method(355 / 113) 42 >>> sc4 = SomeClass4() >>> sc4.method(365 / 113) 42 >>> sc4s = SomeClass4s() >>> sc4s.method(365 / 113) 42 >>> logging.shutdown() """ __test__ = {name: value for name, value in locals().items() if name.startswith("test_")} if __name__ == "__main__": import doctest doctest.testmod(verbose=False) ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019 Packt Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Mastering Object-Oriented Python - Second Edition <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> This 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. **Build powerful applications with reusable code using OOP design patterns and Python 3.7** ## What is this book about? Object-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. This book covers the following exciting features: * Explore a variety of different design patterns for the __init__() method * Learn to use Flask to build a RESTful web service * Discover SOLID design patterns and principles * Use the features of Python 3's abstract base * Create classes for your own applications * Design testable code using pytest and fixtures * Understand how to design context managers that leverage the 'with' statement * Create a new type of collection using standard library and design techniques * Develop new number types above and beyond the built-in classes of numbers If you feel this book is for you, get your [copy](https://www.amazon.com/dp/1789531365) today! <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" alt="https://www.packtpub.com/" border="5" /></a> ## Instructions and Navigations All of the code is organized into folders. For example, Chapter02. The code will look like the following: ``` def F(n: int) -> int: if n in (0, 1): return 1 else: return F(n-1) + F(n-2) ``` **Following is what you need for this book:** This 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. With the following software and hardware list you can run all code files present in the book (Chapter 1-20). ### Software and Hardware List | Chapter | Software required | OS required | | -------- | ------------------------------------ | ----------------------------------- | | 1-20 | Python 3.7 | Any | ### Related products * 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) * 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) ## Get to Know the Author **Steven F. Lott** has 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. ## Other books by the author [Modern Python Cookbook ](https://www.packtpub.com/application-development/modern-python-cookbook?utm_source=github&utm_medium=repository&utm_campaign=9781786469250) [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) ### Suggestions and Feedback [Click here](https://docs.google.com/forms/d/e/1FAIpQLSdy7dATC6QmEL81FIUuymZ0Wy9vH1jHkvpY57OiMeKGqib_Ow/viewform) if you have any feedback or suggestions. ### Download a free PDF <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> <p align="center"> <a href="https://packt.link/free-ebook/9781789531367">https://packt.link/free-ebook/9781789531367 </a> </p> ================================================ FILE: data/ch17_data.csv ================================================ rate_in,time_in,distance_in,rate_out,time_out,distance_out 2,3,,2,3,6 5,,7,5,1.4,7 ,11,13,1.18,11,13 ================================================ FILE: data/ch17_sample.csv ================================================ not_player,bet,rounds,final data,1,1,1 ================================================ FILE: environment.yaml ================================================ name: mastering channels: - defaults dependencies: - alabaster=0.7.12=py37_0 - asn1crypto=0.24.0=py37_0 - astroid=2.0.4=py37_0 - atomicwrites=1.2.1=py37_0 - attrs=18.2.0=py37h28b3542_0 - babel=2.6.0=py37_0 - ca-certificates=2019.1.23=0 - certifi=2019.3.9=py37_0 - cffi=1.11.5=py37h6174b99_1 - chardet=3.0.4=py37_1 - cryptography=2.5=py37ha12b0ac_0 - docutils=0.14=py37_0 - flask=1.0.2=py37_1 - idna=2.7=py37_0 - imagesize=1.1.0=py37_0 - isort=4.3.4=py37_0 - itsdangerous=1.1.0=py37_0 - jinja2=2.10=py37_0 - lazy-object-proxy=1.3.1=py37h1de35cc_2 - libcxx=4.0.1=h579ed51_0 - libcxxabi=4.0.1=hebd6815_0 - libedit=3.1.20170329=hb402a30_2 - libffi=3.2.1=h475c297_4 - markupsafe=1.0=py37h1de35cc_1 - mccabe=0.6.1=py37_1 - more-itertools=4.3.0=py37_0 - mypy_extensions=0.4.1=py37_0 - ncurses=6.1=h0a44026_0 - openssl=1.1.1b=h1de35cc_1 - packaging=18.0=py37_0 - pluggy=0.7.1=py37h28b3542_0 - psutil=5.4.7=py37h1de35cc_0 - py=1.7.0=py37_0 - pycparser=2.19=py37_0 - pygments=2.2.0=py37_0 - pylint=2.1.1=py37_0 - pyopenssl=18.0.0=py37_0 - pyparsing=2.2.2=py37_0 - pysocks=1.6.8=py37_0 - pytest=3.8.2=py37_0 - python=3.7.2=haf84260_0 - pytz=2018.5=py37_0 - pyyaml=5.1=py37h1de35cc_0 - readline=7.0=h1de35cc_5 - requests=2.21.0=py37_0 - setuptools=40.8.0=py37_0 - six=1.11.0=py37_1 - snowballstemmer=1.2.1=py37_0 - sphinx=1.8.5=py37_0 - sphinxcontrib=1.0=py37_1 - sphinxcontrib-websupport=1.1.0=py37_1 - sqlalchemy=1.2.12=py37h1de35cc_0 - sqlite=3.26.0=ha441bb4_0 - tk=8.6.8=ha441bb4_0 - urllib3=1.23=py37_0 - werkzeug=0.14.1=py37_0 - wheel=0.33.1=py37_0 - wrapt=1.10.11=py37h1de35cc_2 - xz=5.2.4=h1de35cc_4 - yaml=0.1.7=hc338f04_2 - zlib=1.2.11=hf3cbc9b_2 - pip: - appdirs==1.4.3 - black==18.9b0 - click==7.0 - mypy==0.670 - pip==19.0.2 - toml==0.10.0 - typed-ast==1.3.1 prefix: /Users/slott/miniconda3/envs/mastering ================================================ FILE: requirements.txt ================================================ # This file may be used to create an environment using: # $ conda create --name <env> --file <this file> # platform: osx-64 alabaster=0.7.12=py37_0 appdirs=1.4.3=pypi_0 asn1crypto=0.24.0=py37_0 astroid=2.0.4=py37_0 atomicwrites=1.2.1=py37_0 attrs=18.2.0=py37h28b3542_0 babel=2.6.0=py37_0 black=18.9b0=pypi_0 ca-certificates=2019.1.23=0 certifi=2019.3.9=py37_0 cffi=1.11.5=py37h6174b99_1 chardet=3.0.4=py37_1 click=7.0=pypi_0 cryptography=2.5=py37ha12b0ac_0 docutils=0.14=py37_0 flask=1.0.2=py37_1 idna=2.7=py37_0 imagesize=1.1.0=py37_0 isort=4.3.4=py37_0 itsdangerous=1.1.0=py37_0 jinja2=2.10=py37_0 lazy-object-proxy=1.3.1=py37h1de35cc_2 libcxx=4.0.1=h579ed51_0 libcxxabi=4.0.1=hebd6815_0 libedit=3.1.20170329=hb402a30_2 libffi=3.2.1=h475c297_4 markupsafe=1.0=py37h1de35cc_1 mccabe=0.6.1=py37_1 more-itertools=4.3.0=py37_0 mypy=0.670=pypi_0 mypy_extensions=0.4.1=py37_0 ncurses=6.1=h0a44026_0 openssl=1.1.1b=h1de35cc_1 packaging=18.0=py37_0 pip=19.0.2=pypi_0 pluggy=0.7.1=py37h28b3542_0 psutil=5.4.7=py37h1de35cc_0 py=1.7.0=py37_0 pycparser=2.19=py37_0 pygments=2.2.0=py37_0 pylint=2.1.1=py37_0 pyopenssl=18.0.0=py37_0 pyparsing=2.2.2=py37_0 pysocks=1.6.8=py37_0 pytest=3.8.2=py37_0 python=3.7.2=haf84260_0 pytz=2018.5=py37_0 pyyaml=5.1=py37h1de35cc_0 readline=7.0=h1de35cc_5 requests=2.21.0=py37_0 setuptools=40.8.0=py37_0 six=1.11.0=py37_1 snowballstemmer=1.2.1=py37_0 sphinx=1.8.5=py37_0 sphinxcontrib=1.0=py37_1 sphinxcontrib-websupport=1.1.0=py37_1 sqlalchemy=1.2.12=py37h1de35cc_0 sqlite=3.26.0=ha441bb4_0 tk=8.6.8=ha441bb4_0 toml=0.10.0=pypi_0 typed-ast=1.3.1=pypi_0 urllib3=1.23=py37_0 werkzeug=0.14.1=py37_0 wheel=0.33.1=py37_0 wrapt=1.10.11=py37h1de35cc_2 xz=5.2.4=h1de35cc_4 yaml=0.1.7=hc338f04_2 zlib=1.2.11=hf3cbc9b_2 ================================================ FILE: show_hierarchies.py ================================================ """ Create ASCII Art for hierarchies Uses asciitree. https://pypi.org/project/asciitree/0.3.3/ """ from asciitree import LeftAligned from asciitree.drawing import BOX_HEAVY rendering = LeftAligned() rendering.draw.gfx = BOX_HEAVY simple = { 'module.py': { 'class A:': {'def method(self): ...': {}}, 'class B:': {'def method(self): ...': {}}, 'def function():': {}} } print(rendering(simple)) package = { 'package': { '__init__.py': {}, 'module1.py': { 'class A:': {'def method(self): ...': {}}, 'def function():': {}}, 'module2.py': {'...': {}}, } } print(rendering(package)) cpx = { 'gemma': { 'baseline': {'code.py': {}}, 'my-first-project': {'code.py': {}}, 'another-project': {'code.py': {}}, 'os-upgrade': {'other files...': {}} } } print(rendering(cpx)) ================================================ FILE: stubs/sqlite3.pyi ================================================ """A start of a stub file to correct the error in the current sqlite3 definition.""" from typing import Union, Type, Optional from pathlib import Path import sqlite3 def connect( database: Union[bytes, str, Path], timeout: Optional[float] = None, detect_types: Optional[int] = None, isolation_level: Optional[str] = None, check_same_thread: Optional[bool] = None, factory: Optional[Type[sqlite3.dbapi2.Connection]] = None, cached_statements: Optional[int] = None, uri: Optional[bool] = None ) -> sqlite3.dbapi2.Connection: ... ================================================ FILE: test_all.py ================================================ #!/usr/bin/env python3 """Run all the chapter modules, doctests or performance() function This is run from the top-level directory, where all of the sample data files are also located. When runnning individual examples, working directory is expected to be this top-level directory. """ import doctest import runpy import unittest import sys import time from enum import Enum import importlib from pathlib import Path from typing import Any, Iterator, Tuple, Iterable import pytest DEBUG = False # Can't easily use logging -- can conflict with chapters on logging. DOCTEST_EXCLUDE = { 'ch13_ex4' # Requires a separate server to be started, too complex for this script } def package_module_iter(packages: Iterable[Path]) -> Iterator[Tuple[Path, Iterator[Path]]]: """For a given list of packages, emit the package name and a generator for all modules in the package. Structured like ``itertools.groupby()``. With a filter to reject caches of various kinds. keep = lambda path: not all( [filename.stem.startswith("__"), filename.stem.endswith("__"), filename.suffix == ".py"] ) yield package, filter(keep, package.glob("*.py")) """ def module_iter(package: Path, module_iter: Iterable[Path]) -> Iterator[Path]: """ A filter to reject __init__.py and similar names. """ if DEBUG: print(f"Package {package}") for filename in module_iter: if ( filename.stem.startswith("__") and filename.stem.endswith("__") and filename.suffix == ".py" ): continue if DEBUG: print(f" file {filename.name} module {filename.stem}") yield filename for package in packages: yield (package, module_iter(package, package.glob("*.py"))) def run(pkg_mod_iter: Iterable[Tuple[Path, Iterable[Path]]]) -> None: """Run each module, with a few exclusions.""" for package, module_iter in pkg_mod_iter: print() print(package.name) print("="*len(package.name)) print() for module in module_iter: if module.stem in DOCTEST_EXCLUDE: print(f"Excluding {module}") continue status = runpy.run_path(module, run_name="__main__") if status != 0: sys.exit(f"Failure: {module}") import subprocess def run_doctest_suite(pkg_mod_iter: Iterable[Tuple[Path, Iterable[Path]]]) -> None: """Doctest each module individually. With a few exclusions. Might be simpler to use doctest.testfile()? However, the examples aren't laid out for this. """ for package, module_iter in pkg_mod_iter: print() print(package.name) print("="*len(package.name)) print() for module_path in module_iter: if module_path.stem in DOCTEST_EXCLUDE: print(f"Excluding {module_path}") continue result = subprocess.run(['python3', '-m', 'doctest', str(module_path)]) if result.returncode != 0: sys.exit(f"Failure {result!r} in {module_path}") class PytestExit(int, Enum): Success = 0 Failures = 1 Interrupted = 2 InternalError = 3 CommandLineError = 4 NoTests = 5 def run_pytest_suite(pkg_mod_iter: Iterable[Tuple[Path, Iterable[Path]]]) -> None: """Pytest each module's modules. """ for package, module_iter in pkg_mod_iter: print() print(package.name) print("="*len(package.name)) print() names = [f"{m.parent.name}/{m.name}" for m in (module_iter)] print(names) status = pytest.main(names) if status not in (PytestExit.Success, PytestExit.NoTests): sys.exit(f"Failure {PytestExit(status)!r} in {names}") def run_performance(pkg_mod_iter: Iterable[Tuple[Path, Iterable[Path]]]) -> None: """Locate a performance() function in each module and run it.""" for package, module_iter in pkg_mod_iter: print() print(package.name) print("="*len(package.name)) print() for module in module_iter: print(module) try: imported_module = __import__( f"{package.name}.{module.stem}", fromlist=[module.stem, "performance"]) imported_module.performance() except AttributeError: pass # no performance() function in the module. def master_test_suite(pkg_mod_iter: Iterable[Tuple[Path, Iterable[Path]]]) -> None: """Deprecated. Use pytest, instead. Build a master unittest test suite from all modules and run that. """ master_suite = unittest.TestSuite() for package, module_iter in pkg_mod_iter: for module in module_iter: print(f"{package.name}.{module.stem}", file=sys.stderr) suite = doctest.DocTestSuite(f"{package.name}.{module.stem}") print(" ", suite, file=sys.stderr) master_suite.addTests(suite) runner = unittest.TextTestRunner(verbosity=1) runner.run(master_suite) def chap_key(name: Path) -> int: _, _, n = name.stem.partition("_") return int(n) if __name__ == "__main__": content = sorted(Path.cwd().glob("Chapter_*"), key=chap_key) if DEBUG: print(content, file=sys.stderr) run_doctest_suite(package_module_iter(content)) run_pytest_suite(package_module_iter(content)) ================================================ FILE: tox.ini ================================================ [tox] skipsdist = True envlist = py37 [testenv] deps = pytest flask requests jinja2 sqlalchemy pyyaml mypy setenv = PYTHONPATH = {env:PYTHONPATH:}{:}Chapter_20/src commands = python3 test_all.py python3 -m doctest Chapter_17/ch17_ex1.py python3 -m doctest Chapter_17/test_ch17.py # mypy can't be run on all files... # It might be slightly cleaner to provide a separate mypy config file. mypy Chapter_1 mypy Chapter_2 mypy Chapter_3 mypy Chapter_4 # Chapter_5/ch05_ex1.py:114: error: Signature of "aMethod" incompatible with supertype "LikeAbstract" mypy Chapter_5/ch05_ex2.py # Chapter_6/ch06_ex1.py:68: error: Incompatible types in assignment (expression has type "Power3", variable has type "Callable[[int, int], int]") mypy Chapter_6/ch06_ex2.py mypy Chapter_7 mypy Chapter_8 mypy Chapter_9/ch09_ex1.py # Chapter_9/ch09_ex2.py:54: error: "SomeClass" has no attribute "logger" # Chapter_9/ch09_ex2.py:57: error: "SomeClass" has no attribute "logger" mypy Chapter_10 mypy Chapter_11 mypy Chapter_12/ch12_ex1.py Chapter_12/ch12_ex2.py Chapter_12/ch12_ex3.py # Chapter_12/ch12_ex4.py relies on SQLAlchemy which has no stubs 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 # Chapter_13/ch13_ex4.py:37: error: No library stub file for module 'pytest' mypy --ignore-missing-imports Chapter_13/ch13_ex4.py mypy Chapter_14 mypy Chapter_15 mypy Chapter_16/ch16_ex1.py Chapter_16/ch16_ex3.py Chapter_16/ch16_ex4.py Chapter_16/ch16_ex5.py mypy Chapter_16/ch16_ex6.py Chapter_16/ch16_ex9.py Chapter_16/ch16_ex10.py mypy --ignore-missing-imports Chapter_16/ch16_ex7.py # Chapter_16/ch16_ex2.py:41: error: "Player" has no attribute "audit" # Chapter_16/ch16_ex2.py:42: error: "Player" has no attribute "verbose" # Chapter_16/ch16_ex2.py:50: error: "Table" has no attribute "security" # Chapter_16/ch16_ex8.py:29: error: "TailHandler" has no attribute "flushLevel" # Chapter_16/ch16_ex8.py:31: error: "TailHandler" has no attribute "buffer" # Chapter_16/ch16_ex8.py:31: error: "TailHandler" has no attribute "capacity" # Chapter_16/ch16_ex8.py:34: error: "TailHandler" has no attribute "buffer" mypy --ignore-missing-imports Chapter_17/ch17_ex1.py # Chapter_17/ch17_ex1.py:493: error: No library stub file for module 'pytest' mypy --ignore-missing-imports --follow-imports=skip Chapter_17/ch17_ex2.py # Chapter_17/ch17_ex2.py relies on SQLAlchemy which has no stubs mypy --ignore-missing-imports --follow-imports=skip Chapter_17/test_ch17.py # Chapter_17/test_ch17.py relies on test discovery for ch12_ex4 with SQL Alchemy, also. mypy --ignore-missing-imports Chapter_18 # Chapter_18/ch18_ex1.py:148: error: No library stub file for module 'pytest' mypy Chapter_19/some_algorithm mypy Chapter_19/ch19_ex1.py mypy --ignore-missing-imports Chapter_19/ch19_ex2.py # Chapter_19/ch19_ex2.py:10: error: No library stub file for module 'pytest' mypy Chapter_20 [testenv:doc] deps = sphinx changedir = Chapter_20/docs setenv = PYTHONPATH = {env:PYTHONPATH}{:}{toxinidir}/Chapter_20/src commands = sphinx-build -M html . _build