Mastering Python Second Edition Release Code

This commit is contained in:
Rick van Hattem
2022-05-05 18:25:55 +02:00
commit 3223a43fe3
454 changed files with 20230 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
Chapter 10, Testing and Logging
##############################################################################
| Preparing for Bugs explains how code can be tested and how logging can be added to enable easy debugging in the case of bugs at a later time.
@@ -0,0 +1,34 @@
def square(n: int) -> int:
'''
Returns the input number, squared
>>> square(0)
0
>>> square(1)
1
>>> square(2)
4
>>> square(3)
9
>>> square()
Traceback (most recent call last):
...
TypeError: square() missing 1 required positional argument: 'n'
>>> square('x')
Traceback (most recent call last):
...
TypeError: can't multiply sequence by non-int of type 'str'
Args:
n (int): The number to square
Returns:
int: The squared result
'''
return n * n
if __name__ == '__main__':
import doctest
doctest.testmod()
@@ -0,0 +1,14 @@
def square(n: int) -> int:
'''
>>> square('x')
Traceback (most recent call last):
...
TypeError: unsupported operand type(s) for ** or pow(): ...
'''
return n ** 2
if __name__ == '__main__':
import doctest
doctest.testmod(optionflags=doctest.ELLIPSIS)
@@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
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)
@@ -0,0 +1,61 @@
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
import sys
sys.path.insert(0, os.path.abspath('..'))
# -- Project information -----------------------------------------------------
project = 'doctest support'
copyright = '2020, Rick van Hattem'
author = 'Rick van Hattem'
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.intersphinx',
'sphinx.ext.napoleon',
'sphinx.ext.autodoc',
'sphinx.ext.doctest',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'alabaster'
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
intersphinx_mapping = {
'python': ('https://docs.python.org/', None),
'sphinx': ('https://www.sphinx-doc.org/', None),
}
@@ -0,0 +1,21 @@
.. doctest support documentation master file, created by
sphinx-quickstart on Sun Oct 4 17:57:12 2020.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to doctest support's documentation!
===========================================
.. toctree::
:maxdepth: 2
:caption: Contents:
square
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
@@ -0,0 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd
@@ -0,0 +1,34 @@
square module
=============
.. automodule:: square
:members:
:undoc-members:
:show-inheritance:
Examples:
.. testsetup::
from square import square
.. doctest::
# pytest does not recognize testsetup
>>> from square import square
>>> square(100)
10000
>>> square(0)
0
>>> square(1)
1
>>> square(3)
9
>>> square()
Traceback (most recent call last):
...
TypeError: square() missing 1 required positional argument: 'n'
>>> square('x')
Traceback (most recent call last):
...
TypeError: can't multiply sequence by non-int of type 'str'
@@ -0,0 +1,11 @@
'''
>>> False
0
>>> True
1
'''
if __name__ == '__main__':
import doctest
doctest.testmod()
doctest.testmod(optionflags=doctest.DONT_ACCEPT_TRUE_FOR_1)
@@ -0,0 +1,15 @@
'''
>>> [list(range(5)) for i in range(3)]
[[0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4]]
>>> # doctest: +NORMALIZE_WHITESPACE
... [list(range(5)) for i in range(3)]
[[0, 1, 2, 3, 4],
[0, 1, 2, 3, 4],
[0, 1, 2, 3, 4]]
'''
if __name__ == '__main__':
import doctest
doctest.testmod()
@@ -0,0 +1,24 @@
'''
>>> {10: 'a', 20: 'b'} # doctest: +ELLIPSIS
{...}
>>> [True, 1, 'a'] # doctest: +ELLIPSIS
[...]
>>> True, # doctest: +ELLIPSIS
(...)
>>> [1, 2, 3, 4] # doctest: +ELLIPSIS
[1, ..., 4]
>>> [1, 0, 0, 0, 0, 0, 4] # doctest: +ELLIPSIS
[1, ..., 4]
------------------------------------------------------------------------------
>>> class Spam(object):
... pass
>>> Spam() # doctest: +ELLIPSIS
<...Spam object at 0x...>
'''
if __name__ == '__main__':
import doctest
doctest.testmod()
@@ -0,0 +1,17 @@
>>> import pprint
>>> data = dict.fromkeys('spam')
>>> pprint.pprint(data)
{'a': None, 'm': None, 'p': None, 's': None}
------------------------------------------------------------------------------
>>> data = dict.fromkeys('spam')
>>> sorted(data.items())
[('a', None), ('m', None), ('p', None), ('s', None)]
------------------------------------------------------------------------------
>>> data = dict.fromkeys('spam')
>>> data == {'a': None, 'm': None, 'p': None, 's': None}
True
@@ -0,0 +1,11 @@
>>> 1/3 # doctest: +ELLIPSIS
0.333...
>>> f'{1/3:.3f}'
'0.333'
>>> '{:.3f}'.format(1/3)
'0.333'
>>> round(1/3, 3)
0.333
>>> 0.333 < 1/3 < 0.334
True
@@ -0,0 +1,16 @@
>>> import time
>>> a = time.time()
>>> b = time.time()
>>> (b - a) < 0.01
True
------------------------------------------------------------------------------
>>> import datetime
>>> a = datetime.datetime.now()
>>> b = datetime.datetime.now()
>>> str(b - a) # doctest: +ELLIPSIS
'0:00:00.000...
@@ -0,0 +1,29 @@
import unittest
import cube
class TestCube(unittest.TestCase):
def test_0(self):
self.assertEqual(cube.cube(0), 0)
def test_1(self):
self.assertEqual(cube.cube(1), 1)
def test_2(self):
self.assertEqual(cube.cube(2), 8)
def test_3(self):
self.assertEqual(cube.cube(3), 27)
def test_no_arguments(self):
with self.assertRaises(TypeError):
cube.cube()
def test_exception_str(self):
with self.assertRaises(TypeError):
cube.cube('x')
if __name__ == '__main__':
unittest.main()
@@ -0,0 +1,29 @@
import unittest
import cube
n = 2
expected = 8
# Regular unit test
class TestCube(unittest.TestCase):
def test_2(self):
self.assertEqual(cube.cube(n), expected)
def test_no_arguments(self):
with self.assertRaises(TypeError):
cube.cube()
# py.test class
class TestPyCube:
def test_2(self):
assert cube.cube(n) == expected
# py.test functions
def test_2():
assert cube.cube(n) == expected
@@ -0,0 +1,13 @@
class User:
def __init__(self, name):
self.name = name
def __eq__(self, other):
return self.name == other.name
def test_user_equal():
a = User('Rick')
b = User('Guido')
assert a == b
@@ -0,0 +1,13 @@
class User:
def __init__(self, name):
self.name = name
def __eq__(self, other):
return self.name == other.name
def test_user_equal():
a = User('Rick')
b = User('Guido')
assert a == b
@@ -0,0 +1,16 @@
import pytest
import cube
cubes = (
(0, 0),
(1, 1),
(2, 8),
(3, 27),
)
@pytest.mark.parametrize('n,expected', cubes)
def test_cube(n, expected):
assert cube.cube(n) == expected
@@ -0,0 +1,68 @@
##################################################################
import pytest
@pytest.fixture
def name():
return 'Rick'
def test_something(name):
assert name == 'Rick'
##################################################################
def test_cache(cache):
counter = cache.get('counter', 0) + 1
assert counter
cache.set('counter', counter)
##################################################################
import pytest
@pytest.fixture
def some_yield_fixture():
with open(__file__ + '.txt', 'w') as fh:
# Before the function
yield fh
# After the function
@pytest.fixture
def some_regular_fixture():
# Do something here
return 'some_value_to_pass_as_parameter'
def some_test(some_yield_fixture, some_regular_fixture):
some_yield_fixture.write(some_regular_fixture)
##################################################################
import pytest
import sqlite3
@pytest.fixture(params=[':memory:'])
def connection(request):
return sqlite3.connect(request.param)
@pytest.yield_fixture
def transaction(connection):
with connection:
yield connection
def test_insert(transaction):
transaction.execute('create table test (id integer)')
for i in range(3):
transaction.execute('insert into test values (?)', (i,))
@@ -0,0 +1,15 @@
import os
import sys
import logging
def test_print():
print('Printing to stdout')
print('Printing to stderr', file=sys.stderr)
logging.debug('Printing to debug')
logging.info('Printing to info')
logging.warning('Printing to warning')
logging.error('Printing to error')
# We don't want to display os.environ so hack around it
fail = 'FAIL' in os.environ
assert not fail
@@ -0,0 +1,16 @@
import pytest
import cube_root
cubes = (
(0, 0),
(1, 1),
(8, 2),
(27, 3),
)
@pytest.mark.parametrize('n,expected', cubes)
def test_cube_root(n, expected):
assert cube_root.cube_root(n) == expected
@@ -0,0 +1,21 @@
import pytest
import cube_root
cubes = (
(0, 0),
(1, 1),
(8, 2),
(27, 3),
)
@pytest.mark.parametrize('n,expected', cubes)
def test_cube_root(n, expected):
assert cube_root.cube_root(n) == expected
def test_cube_root_below_zero():
with pytest.raises(ValueError):
cube_root.cube_root(-1)
@@ -0,0 +1,3 @@
import os
def test(a,b):
return c
+16
View File
@@ -0,0 +1,16 @@
[pytest]
python_files =
your_project_source/*.py
tests/*.py
addopts =
--doctest-modules
--cov your_project_source
--cov-report term-missing
--cov-report html
--flake8
--mypy
# W391 is the error about blank lines at the end of a file
flake8-ignore =
*.py W391
@@ -0,0 +1,38 @@
from unittest import mock
import random
@mock.patch('random.random')
def test_random(mock_random):
# Specify our mock return value
mock_random.return_value = 0.1
# Test for the mock return value
assert random.random() == 0.1
assert mock_random.call_count == 1
def test_random_with():
with mock.patch('random.random') as mock_random:
mock_random.return_value = 0.1
assert random.random() == 0.1
##############################################################################
import os
from unittest import mock
def delete_file(filename):
while os.path.exists(filename):
os.unlink(filename)
@mock.patch('os.path.exists', side_effect=(True, False, False))
@mock.patch('os.unlink')
def test_delete_file(mock_exists, mock_unlink):
# First try:
delete_file('some non-existing file')
# Second try:
delete_file('some non-existing file')
@@ -0,0 +1,16 @@
import os
def test_chdir_monkeypatch(monkeypatch):
monkeypatch.chdir('/')
assert os.getcwd() == '/'
def test_chdir():
original_directory = os.getcwd()
try:
os.chdir('/')
assert os.getcwd() == '/'
finally:
os.chdir(original_directory)
@@ -0,0 +1,4 @@
def test_dict_merge():
a = dict(a=123)
b = dict(b=456)
assert a | b
@@ -0,0 +1,9 @@
[tox]
envlist = py3{8,9}
skipsdist = True
[testenv]
deps =
pytest
commands =
pytest test.py
@@ -0,0 +1,7 @@
import logging
logging.debug('debug')
logging.info('info')
logging.warning('warning')
logging.error('error')
logging.critical('critical')
@@ -0,0 +1,25 @@
import logging
log_format = (
'%(levelname)-8s %(name)-12s %(message)s')
logging.basicConfig(
filename='debug.log',
format=log_format,
level=logging.DEBUG,
)
formatter = logging.Formatter(log_format)
handler = logging.StreamHandler()
handler.setLevel(logging.WARNING)
handler.setFormatter(formatter)
logging.getLogger().addHandler(handler)
logging.debug('debug')
logging.info('info')
some_logger = logging.getLogger('some')
some_logger.warning('warning')
some_logger.error('error')
other_logger = some_logger.getChild('other')
other_logger.critical('critical')
@@ -0,0 +1,30 @@
from logging import config
config.dictConfig({
'version': 1,
'formatters': {
'standard': {
'format': '%(levelname)-8s %(name)-12s %(message)s',
},
},
'handlers': {
'file': {
'filename': 'debug.log',
'level': 'DEBUG',
'class': 'logging.FileHandler',
'formatter': 'standard',
},
'stream': {
'level': 'WARNING',
'class': 'logging.StreamHandler',
'formatter': 'standard',
},
},
'loggers': {
'': {
'handlers': ['file', 'stream'],
'level': 'DEBUG',
},
},
})
@@ -0,0 +1,27 @@
{
"version": 1,
"formatters": {
"standard": {
"format": "%(levelname)-8s %(name)-12s %(message)s"
}
},
"handlers": {
"file": {
"filename": "debug.log",
"level": "DEBUG",
"class": "logging.FileHandler",
"formatter": "standard"
},
"stream": {
"level": "WARNING",
"class": "logging.StreamHandler",
"formatter": "standard"
}
},
"loggers": {
"": {
"handlers": ["file", "stream"],
"level": "DEBUG"
}
}
}
@@ -0,0 +1,12 @@
import os
import json
from logging import config
name = os.path.splitext(__file__)[0]
json_filename = os.path.join(os.path.dirname(__file__),
f'{name}.json')
with open(json_filename) as fh:
config.dictConfig(json.load(fh))
@@ -0,0 +1,28 @@
[formatters]
keys=standard
[handlers]
keys=file,stream
[loggers]
keys=root
[formatter_standard]
format=%(levelname)-8s %(name)-12s %(message)s
[handler_file]
level=DEBUG
class=FileHandler
formatter=standard
args=('debug.log',)
[handler_stream]
level=WARNING
class=StreamHandler
formatter=standard
args=(sys.stderr,)
[logger_root]
handlers=file,stream
level=DEBUG
@@ -0,0 +1,7 @@
import os
from logging import config
name = os.path.splitext(__file__)[0]
config.fileConfig(os.path.join(os.path.dirname(__file__),
f'{name}.ini'))
@@ -0,0 +1,33 @@
[formatters]
keys=standard
[handlers]
keys=file,stream
[loggers]
keys=root,some
[formatter_standard]
format=%(levelname)-8s %(name)-12s %(message)s
[handler_file]
level=DEBUG
class=FileHandler
formatter=standard
args=('debug.log',)
[handler_stream]
level=WARNING
class=StreamHandler
formatter=standard
args=(sys.stderr,)
[logger_root]
handlers=file,stream
level=DEBUG
[logger_some]
level=DEBUG
qualname=some
handlers=
@@ -0,0 +1,58 @@
import sys
def receive():
import time
import logging
from logging import config
listener = config.listen()
listener.start()
try:
while True:
logging.debug('debug')
logging.info('info')
some_logger = logging.getLogger('some')
some_logger.warning('warning')
some_logger.error('error')
other_logger = some_logger.getChild('other')
other_logger.critical('critical')
time.sleep(5)
except KeyboardInterrupt:
# Stop listening and finish the listening thread
config.stopListening()
listener.join()
def send():
import os
import struct
import socket
from logging import config
ini_filename = os.path.splitext(__file__)[0] + '.ini'
with open(ini_filename, 'rb') as fh:
data = fh.read()
# Open the socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Connect to the server
sock.connect(('127.0.0.1', config.DEFAULT_LOGGING_CONFIG_PORT))
# Send the magic logging packet
sock.send(struct.pack('>L', len(data)))
# Send the config
sock.send(data)
# And close the connection again
sock.close()
if __name__ == '__main__':
if sys.argv[-1] == 'send':
send()
elif sys.argv[-1] == 'receive':
receive()
else:
print(f'Usage: {sys.argv[0]} [send/receive]')
@@ -0,0 +1,35 @@
import logging
logger = logging.getLogger(__name__)
class MyClass(object):
def __init__(self, count):
self.logger = logger.getChild(self.__class__.__name__)
##################################################################
import logging
logger = logging.getLogger('main_module.sub_module')
logger.addHandler(logging.FileHandler('sub_module.log'))
##################################################################
import logging
logger = logging.getLogger('main_module.sub_module')
logger.setLevel(logging.DEBUG)
##################################################################
import logging
logger = logging.getLogger()
exception = 'Oops...'
logger.error('Some horrible error: %r', exception)
@@ -0,0 +1,17 @@
##################################################################
import logging
logger = logging.getLogger()
logger.error('simple error', extra=dict(some_variable='my value'))
##################################################################
import logging
logging.basicConfig(format='%(some_variable)s: %(message)s')
logger = logging.getLogger()
logger.error('the message', extra=dict(some_variable='my value'))
@@ -0,0 +1,12 @@
import logging
logging.basicConfig()
logger = logging.getLogger()
try:
raise RuntimeError('some runtime error')
except Exception as exception:
logger.exception('Got an exception: %s', exception)
logger.error('And an error')
@@ -0,0 +1,8 @@
import logging
formatter = logging.Formatter('{levelname} {message}', style='{')
handler = logging.StreamHandler()
handler.setFormatter(formatter)
logging.error('formatted message?')
@@ -0,0 +1,23 @@
import logging
class FormattingMessage:
def __init__(self, message, kwargs):
self.message = message
self.kwargs = kwargs
def __str__(self):
return self.message.format(**self.kwargs)
class FormattingAdapter(logging.LoggerAdapter):
def process(self, msg, kwargs):
msg, kwargs = super().process(msg, kwargs)
return FormattingMessage(msg, kwargs), dict()
logger = FormattingAdapter(logging.root, dict())
logger.error('Hi {name}', name='Rick')
@@ -0,0 +1,9 @@
import logging
a = logging.getLogger('a')
ab = logging.getLogger('a.b')
ab.error('before setting level')
a.setLevel(logging.CRITICAL)
ab.error('after setting level')
@@ -0,0 +1,13 @@
import logging
ab = logging.getLogger('a.b')
ab.setLevel(logging.ERROR)
ab.propagate = False
ab.addHandler(logging.StreamHandler())
a = logging.getLogger('a')
ab.error('before setting level')
a.setLevel(logging.CRITICAL)
ab.error('after setting level')
@@ -0,0 +1,47 @@
import logging
def get_handlers(logger):
handlers = []
# Walk through the loggers and their parents recursively to
# fetch the handlers
while logger:
handlers += logger.handlers
if logger.propagate:
logger = logger.parent
else:
break
# Python has a lastResort handler in case no handlers are
# defined
if not handlers and logging.lastResort:
handlers.append(logging.lastResort)
return handlers
def debug_loggers():
logger: logging.Logger
for name, logger in logging.root.manager.loggerDict.items():
# Placeholders are loggers without settings
if isinstance(logger, logging.PlaceHolder):
print('skipping', name)
continue
level = logging.getLevelName(logger.getEffectiveLevel())
handlers = get_handlers(logger)
print(f'{name}@{level}: {handlers}')
if __name__ == '__main__':
a = logging.getLogger('a')
a.setLevel(logging.INFO)
handler = logging.StreamHandler()
handler.setLevel(logging.INFO)
ab = logging.getLogger('a.b')
ab.setLevel(logging.DEBUG)
ab.addHandler(handler)
debug_loggers()
+37
View File
@@ -0,0 +1,37 @@
[report]
# The test coverage you require, keeping to 100% is not easily
# possible for all projects but its a good default for new projects.
fail_under = 100
# These functions are generally only needed for debugging and/or
# extra safety so we want to ignore them from the coverage
# requirements
exclude_lines =
# Make it possible to ignore blocks of code
pragma: no cover
# Generally only debug code uses this
def __repr__
# If a debug setting is set, skip testing
if self\.debug:
if settings.DEBUG
# Dont worry about safety checks and expected errors
raise AssertionError
raise NotImplementedError
# Do not complain about code that will never run
if 0:
if __name__ == .__main__.:
@abc.abstractmethod
[run]
# Make sure we require that all branches of the code is covered.
# So both the if and the else
branch = True
# No need to require coverage of testing code
omit =
test_*.py
+23
View File
@@ -0,0 +1,23 @@
import sys
import pathlib
# Little hack to add the current directory to sys.path so we can
# find the imports
path = pathlib.Path(__file__).parent
sys.path.append(str(path.resolve()))
from T_12_assert_representation import User
def is_user(value):
return isinstance(value, User)
def pytest_assertrepr_compare(config, op, left, right):
if is_user(left) and is_user(right) and op == '==':
return [
'Comparing User instances:',
f' name: {left.name} != {right.name}',
]
+11
View File
@@ -0,0 +1,11 @@
def cube(n: int) -> int:
'''
Returns the input number, cubed
Args:
n (int): The number to cube
Returns:
int: The cubed result
'''
return n ** 3
+15
View File
@@ -0,0 +1,15 @@
def cube_root(n: int) -> int:
'''
Returns the cube root of the input number
Args:
n (int): The number to cube root
Returns:
int: The cube root result
'''
if n >= 0:
return n ** (1 / 3)
else:
raise ValueError('A number larger than 0 was expected')
+17
View File
@@ -0,0 +1,17 @@
Welcome to Mastering Python's documentation!
============================================
Contents:
.. toctree::
:maxdepth: 2
README
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
+20
View File
@@ -0,0 +1,20 @@
def square(n: int) -> int:
'''
Returns the input number, squared
>>> square(2)
4
Args:
n (int): The number to square
Returns:
int: The squared result
'''
return n * n
if __name__ == '__main__':
import doctest
doctest.testmod()