├── .gitignore ├── Chapter_01 ├── National Weather Service Text Product Display.html ├── examples.txt ├── examples_network.txt └── some_file.txt ├── Chapter_02 ├── ch02_r09.txt └── examples.txt ├── Chapter_03 ├── __init__.py ├── ch03_r01.py ├── ch03_r02.py ├── ch03_r03.py ├── ch03_r04.py ├── ch03_r05.py ├── ch03_r06.py ├── ch03_r07.py ├── ch03_r08.py ├── ch03_r09.py ├── ch03_r10.py ├── ch03_r11.py └── examples.txt ├── Chapter_04 ├── __init__.py ├── ch04_r01.py ├── ch04_r02.py ├── ch04_r03.py ├── ch04_r04.py ├── ch04_r05.py ├── ch04_r06.py ├── ch04_r07.py ├── ch04_r08.py ├── ch04_r09.py └── examples.txt ├── Chapter_05 ├── ch05_r01.py ├── ch05_r02.py ├── ch05_r03.py ├── ch05_r04.py ├── ch05_r07.py └── examples.txt ├── Chapter_06 ├── __init__.py ├── ch06_r01.py ├── ch06_r02.py ├── ch06_r04.py ├── ch06_r05_dice.py ├── ch06_r05_roulette.py ├── ch06_r05_zonk.py ├── ch06_r06.py ├── examples.txt └── test_ch06_r04.py ├── Chapter_07 ├── __init__.py ├── ch07_r01.py ├── ch07_r02.py ├── ch07_r03.py ├── ch07_r04.py ├── ch07_r05.py ├── ch07_r06.py ├── ch07_r07.py ├── ch07_r09.py ├── ch07_r10.py ├── ch07_r11.py ├── ch07_r11a.py ├── ch07_r11b.py ├── ch07_r12.py └── examples.txt ├── Chapter_08 ├── __init__.py ├── ch08_r01.py ├── ch08_r02.py ├── ch08_r02_timing.py ├── ch08_r03.py ├── ch08_r04.py ├── ch08_r05.py ├── ch08_r06.py ├── ch08_r06_bug.py ├── ch08_r07.py ├── ch08_r08.py └── examples.txt ├── Chapter_09 ├── __init__.py ├── ch09_r01.py ├── ch09_r02.py ├── ch09_r03.py ├── ch09_r04.py ├── ch09_r05.py ├── ch09_r06.py ├── ch09_r07.py ├── ch09_r08.py ├── ch09_r09.py ├── ch09_r10.py └── examples.txt ├── Chapter_10 ├── __init__.py ├── ch10_a.py ├── ch10_b.py ├── ch10_r02.py ├── ch10_r03.py ├── ch10_r04.py ├── ch10_r05.py ├── ch10_r06.py ├── ch10_r07.py ├── ch10_r08.py ├── ch10_r09.py └── examples.txt ├── Chapter_11 ├── __init__.py ├── ch11_r01.py ├── ch11_r03.py ├── ch11_r08.py ├── ch11_r09.py ├── examples.txt ├── test_ch11_r04.py ├── test_ch11_r04_more.py ├── test_ch11_r06.py ├── test_ch11_r08.py ├── test_ch11_r08_ut.py ├── test_ch11_r09.py ├── test_ch11_r09_ut.py └── test_ch11_r10.py ├── Chapter_12 ├── __init__.py ├── card_model.py ├── ch12_r01.py ├── ch12_r02.py ├── ch12_r03.py ├── ch12_r04_client.py ├── ch12_r04_server.py ├── ch12_r05_client.py ├── ch12_r05_server.py ├── ch12_r06_client.py ├── ch12_r06_requests.py ├── ch12_r06_server.py ├── ch12_r06_user.py ├── ch12_wsgi.py ├── examples.txt ├── test_ch12_all_integration.py ├── test_ch12_r01.py ├── test_ch12_r02.py ├── test_ch12_r03.py ├── test_ch12_r04_client.py ├── test_ch12_r04_server.py ├── test_ch12_r05_client.py ├── test_ch12_r05_server.py ├── test_ch12_r06_client.py ├── test_ch12_r06_requests.py ├── test_ch12_r06_server.py └── test_ch12_wsgi.py ├── Chapter_13 ├── __init__.py ├── ch13_r01.py ├── ch13_r02.py ├── ch13_r03.py ├── ch13_r04.py ├── ch13_r05.py ├── ch13_r05a.py ├── ch13_r06.py ├── confirm_destination.py ├── examples.txt ├── settings.py ├── test_ch13_r01.py ├── test_ch13_r01_ut.py ├── test_ch13_r02.py ├── test_ch13_r03.py ├── test_ch13_r04.py ├── test_ch13_r05.py ├── test_ch13_r05_ut.py └── test_ch13_r06.py ├── Chapter_14 ├── __init__.py ├── ch14_r01.py ├── ch14_r02.py ├── ch14_r03.py ├── ch14_r04.py ├── ch14_r05.py ├── ch14_r06.py ├── examples.txt ├── test_ch14_r01.py ├── test_ch14_r02.py ├── test_ch14_r03.py ├── test_ch14_r04.py ├── test_ch14_r04_ut.py ├── test_ch14_r05.py └── test_ch14_r06.py ├── Chapter_15 ├── __init__.py ├── ch15_r01.py ├── ch15_r02.py ├── ch15_r03.py ├── ch15_r04.py ├── ch15_r05.py ├── ch15_r06.py ├── ch15_r07.py ├── ch15_r08.py ├── collect_biased.py ├── collector.py ├── collector_expected_values.py └── examples.txt ├── README.rst ├── anscome_json.py ├── cert.conf ├── data ├── Volvo Ocean Race.html ├── anscombe.json ├── ch07_r13.csv ├── ch07_r13.csv.bz2 ├── ch10_file1.yaml ├── ch10_file2.yaml ├── ch10_r10.log ├── ch12_r05_test.yaml ├── ch13_r05_test.yaml ├── co2_mm_mlo.txt ├── craps.csv ├── ex2_r12.csv ├── extra_detail.log ├── extract_20170704010203.json ├── fuel.csv ├── fuel.ods ├── fuel2.csv ├── game_0.yaml ├── game_1.yaml ├── output.csv ├── output.csv.old ├── quotient.csv ├── quotient.csv.old ├── race_result.json ├── race_result.xml ├── sample.csv ├── sample.log ├── some_file.txt ├── ssl.cert ├── ssl.key ├── sum.yaml ├── summary.dat ├── summary_log.csv ├── swagger.json ├── waypoints.csv ├── wc.csv ├── wc1.csv ├── write.log ├── x.yaml ├── x12.yaml ├── x13.yaml ├── y.yaml ├── y12.yaml └── y13.yaml ├── hint_game.py ├── html └── Chapter_04 │ └── ch04_r05.py.html ├── renamer.py ├── requirements.in ├── requirements.txt ├── route.csv ├── setup.sh ├── size.py ├── temp ├── anscombe_raw.csv ├── sample.csv └── some_file.txt └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.pyc 3 | .idea/* 4 | .mypy_cache/* 5 | .tox/* 6 | backup/* 7 | *.cert 8 | *.key 9 | renamer.py 10 | -------------------------------------------------------------------------------- /Chapter_01/examples_network.txt: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2 | 3 | Chapter 1, Examples from the text. 4 | 5 | • Decoding bytes – how to get proper characters from some bytes 6 | Portions of this example require internet access. 7 | 8 | """ 9 | 10 | # Recipe 10 11 | # Decoding bytes – how to get proper characters from some bytes 12 | # This can be skipped in the event of an unreliable internet connection. 13 | 14 | >>> import urllib.request 15 | >>> warnings_uri= 'https://forecast.weather.gov/product.php?site=CRH&issuedby=AKQ&product=SMW&format=TXT' 16 | >>> with urllib.request.urlopen(warnings_uri) as source: 17 | ... warnings_text = source.read() 18 | 19 | >>> warnings_text[:80] 20 | b'>> document = warnings_text.decode("UTF-8") 23 | >>> document[:80] 24 | '>> from pathlib import Path 9 | >>> import shutil 10 | >>> import os 11 | 12 | # Prepare for the demo by removing the target directory. 13 | >>> shutil.rmtree(Path.cwd()/"backup") 14 | 15 | >>> source_dir = Path.cwd()/"data" 16 | >>> target_dir = Path.cwd()/"backup" 17 | >>> for source_path in source_dir.glob('**/*.csv'): 18 | ... source_name = source_path.relative_to(source_dir) 19 | ... target_path = target_dir/source_name 20 | ... shutil.copy(source_path, target_path) 21 | Traceback (most recent call last): 22 | File "/Applications/PyCharm CE.app/Contents/helpers/pycharm/docrunner.py", line 139, in __run 23 | exec(compile(example.source, filename, "single", 24 | File "", line 4, in 25 | shutil.copy(source_path, target_path) 26 | File "/Users/slott/miniconda3/envs/cookbook/lib/python3.8/shutil.py", line 409, in copy 27 | copyfile(src, dst, follow_symlinks=follow_symlinks) 28 | File "/Users/slott/miniconda3/envs/cookbook/lib/python3.8/shutil.py", line 259, in copyfile 29 | with open(src, 'rb') as fsrc, open(dst, 'wb') as fdst: 30 | FileNotFoundError: [Errno 2] No such file or directory: '/Users/slott/Documents/Writing/Python/Python Cookbook 2e/Modern-Python-Cookbook-Second-Edition/backup/wc1.csv' 31 | 32 | >>> for source_path in source_dir.glob('**/*.csv'): 33 | ... source_name = source_path.relative_to(source_dir) 34 | ... target_path = target_dir/source_name 35 | ... try: 36 | ... target = shutil.copy(source_path, target_path) 37 | ... except FileNotFoundError: 38 | ... target_path.parent.mkdir(exist_ok=True, parents=True) 39 | ... target = shutil.copy(source_path, target_path) 40 | ... except OSError as ex: 41 | ... print(f"Copy {source_path} to {target_path} error {ex}") 42 | 43 | >>> for source_path in source_dir.glob('**/*.csv'): 44 | ... source_name = source_path.relative_to(source_dir) 45 | ... target_path = target_dir/source_name 46 | ... try: 47 | ... target = shutil.copy(source_path, target_path) 48 | ... except FileNotFoundError: 49 | ... try: 50 | ... target_path.parent.mkdir(exist_ok=True, parents=True) 51 | ... target = shutil.copy(source_path, target_path) 52 | ... except OSError as ex2: 53 | ... print(f"{target_path.parent} problem: {ex2}") 54 | ... except OSError as ex: 55 | ... print(f"Copy {source_path} to {target_path} error {ex}") 56 | 57 | -------------------------------------------------------------------------------- /Chapter_03/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Modern-Python-Cookbook-Second-Edition/0182ee7f17ae58cff2775290547321ebed443ae3/Chapter_03/__init__.py -------------------------------------------------------------------------------- /Chapter_03/ch03_r02.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2 | 3 | Chapter 3, recipe 2, Designing functions with optional parameters 4 | """ 5 | import random 6 | from typing import Tuple 7 | 8 | 9 | def die() -> int: 10 | return random.randint(1, 6) 11 | 12 | 13 | def craps() -> Tuple[int, int]: 14 | return (die(), die()) 15 | 16 | 17 | test_die_craps = """ 18 | >>> random.seed(113) 19 | >>> die(), die() 20 | (1, 6) 21 | >>> craps() 22 | (6, 3) 23 | >>> craps() 24 | (1, 4) 25 | """ 26 | 27 | 28 | def zonk() -> Tuple[int, ...]: 29 | return tuple(die() for x in range(6)) 30 | 31 | 32 | test_zonk = """ 33 | >>> zonk() 34 | (5, 3, 2, 4, 1, 1) 35 | """ 36 | 37 | # Revision #2 38 | 39 | def craps_v2() -> Tuple[int, ...]: 40 | return tuple(die() for x in range(2)) 41 | 42 | 43 | def dice_v2(n: int) -> Tuple[int, ...]: 44 | return tuple(die() for x in range(n)) 45 | 46 | 47 | test_craps2_dice2 = """ 48 | >>> random.seed(113) 49 | >>> craps_v2() 50 | (1, 6) 51 | >>> dice_v2(2) 52 | (6, 3) 53 | >>> dice_v2(6) 54 | (1, 4, 5, 3, 2, 4) 55 | """ 56 | 57 | 58 | def dice_v3(n: int = 2) -> Tuple[int, ...]: 59 | return tuple(die() for x in range(n)) 60 | 61 | 62 | def craps_v3() -> Tuple[int, ...]: 63 | return dice_v3(2) 64 | 65 | 66 | def zonk_v3() -> Tuple[int, ...]: 67 | return dice_v3(6) 68 | 69 | 70 | test_dice3_craps3_zonk3 = """ 71 | >>> random.seed(113) 72 | >>> craps_v3() 73 | (1, 6) 74 | >>> zonk_v3() 75 | (6, 3, 1, 4, 5, 3) 76 | """ 77 | 78 | 79 | def die_v4(sides: int = 6) -> int: 80 | return random.randint(1, 6) 81 | 82 | 83 | def dice_v4(n: int = 2, sides: int = 6) -> Tuple[int, ...]: 84 | return tuple(die_v4(sides) for x in range(n)) 85 | 86 | 87 | test_dice4 = """ 88 | >>> random.seed(113) 89 | >>> dice_v4() 90 | (1, 6) 91 | >>> dice_v4(n=6) 92 | (6, 3, 1, 4, 5, 3) 93 | """ 94 | __test__ = {n: v for n, v in locals().items() if n.startswith("test_")} 95 | -------------------------------------------------------------------------------- /Chapter_03/ch03_r03.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 3, recipe 3, Designing type hints for optional parameters 4 | """ 5 | import random 6 | 7 | 8 | def dice(n, sides=6): 9 | return tuple(random.randint(1, sides) for _ in range(n)) 10 | 11 | 12 | test_dice = """ 13 | >>> random.seed(113) 14 | >>> dice(2) 15 | (1, 6) 16 | """ 17 | 18 | from typing import Tuple 19 | 20 | 21 | def dice_t(n: int, sides: int = 6) -> Tuple[int, ...]: 22 | return tuple(random.randint(1, sides) for _ in range(n)) 23 | 24 | 25 | Dice = Tuple[int, ...] 26 | 27 | 28 | def craps() -> Dice: 29 | return dice_t(2) 30 | 31 | 32 | def zonk(n: int = 6) -> Dice: 33 | return dice_t(n) 34 | 35 | 36 | def mage_hitpoints(n) -> int: 37 | return sum(dice_t(n, 4)) 38 | 39 | 40 | test_craps = """ 41 | >>> random.seed(113) 42 | >>> craps() 43 | (1, 6) 44 | """ 45 | 46 | test_zonk = """ 47 | >>> random.seed(113) 48 | >>> zonk() 49 | (1, 6, 6, 3, 1, 4) 50 | """ 51 | 52 | test_mage = """ 53 | >>> random.seed(113) 54 | >>> mage_hitpoints(8) 55 | 19 56 | """ 57 | 58 | from typing import Optional, Tuple 59 | 60 | 61 | def polydice(n: Optional[int] = None, sides: int = 6) -> Tuple[int, ...]: 62 | if n is None: 63 | n = 2 if sides == 6 else 1 64 | return tuple(random.randint(1, sides) for _ in range(n)) 65 | 66 | 67 | test_polydice = """ 68 | >>> random.seed(113) 69 | >>> polydice() 70 | (1, 6) 71 | >>> polydice(6) 72 | (6, 3, 1, 4, 5, 3) 73 | >>> polydice(sides=8) 74 | (4,) 75 | >>> polydice(n=8, sides=4) 76 | (4, 1, 1, 3, 2, 3, 4, 3) 77 | """ 78 | 79 | __test__ = {n: v for n, v in locals().items() if n.startswith("test_")} 80 | -------------------------------------------------------------------------------- /Chapter_03/ch03_r05.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 3, recipe 5, Forcing keyword-only arguments with the * separator 4 | """ 5 | 6 | 7 | def Twc(T: float, V: float) -> float: 8 | return 13.12 + 0.6215 * T - 11.37 * V ** 0.16 + 0.3965 * T * V ** 0.16 9 | 10 | 11 | import csv 12 | from typing import TextIO 13 | 14 | 15 | def wind_chill( 16 | *, 17 | start_T: int, 18 | stop_T: int, 19 | step_T: int, 20 | start_V: int, 21 | stop_V: int, 22 | step_V: int, 23 | target: TextIO 24 | ) -> None: 25 | """Wind Chill Table.""" 26 | writer = csv.writer(target) 27 | heading = [""] + [str(t) for t in range(start_T, stop_T, step_T)] 28 | writer.writerow(heading) 29 | for V in range(start_V, stop_V, step_V): 30 | row = [float(V)] + [Twc(T, V) for T in range(start_T, stop_T, step_T)] 31 | writer.writerow(row) 32 | 33 | 34 | test_wc_good = """ 35 | >>> from pathlib import Path 36 | 37 | >>> p = Path('data/wc1.csv') 38 | >>> with p.open('w', newline='') as output_file: 39 | ... wind_chill(start_T=0, stop_T=-45, step_T=-5, 40 | ... start_V=0, stop_V=20, step_V=2, 41 | ... target=output_file) 42 | """ 43 | 44 | test_wc_fail = """ 45 | >>> from pathlib import Path 46 | 47 | >>> p = Path('data/wc1.csv') 48 | >>> with p.open('w', newline='') as target: 49 | ... wind_chill(0, -45, -5, 0, 20, 2, target) 50 | Traceback (most recent call last): 51 | File "", line 1, in 52 | TypeError: wind_chill() takes 0 positional arguments but 7 were given 53 | """ 54 | 55 | __test__ = {n: v for n, v in locals().items() if n.startswith("test_")} 56 | -------------------------------------------------------------------------------- /Chapter_03/ch03_r06.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 3, recipe 6, Defining position-only parameters with the / separator 4 | 5 | """ 6 | 7 | 8 | def F(c: float, /) -> float: 9 | return 32 + c * (9 / 5) 10 | 11 | 12 | def C(f: float, /, truncate: bool=False) -> float: 13 | c = 5 * (f - 32) / 9 14 | if truncate: 15 | return round(c, 0) 16 | return c 17 | 18 | 19 | test_f = """ 20 | >>> F(0) 21 | 32.0 22 | >>> F(25) 23 | 77.0 24 | """ 25 | 26 | test_c = """ 27 | >>> C(32) 28 | 0.0 29 | >>> C(77) 30 | 25.0 31 | >>> round(C(72), 3) 32 | 22.222 33 | >>> C(72) # doctest: +ELLIPSIS 34 | 22.22222222222222... 35 | >>> C(72, truncate=True) 36 | 22.0 37 | >>> C(72, True) 38 | 22.0 39 | """ 40 | 41 | __test__ = {n: v for n, v in locals().items() if n.startswith("test_")} 42 | -------------------------------------------------------------------------------- /Chapter_03/ch03_r07.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 3, recipe 7, Writing hints for more complex types 4 | """ 5 | 6 | from typing import Optional, Union, Dict 7 | from decimal import Decimal 8 | 9 | 10 | def temperature( 11 | *, 12 | f_temp: Optional[float] = None, 13 | c_temp: Optional[float] = None 14 | ) -> Dict[str, float]: 15 | """Convert between Fahrenheit temperature and 16 | Celsius temperature. 17 | 18 | :key f_temp: Temperature in °F. 19 | :key c_temp: Temperature in °C. 20 | :returns: dictionary with two keys: 21 | :f_temp: Temperature in °F. 22 | :c_temp: Temperature in °C. 23 | """ 24 | 25 | if f_temp is not None: 26 | c_temp = 5 * (f_temp - 32) / 9 27 | elif c_temp is not None: 28 | f_temp = 32 + 9 * c_temp / 5 29 | else: 30 | raise TypeError("One of f_temp or c_temp must be provided") 31 | result: Dict[str, float] = {"c_temp": c_temp, "f_temp": f_temp} 32 | return result 33 | 34 | 35 | def temperature_bad( 36 | *, f_temp: Optional[float] = None, c_temp: Optional[float] = None 37 | ) -> float: 38 | if f_temp is not None: 39 | c_temp = 5 * (f_temp - 32) / 9 40 | elif f_temp is not None: 41 | f_temp = 32 + 9 * c_temp / 5 42 | else: 43 | raise TypeError("One of f_temp or c_temp must be provided") 44 | result = {"c_temp": c_temp, "f_temp": f_temp} 45 | return result # type: ignore 46 | 47 | 48 | # Without the type: ignore, we'll get mypy errors. 49 | # Chapter_03/ch03_r07.py:45: error: Incompatible return value type (got "Dict[str, float]", expected "float") 50 | 51 | from pytest import approx # type: ignore 52 | 53 | 54 | def test_temperature(): 55 | assert temperature(f_temp=72) == {"c_temp": approx(22.22222), "f_temp": 72} 56 | assert temperature(c_temp=22.2) == {"c_temp": 22.2, "f_temp": approx(71.96)} 57 | 58 | 59 | from mypy_extensions import TypedDict 60 | 61 | TempDict = TypedDict("TempDict", {"c_temp": float, "f_temp": float,}) 62 | 63 | 64 | def temperature_d( 65 | *, f_temp: Optional[float] = None, c_temp: Optional[float] = None 66 | ) -> TempDict: 67 | """Convert between Fahrenheit temperature and 68 | Celsius temperature. 69 | 70 | :key f_temp: Temperature in °F. 71 | :key c_temp: Temperature in °C. 72 | :returns: dictionary with two keys: 73 | :f_temp: Temperature in °F. 74 | :c_temp: Temperature in °C. 75 | """ 76 | 77 | if f_temp is not None: 78 | c_temp = 5 * (f_temp - 32) / 9 79 | elif c_temp is not None: 80 | f_temp = 32 + 9 * c_temp / 5 81 | else: 82 | raise TypeError("One of f_temp or c_temp must be provided") 83 | result: TempDict = {"c_temp": c_temp, "f_temp": f_temp} 84 | return result 85 | -------------------------------------------------------------------------------- /Chapter_03/ch03_r09.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 3, Recipe 9, Writing clear documentation strings with RST markup 4 | """ 5 | 6 | 7 | def Twc(T: float, V: float) -> float: 8 | """Computes the wind chill temperature 9 | 10 | The wind-chill, :math:`T_{wc}`, is based on 11 | air temperature, T, and wind speed, V. 12 | 13 | See https://en.wikipedia.org/wiki/Wind_chill 14 | 15 | .. math:: 16 | 17 | T_{wc}(T_a, V) = 13.12 + 0.6215 T_a - 11.37 V^{0.16} + 0.3965 18 | T_a V^{0.16} 19 | 20 | :param T: Temperature in °C 21 | :param V: Wind Speed in kph 22 | :returns: Wind-Chill temperature in °C 23 | :raises ValueError: for wind speeds under 4.8 kph or T above 10°C 24 | 25 | >>> round(Twc(-10, 25), 1) 26 | -18.8 27 | """ 28 | if V < 4.8 or T > 10.0: 29 | raise ValueError( 30 | "V must be over 4.8 kph, T must be below 10°C") 31 | return ( 32 | 13.12 + 0.6215 * T 33 | - 11.37 * V ** 0.16 + 0.3965 * T * V ** 0.16 34 | ) 35 | 36 | 37 | from pytest import approx # type: ignore 38 | 39 | 40 | def test_Twc(): 41 | assert Twc(-10, 25) == approx(-18.76076) 42 | -------------------------------------------------------------------------------- /Chapter_03/ch03_r11.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 3, Recipe 11, Writing reusable scripts with the script-library switch 4 | 5 | Note: Output from this is used in Chapter 4 examples. 6 | """ 7 | import csv 8 | from pathlib import Path 9 | from math import radians, sin, cos, sqrt, asin 10 | from functools import partial 11 | from pathlib import Path 12 | 13 | MI = 3959 14 | NM = 3440 15 | KM = 6373 16 | 17 | 18 | def haversine( 19 | lat_1: float, lon_1: float, lat_2: float, lon_2: float, *, R: float 20 | ) -> float: 21 | """Distance between points. 22 | 23 | R is radius, R=MI computes in miles. Default is nautical miles. 24 | 25 | >>> round(haversine(36.12, -86.67, 33.94, -118.40, R=6372.8), 5) 26 | 2887.25995 27 | """ 28 | Δ_lat = radians(lat_2) - radians(lat_1) 29 | Δ_lon = radians(lon_2) - radians(lon_1) 30 | lat_1 = radians(lat_1) 31 | lat_2 = radians(lat_2) 32 | 33 | a = sqrt(sin(Δ_lat / 2) ** 2 + cos(lat_1) * cos(lat_2) * sin(Δ_lon / 2) ** 2) 34 | c = 2 * asin(a) 35 | 36 | return R * c 37 | 38 | 39 | nm_haversine = partial(haversine, R=NM) 40 | 41 | 42 | def distances(source_path: Path = Path("data/waypoints.csv")) -> None: 43 | with source_path.open() as source_file: 44 | reader = csv.DictReader(source_file) 45 | start = next(reader) # Seed pairing with the first row 46 | for point in reader: 47 | d = nm_haversine( 48 | float(start["lat"]), 49 | float(start["lon"]), 50 | float(point["lat"]), 51 | float(point["lon"]), 52 | ) 53 | print(start, point, d) 54 | start = point 55 | 56 | 57 | def test_distance(capsys): 58 | distances() 59 | out, err = capsys.readouterr() 60 | assert out.splitlines() == [ 61 | "{'lat': '32.8321666666667', 'lon': '-79.9338333333333', 'date': " 62 | "'2012-11-27', 'time': '09:15:00'} {'lat': '31.6714833333333', 'lon': " 63 | "'-80.93325', 'date': '2012-11-28', 'time': '00:00:00'} 86.20442280809627", 64 | "{'lat': '31.6714833333333', 'lon': '-80.93325', 'date': '2012-11-28', " 65 | "'time': '00:00:00'} {'lat': '30.7171666666667', 'lon': '-81.5525', 'date': " 66 | "'2012-11-28', 'time': '11:35:00'} 65.5310772193093", 67 | ] 68 | 69 | 70 | if __name__ == "__main__": 71 | distances() 72 | -------------------------------------------------------------------------------- /Chapter_03/examples.txt: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2 | 3 | Chapter 3, Examples from the text. 4 | 5 | Note: Output from these examples is used in Chapter 4 examples. 6 | 7 | • Function parameters and type hints 8 | • Designing functions with optional parameters 9 | • Type hints for optional parameters 10 | • Using super flexible keyword parameters 11 | • Forcing keyword-only arguments with the * separator 12 | • Defining position-only parameters with the / separator 13 | • Writing more complex types of function parameters 14 | • Picking an order for parameters based on partial functions 15 | • Writing clear documentation strings with RST markup 16 | • Designing recursive functions around Python's stack limits 17 | • Writing reusable scripts with the script library switch 18 | 19 | This includes a few interactive examples 20 | """ 21 | 22 | 23 | 24 | # Function parameters and type hints 25 | >>> def fibo(n: int) -> int: 26 | ... if n <= 1: return 1 27 | ... return fibo(n-1)+fibo(n-2) 28 | >>> fibo(6) 29 | 13 30 | 31 | # Designing functions with optional parameters 32 | 33 | 34 | # Defining position-only parameters with the / separator 35 | 36 | >>> import math 37 | >>> math.sin(x=0.5) 38 | Traceback (most recent call last): 39 | File "/Users/slott/miniconda3/envs/cookbook2/lib/python3.9/doctest.py", line 1334, in __run 40 | exec(compile(example.source, filename, "single", 41 | File "", line 1, in 42 | math.sin(x=0.5) 43 | TypeError: math.sin() takes no keyword arguments 44 | 45 | 46 | # Recipe 9 47 | >>> from math import sin, cos, asin, sqrt, radians 48 | >>> MI = 3959 49 | >>> NM = 3440 50 | >>> KM = 6372 51 | 52 | >>> def haversine(lat_1: float, lon_1: float, 53 | ... lat_2: float, lon_2: float, R: float) -> float: 54 | ... """Distance between points. 55 | ... 56 | ... R is Earth's radius. 57 | ... R=MI computes in miles. Default is nautical miles. 58 | ... 59 | ... >>> round(haversine(36.12, -86.67, 33.94, -118.40, R=6372.8), 5) 60 | ... 2887.25995 61 | ... """ 62 | ... Δ_lat = radians(lat_2) - radians(lat_1) 63 | ... Δ_lon = radians(lon_2) - radians(lon_1) 64 | ... lat_1 = radians(lat_1) 65 | ... lat_2 = radians(lat_2) 66 | ... 67 | ... a = sin(Δ_lat/2)**2 + cos(lat_1)*cos(lat_2)*sin(Δ_lon/2)**2 68 | ... c = 2*asin(sqrt(a)) 69 | ... 70 | ... return R * c 71 | 72 | >>> round(haversine(36.12, -86.67, 33.94, -118.40, R=6372.8), 5) 73 | 2887.25995 74 | 75 | >>> def nm_haversine(*args): 76 | ... return haversine(*args, R=NM) 77 | 78 | >>> round(nm_haversine(36.12, -86.67, 33.94, -118.40), 2) 79 | 1558.53 80 | 81 | >>> round(nm_haversine(36.12, -86.67, 33.94, -118.40), 2) 82 | 1558.53 83 | 84 | >>> def Twc(T, V): 85 | ... """Wind Chill Temperature.""" 86 | ... if V < 4.8 or T > 10.0: 87 | ... raise ValueError("V must be over 4.8 kph, T must be below 10°C") 88 | ... return 13.12 + 0.6215*T - 11.37*V**0.16 + 0.3965*T*V**0.16 89 | 90 | >>> round(Twc(-10, 25), 1) 91 | -18.8 92 | 93 | -------------------------------------------------------------------------------- /Chapter_04/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Modern-Python-Cookbook-Second-Edition/0182ee7f17ae58cff2775290547321ebed443ae3/Chapter_04/__init__.py -------------------------------------------------------------------------------- /Chapter_04/ch04_r01.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 4, recipe 1, Choosing a data structure 4 | """ 5 | 6 | def confirm() -> bool: 7 | yes = {"yes", "y"} 8 | no = {"no", "n"} 9 | while (answer := input("Confirm: ")).lower() not in (yes | no): 10 | print("Please respond with yes or no") 11 | return answer in yes 12 | 13 | 14 | test_confirm = """ 15 | >>> from unittest.mock import Mock, patch 16 | >>> mock_input = Mock(side_effect=["what", "yes"]) 17 | >>> __builtins__['input'] = mock_input # Patch 18 | >>> answer = confirm() 19 | Please respond with yes or no 20 | >>> answer 21 | True 22 | """ 23 | 24 | test_examples = """ 25 | >>> yes = {"yes", "y"} 26 | >>> no = {"no", "n"} 27 | >>> valid_inputs = yes | no 28 | >>> valid_inputs.add("y") 29 | >>> valid_inputs == {'yes', 'no', 'n', 'y'} 30 | True 31 | 32 | >>> month_name_list = ["Jan", "Feb", "Mar", "Apr", 33 | ... "May", "Jun", "Jul", "Aug", 34 | ... "Sep", "Oct", "Nov", "Dec"] 35 | >>> month_name_list[8] 36 | 'Sep' 37 | >>> month_name_list.index("Feb") 38 | 1 39 | 40 | >>> scheme = {"Crimson": (220, 14, 60), 41 | ... "DarkCyan": (0, 139, 139), 42 | ... "Yellow": (255, 255, 00)} 43 | >>> scheme['Crimson'] 44 | (220, 14, 60) 45 | """ 46 | 47 | 48 | __test__ = {n: v for n, v in locals().items() if n.startswith("test_")} 49 | -------------------------------------------------------------------------------- /Chapter_04/ch04_r02.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 4, recipe 2, Building lists – literals, appending, and comprehensions 4 | 5 | Some of these depend in other chapter source files 6 | of known sizes. Changes to other examples will lead to 7 | changes here. 8 | """ 9 | from pathlib import Path 10 | 11 | test_raw_data = """ 12 | >>> home = Path.cwd() 13 | >>> for path in home.glob('data/*.csv'): 14 | ... print(path.stat().st_size, path.name) 15 | 1810 wc1.csv 16 | 28 ex2_r12.csv 17 | 1790 wc.csv 18 | 160 ch07_r13.csv 19 | 215 sample.csv 20 | 45 craps.csv 21 | 28 output.csv 22 | 225 fuel.csv 23 | 166 waypoints.csv 24 | 39 quotient.csv 25 | 412 summary_log.csv 26 | 156 fuel2.csv 27 | """ 28 | 29 | test_gather = """ 30 | >>> file_sizes = [] 31 | >>> home = Path.cwd() 32 | >>> for path in home.glob('data/*.csv'): 33 | ... file_sizes.append(path.stat().st_size) 34 | >>> print(file_sizes) 35 | [1810, 28, 1790, 160, 215, 45, 28, 225, 166, 39, 412, 156] 36 | >>> print(sum(file_sizes)) 37 | 5074 38 | """ 39 | 40 | 41 | test_comprehensions = """ 42 | >>> home = Path.cwd() 43 | >>> [path.stat().st_size 44 | ... for path in home.glob('data/*.csv')] 45 | [1810, 28, 1790, 160, 215, 45, 28, 225, 166, 39, 412, 156] 46 | """ 47 | 48 | test_generator = """ 49 | >>> home = Path.cwd() 50 | >>> list(path.stat().st_size 51 | ... for path in home.glob('data/*.csv')) 52 | [1810, 28, 1790, 160, 215, 45, 28, 225, 166, 39, 412, 156] 53 | 54 | >>> sizes = list(path.stat().st_size 55 | ... for path in home.glob('data/*.csv')) 56 | >>> sum(sizes) 57 | 5074 58 | >>> max(sizes) 59 | 1810 60 | >>> min(sizes) 61 | 28 62 | >>> from statistics import mean 63 | >>> round(mean(sizes), 3) 64 | 422.833 65 | >>> sizes.index(min(sizes)) 66 | 1 67 | """ 68 | 69 | test_list_extend = """ 70 | >>> home = Path.cwd() 71 | >>> ch3 = list(path.stat().st_size 72 | ... for path in home.glob('Chapter_03/*.py')) 73 | >>> ch4 = list(path.stat().st_size 74 | ... for path in home.glob('Chapter_04/*.py')) 75 | >>> len(ch3) 76 | 12 77 | >>> len(ch4) 78 | 10 79 | >>> final = ch3 + ch4 80 | >>> len(final) 81 | 22 82 | >>> sum(final) 83 | 45216 84 | 85 | >>> final_ex = [] 86 | >>> final_ex.extend(ch3) 87 | >>> final_ex.extend(ch4) 88 | >>> len(final_ex) 89 | 22 90 | >>> sum(final_ex) 91 | 45216 92 | """ 93 | 94 | test_insert = """ 95 | >>> p = [3, 5, 11, 13] 96 | >>> p.insert(0, 2) 97 | >>> p 98 | [2, 3, 5, 11, 13] 99 | >>> p.insert(3, 7) 100 | >>> p 101 | [2, 3, 5, 7, 11, 13] 102 | """ 103 | 104 | 105 | __test__ = {n: v for n, v in locals().items() if n.startswith("test_")} 106 | -------------------------------------------------------------------------------- /Chapter_04/ch04_r03.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 4, recipe 3, Slicing and dicing a list 4 | """ 5 | 6 | import csv 7 | from pathlib import Path 8 | from typing import List, Any 9 | 10 | 11 | def get_fuel_use(path: Path) -> List[List[Any]]: 12 | with path.open() as source_file: 13 | reader = csv.reader(source_file) 14 | log_rows = list(reader) 15 | return log_rows 16 | 17 | 18 | test_load = """ 19 | >>> from pprint import pprint 20 | >>> log_rows = get_fuel_use(Path('data/fuel.csv')) 21 | >>> log_rows[0] 22 | ['date', 'engine on', 'fuel height'] 23 | >>> log_rows[-1] 24 | ['', "choppy -- anchor in jackson's creek", ''] 25 | >>> pprint(log_rows) 26 | [['date', 'engine on', 'fuel height'], 27 | ['', 'engine off', 'fuel height'], 28 | ['', 'Other notes', ''], 29 | ['', '', ''], 30 | ['10/25/13', '08:24:00 AM', '29'], 31 | ['', '01:15:00 PM', '27'], 32 | ['', "calm seas -- anchor solomon's island", ''], 33 | ['10/26/13', '09:12:00 AM', '27'], 34 | ['', '06:25:00 PM', '22'], 35 | ['', "choppy -- anchor in jackson's creek", '']] 36 | """ 37 | 38 | # Test Data used below. 39 | log_rows = [ 40 | ["date", "engine on", "fuel height"], 41 | ["", "engine off", "fuel height"], 42 | ["", "Other notes", ""], 43 | ["", "", ""], 44 | ["10/25/13", "08:24:00 AM", "29"], 45 | ["", "01:15:00 PM", "27"], 46 | ["", "calm seas -- anchor solomon's island", ""], 47 | ["10/26/13", "09:12:00 AM", "27"], 48 | ["", "06:25:00 PM", "22"], 49 | ["", "choppy -- anchor in jackson's creek", ""], 50 | ] 51 | 52 | test_recipe = """ 53 | >>> from pprint import pprint 54 | 55 | Step 1 56 | 57 | >>> head, tail = log_rows[:4], log_rows[4:] 58 | >>> head[0] 59 | ['date', 'engine on', 'fuel height'] 60 | >>> head[-1] 61 | ['', '', ''] 62 | >>> tail[0] 63 | ['10/25/13', '08:24:00 AM', '29'] 64 | >>> tail[-1] 65 | ['', "choppy -- anchor in jackson's creek", ''] 66 | >>> pprint(tail[0::3]) 67 | [['10/25/13', '08:24:00 AM', '29'], ['10/26/13', '09:12:00 AM', '27']] 68 | >>> pprint(tail[1::3]) 69 | [['', '01:15:00 PM', '27'], ['', '06:25:00 PM', '22']] 70 | 71 | 72 | >>> tail[2::3] # doctest: +NORMALIZE_WHITESPACE 73 | [['', "calm seas -- anchor solomon's island", ''], 74 | ['', "choppy -- anchor in jackson's creek", '']] 75 | 76 | Step 2 77 | 78 | >>> list(zip(tail[0::3], tail[1::3])) # doctest: +NORMALIZE_WHITESPACE 79 | [(['10/25/13', '08:24:00 AM', '29'], ['', '01:15:00 PM', '27']), 80 | (['10/26/13', '09:12:00 AM', '27'], ['', '06:25:00 PM', '22'])] 81 | 82 | Step 3 83 | 84 | >>> list(zip(tail[0::3], tail[1::3])) # doctest: +NORMALIZE_WHITESPACE 85 | [(['10/25/13', '08:24:00 AM', '29'], ['', '01:15:00 PM', '27']), 86 | (['10/26/13', '09:12:00 AM', '27'], ['', '06:25:00 PM', '22'])] 87 | 88 | Final 89 | 90 | >>> paired_rows = list( zip(tail[0::3], tail[1::3]) ) 91 | >>> [a+b for a, b in paired_rows] 92 | [['10/25/13', '08:24:00 AM', '29', '', '01:15:00 PM', '27'], ['10/26/13', '09:12:00 AM', '27', '', '06:25:00 PM', '22']] 93 | 94 | """ 95 | 96 | 97 | __test__ = {n: v for n, v in locals().items() if n.startswith("test_")} 98 | -------------------------------------------------------------------------------- /Chapter_04/ch04_r05.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 4, recipe 5, Writing list-related type hints 4 | """ 5 | 6 | from typing import List, Tuple, Union 7 | 8 | scheme = [ 9 | ("Brick_Red", (198, 45, 66)), 10 | ("color1", (198.00, 100.50, 45.00)), 11 | ("color2", (198.00, 45.00, 142.50)), 12 | ] 13 | 14 | RGB_I = Tuple[int, int, int] 15 | RGB_F = Tuple[float, float, float] 16 | ColorRGB = Tuple[str, Union[RGB_I, RGB_F]] 17 | ColorRGBList = List[ColorRGB] 18 | 19 | BadIdea = List[Tuple[str, Union[Tuple[int, int, int], Tuple[float, float, float]]]] 20 | 21 | 22 | def hexify(r: float, g: float, b: float) -> str: 23 | """ 24 | >>> hexify(198, 45, 66) 25 | '#C62D42' 26 | >>> r, g, b = 198, 45, 66 27 | >>> f"#{int(r):02X}{int(g):02X}{int(b):02X}" 28 | '#C62D42' 29 | """ 30 | return f"#{int(r)<<16 | int(g)<<8 | int(b):06X}" 31 | 32 | 33 | ColorCode = Tuple[str, str] 34 | ColorCodeList = List[ColorCode] 35 | 36 | 37 | def source_to_hex(src: ColorRGBList) -> ColorCodeList: 38 | return [(n, hexify(*color)) for n, color in src] 39 | 40 | 41 | test_list = """ 42 | >>> scheme 43 | [('Brick_Red', (198, 45, 66)), ('color1', (198.0, 100.5, 45.0)), ('color2', (198.0, 45.0, 142.5))] 44 | >>> source_to_hex(scheme) 45 | [('Brick_Red', '#C62D42'), ('color1', '#C6642D'), ('color2', '#C62D8E')] 46 | """ 47 | 48 | __test__ = {n: v for n, v in locals().items() if n.startswith("test_")} 49 | -------------------------------------------------------------------------------- /Chapter_04/ch04_r06.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 4, recipe 6, Reversing a copy of a list 4 | """ 5 | 6 | test_construct = """ 7 | >>> week = 13 8 | >>> day = 2 9 | >>> hour = 7 10 | >>> minute = 53 11 | >>> second = 19 12 | >>> t_s = (((week*7+day)*24+hour)*60+minute)*60+second 13 | >>> t_s 14 | 8063599 15 | """ 16 | 17 | test_destruct = """ 18 | >>> t_s = 8063599 19 | >>> fields = [] 20 | >>> for b in 60, 60, 24, 7: 21 | ... t_s, f = divmod(t_s, b) 22 | ... fields.append(f) 23 | >>> fields.append(t_s) 24 | >>> fields 25 | [19, 53, 7, 2, 13] 26 | 27 | One reversal. 28 | 29 | >>> fields_copy1 = fields.copy() 30 | >>> fields_copy1.reverse() 31 | >>> fields_copy1 32 | [13, 2, 7, 53, 19] 33 | 34 | Another reversal. 35 | 36 | >>> fields_copy2 = fields[::-1] 37 | >>> fields_copy2 38 | [13, 2, 7, 53, 19] 39 | 40 | A third. 41 | 42 | >>> fields_copy3 = list(reversed(fields)) 43 | >>> fields_copy3 44 | [13, 2, 7, 53, 19] 45 | """ 46 | 47 | test_walrus = """ 48 | >>> t_s = (8063599, 0) 49 | >>> fields = [(t_s := divmod(t_s[0], b))[1] for b in (60, 60, 24, 7)] 50 | >>> list(reversed(fields + [t_s[0]])) 51 | [13, 2, 7, 53, 19] 52 | """ 53 | 54 | 55 | __test__ = {n: v for n, v in locals().items() if n.startswith("test_")} 56 | -------------------------------------------------------------------------------- /Chapter_04/ch04_r08.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 4, recipe 8, Removing items from a set – remove(), pop(), and difference 4 | """ 5 | import re 6 | 7 | 8 | log = """ 9 | [2016-03-05T09:29:31-05:00] INFO: Processing ruby_block[print IP] action run (@recipe_files::/home/slott/ch4/deploy.rb line 9) 10 | [2016-03-05T09:29:31-05:00] INFO: Installed IP: 111.222.111.222 11 | [2016-03-05T09:29:31-05:00] INFO: ruby_block[print IP] called 12 | 13 | - execute the ruby block print IP 14 | [2016-03-05T09:29:31-05:00] INFO: Chef Run complete in 23.233811181 seconds 15 | 16 | Running handlers: 17 | [2016-03-05T09:29:31-05:00] INFO: Running report handlers 18 | Running handlers complete 19 | [2016-03-05T09:29:31-05:00] INFO: Report handlers complete 20 | Chef Client finished, 2/2 resources updated in 29.233811181 seconds 21 | """ 22 | 23 | test_subtract = r""" 24 | >>> pattern = re.compile(r"IP: \d+\.\d+\.\d+\.\d+") 25 | >>> matches = set(pattern.findall(log)) 26 | >>> matches 27 | {'IP: 111.222.111.222'} 28 | 29 | >>> to_be_ignored = {'IP: 0.0.0.0', 'IP: 1.2.3.4'} 30 | >>> matches = {'IP: 111.222.111.222', 'IP: 1.2.3.4'} 31 | >>> matches - to_be_ignored 32 | {'IP: 111.222.111.222'} 33 | >>> matches.difference(to_be_ignored) 34 | {'IP: 111.222.111.222'} 35 | 36 | >>> valid_matches = matches - to_be_ignored 37 | >>> valid_matches 38 | {'IP: 111.222.111.222'} 39 | """ 40 | 41 | test_difference = r""" 42 | >>> pattern = re.compile(r"IP: \d+\.\d+\.\d+\.\d+") 43 | >>> matches = set(pattern.findall(log)) 44 | >>> to_be_ignored = {'IP: 0.0.0.0', 'IP: 1.2.3.4'} 45 | 46 | >>> matches.difference_update(to_be_ignored) 47 | >>> matches 48 | {'IP: 111.222.111.222'} 49 | """ 50 | 51 | test_remove = r""" 52 | >>> pattern = re.compile(r"IP: \d+\.\d+\.\d+\.\d+") 53 | >>> matches = set(pattern.findall(log)) 54 | >>> to_be_ignored = {'IP: 0.0.0.0', 'IP: 1.2.3.4'} 55 | 56 | >>> for item in to_be_ignored: 57 | ... if item in matches: 58 | ... matches.remove(item) 59 | 60 | >>> matches 61 | {'IP: 111.222.111.222'} 62 | """ 63 | 64 | __test__ = {n: v for n, v in locals().items() if n.startswith("test_")} 65 | -------------------------------------------------------------------------------- /Chapter_04/ch04_r09.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 4, recipe 9, Set-related type hints 4 | """ 5 | 6 | import collections 7 | from enum import Enum 8 | import random 9 | from typing import List, Tuple, Set 10 | 11 | 12 | class Die(str, Enum): 13 | d_1 = "\u2680" 14 | d_2 = "\u2681" 15 | d_3 = "\u2682" 16 | d_4 = "\u2683" 17 | d_5 = "\u2684" 18 | d_6 = "\u2685" 19 | 20 | 21 | def zonk(n: int = 6) -> Tuple[Die, ...]: 22 | faces = list(Die) 23 | return tuple(random.choice(faces) for _ in range(n)) 24 | 25 | 26 | test_zonk = """ 27 | >>> random.seed(42) 28 | >>> zonk() 29 | (, , , , , ) 30 | 31 | """ 32 | 33 | 34 | def eval_zonk_6(hand: Tuple[Die, ...]) -> str: 35 | assert len(hand) == 6, "Only works for 6-dice zonk." 36 | faces = list(Die) 37 | small_straights = [ 38 | set(faces[:-1]), set(faces[1:]) 39 | ] 40 | unique: Set[Die] = set(hand) 41 | # print(f"{unique=}") 42 | if len(unique) == 6: 43 | return "large straight" 44 | elif len(unique) == 5 and unique in small_straights: 45 | return "small straight" 46 | elif len(unique) == 2: 47 | return "three of a kind" 48 | elif len(unique) == 1: 49 | return "six of a kind!" 50 | elif len(unique) in {3, 4}: 51 | # len(unique) == 4: wwwxyz (good) or wwxxyz (non-scoring) 52 | # len(unique) == 3: xxxxyz, xxxyyz (good) or xxyyzz (non-scoring) 53 | frequencies: Set[int] = set(collections.Counter(hand).values()) 54 | # print(f"{frequencies=}") 55 | if 3 in frequencies or 4 in frequencies: 56 | return "three of a kind" 57 | elif Die.d_1 in unique: 58 | return "ace" 59 | return "Zonk!" 60 | 61 | 62 | test_eval_zonk_6 = """ 63 | >>> eval_zonk_6([Die.d_1, Die.d_1, Die.d_1, Die.d_1, Die.d_1, Die.d_1]) 64 | 'six of a kind!' 65 | >>> eval_zonk_6([Die.d_1, Die.d_2, Die.d_1, Die.d_1, Die.d_1, Die.d_1]) 66 | 'three of a kind' 67 | >>> eval_zonk_6([Die.d_1, Die.d_2, Die.d_3, Die.d_2, Die.d_2, Die.d_2]) 68 | 'three of a kind' 69 | >>> eval_zonk_6([Die.d_1, Die.d_2, Die.d_1, Die.d_2, Die.d_1, Die.d_2]) 70 | 'three of a kind' 71 | >>> eval_zonk_6([Die.d_1, Die.d_2, Die.d_3, Die.d_4, Die.d_5, Die.d_2]) 72 | 'small straight' 73 | >>> eval_zonk_6([Die.d_1, Die.d_2, Die.d_3, Die.d_4, Die.d_5, Die.d_6]) 74 | 'large straight' 75 | >>> eval_zonk_6([Die.d_1, Die.d_2, Die.d_3, Die.d_2, Die.d_3, Die.d_4]) 76 | 'ace' 77 | >>> eval_zonk_6([Die.d_2, Die.d_2, Die.d_3, Die.d_3, Die.d_4, Die.d_4]) 78 | 'Zonk!' 79 | >>> eval_zonk_6([Die.d_1, Die.d_2, Die.d_3, Die.d_3, Die.d_5, Die.d_6]) 80 | 'Zonk!' 81 | 82 | """ 83 | 84 | test_integration = """ 85 | >>> random.seed(42) 86 | >>> roll = zonk() 87 | >>> roll 88 | (, , , , , ) 89 | >>> eval_zonk_6(roll) 90 | 'ace' 91 | >>> roll = zonk() 92 | >>> roll 93 | (, , , , , ) 94 | >>> eval_zonk_6(roll) 95 | 'three of a kind' 96 | """ 97 | 98 | __test__ = {n: v for n, v in locals().items() if n.startswith("test_")} 99 | -------------------------------------------------------------------------------- /Chapter_05/ch05_r03.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 5, recipe 3, Controlling the order of dict keys 4 | """ 5 | 6 | import collections 7 | import csv 8 | from pathlib import Path 9 | from typing import List, Dict, cast, Sequence 10 | 11 | 12 | def get_fuel_use(source_path: Path) -> List[Dict[str, str]]: 13 | with source_path.open() as source_file: 14 | rdr = csv.DictReader(source_file) 15 | data: List[Dict[str, str]] = list(rdr) 16 | return data 17 | 18 | 19 | test_get_fuel_use = """ 20 | >>> source_path = Path("data/fuel2.csv") 21 | >>> fuel_use = get_fuel_use(source_path) 22 | >>> for row in fuel_use: 23 | ... print(row) 24 | {'date': '10/25/13', 'engine on': '08:24:00', 'fuel height on': '29', 'engine off': '13:15:00', 'fuel height off': '27'} 25 | {'date': '10/26/13', 'engine on': '09:12:00', 'fuel height on': '27', 'engine off': '18:25:00', 'fuel height off': '22'} 26 | {'date': '10/28/13', 'engine on': '13:21:00', 'fuel height on': '22', 'engine off': '06:25:00', 'fuel height off': '14'} 27 | """ 28 | 29 | 30 | def get_fuel_use_od(source_path: Path) -> List[Dict[str, str]]: 31 | with source_path.open() as source_file: 32 | rdr = csv.DictReader(source_file) 33 | od = ( 34 | collections.OrderedDict( 35 | [(column, row[column]) 36 | for column in cast(Sequence[str], rdr.fieldnames)] 37 | ) 38 | for row in rdr 39 | ) 40 | data: List[Dict[str, str]] = list(od) 41 | return data 42 | 43 | 44 | test_get_fuel_use_od = """ 45 | >>> source_path = Path("data/fuel2.csv") 46 | >>> fuel_use = get_fuel_use_od(source_path) 47 | >>> for row in fuel_use: 48 | ... print(row) 49 | OrderedDict([('date', '10/25/13'), ('engine on', '08:24:00'), ('fuel height on', '29'), ('engine off', '13:15:00'), ('fuel height off', '27')]) 50 | OrderedDict([('date', '10/26/13'), ('engine on', '09:12:00'), ('fuel height on', '27'), ('engine off', '18:25:00'), ('fuel height off', '22')]) 51 | OrderedDict([('date', '10/28/13'), ('engine on', '13:21:00'), ('fuel height on', '22'), ('engine off', '06:25:00'), ('fuel height off', '14')]) 52 | """ 53 | 54 | import datetime 55 | 56 | parse_date = lambda text: datetime.datetime.strptime(text, "%m/%d/%y").date() 57 | parse_time = lambda text: datetime.datetime.strptime(text, "%H:%M:%S").time() 58 | 59 | 60 | def summarize(data: List[Dict[str, str]]) -> None: 61 | for row in data: 62 | date = parse_date(row["date"]) 63 | start = datetime.datetime.combine(date, parse_time(row["engine on"])) 64 | end = datetime.datetime.combine(date, parse_time(row["engine off"])) 65 | print( 66 | f"{(end-start).seconds/60/60:.1f} hr. {float(row['fuel height on'])} in. to {float(row['fuel height off'])} in." 67 | ) 68 | 69 | 70 | test_summarize = """ 71 | >>> source_path = Path("data/fuel2.csv") 72 | >>> fuel_use = get_fuel_use_od(source_path) 73 | >>> summarize(fuel_use) 74 | 4.8 hr. 29.0 in. to 27.0 in. 75 | 9.2 hr. 27.0 in. to 22.0 in. 76 | 17.1 hr. 22.0 in. to 14.0 in. 77 | """ 78 | 79 | __test__ = {n: v for n, v in locals().items() if n.startswith("test_")} 80 | -------------------------------------------------------------------------------- /Chapter_05/ch05_r07.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 5, recipe 7, Avoiding mutable default values for function parameters 4 | 5 | """ 6 | 7 | import collections 8 | from random import randint, seed 9 | from typing import Counter, Optional, Callable, TypeVar, Iterable 10 | 11 | 12 | def gather_stats_bad( 13 | n: int, 14 | samples: int = 1000, 15 | summary: Counter[int] = collections.Counter() 16 | ) -> Counter: 17 | summary.update( 18 | sum(randint(1, 6) 19 | for d in range(n)) for _ in range(samples)) 20 | return summary 21 | 22 | 23 | test_default_counter = """ 24 | >>> seed(1) 25 | >>> s1 = gather_stats_bad(2) 26 | >>> s1 27 | Counter({7: 168, 6: 147, 8: 136, 9: 114, 5: 110, 10: 77, 11: 71, 4: 70, 3: 52, 12: 29, 2: 26}) 28 | """ 29 | 30 | test_explicit_counter = """ 31 | >>> seed(1) 32 | >>> mc = Counter() 33 | >>> gather_stats_bad(2, summary=mc) # doctest: +ELLIPSIS 34 | Counter... 35 | >>> mc 36 | Counter({7: 168, 6: 147, 8: 136, 9: 114, 5: 110, 10: 77, 11: 71, 4: 70, 3: 52, 12: 29, 2: 26}) 37 | """ 38 | 39 | test_default_counter_again = """ 40 | >>> seed(1) 41 | >>> s3b = gather_stats_bad(2) 42 | >>> s3b 43 | Counter({7: 336, 6: 294, 8: 272, 9: 228, 5: 220, 10: 154, 11: 142, 4: 140, 3: 104, 12: 58, 2: 52}) 44 | """ 45 | 46 | 47 | def create_stats(n: int, samples: int = 1000) -> Counter[int]: 48 | return update_stats(n, samples, Counter()) 49 | 50 | 51 | def update_stats(n: int, samples: int, summary: Counter[int]) -> Counter[int]: 52 | summary.update(sum(randint(1, 6) for d in range(n)) for _ in range(samples)) 53 | return summary 54 | 55 | 56 | def gather_stats_good( 57 | n: int, samples: int = 1000, summary: Optional[Counter[int]] = None 58 | ) -> Counter[int]: 59 | if summary is None: 60 | summary = Counter() 61 | summary.update(sum(randint(1, 6) for d in range(n)) for _ in range(samples)) 62 | return summary 63 | 64 | 65 | test_good = """ 66 | >>> seed(1) 67 | >>> s3a = gather_stats_good(2) 68 | >>> seed(1) 69 | >>> s3b = gather_stats_good(2) 70 | >>> s3a 71 | Counter({7: 168, 6: 147, 8: 136, 9: 114, 5: 110, 10: 77, 11: 71, 4: 70, 3: 52, 12: 29, 2: 26}) 72 | >>> s3b 73 | Counter({7: 168, 6: 147, 8: 136, 9: 114, 5: 110, 10: 77, 11: 71, 4: 70, 3: 52, 12: 29, 2: 26}) 74 | >>> s3a is s3b 75 | False 76 | """ 77 | 78 | 79 | T = TypeVar("T") 80 | Summarizer = Callable[[Iterable[T]], Counter[T]] 81 | 82 | 83 | def gather_stats_flex( 84 | n: int, samples: int = 1000, summary_func: Summarizer = collections.Counter 85 | ) -> Counter[int]: 86 | summary = summary_func(sum(randint(1, 6) for d in range(n)) for _ in range(samples)) 87 | return summary 88 | 89 | 90 | test_flex = """ 91 | >>> seed(1) 92 | >>> gather_stats_flex(2, 12, summary_func=list) 93 | [7, 4, 5, 8, 10, 3, 5, 8, 6, 10, 9, 7] 94 | >>> seed(1) 95 | >>> gather_stats_flex(2, 12, summary_func=collections.Counter) 96 | Counter({7: 2, 5: 2, 8: 2, 10: 2, 4: 1, 3: 1, 6: 1, 9: 1}) 97 | >>> seed(1) 98 | >>> gather_stats_flex(2, 12) 99 | Counter({7: 2, 5: 2, 8: 2, 10: 2, 4: 1, 3: 1, 6: 1, 9: 1}) 100 | """ 101 | 102 | 103 | __test__ = {n: v for n, v in locals().items() if n.startswith("test_")} 104 | -------------------------------------------------------------------------------- /Chapter_06/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Modern-Python-Cookbook-Second-Edition/0182ee7f17ae58cff2775290547321ebed443ae3/Chapter_06/__init__.py -------------------------------------------------------------------------------- /Chapter_06/ch06_r01.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 6, recipe 2, Using features of the print() function 4 | """ 5 | from pathlib import Path 6 | import csv 7 | from typing import Dict, List 8 | 9 | 10 | def get_fuel_use(source_path: Path) -> List[Dict[str, str]]: 11 | with source_path.open() as source_file: 12 | rdr = csv.DictReader(source_file) 13 | return list(rdr) 14 | 15 | 16 | test_get_fuel_use = """ 17 | >>> source_path = Path("data/fuel2.csv") 18 | >>> fuel_use = get_fuel_use(source_path) 19 | >>> for row in fuel_use: 20 | ... print(row) 21 | {'date': '10/25/13', 'engine on': '08:24:00', 'fuel height on': '29', 'engine off': '13:15:00', 'fuel height off': '27'} 22 | {'date': '10/26/13', 'engine on': '09:12:00', 'fuel height on': '27', 'engine off': '18:25:00', 'fuel height off': '22'} 23 | {'date': '10/28/13', 'engine on': '13:21:00', 'fuel height on': '22', 'engine off': '06:25:00', 'fuel height off': '14'} 24 | """ 25 | 26 | test_print_1 = """ 27 | >>> fuel_use = get_fuel_use(Path("data/fuel2.csv")) 28 | >>> for leg in fuel_use: 29 | ... start = float(leg["fuel height on"]) 30 | ... finish = float(leg["fuel height off"]) 31 | ... print("On", leg["date"], 32 | ... "from", leg["engine on"], 33 | ... "to", leg["engine off"], 34 | ... "change", start-finish, "in.") 35 | On 10/25/13 from 08:24:00 to 13:15:00 change 2.0 in. 36 | On 10/26/13 from 09:12:00 to 18:25:00 change 5.0 in. 37 | On 10/28/13 from 13:21:00 to 06:25:00 change 8.0 in. 38 | """ 39 | 40 | 41 | test_print_2 = """ 42 | >>> fuel_use = get_fuel_use(Path("data/fuel2.csv")) 43 | >>> print("date", "start", "end", "depth", sep=" | ") 44 | date | start | end | depth 45 | >>> for leg in fuel_use: 46 | ... start = float(leg["fuel height on"]) 47 | ... finish = float(leg["fuel height off"]) 48 | ... print(leg["date"], leg["engine on"], 49 | ... leg["engine off"], start-finish, sep=" | ") 50 | 10/25/13 | 08:24:00 | 13:15:00 | 2.0 51 | 10/26/13 | 09:12:00 | 18:25:00 | 5.0 52 | 10/28/13 | 13:21:00 | 06:25:00 | 8.0 53 | """ 54 | 55 | test_print_3 = """ 56 | >>> fuel_use = get_fuel_use(Path("data/fuel2.csv")) 57 | >>> for leg in fuel_use: 58 | ... start = float(leg["fuel height on"]) 59 | ... finish = float(leg["fuel height off"]) 60 | ... print("date", leg["date"], sep="=", end=", ") 61 | ... print("on", leg["engine on"], sep="=", end=", ") 62 | ... print("off", leg["engine off"], sep="=", end=", ") 63 | ... print("change", start-finish, sep="=") 64 | date=10/25/13, on=08:24:00, off=13:15:00, change=2.0 65 | date=10/26/13, on=09:12:00, off=18:25:00, change=5.0 66 | date=10/28/13, on=13:21:00, off=06:25:00, change=8.0 67 | """ 68 | 69 | 70 | import sys 71 | 72 | 73 | def print_like(*args, sep=None, end=None, file=sys.stdout): 74 | if sep is None: 75 | sep = " " 76 | if end is None: 77 | end = "\n" 78 | arg_iter = iter(args) 79 | value = next(arg_iter) 80 | file.write(str(value)) 81 | for value in arg_iter: 82 | file.write(sep) 83 | file.write(str(value)) 84 | file.write(end) 85 | file.flush() 86 | 87 | 88 | test_print_like = """ 89 | >>> print_like("number", 1, sep="=", end=", ") 90 | >>> print_like("string", "value", sep="=") 91 | """ 92 | 93 | 94 | __test__ = {n: v for n, v in locals().items() if n.startswith("test_")} 95 | -------------------------------------------------------------------------------- /Chapter_06/ch06_r04.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 6, recipe 4, Using argparse to get command-line input 4 | """ 5 | 6 | import argparse 7 | import sys 8 | from typing import Tuple, List 9 | from Chapter_03.ch03_r08 import haversine, MI, NM, KM 10 | 11 | 12 | def point_type(text: str) -> Tuple[float, float]: 13 | """ 14 | >>> point_type('36.12, -86.67') 15 | (36.12, -86.67) 16 | >>> point_type('36.12, 76.abc') 17 | Traceback (most recent call last): 18 | File "/Users/slott/miniconda3/envs/cookbook/lib/python3.8/doctest.py", line 1328, in __run 19 | exec(compile(example.source, filename, "single", 20 | File "", line 1, in 21 | point_type('36.12, 76.abc') 22 | File "Chapter_06.ch06_r04.py", line 22, in point_type 23 | raise argparse.ArgumentTypeError from ex 24 | argparse.ArgumentTypeError: could not convert string to float: ' 76.abc' 25 | """ 26 | try: 27 | lat_str, lon_str = text.split(",") 28 | lat = float(lat_str) 29 | lon = float(lon_str) 30 | return lat, lon 31 | except ValueError as ex: 32 | raise argparse.ArgumentTypeError(ex) 33 | 34 | 35 | def get_options(argv: List[str]) -> argparse.Namespace: 36 | """ 37 | >>> opts = get_options(['-u', 'KM', '36.12,-86.67', '33.94,-118.4']) 38 | >>> opts.units 39 | 'KM' 40 | >>> opts.p1 41 | (36.12, -86.67) 42 | >>> opts.p2 43 | (33.94, -118.4) 44 | """ 45 | parser = argparse.ArgumentParser() 46 | parser.add_argument( 47 | "-u", "--units", 48 | action="store", choices=("NM", "MI", "KM"), default="NM") 49 | parser.add_argument( 50 | "p1", action="store", type=point_type) 51 | parser.add_argument( 52 | "p2", action="store", type=point_type) 53 | options = parser.parse_args(argv) 54 | return options 55 | 56 | 57 | def display(lat1: float, lon1: float, lat2: float, lon2: float, r: str) -> None: 58 | """ 59 | >>> display(36.12, -86.67, 33.94, -118.4, 'NM') 60 | From 36.12,-86.67 to 33.94,-118.4 in NM = 1558.53 61 | """ 62 | r_float = {"NM": NM, "KM": KM, "MI": MI}[r] 63 | d = haversine(lat1, lon1, lat2, lon2, r_float) 64 | print(f"From {lat1},{lon1} to {lat2},{lon2} in {r} = {d:.2f}") 65 | 66 | 67 | def main(argv: List[str] = sys.argv[1:]) -> None: 68 | options = get_options(argv) 69 | lat_1, lon_1 = options.p1 70 | lat_2, lon_2 = options.p2 71 | display(lat_1, lon_1, lat_2, lon_2, options.units) 72 | 73 | 74 | if __name__ == "__main__": 75 | main() 76 | -------------------------------------------------------------------------------- /Chapter_06/ch06_r05_dice.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 6, recipe 5, Using cmd for creating command-line applications 4 | """ 5 | import cmd 6 | import random 7 | from typing import Set, List, Iterable, Optional 8 | 9 | 10 | class DiceCLI(cmd.Cmd): 11 | """A handy tool for rolling a number of dice, used in a variety of games.""" 12 | 13 | use_rawinput = False # sys.stdout.write() and sys.stdin.readline() are used 14 | 15 | prompt = "] " 16 | intro = "A dice rolling tool. ? for help." 17 | 18 | def preloop(self): 19 | self.n_dice = 6 20 | self.dice = None # no initial roll. 21 | self.reroll_count = 0 22 | 23 | def do_dice(self, arg: str) -> bool: 24 | """Sets the number of dice to roll.""" 25 | try: 26 | self.n_dice = int(arg) 27 | self.dice = None 28 | print(f"Rolling {self.n_dice} dice") 29 | except ValueError: 30 | print(f"{arg!r} is invalid") 31 | return False 32 | 33 | def do_EOF(self, arg: str) -> bool: 34 | return True 35 | 36 | def do_quit(self, arg: str) -> bool: 37 | return True 38 | 39 | def emptyline(self) -> bool: 40 | """Shows current state of the dice.""" 41 | if self.dice: 42 | print(f"{self.dice}", end=" ") 43 | if self.reroll_count: 44 | print(f"(reroll {self.reroll_count})") 45 | else: 46 | print() 47 | return False 48 | 49 | def do_roll(self, arg: str) -> bool: 50 | """Roll the dice. Use the dice command to set the number of dice.""" 51 | self.dice = [random.randint(1, 6) for _ in range(self.n_dice)] 52 | print(f"{self.dice}") 53 | return False 54 | 55 | def do_reroll(self, arg: str) -> bool: 56 | """Reroll selected dice. Provide the 0-based positions.""" 57 | try: 58 | positions = map(int, arg.split()) 59 | except ValueError as ex: 60 | print(ex) 61 | return False 62 | for p in positions: 63 | self.dice[p] = random.randint(1, 6) 64 | self.reroll_count += 1 65 | print(f"{self.dice} (reroll {self.reroll_count})") 66 | return False 67 | 68 | 69 | if __name__ == "__main__": 70 | game = DiceCLI() 71 | game.cmdloop() 72 | 73 | 74 | ### Unit test ### 75 | 76 | from unittest.mock import Mock, call 77 | 78 | 79 | def test_command(capsys): 80 | mock_input = Mock( 81 | readline=Mock(side_effect=["roll", "reroll 1 2 4 5", "roll", "quit"]) 82 | ) 83 | mock_output = Mock() 84 | random.seed(42) 85 | r = DiceCLI(stdin=mock_input, stdout=mock_output) 86 | r.cmdloop() 87 | assert mock_output.write.mock_calls == [ 88 | call("A dice rolling tool. ? for help.\n"), 89 | call("] "), 90 | call("] "), 91 | call("] "), 92 | call("] "), 93 | ] 94 | out, err = capsys.readouterr() 95 | assert out.splitlines() == [ 96 | "[6, 1, 1, 6, 3, 2]", 97 | "[6, 2, 2, 6, 6, 1] (reroll 1)", 98 | "[6, 6, 5, 1, 5, 4]", 99 | ] 100 | -------------------------------------------------------------------------------- /Chapter_06/ch06_r06.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 6, recipe 6, Using the OS environment settings 4 | """ 5 | import argparse 6 | import os 7 | import sys 8 | from typing import List 9 | 10 | from Chapter_03.ch03_r08 import haversine, MI, NM, KM 11 | from Chapter_06.ch06_r04 import point_type, display 12 | 13 | 14 | def get_options(argv: List[str] = sys.argv[1:]) -> argparse.Namespace: 15 | """ 16 | >>> os.environ['UNITS'] = 'NM' 17 | >>> get_options(['36.12,-86.67']) 18 | Traceback (most recent call last): 19 | File "/Users/slott/miniconda3/envs/cookbook/lib/python3.8/doctest.py", line 1328, in __run 20 | compileflags, 1), test.globs) 21 | File "", line 1, in 22 | get_options(['ch06_r06.py', '36.12,-86.67']) 23 | File "/Users/slott/Documents/Writing/Python/Python Cookbook 2e/Code/ch06_r06.py", line 49, in get_options 24 | sys.exit("Neither HOME_PORT nor p2 argument provided.") 25 | SystemExit: Neither HOME_PORT nor p2 argument provided. 26 | 27 | >>> os.environ['UNITS'] = 'NM' 28 | >>> os.environ['HOME_PORT'] = '36.842952,-76.300171' 29 | >>> get_options(['36.12,-86.67']) 30 | Namespace(units='NM', p1=(36.12, -86.67), p2=(36.842952, -76.300171)) 31 | 32 | >>> os.environ['UNITS'] = 'XX' 33 | >>> os.environ['HOME_PORT'] = '36.842952,-76.300171' 34 | >>> get_options(['36.12,-86.67']) 35 | Traceback (most recent call last): 36 | File "/Users/slott/miniconda3/envs/cookbook/lib/python3.8/doctest.py", line 1328, in __run 37 | compileflags, 1), test.globs) 38 | File "", line 1, in 39 | get_options(['ch06_r06.py', '36.12,-86.67']) 40 | File "/Users/slott/Documents/Writing/Python/Python Cookbook 2e/Code/ch06_r06.py", line 27, in get_options 41 | sys.exit("Invalid value for UNITS, not KM, NM, or MI") 42 | SystemExit: Invalid UNITS, 'XX' not KM, NM, or MI 43 | """ 44 | default_units = os.environ.get("UNITS", "KM") 45 | if default_units not in ("KM", "NM", "MI"): 46 | sys.exit(f"Invalid UNITS, {default_units!r} not KM, NM, or MI") 47 | default_home_port = os.environ.get("HOME_PORT") 48 | parser = argparse.ArgumentParser() 49 | 50 | parser.add_argument( 51 | "-u", "--units", 52 | action="store", choices=("NM", "MI", "KM"), default=default_units 53 | ) 54 | parser.add_argument("p1", action="store", type=point_type) 55 | parser.add_argument( 56 | "p2", nargs="?", action="store", type=point_type, default=default_home_port 57 | ) 58 | options = parser.parse_args(argv) 59 | 60 | if options.p2 is None: 61 | sys.exit("Neither HOME_PORT nor p2 argument provided.") 62 | 63 | return options 64 | 65 | 66 | def main(argv: List[str] = sys.argv[1:]) -> None: 67 | options = get_options() 68 | lat_1, lon_1 = options.p1 69 | lat_2, lon_2 = options.p2 70 | display(lat_1, lon_1, lat_2, lon_2, options.units) 71 | 72 | 73 | if __name__ == "__main__": 74 | main() 75 | -------------------------------------------------------------------------------- /Chapter_06/test_ch06_r04.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 6, recipe 4, Using argparse to get command-line input 4 | 5 | The test cases need to be kept separate from the main processing 6 | of the ch06_r04 module. 7 | """ 8 | 9 | from pytest import * # type: ignore 10 | from Chapter_06 import ch06_r04 11 | 12 | 13 | def test_main_good(capsys): 14 | ch06_r04.main(["-u", "KM", "36.12,-86.67", "33.94,-118.40"]) 15 | out, err = capsys.readouterr() 16 | assert out.splitlines() == ["From 36.12,-86.67 to 33.94,-118.4 in KM = 2887.35"] 17 | 18 | 19 | def test_main_bad(capsys): 20 | with raises(SystemExit): 21 | ch06_r04.main(["-u", "KM", "36.12,-86.67", "33.94,-118asd"]) 22 | out, err = capsys.readouterr() 23 | assert out == "" 24 | assert err.splitlines() == [ 25 | "usage: pytest [-h] [-u {NM,MI,KM}] p1 p2", 26 | "pytest: error: argument p2: could not convert string to float: '-118asd'", 27 | ] 28 | -------------------------------------------------------------------------------- /Chapter_07/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Modern-Python-Cookbook-Second-Edition/0182ee7f17ae58cff2775290547321ebed443ae3/Chapter_07/__init__.py -------------------------------------------------------------------------------- /Chapter_07/ch07_r01.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 7, recipe 1, Using a class to encapsulate data and processing 4 | """ 5 | from random import randint 6 | from typing import Tuple 7 | 8 | 9 | class Dice: 10 | def __init__(self) -> None: 11 | # No value provided, only a type hint. 12 | self.faces: Tuple[int, int] = (0, 0) 13 | 14 | def roll(self) -> None: 15 | self.faces = (randint(1, 6), randint(1, 6)) 16 | 17 | def total(self) -> int: 18 | return sum(self.faces) 19 | 20 | def hardway(self) -> bool: 21 | return self.faces[0] == self.faces[1] 22 | 23 | def easyway(self) -> bool: 24 | return self.faces[0] != self.faces[1] 25 | 26 | 27 | test_example1 = """ 28 | >>> import random 29 | >>> random.seed(42) 30 | >>> d1 = Dice() 31 | >>> d1.roll() 32 | >>> d1.total() 33 | 7 34 | >>> d1.faces 35 | (6, 1) 36 | 37 | >>> d1.total() 38 | 7 39 | """ 40 | 41 | test_example2 = """ 42 | >>> d2 = Dice() 43 | >>> d2.roll() 44 | >>> d2.total() 45 | 7 46 | >>> d2.hardway() 47 | False 48 | >>> d2.faces 49 | (1, 6) 50 | """ 51 | 52 | __test__ = {n: v for n, v in locals().items() if n.startswith("test_")} 53 | -------------------------------------------------------------------------------- /Chapter_07/ch07_r02.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 7, recipe 2, Essential type hints for class definitions 4 | """ 5 | import random 6 | from typing import Set, List 7 | 8 | 9 | class Dice: 10 | RNG = random.Random() 11 | 12 | def __init__(self, n: int, sides: int = 6) -> None: 13 | self.n_dice = n 14 | self.sides = sides 15 | self.faces: List[int] 16 | self.roll_number = 0 17 | 18 | def __str__(self) -> str: 19 | return ", ".join( 20 | f"{i}: {f}" 21 | for i, f in enumerate(self.faces) 22 | ) 23 | 24 | def total(self) -> int: 25 | return sum(self.faces) 26 | 27 | def average(self) -> float: 28 | return sum(self.faces) / self.n_dice 29 | 30 | def first_roll(self) -> List[int]: 31 | self.roll_number = 0 32 | self.faces = [self.RNG.randint(1, self.sides) for _ in range(self.n_dice)] 33 | return self.faces 34 | 35 | def reroll(self, positions: Set[int]) -> List[int]: 36 | self.roll_number += 1 37 | for p in positions: 38 | self.faces[p] = self.RNG.randint(1, self.sides) 39 | return self.faces 40 | 41 | 42 | # The following example has type checking disabled. 43 | # To see the effect of using a wrong type, remove the type: ignore comments, 44 | # and run mypy on this module. 45 | 46 | 47 | def example_mypy_failure() -> None: 48 | d = Dice(2.5) # type: ignore 49 | r1: List[str] = d.first_roll() # type: ignore 50 | print(d) 51 | 52 | 53 | test_dice = """ 54 | >>> d1 = Dice(5) 55 | >>> d1.RNG.seed(42) 56 | >>> d1.first_roll() 57 | [6, 1, 1, 6, 3] 58 | >>> d1.reroll({0, 3, 4}) 59 | [2, 1, 1, 2, 2] 60 | >>> str(d1) 61 | '0: 2, 1: 1, 2: 1, 3: 2, 4: 2' 62 | """ 63 | 64 | test_dice_failure = """ 65 | >>> example_mypy_failure() 66 | Traceback (most recent call last): 67 | ... 68 | TypeError: 'float' object cannot be interpreted as an integer 69 | 70 | >>> bad = Dice(2.5) 71 | >>> bad.first_roll() 72 | Traceback (most recent call last): 73 | ... 74 | TypeError: 'float' object cannot be interpreted as an integer 75 | 76 | """ 77 | 78 | __test__ = {n: v for n, v in locals().items() if n.startswith("test_")} 79 | -------------------------------------------------------------------------------- /Chapter_07/ch07_r04.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 7, recipe 4, Using typing.NamedTuple for immutable objects 4 | """ 5 | 6 | 7 | from typing import NamedTuple 8 | 9 | 10 | class Card(NamedTuple): 11 | rank: int 12 | suit: str 13 | 14 | 15 | test_card = """ 16 | >>> eight_hearts = Card(rank=8, suit='\N{White Heart Suit}') 17 | >>> eight_hearts 18 | Card(rank=8, suit='♡') 19 | >>> eight_hearts.rank 20 | 8 21 | >>> eight_hearts.suit 22 | '♡' 23 | >>> eight_hearts[0] 24 | 8 25 | 26 | >>> eight_hearts.suit = '\N{Black Spade Suit}' 27 | Traceback (most recent call last): 28 | File "/Users/slott/miniconda3/envs/cookbook/lib/python3.8/doctest.py", line 1328, in __run 29 | compileflags, 1), test.globs) 30 | File "", line 1, in 31 | eight_hearts.suit = '\N{Black Spade Suit}' 32 | AttributeError: can't set attribute 33 | """ 34 | 35 | 36 | class CardPoints(NamedTuple): 37 | rank: int 38 | suit: str 39 | 40 | def points(self) -> int: 41 | if 1 <= self.rank < 10: 42 | return self.rank 43 | else: 44 | return 10 45 | 46 | 47 | test_card_points = """ 48 | >>> hj = CardPoints(rank=11, suit='\N{White Heart Suit}') 49 | >>> h5 = CardPoints(rank=5, suit='\N{White Heart Suit}') 50 | >>> print(f"Hand: {hj=}, {h5=}") 51 | Hand: hj=CardPoints(rank=11, suit='♡'), h5=CardPoints(rank=5, suit='♡') 52 | >>> print(f"Total: {hj.points() + h5.points()=}") 53 | Total: hj.points() + h5.points()=15 54 | """ 55 | 56 | __test__ = {n: v for n, v in locals().items() if n.startswith("test_")} 57 | -------------------------------------------------------------------------------- /Chapter_07/ch07_r05.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 7, recipe 5, Using dataclasses for mutable objects 4 | """ 5 | 6 | from dataclasses import dataclass 7 | from typing import List, ClassVar, Tuple 8 | from Chapter_07.ch07_r04 import CardPoints 9 | import random 10 | 11 | 12 | @dataclass(init=False) 13 | class Deck: 14 | suits: ClassVar[Tuple[str, ...]] = ( 15 | "\N{Black Club Suit}", 16 | "\N{White Diamond Suit}", 17 | "\N{White Heart Suit}", 18 | "\N{Black Spade Suit}", 19 | ) 20 | cards: List[CardPoints] 21 | 22 | def __init__(self) -> None: 23 | self.cards = [ 24 | CardPoints(rank=r, suit=s) for r in range(1, 14) for s in self.suits 25 | ] 26 | random.shuffle(self.cards) 27 | 28 | 29 | @dataclass 30 | class CribbageHand: 31 | cards: List[CardPoints] 32 | 33 | def to_crib(self, card1, card2): 34 | self.cards.remove(card1) 35 | self.cards.remove(card2) 36 | 37 | 38 | test_Dataclass = """ 39 | >>> random.seed(42) 40 | >>> d = Deck() 41 | >>> cards = d.cards[:6] 42 | >>> cards 43 | [CardPoints(rank=3, suit='♢'), CardPoints(rank=6, suit='♠'), CardPoints(rank=7, suit='♢'), CardPoints(rank=1, suit='♠'), CardPoints(rank=6, suit='♢'), CardPoints(rank=10, suit='♡')] 44 | 45 | >>> cards = [ 46 | ... CardPoints(rank=3, suit='♢'), 47 | ... CardPoints(rank=6, suit='♠'), 48 | ... CardPoints(rank=7, suit='♢'), 49 | ... CardPoints(rank=1, suit='♠'), 50 | ... CardPoints(rank=6, suit='♢'), 51 | ... CardPoints(rank=10, suit='♡')] 52 | >>> ch1 = CribbageHand(cards) 53 | >>> ch1 54 | CribbageHand(cards=[CardPoints(rank=3, suit='♢'), CardPoints(rank=6, suit='♠'), CardPoints(rank=7, suit='♢'), CardPoints(rank=1, suit='♠'), CardPoints(rank=6, suit='♢'), CardPoints(rank=10, suit='♡')]) 55 | >>> [c.points() for c in ch1.cards] 56 | [3, 6, 7, 1, 6, 10] 57 | >>> ch1.to_crib(CardPoints(rank=3, suit='♢'), CardPoints(rank=1, suit='♠')) 58 | >>> ch1 59 | CribbageHand(cards=[CardPoints(rank=6, suit='♠'), CardPoints(rank=7, suit='♢'), CardPoints(rank=6, suit='♢'), CardPoints(rank=10, suit='♡')]) 60 | >>> [c.points() for c in ch1.cards] 61 | [6, 7, 6, 10] 62 | """ 63 | 64 | __test__ = {n: v for n, v in locals().items() if n.startswith("test_")} 65 | -------------------------------------------------------------------------------- /Chapter_07/ch07_r06.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 7, recipe 6, Using frozen dataclasses for immutable objects 4 | """ 5 | 6 | 7 | from dataclasses import dataclass 8 | 9 | 10 | @dataclass(frozen=True, order=True) 11 | class Card: 12 | rank: int 13 | suit: str 14 | 15 | 16 | test_card = """ 17 | >>> eight_hearts = Card(rank=8, suit='\N{White Heart Suit}') 18 | >>> eight_hearts 19 | Card(rank=8, suit='♡') 20 | >>> eight_hearts.rank 21 | 8 22 | >>> eight_hearts.suit 23 | '♡' 24 | 25 | 26 | >>> eight_hearts.suit = '\N{Black Spade Suit}' 27 | Traceback (most recent call last): 28 | File "/Users/slott/miniconda3/envs/cookbook/lib/python3.8/doctest.py", line 1328, in __run 29 | compileflags, 1), test.globs) 30 | File "", line 1, in 31 | eight_hearts.suit = '\N{Black Spade Suit}' 32 | dataclasses.FrozenInstanceError: cannot assign to field 'suit' 33 | """ 34 | 35 | 36 | @dataclass(frozen=True, order=True) 37 | class CardPoints: 38 | rank: int 39 | suit: str 40 | 41 | def points(self) -> int: 42 | if 1 <= self.rank < 10: 43 | return self.rank 44 | else: 45 | return 10 46 | 47 | 48 | test_card_points = """ 49 | >>> hj = CardPoints(rank=11, suit='\N{White Heart Suit}') 50 | >>> h5 = CardPoints(rank=5, suit='\N{White Heart Suit}') 51 | >>> print(f"Hand: {hj=}, {h5=}") 52 | Hand: hj=CardPoints(rank=11, suit='♡'), h5=CardPoints(rank=5, suit='♡') 53 | >>> print(f"Total: {hj.points() + h5.points()=}") 54 | Total: hj.points() + h5.points()=15 55 | """ 56 | 57 | from dataclasses import field 58 | from typing import List 59 | 60 | 61 | @dataclass(frozen=True, order=True) 62 | class Hand: 63 | cards: List[CardPoints] = field(default_factory=list) 64 | 65 | 66 | test_hand = """ 67 | >>> cards = [ 68 | ... CardPoints(rank=3, suit='♢'), 69 | ... CardPoints(rank=6, suit='♠'), 70 | ... CardPoints(rank=7, suit='♢'), 71 | ... CardPoints(rank=1, suit='♠'), 72 | ... CardPoints(rank=6, suit='♢'), 73 | ... CardPoints(rank=10, suit='♡')] 74 | >>> 75 | >>> h = Hand(cards) 76 | >>> crib = Hand() 77 | >>> d3 = CardPoints(rank=3, suit='♢') 78 | >>> h.cards.remove(d3) 79 | >>> crib.cards.append(d3) 80 | >>> s1 = CardPoints(rank=1, suit='♠') 81 | >>> h.cards.remove(s1) 82 | >>> crib.cards.append(s1) 83 | >>> crib 84 | Hand(cards=[CardPoints(rank=3, suit='♢'), CardPoints(rank=1, suit='♠')]) 85 | >>> crib.another_attribute = "dealer" 86 | Traceback (most recent call last): 87 | ... 88 | dataclasses.FrozenInstanceError: cannot assign to field 'another_attribute' 89 | >>> crib.cards = 355/113 90 | Traceback (most recent call last): 91 | ... 92 | dataclasses.FrozenInstanceError: cannot assign to field 'cards' 93 | 94 | """ 95 | 96 | __test__ = {n: v for n, v in locals().items() if n.startswith("test_")} 97 | -------------------------------------------------------------------------------- /Chapter_07/ch07_r07.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 7, recipe 7, Optimizing small objects with __slots__ 4 | """ 5 | import random 6 | from typing import Optional 7 | 8 | # Card = collections.namedtuple('Card', ('rank', 'suit')) 9 | 10 | from typing import NamedTuple, List, Union 11 | 12 | 13 | class Card(NamedTuple): 14 | rank: int 15 | suit: str 16 | 17 | 18 | class Hand: 19 | """ 20 | >>> h = Hand(1) 21 | >>> h.deal(Card(1,'\N{white heart suit}')) 22 | >>> h.deal(Card(10, '\N{black club suit}')) 23 | >>> h 24 | Hand(bet=1, hand=[Card(rank=1, suit='♡'), Card(rank=10, suit='♣')]) 25 | >>> h.total = 11 26 | Traceback (most recent call last): 27 | File "/Users/slott/miniconda3/envs/cookbook/lib/python3.8/doctest.py", line 1328, in __run 28 | compileflags, 1), test.globs) 29 | File "", line 1, in 30 | h.total = 11 #doctest: +IGNORE_EXCEPTION_DETAIL 31 | AttributeError: 'Hand' object has no attribute 'total' 32 | """ 33 | 34 | __slots__ = ("cards", "bet") 35 | 36 | def __init__(self, bet: int, hand: Union["Hand", List[Card], None] = None) -> None: 37 | self.cards: List[Card] = ( 38 | [] if hand is None else hand.cards if isinstance(hand, Hand) else hand 39 | ) 40 | self.bet: int = bet 41 | 42 | def deal(self, card: Card) -> None: 43 | self.cards.append(card) 44 | 45 | def __repr__(self) -> str: 46 | return f"{self.__class__.__name__}(" f"bet={self.bet}, hand={self.cards})" 47 | 48 | 49 | if __name__ == "__main__": 50 | 51 | SUITS = ( 52 | "\N{black spade suit}", 53 | "\N{white heart suit}", 54 | "\N{white diamond suit}", 55 | "\N{black club suit}", 56 | ) 57 | deck = [Card(r, s) for r in range(1, 14) for s in SUITS] 58 | random.seed(2) 59 | random.shuffle(deck) 60 | dealer = iter(deck) 61 | 62 | h = Hand(2) 63 | h.deal(next(dealer)) 64 | h.deal(next(dealer)) 65 | print(h) 66 | -------------------------------------------------------------------------------- /Chapter_07/ch07_r09.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 7, recipe 9, Extending a built-in collection – a list that does statistics 4 | """ 5 | import math 6 | 7 | 8 | class StatsList(list): 9 | """ 10 | >>> subset1 = StatsList([10, 8, 13, 9, 11]) 11 | >>> data = StatsList([14, 6, 4, 12, 7, 5]) 12 | >>> data.extend(subset1) 13 | >>> data 14 | [14, 6, 4, 12, 7, 5, 10, 8, 13, 9, 11] 15 | >>> round(data.mean(), 1) 16 | 9.0 17 | >>> round(data.variance(), 1) 18 | 11.0 19 | """ 20 | 21 | def sum(self) -> float: 22 | return sum(v for v in self) 23 | 24 | def size(self) -> float: 25 | return sum(1 for v in self) 26 | 27 | def mean(self) -> float: 28 | return self.sum() / self.size() 29 | 30 | def sum2(self) -> float: 31 | return sum(v ** 2 for v in self) 32 | 33 | def variance(self) -> float: 34 | return (self.sum2() - self.sum() ** 2 / self.size()) / (self.size() - 1) 35 | 36 | def stddev(self) -> float: 37 | return math.sqrt(self.variance()) 38 | -------------------------------------------------------------------------------- /Chapter_07/ch07_r11.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 7, recipe 11, Using contexts and context managers 4 | """ 5 | 6 | from Chapter_03.ch03_r08 import haversine, MI, NM, KM 7 | from typing import Type, NamedTuple, Optional, Callable, List 8 | from types import TracebackType 9 | 10 | 11 | class Point(NamedTuple): 12 | lat: float 13 | lon: float 14 | 15 | 16 | class Distance: 17 | def __init__(self, r: float) -> None: 18 | self.r = r 19 | 20 | def __enter__(self) -> Callable[[Point, Point], float]: 21 | return self.distance 22 | 23 | def __exit__( 24 | self, 25 | exc_type: Optional[Type[Exception]], 26 | exc_val: Optional[Exception], 27 | exc_tb: Optional[TracebackType], 28 | ) -> Optional[bool]: 29 | return None 30 | 31 | def distance(self, p1: Point, p2: Point) -> float: 32 | return haversine(p1.lat, p1.lon, p2.lat, p2.lon, self.r) 33 | 34 | 35 | test_distance = """ 36 | >>> p1 = Point(38.9784, -76.4922) 37 | >>> p2 = Point(36.8443, -76.2922) 38 | >>> with Distance(r=NM) as nm_dist: 39 | ... print(f"{nm_dist(p1, p2)=:.2f}") 40 | nm_dist(p1, p2)=128.48 41 | 42 | >>> nm_distance = Distance(r=NM) 43 | >>> with nm_distance as nm_calc: 44 | ... print(f"{nm_calc(p1, p2)=:.2f}") 45 | nm_calc(p1, p2)=128.48 46 | """ 47 | 48 | 49 | class Distance_2: 50 | def __init__(self, r: float) -> None: 51 | self.r = r 52 | 53 | def __enter__(self) -> Callable[[Point, Point], float]: 54 | return self.distance 55 | 56 | def __exit__( 57 | self, 58 | exc_type: Optional[Type[Exception]], 59 | exc_val: Optional[Exception], 60 | exc_tb: Optional[TracebackType], 61 | ) -> Optional[bool]: 62 | if exc_type == TypeError: 63 | raise ValueError(f"Invalid r={self.r}") 64 | return None 65 | 66 | def distance(self, p1: Point, p2: Point) -> float: 67 | return haversine(p1.lat, p1.lon, p2.lat, p2.lon, self.r) 68 | 69 | 70 | test_bad = """ 71 | >>> p1 = Point(38.9784, -76.4922) 72 | >>> p2 = Point(36.8443, -76.2922) 73 | >>> with Distance(None) as nm_dist: 74 | ... print(f"{nm_dist(p1, p2)=:.2f}") 75 | Traceback (most recent call last): 76 | File "/Users/slott/miniconda3/envs/cookbook/lib/python3.8/doctest.py", line 1328, in __run 77 | exec(compile(example.source, filename, "single", 78 | File "", line 2, in 79 | File "/Users/slott/Documents/Writing/Python/Python Cookbook 2e/Modern-Python-Cookbook-Second-Edition/Chapter_07.ch07_r12.py", line 32, in distance 80 | return haversine(p1.lat, p1.lon, p2.lat, p2.lon, self.r) 81 | File "/Users/slott/Documents/Writing/Python/Python Cookbook 2e/Modern-Python-Cookbook-Second-Edition/Chapter_03/ch03_r08.py", line 30, in haversine 82 | return R * 2 * asin(a) 83 | TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' 84 | 85 | """ 86 | 87 | test_bad_2 = """ 88 | >>> p1 = Point(38.9784, -76.4922) 89 | >>> p2 = Point(36.8443, -76.2922) 90 | >>> with Distance_2(None) as nm_dist: 91 | ... print(f"{nm_dist(p1, p2)=:.2f}") 92 | Traceback (most recent call last): 93 | ... 94 | ValueError: Invalid r=None 95 | """ 96 | 97 | 98 | __test__ = {n: v for n, v in locals().items() if n.startswith("test_")} 99 | -------------------------------------------------------------------------------- /Chapter_07/ch07_r11a.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 7, recipe 11a, Using contexts and context managers 4 | This (and related recipes) are part of Chapter 7, Recipe 11. 5 | 6 | FileFacts(name=PosixPath('anscome_json.py'), modified='2019-11-08T16:24:50.834272', size=993, checksum='ab8cea15584ffbe17eb3763205cae947') 7 | FileFacts(name=PosixPath('hint_game.py'), modified='2016-03-16T14:36:20', size=1152, checksum='43b9119fb44ba26be022ba11928b33ad') 8 | 9 | """ 10 | from pathlib import Path 11 | 12 | import datetime 13 | from hashlib import md5 14 | 15 | # from types import SimpleNamespace as FileFacts 16 | from typing import NamedTuple 17 | 18 | 19 | class FileFacts(NamedTuple): 20 | name: Path 21 | modified: str 22 | size: int 23 | checksum: str 24 | 25 | 26 | def file_facts(path: Path) -> FileFacts: 27 | return FileFacts( 28 | name=path, 29 | modified=datetime.datetime.fromtimestamp(path.stat().st_mtime).isoformat(), 30 | size=path.stat().st_size, 31 | checksum=md5(path.read_bytes()).hexdigest(), 32 | ) 33 | 34 | 35 | if __name__ == "__main__": 36 | summary_path = Path("data/summary.dat") 37 | with summary_path.open("w") as summary_file: 38 | 39 | base = Path.cwd() 40 | for member in base.glob("Chapter_*/*.py"): 41 | print(file_facts(member), file=summary_file) 42 | 43 | with summary_path.open() as summary_file: 44 | for line in summary_file: 45 | print(line.rstrip()) 46 | -------------------------------------------------------------------------------- /Chapter_07/ch07_r11b.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 7, recipe 11b, Using contexts and context managers 4 | """ 5 | import random 6 | import secrets 7 | from typing import Iterable, List, Set, Tuple, Optional, Type 8 | from types import TracebackType 9 | 10 | class Dice: 11 | def __init__(self, n: int, /, d: int = 6) -> None: 12 | self.n_dice = n 13 | self.faces = d 14 | self.history: List[Tuple[int, ...]] = list() 15 | 16 | def roller(self) -> 'Roller': 17 | return Roller(self, 3) 18 | 19 | @property 20 | def roll(self) -> Tuple[int, ...]: 21 | if self.history: 22 | return self.history[-1] 23 | raise ValueError("Dice were never rolled") 24 | 25 | class Roller: 26 | def __init__(self, dice: Dice, tries: int) -> None: 27 | self.dice = dice 28 | self.tries = tries 29 | self._roll: List[int] = [0 for _ in range(self.dice.n_dice)] 30 | self.frozen: Set[int] = set() 31 | self.history: List[Tuple[int, ...]] = list() 32 | 33 | def __enter__(self) -> 'Roller': 34 | return self 35 | 36 | def __exit__( 37 | self, 38 | exc_type: Optional[Type[Exception]], 39 | exc_val: Optional[Exception], 40 | exc_tb: Optional[TracebackType] 41 | ) -> Optional[bool]: 42 | self.dice.history.extend(self.history) 43 | return None 44 | 45 | def roll(self) -> Tuple[int, ...]: 46 | if len(self.history) != self.tries: 47 | for i in range(self.dice.n_dice): 48 | if i not in self.frozen: 49 | self._roll[i] = random.randint(1, self.dice.faces) 50 | self.history.append(tuple(self._roll)) 51 | return self.history[-1] 52 | 53 | def freeze(self, *positions: int) -> None: 54 | self.frozen = set(positions) 55 | 56 | 57 | if __name__ == "__main__": 58 | random.seed(42) 59 | hand = Dice(5, d=6) 60 | with hand.roller() as roller: 61 | r1 = roller.roll() 62 | print(r1) 63 | roller.freeze(0, 3) 64 | r2 = roller.roll() 65 | print(r2) 66 | roller.freeze(1, 2, 4) 67 | r3 = roller.roll() 68 | print(r3) 69 | print(hand.roll) 70 | print(hand.history) 71 | 72 | test_dice_roller = """ 73 | >>> random.seed(42) 74 | >>> hand = Dice(5, d=6) 75 | >>> with hand.roller() as roller: 76 | ... r1 = roller.roll() 77 | ... print(r1) 78 | ... roller.freeze(0, 3) 79 | ... r2 = roller.roll() 80 | ... print(r2) 81 | ... roller.freeze(1, 2, 4) 82 | ... r3 = roller.roll() 83 | ... print(r3) 84 | (6, 1, 1, 6, 3) 85 | (6, 2, 2, 6, 2) 86 | (6, 2, 2, 1, 2) 87 | >>> print(hand.roll) 88 | (6, 2, 2, 1, 2) 89 | >>> print(hand.history) 90 | [(6, 1, 1, 6, 3), (6, 2, 2, 6, 2), (6, 2, 2, 1, 2)] 91 | """ 92 | 93 | __test__ = {n: v for n, v in locals().items() if n.startswith("test_")} 94 | -------------------------------------------------------------------------------- /Chapter_08/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Modern-Python-Cookbook-Second-Edition/0182ee7f17ae58cff2775290547321ebed443ae3/Chapter_08/__init__.py -------------------------------------------------------------------------------- /Chapter_08/ch08_r01.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 8, recipe 1, Choosing between inheritance and extension – the “is-a” question 4 | 5 | """ 6 | from typing import NamedTuple, List 7 | import random 8 | 9 | # from collections import namedtuple 10 | # Card = namedtuple('Card', ('rank', 'suit')) 11 | 12 | 13 | class Card(NamedTuple): 14 | rank: int 15 | suit: str 16 | 17 | 18 | SUITS = "\u2660\u2661\u2662\u2663" 19 | Spades, Hearts, Diamonds, Clubs = tuple(SUITS) 20 | 21 | test_Card = """ 22 | >>> c_2s = Card(2, Spades) 23 | >>> c_2s 24 | Card(rank=2, suit='♠') 25 | """ 26 | 27 | 28 | class Deck_W: 29 | def __init__(self, cards: List[Card]) -> None: 30 | self.cards = cards 31 | self.deal_iter = iter(cards) 32 | 33 | def shuffle(self) -> None: 34 | random.shuffle(self.cards) 35 | self.deal_iter = iter(self.cards) 36 | 37 | def deal(self) -> Card: 38 | return next(self.deal_iter) 39 | 40 | 41 | test_Deck_W = """ 42 | >>> domain = list(Card(r+1,s) for r in range(13) for s in SUITS) 43 | >>> d = Deck_W(domain) 44 | >>> random.seed(1) 45 | >>> d.shuffle() 46 | >>> [d.deal() for _ in range(5)] 47 | [Card(rank=13, suit='♡'), Card(rank=3, suit='♡'), Card(rank=10, suit='♡'), Card(rank=6, suit='♢'), Card(rank=1, suit='♢')] 48 | """ 49 | 50 | 51 | class Deck_X(list): 52 | def shuffle(self) -> None: 53 | random.shuffle(self) 54 | self.deal_iter = iter(self) 55 | 56 | def deal(self) -> Card: 57 | return next(self.deal_iter) 58 | 59 | 60 | test_Deck_X = """ 61 | >>> domain = Deck_X(Card(r+1,s) for r in range(13) for s in SUITS) 62 | >>> d = Deck_X(domain) 63 | >>> random.seed(1) 64 | >>> d.shuffle() 65 | >>> [d.deal() for _ in range(5)] 66 | [Card(rank=13, suit='♡'), Card(rank=3, suit='♡'), Card(rank=10, suit='♡'), Card(rank=6, suit='♢'), Card(rank=1, suit='♢')] 67 | 68 | >>> d2 = Deck_X(Card(r+1,s) for r in range(13) for s in SUITS) 69 | >>> random.seed(1) 70 | >>> d2.shuffle() 71 | >>> [d2.deal() for _ in range(5)] 72 | [Card(rank=13, suit='♡'), Card(rank=3, suit='♡'), Card(rank=10, suit='♡'), Card(rank=6, suit='♢'), Card(rank=1, suit='♢')] 73 | """ 74 | 75 | __test__ = {n: v for n, v in locals().items() if n.startswith("test_")} 76 | -------------------------------------------------------------------------------- /Chapter_08/ch08_r02_timing.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 8, recipe 2, Separating concerns via multiple inheritance 4 | 5 | Timing comparison 6 | """ 7 | 8 | import timeit 9 | 10 | if __name__ == "__main__": 11 | 12 | m1 = timeit.timeit( 13 | """repr(card)""", 14 | setup=""" 15 | from Chapter_08.ch08_r02 import make_card 16 | card = make_card(10,'S') 17 | """, 18 | ) 19 | 20 | m2 = timeit.timeit( 21 | """str(card)""", 22 | setup=""" 23 | from Chapter_08.ch08_r02 import make_card 24 | card = make_card(10,'S') 25 | """, 26 | ) 27 | 28 | print(f"Card.__repr__ make time {m1:.4f}") 29 | print(f"object.__str__ make time {m2:.4f}") 30 | print(f"{abs(m1-m2)/m1:.1%} difference") 31 | -------------------------------------------------------------------------------- /Chapter_08/ch08_r03.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 8, recipe 3, Leveraging Python's duck typing 4 | """ 5 | import random 6 | from typing import Tuple, Optional, List, Iterator, Type 7 | 8 | 9 | class Dice1: 10 | def __init__(self, seed=None) -> None: 11 | self._rng = random.Random(seed) 12 | self.roll() 13 | 14 | def roll(self) -> Tuple[int, ...]: 15 | self.dice = (self._rng.randint(1, 6), self._rng.randint(1, 6)) 16 | return self.dice 17 | 18 | 19 | class Die: 20 | def __init__(self, rng: random.Random) -> None: 21 | self._rng = rng 22 | 23 | def roll(self) -> int: 24 | return self._rng.randint(1, 6) 25 | 26 | 27 | class Dice2: 28 | def __init__(self, seed: Optional[int] = None) -> None: 29 | self._rng = random.Random(seed) 30 | self._dice = [Die(self._rng) for _ in range(2)] 31 | self.roll() 32 | 33 | def roll(self) -> Tuple[int, ...]: 34 | self.dice = tuple(d.roll() for d in self._dice) 35 | return self.dice 36 | 37 | 38 | def roller( 39 | dice_class: Type[Dice2], seed: Optional[int] = None, *, samples: int = 10 40 | ) -> Iterator[Tuple[int, ...]]: 41 | dice = dice_class(seed) 42 | for _ in range(samples): 43 | yield dice.roll() 44 | 45 | 46 | test_roller = """ 47 | >>> from Chapter_08.ch08_r03 import roller, Dice1, Dice2 48 | >>> list(roller(Dice1, 1, samples=5)) 49 | [(1, 3), (1, 4), (4, 4), (6, 4), (2, 1)] 50 | >>> list(roller(Dice2, 1, samples=5)) 51 | [(1, 3), (1, 4), (4, 4), (6, 4), (2, 1)] 52 | """ 53 | 54 | __test__ = {n: v for n, v in locals().items() if n.startswith("test_")} 55 | -------------------------------------------------------------------------------- /Chapter_08/ch08_r04.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 8, recipe 4, Managing global and singleton objects 4 | """ 5 | 6 | import collections 7 | from typing import List, Tuple, Any, Counter 8 | 9 | _global_counter: Counter[str] = collections.Counter() 10 | 11 | 12 | def count(key: str, increment: int = 1) -> None: 13 | _global_counter[key] += increment 14 | 15 | 16 | def count_summary() -> List[Tuple[str, int]]: 17 | return _global_counter.most_common() 18 | 19 | 20 | class EventCounter: 21 | _class_counter: Counter[str] = collections.Counter() 22 | 23 | def count(self, key: str, increment: int = 1) -> None: 24 | EventCounter._class_counter[key] += increment 25 | 26 | def summary(self) -> List[Tuple[str, int]]: 27 | return EventCounter._class_counter.most_common() 28 | 29 | 30 | test_module_global = """ 31 | >>> from Chapter_08.ch08_r04 import * 32 | >>> from Chapter_08.ch08_r03 import Dice1 33 | >>> d = Dice1(1) 34 | >>> for _ in range(1000): 35 | ... if sum(d.roll()) == 7: count('seven') 36 | ... else: count('other') 37 | >>> print(count_summary()) 38 | [('other', 833), ('seven', 167)] 39 | """ 40 | 41 | test_class_variable = """ 42 | >>> from Chapter_08.ch08_r04 import * 43 | >>> c1 = EventCounter() 44 | >>> c1.count('input') 45 | >>> c2 = EventCounter() 46 | >>> c2.count('input') 47 | >>> c3 = EventCounter() 48 | >>> c3.summary() 49 | [('input', 2)] 50 | """ 51 | 52 | __test__ = {n: v for n, v in locals().items() if n.startswith("test_")} 53 | -------------------------------------------------------------------------------- /Chapter_08/ch08_r06_bug.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 8, recipe 6, Creating a class that has orderable objects 4 | """ 5 | 6 | # https://github.com/python/mypy/issues/4610 7 | 8 | from functools import total_ordering 9 | from typing import Any 10 | 11 | 12 | @total_ordering 13 | class Ord: 14 | 15 | def __eq__(self, other: Any) -> bool: 16 | return False 17 | 18 | def __lt__(self, other: "Ord") -> bool: 19 | return False 20 | 21 | 22 | Ord() <= Ord() # type: ignore 23 | 24 | # Chapter_08/ch08_r06_bug.py:22: error: Unsupported left operand type for <= ("Ord") 25 | -------------------------------------------------------------------------------- /Chapter_08/ch08_r07.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 8, recipe 7, Improving performance with an an ordered collection 4 | """ 5 | import bisect 6 | from typing import Iterable, Iterator 7 | from Chapter_08.ch08_r06 import * 8 | 9 | 10 | class Hand: 11 | def __init__(self, card_iter: Iterable[Card]) -> None: 12 | self.cards = list(card_iter) 13 | self.cards.sort() 14 | 15 | def add(self, aCard: Card) -> None: 16 | bisect.insort(self.cards, aCard) # type: ignore[type-var] 17 | 18 | def index(self, aCard: Card) -> int: 19 | i = bisect.bisect_left(self.cards, aCard) # type: ignore[type-var] 20 | if i != len(self.cards) and self.cards[i] == aCard: 21 | return i 22 | raise ValueError 23 | 24 | def __contains__(self, aCard: Card) -> bool: 25 | try: 26 | self.index(aCard) 27 | return True 28 | except ValueError: 29 | return False 30 | 31 | def __iter__(self) -> Iterator[Card]: 32 | return iter(self.cards) 33 | 34 | def __le__(self, other: Any) -> bool: 35 | for card in self: 36 | if card not in other: 37 | return False 38 | return True 39 | 40 | 41 | test_hand = """ 42 | >>> import random 43 | >>> random.seed(4) 44 | >>> deck = make_deck() 45 | >>> random.shuffle(deck) 46 | >>> h = Hand(deck[:12]) 47 | >>> [str(c) for c in h.cards] 48 | [' 9 ♣', '10 ♣', ' J ♠', ' J ♢', ' J ♢', ' Q ♠', ' Q ♣', ' K ♠', ' K ♠', ' K ♣', ' A ♡', ' A ♣'] 49 | 50 | >>> pinochle = Hand([make_card(11,'♢'), make_card(12,'♠')]) 51 | >>> pinochle <= h 52 | True 53 | >>> sum(c.points() for c in h) 54 | 56 55 | """ 56 | 57 | __test__ = {n: v for n, v in locals().items() if n.startswith("test_")} 58 | 59 | import random 60 | 61 | 62 | def pick_seed(): 63 | pinochle = Hand([make_card(11, "♢"), make_card(12, "♠")]) 64 | for seed in range(4096): 65 | random.seed(seed) 66 | deck = make_deck() 67 | random.shuffle(deck) 68 | h = Hand(deck[:12]) 69 | if pinochle <= h: 70 | print(seed, h.cards) 71 | return 72 | print("No Pinochle in range(4096)") 73 | 74 | 75 | if __name__ == "__main__": 76 | pick_seed() 77 | -------------------------------------------------------------------------------- /Chapter_09/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Modern-Python-Cookbook-Second-Edition/0182ee7f17ae58cff2775290547321ebed443ae3/Chapter_09/__init__.py -------------------------------------------------------------------------------- /Chapter_09/ch09_r05.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 9, recipe 5, Summarizing a collection – how to reduce. 4 | """ 5 | from typing import Iterable 6 | 7 | 8 | from functools import reduce 9 | 10 | 11 | def mul(a: int, b: int) -> int: 12 | return a * b 13 | 14 | 15 | def prod(values: Iterable[int]) -> int: 16 | """ 17 | >>> prod(range(1, 5+1)) 18 | 120 19 | """ 20 | return reduce(mul, values, 1) 21 | 22 | 23 | def factorial(n: int) -> int: 24 | """ 25 | >>> factorial(52) 26 | 80658175170943878571660636856403766975289505440883277824000000000000 27 | """ 28 | return prod(range(1, n + 1)) 29 | 30 | 31 | test_factorial = """ 32 | >>> factorial(52)//(factorial(5)*factorial(52-5)) 33 | 2598960 34 | """ 35 | 36 | from typing import Callable 37 | 38 | l_add: Callable[[int, int], int] = lambda a, b: a + b 39 | l_mul: Callable[[int, int], int] = lambda a, b: a * b 40 | 41 | # Or use 42 | # from operator import add, mul 43 | 44 | 45 | def prod2(values: Iterable[int]) -> int: 46 | """ 47 | >>> prod2(range(1, 5+1)) 48 | 120 49 | """ 50 | return reduce(l_mul, values, 1) 51 | 52 | 53 | def prod3(values: Iterable[int]) -> int: 54 | """ 55 | >>> prod3(range(1, 5+1)) 56 | 120 57 | """ 58 | return reduce(lambda a, b: a * b, values, 1) 59 | 60 | 61 | __test__ = {n: v for n, v in locals().items() if n.startswith("test_")} 62 | -------------------------------------------------------------------------------- /Chapter_09/ch09_r07.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 9, recipe 7, Implementing "there exists" processing. 4 | """ 5 | from itertools import takewhile, dropwhile 6 | from typing import Iterable, Iterator, TypeVar, Callable 7 | 8 | T_ = TypeVar("T_") 9 | Predicate = Callable[[T_], bool] 10 | 11 | 12 | def find_first(fn: Predicate, source: Iterable[T_]) -> Iterator[T_]: 13 | for item in source: 14 | if fn(item): 15 | yield item 16 | break 17 | 18 | 19 | import math 20 | 21 | 22 | def prime(n: int) -> bool: 23 | """ 24 | >>> p = {2, 3, 5, 7, 11, 13, 17, 19} 25 | >>> tests = (prime(n) == (n in p) 26 | ... for n in range(2, 21) 27 | ... ) 28 | >>> all(tests) 29 | True 30 | >>> prime(9973) 31 | True 32 | >>> prime(9997) 33 | False 34 | """ 35 | factors = find_first(lambda i: n % i == 0, range(2, int(math.sqrt(n) + 1))) 36 | return len(list(factors)) == 0 37 | 38 | 39 | def prime_t(n: int) -> bool: 40 | """ 41 | >>> p = {2, 3, 5, 7, 11, 13, 17, 19} 42 | >>> tests = (prime_t(n) == (n in p) 43 | ... for n in range(2, 21) 44 | ... ) 45 | >>> all(tests) 46 | True 47 | >>> prime_t(9973) 48 | True 49 | >>> prime_t(9997) 50 | False 51 | """ 52 | tests = set(range(2, int(math.sqrt(n) + 1))) 53 | non_factors = set(takewhile(lambda i: n % i != 0, tests)) 54 | return tests == non_factors 55 | 56 | 57 | def prime_any(n: int) -> bool: 58 | """ 59 | >>> p = {2, 3, 5, 7, 11, 13, 17, 19} 60 | >>> tests = (prime_any(n) == (n in p) 61 | ... for n in range(2, 21) 62 | ... ) 63 | >>> all(tests) 64 | True 65 | >>> prime_any(9973) 66 | True 67 | >>> prime_any(9997) 68 | False 69 | 70 | """ 71 | tests = range(2, int(math.sqrt(n) + 1)) 72 | has_factors = any(n % t == 0 for t in tests) 73 | return not has_factors 74 | 75 | def primeset(source: Iterable[int]) -> Iterator[int]: 76 | """ 77 | >>> list(primeset(range(2, 21))) 78 | [2, 3, 5, 7, 11, 13, 17, 19] 79 | """ 80 | for i in source: 81 | if prime(i): 82 | yield i 83 | 84 | 85 | import random 86 | 87 | 88 | def fermat_prime(n: int, k: int) -> int: 89 | """ 90 | >>> fermat_prime(9973, 5) 91 | True 92 | >>> fermat_prime(9997, 5) 93 | False 94 | >>> random.seed(42) 95 | >>> for k in range(1, 5): 96 | ... print(f"{k=}, {fermat_prime(96709, k)=}") 97 | k=1, fermat_prime(96709, k)=False 98 | k=2, fermat_prime(96709, k)=False 99 | k=3, fermat_prime(96709, k)=False 100 | k=4, fermat_prime(96709, k)=False 101 | >>> fermat_prime(9967009, k=2) 102 | False 103 | >>> fermat_prime(9999991, k=2) 104 | True 105 | """ 106 | assert n > 3 and k >= 1 107 | for _ in range(k): 108 | a = random.randint(2, n - 2) 109 | if pow(a, n - 1, n) != 1: # (a**(n-1)%n) != 1: 110 | return False 111 | return True 112 | 113 | 114 | __test__ = {n: v for n, v in locals().items() if n.startswith("test_")} 115 | -------------------------------------------------------------------------------- /Chapter_09/ch09_r10.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 9, recipe 10, Writing recursive generator functions with the yield from statement 4 | """ 5 | from typing import List, Dict, Union, Any, Iterator, Optional 6 | 7 | 8 | JSON_DOC = Union[Dict[str, Any], List[Any], str, int, float, bool, None] 9 | 10 | Node_Id = Union[str, int] 11 | 12 | 13 | def find_path( 14 | value: Any, node: JSON_DOC, path: Optional[List[Node_Id]] = None 15 | ) -> Iterator[List[Node_Id]]: 16 | if path is None: 17 | path = [] 18 | if isinstance(node, dict): 19 | for key in sorted(node.keys()): 20 | yield from find_path(value, node[key], path + [key]) 21 | elif isinstance(node, list): 22 | for index in range(len(node)): 23 | yield from find_path(value, node[index], path + [index]) 24 | else: # str, int, float, bool, None 25 | if node == value: 26 | yield path 27 | 28 | 29 | import math 30 | 31 | 32 | def factor_list_recursion(x: int) -> List[int]: 33 | def factors(x: int, n: int) -> List[int]: 34 | if n >= int(math.sqrt(x) + 1): 35 | return [x] 36 | q, r = divmod(x, n) 37 | if r == 0: 38 | return [n] + factors(q, 2) 39 | else: 40 | return factors(x, n+1) 41 | return factors(x, 2) 42 | 43 | def factor_list(x: int) -> List[int]: 44 | limit = int(math.sqrt(x) + 1) 45 | for n in range(2, limit): 46 | q, r = divmod(x, n) 47 | if r == 0: 48 | return [n] + factor_list(q) 49 | return [x] 50 | 51 | 52 | def factor_iter(x: int) -> Iterator[int]: 53 | limit = int(math.sqrt(x) + 1) 54 | for n in range(2, limit): 55 | q, r = divmod(x, n) 56 | if r == 0: 57 | yield n 58 | yield from factor_iter(q) 59 | return 60 | yield x 61 | 62 | 63 | document = { 64 | "field": "value1", 65 | "field2": "value", 66 | "array": [{"array_item_key1": "value"}, {"array_item_key2": "array_item_value2"}], 67 | "object": {"attribute1": "value", "attribute2": "value2"}, 68 | } 69 | 70 | test_find_value1 = """ 71 | >>> list(find_path('value1', document)) 72 | [['field']] 73 | """ 74 | 75 | test_find_array_item_value2 = """ 76 | >>> list(find_path('array_item_value2', document)) 77 | [['array', 1, 'array_item_key2']] 78 | """ 79 | 80 | test_find_object_value2 = """ 81 | >>> list(find_path('value2', document)) 82 | [['object', 'attribute2']] 83 | """ 84 | 85 | test_find_value = """ 86 | >>> list(find_path('value', document)) 87 | [['array', 0, 'array_item_key1'], ['field2'], ['object', 'attribute1']] 88 | """ 89 | 90 | test_factor_list_factor_iter = """ 91 | >>> factor_list_recursion(255255) 92 | [3, 5, 7, 11, 13, 17] 93 | 94 | >>> factor_list(255255) 95 | [3, 5, 7, 11, 13, 17] 96 | 97 | >>> list(factor_iter(255255)) 98 | [3, 5, 7, 11, 13, 17] 99 | 100 | >>> from collections import Counter 101 | >>> Counter(factor_iter(384)) 102 | Counter({2: 7, 3: 1}) 103 | >>> list(factor_iter(384)) 104 | [2, 2, 2, 2, 2, 2, 2, 3] 105 | >>> list(factor_list_recursion(384)) 106 | [2, 2, 2, 2, 2, 2, 2, 3] 107 | 108 | """ 109 | 110 | __test__ = {n: v for n, v in locals().items() if n.startswith("test_")} 111 | -------------------------------------------------------------------------------- /Chapter_10/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Modern-Python-Cookbook-Second-Edition/0182ee7f17ae58cff2775290547321ebed443ae3/Chapter_10/__init__.py -------------------------------------------------------------------------------- /Chapter_10/ch10_b.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 10, Bonus Material b. 4 | 5 | Note: Output from this is used in Chapter 4 examples. 6 | """ 7 | 8 | from pathlib import Path 9 | import csv 10 | import datetime 11 | 12 | # from types import SimpleNamespace as Waypoint 13 | from dataclasses import dataclass 14 | 15 | 16 | @dataclass 17 | class Waypoint: 18 | lat: float 19 | lon: float 20 | timestamp: datetime.datetime 21 | 22 | 23 | make_date = lambda txt: datetime.datetime.strptime(txt, "%Y-%m-%d").date() 24 | make_time = lambda txt: datetime.datetime.strptime(txt, "%H:%M:%S").time() 25 | make_timestamp = lambda date, time: datetime.datetime.combine( 26 | make_date(date), make_time(time) 27 | ) 28 | 29 | 30 | def make_row(source): 31 | return Waypoint( 32 | lat=float(source["lat"]), 33 | lon=float(source["lon"]), 34 | timestamp=make_timestamp(source["date"], source["time"]), 35 | ) 36 | 37 | 38 | def wp_1(waypoints_path: Path): 39 | with waypoints_path.open() as waypoints_file: 40 | raw_reader = csv.DictReader(waypoints_file) 41 | for row in raw_reader: 42 | print(row) 43 | 44 | 45 | def wp_2(waypoints_path: Path): 46 | with waypoints_path.open() as waypoints_file: 47 | raw_reader = csv.DictReader(waypoints_file) 48 | wp_reader = (make_row(row) for row in raw_reader) 49 | for row in wp_reader: 50 | print(row) 51 | 52 | 53 | __test__ = { 54 | "wp_1": """ 55 | >>> waypoints_path = Path.cwd()/'data'/'waypoints.csv' 56 | >>> wp_1(waypoints_path) 57 | {'lat': '32.8321666666667', 'lon': '-79.9338333333333', 'date': '2012-11-27', 'time': '09:15:00'} 58 | {'lat': '31.6714833333333', 'lon': '-80.93325', 'date': '2012-11-28', 'time': '00:00:00'} 59 | {'lat': '30.7171666666667', 'lon': '-81.5525', 'date': '2012-11-28', 'time': '11:35:00'} 60 | """, 61 | "wp_2": """ 62 | >>> waypoints_path = Path.cwd()/'data'/'waypoints.csv' 63 | >>> wp_2(waypoints_path) 64 | Waypoint(lat=32.8321666666667, lon=-79.9338333333333, timestamp=datetime.datetime(2012, 11, 27, 9, 15)) 65 | Waypoint(lat=31.6714833333333, lon=-80.93325, timestamp=datetime.datetime(2012, 11, 28, 0, 0)) 66 | Waypoint(lat=30.7171666666667, lon=-81.5525, timestamp=datetime.datetime(2012, 11, 28, 11, 35)) 67 | """, 68 | } 69 | 70 | 71 | if __name__ == "__main__": 72 | 73 | waypoints_path = Path.cwd() / "data" / "waypoints.csv" 74 | wp_1(waypoints_path) 75 | wp_2(waypoints_path) 76 | -------------------------------------------------------------------------------- /Chapter_10/ch10_r05.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 10, recipe 5, Reading complex formats using regular expressions 4 | 5 | Note: Output from this is used in Chapter 4 examples. 6 | """ 7 | 8 | import re 9 | from pathlib import Path 10 | from pprint import pprint 11 | from typing import Dict, Any, cast, Match, NamedTuple 12 | 13 | pattern_text = ( 14 | r"\[ (?P.*?) \]\s+" 15 | r" (?P\w+) \s+" 16 | r"in\s+(?P\S+?)" 17 | r":\s+ (?P.+)" 18 | ) 19 | pattern = re.compile(pattern_text, re.X) 20 | 21 | 22 | class LogLine(NamedTuple): 23 | date: str 24 | level: str 25 | module: str 26 | message: str 27 | 28 | 29 | def log_parser(source_line: str) -> LogLine: 30 | """ 31 | >>> log_parser("[2019-11-07 11:12:13,098765] info in this.module: example message") 32 | LogLine(date='2019-11-07 11:12:13,098765', level='info', module='this.module', message='example message') 33 | """ 34 | # match = pattern.match(source_line) 35 | # if match: 36 | if match := pattern.match(source_line): 37 | # Chapter_10/ch10_r03.py:38: error: Item "None" of "Optional[Match[str]]" has no attribute "groupdict" 38 | # Force non-None consideration by cast(Match, match).groupdict() 39 | data = cast(Match, match).groupdict() 40 | return LogLine(**data) 41 | raise ValueError(f"Unexpected input {source_line=}") 42 | 43 | 44 | def raw() -> None: 45 | data_path = Path("data") / "sample.log" 46 | with data_path.open() as data_file: 47 | data_reader = map(log_parser, data_file) 48 | for row in data_reader: 49 | pprint(row) 50 | 51 | 52 | import csv 53 | 54 | 55 | def copy(data_path: Path) -> None: 56 | target_path = data_path.with_suffix(".csv") 57 | with target_path.open("w", newline="") as target_file: 58 | writer = csv.DictWriter(target_file, LogLine._fields) 59 | writer.writeheader() 60 | 61 | with data_path.open() as data_file: 62 | reader = map(log_parser, data_file) 63 | writer.writerows(row._asdict() for row in reader) 64 | 65 | 66 | test_raw = """ 67 | >>> raw() 68 | LogLine(date='2016-06-15 17:57:54,715', level='INFO', module='ch10_r10', message='Sample Message One') 69 | LogLine(date='2016-06-15 17:57:54,715', level='DEBUG', module='ch10_r10', message='Debugging') 70 | LogLine(date='2016-06-15 17:57:54,715', level='WARNING', module='ch10_r10', message='Something might have gone wrong') 71 | """ 72 | 73 | test_copy = """ 74 | >>> copy(data_path = Path("data") / "sample.log") 75 | >>> import csv 76 | >>> result_path = Path("data") / "sample.csv" 77 | >>> with result_path.open() as result_file: 78 | ... rdr = csv.DictReader(result_file) 79 | ... data = list(rdr) 80 | >>> pprint(data) # doctest: +NORMALIZE_WHITESPACE 81 | [{'date': '2016-06-15 17:57:54,715', 82 | 'level': 'INFO', 83 | 'message': 'Sample Message One', 84 | 'module': 'ch10_r10'}, 85 | {'date': '2016-06-15 17:57:54,715', 86 | 'level': 'DEBUG', 87 | 'message': 'Debugging', 88 | 'module': 'ch10_r10'}, 89 | {'date': '2016-06-15 17:57:54,715', 90 | 'level': 'WARNING', 91 | 'message': 'Something might have gone wrong', 92 | 'module': 'ch10_r10'}] 93 | """ 94 | 95 | 96 | __test__ = {n: v for n, v in locals().items() if n.startswith("test_")} 97 | -------------------------------------------------------------------------------- /Chapter_10/ch10_r06.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 10, recipe 6, Reading JSON and YAML documents 4 | 5 | Note: Output from this is used in Chapter 4 examples. 6 | """ 7 | 8 | import json 9 | from pathlib import Path 10 | 11 | 12 | def race_summary(source_path: Path) -> None: 13 | document = json.loads(source_path.read_text()) 14 | 15 | for n, leg in enumerate(document["legs"]): 16 | print(leg) 17 | for team_finishes in document["teams"]: 18 | print(team_finishes["name"], team_finishes["position"][n]) 19 | 20 | 21 | test_summary = """ 22 | >>> race_summary(source_path=Path("data") / "race_result.json") # doctest: +ELLIPSIS 23 | ALICANTE - CAPE TOWN 24 | Abu Dhabi Ocean Racing 1 25 | Team Brunel 3 26 | Dongfeng Race Team 2 27 | MAPFRE 7 28 | Team Alvimedica 5 29 | Team SCA 6 30 | Team Vestas Wind 4 31 | ... 32 | LORIENT - GOTHENBURG 33 | Abu Dhabi Ocean Racing 5 34 | Team Brunel 2 35 | Dongfeng Race Team 4 36 | MAPFRE 3 37 | Team Alvimedica 1 38 | Team SCA 7 39 | Team Vestas Wind 6 40 | 41 | """ 42 | 43 | from typing import Any, Union, Dict 44 | import datetime 45 | 46 | 47 | def default_date(object: Any) -> Union[Any, Dict[str, Any]]: 48 | if isinstance(object, datetime.datetime): 49 | return {"$date": object.isoformat()} 50 | return object 51 | 52 | 53 | test_default_date = """ 54 | >>> example_date = datetime.datetime(2014, 6, 7, 8, 9, 10) 55 | >>> document = {'date': example_date} 56 | >>> print( 57 | ... json.dumps(document, default=default_date, indent=2)) 58 | { 59 | "date": { 60 | "$date": "2014-06-07T08:09:10" 61 | } 62 | } 63 | """ 64 | 65 | 66 | def as_date(object: Dict[str, Any]) -> Union[Any, Dict[str, Any]]: 67 | if {"$date"} == set(object.keys()): 68 | # return datetime.datetime.strptime(object["$date"], "%Y-%m-%dT%H:%M:%S") 69 | return datetime.datetime.fromisoformat(object["$date"]) 70 | return object 71 | 72 | 73 | test_as_date = """ 74 | >>> source = '''{"date": {"$date": "2014-06-07T08:09:10"}}''' 75 | >>> json.loads(source, object_hook=as_date) 76 | {'date': datetime.datetime(2014, 6, 7, 8, 9, 10)} 77 | """ 78 | 79 | 80 | __test__ = {n: v for n, v in locals().items() if n.startswith("test_")} 81 | -------------------------------------------------------------------------------- /Chapter_10/ch10_r07.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 10, recipe 7, Reading XML documents 4 | 5 | Note: Output from this is used in Chapter 4 examples. 6 | """ 7 | 8 | import xml.etree.ElementTree as XML 9 | from pathlib import Path 10 | from typing import cast 11 | 12 | 13 | def race_summary(source_path: Path) -> None: 14 | source_text = source_path.read_text(encoding="UTF-8") 15 | document = XML.fromstring(source_text) 16 | 17 | legs = cast(XML.Element, document.find("legs")) 18 | teams = cast(XML.Element, document.find("teams")) 19 | 20 | for leg in legs.findall("leg"): 21 | print(cast(str, leg.text).strip()) 22 | n = leg.attrib["n"] 23 | 24 | for team in teams.findall("team"): 25 | position_leg = cast(XML.Element, team.find(f"position/leg[@n='{n}']")) 26 | name = cast(XML.Element, team.find("name")) 27 | print(cast(str, name.text).strip(), cast(str, position_leg.text).strip()) 28 | 29 | 30 | test_summary = """ 31 | >>> race_summary(source_path=Path("data") / "race_result.xml") # doctest: +ELLIPSIS 32 | ALICANTE - CAPE TOWN 33 | Abu Dhabi Ocean Racing 1 34 | Team Brunel 3 35 | Dongfeng Race Team 2 36 | MAPFRE 7 37 | Team Alvimedica 5 38 | Team SCA 6 39 | Team Vestas Wind 4 40 | ... 41 | LORIENT - GOTHENBURG 42 | Abu Dhabi Ocean Racing 5 43 | Team Brunel 2 44 | Dongfeng Race Team 4 45 | MAPFRE 3 46 | Team Alvimedica 1 47 | Team SCA 7 48 | Team Vestas Wind 6 49 | 50 | """ 51 | 52 | __test__ = {n: v for n, v in locals().items() if n.startswith("test_")} 53 | -------------------------------------------------------------------------------- /Chapter_11/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Modern-Python-Cookbook-Second-Edition/0182ee7f17ae58cff2775290547321ebed443ae3/Chapter_11/__init__.py -------------------------------------------------------------------------------- /Chapter_11/ch11_r08.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 11, recipe 8. Testing things that involve dates or times. 4 | """ 5 | 6 | import datetime 7 | import json 8 | from pathlib import Path 9 | from typing import Any 10 | 11 | 12 | def save_data(base: Path, some_payload: Any) -> None: 13 | now_date = datetime.datetime.utcnow() 14 | now_text = now_date.strftime("extract_%Y%m%d%H%M%S") 15 | file_path = (base / now_text).with_suffix(".json") 16 | with file_path.open("w") as target_file: 17 | json.dump(some_payload, target_file, indent=2) 18 | -------------------------------------------------------------------------------- /Chapter_11/ch11_r09.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 11, recipe 9. Testing things that involve randomness. 4 | """ 5 | import random 6 | from typing import List, Iterator, Counter 7 | import collections 8 | import statistics 9 | 10 | def resample(population: List[int], N: int) -> Iterator[int]: 11 | for i in range(N): 12 | sample = random.choice(population) 13 | yield sample 14 | 15 | def mean_distribution(population: List[int], N: int): 16 | means: Counter[float] = collections.Counter() 17 | for _ in range(1000): 18 | subset = list(resample(population, N)) 19 | measure = round(statistics.mean(subset), 1) 20 | means[measure] += 1 21 | return means 22 | 23 | test_estimate = """ 24 | >>> random.seed(42) 25 | >>> population = [8.04, 6.95, 7.58, 8.81, 8.33, 9.96, 7.24, 4.26, 10.84, 4.82, 5.68] 26 | >>> mean_distribution(population, 4).most_common(5) 27 | [(7.8, 51), (7.2, 45), (7.5, 44), (7.1, 41), (7.7, 40)] 28 | """ 29 | 30 | __test__ = {n: v for n, v in locals().items() if n.startswith("test_")} 31 | -------------------------------------------------------------------------------- /Chapter_11/test_ch11_r04.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 11, recipe 4 and 5. Unit testing with the unittest module, Combining Unittest and Doctest. 4 | 5 | A more conventional name would be test_ch11_r01.py 6 | """ 7 | 8 | import doctest 9 | import unittest 10 | import random 11 | 12 | from Chapter_11.ch11_r01 import Summary 13 | 14 | 15 | class GIVEN_data_WHEN_1k_samples_THEN_mean_median( 16 | unittest.TestCase): 17 | def setUp(self): 18 | self.summary = Summary() 19 | self.data = list(range(1001)) 20 | random.shuffle(self.data) 21 | 22 | def runTest(self): 23 | for sample in self.data: 24 | self.summary.add(sample) 25 | 26 | self.assertEqual(500, self.summary.mean) 27 | self.assertEqual(500, self.summary.median) 28 | 29 | 30 | class GIVEN_Summary_WHEN_1k_samples_THEN_mean_median( 31 | unittest.TestCase): 32 | def setUp(self): 33 | self.summary = Summary() 34 | self.data = list(range(1001)) 35 | random.shuffle(self.data) 36 | for sample in self.data: 37 | self.summary.add(sample) 38 | 39 | def test_mean(self): 40 | self.assertEqual(500, self.summary.mean) 41 | 42 | def test_median(self): 43 | self.assertEqual(500, self.summary.median) 44 | 45 | 46 | class GIVEN_Summary_WHEN_1k_samples_THEN_mode(unittest.TestCase): 47 | def setUp(self): 48 | self.summary = Summary() 49 | self.data = [500] * 97 50 | # Build 903 elements: each value of n occurs n times. 51 | for i in range(1, 43): 52 | self.data += [i] * i 53 | random.shuffle(self.data) 54 | for sample in self.data: 55 | self.summary.add(sample) 56 | 57 | def test_mode(self): 58 | top_3 = self.summary.mode[:3] 59 | self.assertListEqual([(500, 97), (42, 42), (41, 41)], top_3) 60 | 61 | 62 | # Recipe 5 -- Combining Unittest and Doctest. 63 | 64 | import Chapter_11.ch11_r01 65 | 66 | 67 | def load_tests(loader, standard_tests, pattern): 68 | dt = doctest.DocTestSuite(Chapter_11.ch11_r01) 69 | standard_tests.addTests(dt) 70 | return standard_tests 71 | 72 | 73 | if __name__ == "__main__": 74 | unittest.main() 75 | -------------------------------------------------------------------------------- /Chapter_11/test_ch11_r04_more.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 11, recipe 4 and 5. Unit testing with the unittest module, Combining Unittest and Doctest. 4 | 5 | A more conventional name would be test_ch11_r04.py 6 | """ 7 | import doctest 8 | 9 | import Chapter_11.ch11_r01 as ch11_r01 10 | import Chapter_11.ch11_r08 as ch11_r08 11 | import Chapter_11.ch11_r09 as ch11_r09 12 | 13 | # Chapter 3's test_point example has a repr() problem 14 | # The test needs rework to fit this pattern. 15 | import Chapter_11.ch11_r03 as ch11_r03 16 | 17 | 18 | def load_tests(loader, standard_tests, pattern): 19 | for module in ( 20 | ch11_r01, ch11_r08, ch11_r09 21 | ): 22 | dt = doctest.DocTestSuite(module) 23 | standard_tests.addTests(dt) 24 | return standard_tests 25 | -------------------------------------------------------------------------------- /Chapter_11/test_ch11_r06.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 11, recipe 6 and 7. Unit testing with the pytest module, Combining pytest and doctest tests 4 | 5 | A more conventional name would be test_ch11_r01.py 6 | 7 | """ 8 | 9 | import random 10 | 11 | from pytest import * # type: ignore 12 | from Chapter_11.ch11_r01 import Summary 13 | 14 | @fixture # type: ignore 15 | def flat_data(): 16 | data = list(range(1001)) 17 | random.shuffle(data) 18 | return data 19 | 20 | 21 | def test_flat(flat_data): 22 | summary = Summary() 23 | for sample in flat_data: 24 | summary.add(sample) 25 | assert summary.mean == 500 26 | assert summary.median == 500 27 | 28 | @fixture # type: ignore 29 | def summary_object(flat_data): 30 | summary = Summary() 31 | for sample in flat_data: 32 | summary.add(sample) 33 | return summary 34 | 35 | def test_mean(summary_object): 36 | assert summary_object.mean == 500 37 | 38 | def test_median(summary_object): 39 | assert summary_object.median == 500 40 | 41 | 42 | @fixture # type: ignore 43 | def biased_data(): 44 | # Build 500 elements: each with the same large value of 97. 45 | data = [500] * 97 46 | # Build 903 elements: each value of n occurs n times. 47 | for i in range(1, 43): 48 | data += [i] * i 49 | random.shuffle(data) 50 | return data 51 | 52 | def test_biased(biased_data): 53 | summary = Summary() 54 | for sample in biased_data: 55 | summary.add(sample) 56 | assert summary.mean == approx(74.085) 57 | assert summary.median == approx(32.0) 58 | top_3 = summary.mode[:3] 59 | assert top_3 == [(500, 97), (42, 42), (41, 41)] 60 | 61 | 62 | # Need to use a complex command line to combine both. 63 | # pytest Chapter_11.ch11_r06.py --doctest-modules Chapter_11.ch11_r01.py 64 | -------------------------------------------------------------------------------- /Chapter_11/test_ch11_r08.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 11, recipe 8. Testing things that involve dates or times 4 | Pytest Variant. 5 | """ 6 | import datetime 7 | import json 8 | from pathlib import Path 9 | 10 | from unittest.mock import Mock, patch 11 | from pytest import * # type: ignore 12 | 13 | import Chapter_11.ch11_r08 14 | 15 | 16 | @fixture # type: ignore 17 | def mock_datetime(): 18 | return Mock( 19 | wraps="datetime", 20 | datetime=Mock( 21 | utcnow=Mock( 22 | return_value=datetime.datetime( 23 | 2017, 9, 10, 11, 12, 13)) 24 | ), 25 | ) 26 | 27 | 28 | def test_save_data(mock_datetime, tmpdir, monkeypatch): 29 | monkeypatch.setattr( 30 | Chapter_11.ch11_r08, "datetime", mock_datetime) 31 | 32 | data = {"primes": [2, 3, 5, 7, 11, 13, 17, 19]} 33 | Chapter_11.ch11_r08.save_data(Path(tmpdir), data) 34 | 35 | expected_path = ( 36 | Path(tmpdir) / "extract_20170910111213.json") 37 | with expected_path.open() as result_file: 38 | result_data = json.load(result_file) 39 | assert data == result_data 40 | 41 | mock_datetime.datetime.utcnow.assert_called_once_with() 42 | 43 | @fixture # type: ignore 44 | def mock_datetime_now(): 45 | return Mock( 46 | name='mock datetime', 47 | datetime=Mock( 48 | name='mock datetime.datetime', 49 | utcnow=Mock( 50 | return_value=datetime.datetime( 51 | 2017, 7, 4, 1, 2, 3) 52 | ), 53 | now=Mock( 54 | return_value=datetime.datetime( 55 | 2017, 7, 4, 4, 2, 3) 56 | ) 57 | ) 58 | ) 59 | 60 | def test_save_data_now(mock_datetime_now, tmpdir, monkeypatch): 61 | monkeypatch.setattr( 62 | Chapter_11.ch11_r08, "datetime", mock_datetime_now) 63 | 64 | data = {"primes": [2, 3, 5, 7, 11, 13, 17, 19]} 65 | Chapter_11.ch11_r08.save_data(Path(tmpdir), data) 66 | 67 | expected_path = ( 68 | Path(tmpdir) / "extract_20170704010203.json") 69 | with expected_path.open() as result_file: 70 | result_data = json.load(result_file) 71 | assert data == result_data 72 | 73 | mock_datetime_now.datetime.utcnow.assert_called_once_with() 74 | assert mock_datetime_now.datetime.now.mock_calls == [] 75 | -------------------------------------------------------------------------------- /Chapter_11/test_ch11_r08_ut.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 11, recipe 8. Testing things that involve dates or times 4 | Unittest Variant. 5 | """ 6 | import datetime 7 | import json 8 | from pathlib import Path 9 | import unittest 10 | from unittest.mock import Mock, patch 11 | import Chapter_11.ch11_r08 12 | 13 | 14 | class GIVEN_data_WHEN_save_data_THEN_file(unittest.TestCase): 15 | def setUp(self): 16 | self.data = {"primes": [2, 3, 5, 7, 11, 13, 17, 19]} 17 | self.mock_datetime = Mock( 18 | datetime=Mock( 19 | utcnow=Mock(return_value=datetime.datetime(2017, 7, 4, 1, 2, 3)) 20 | ) 21 | ) 22 | self.expected_name = "extract_20170704010203.json" 23 | self.expected_path = Path("data") / self.expected_name 24 | if self.expected_path.exists(): 25 | self.expected_path.unlink() 26 | 27 | def runTest(self): 28 | with patch("Chapter_11.ch11_r08.datetime", self.mock_datetime): 29 | Chapter_11.ch11_r08.save_data(Path("data"), self.data) 30 | with self.expected_path.open() as result_file: 31 | result_data = json.load(result_file) 32 | self.assertDictEqual(self.data, result_data) 33 | 34 | self.mock_datetime.datetime.utcnow.assert_called_once_with() 35 | self.assertFalse(self.mock_datetime.datetime.called) 36 | -------------------------------------------------------------------------------- /Chapter_11/test_ch11_r09.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 11, recipe 9. Testing things that involve randomness 4 | pytest Variant. 5 | """ 6 | 7 | from types import SimpleNamespace 8 | from unittest.mock import Mock, patch, call, sentinel 9 | from pytest import * # type: ignore 10 | 11 | import Chapter_11.ch11_r09 12 | 13 | 14 | # Option 1: Mocking the whole random module. 15 | 16 | @fixture # type: ignore 17 | def mock_random(): 18 | expected_resample_data = [ 19 | 23, 29, 31, 37, 41, 43, 47, 53] 20 | random_module = Mock( 21 | name='mock random', 22 | choice=Mock( 23 | name='mock random.choice()', 24 | side_effect=expected_resample_data)) 25 | return SimpleNamespace(**locals()) 26 | 27 | 28 | def test_resample(mock_random, monkeypatch): 29 | monkeypatch.setattr( 30 | Chapter_11.ch11_r09, 31 | "random", 32 | mock_random.random_module) 33 | 34 | data = [2, 3, 5, 7, 11, 13, 17, 19] 35 | resample_data = list(Chapter_11.ch11_r09.resample(data, 8)) 36 | 37 | assert resample_data == mock_random.expected_resample_data 38 | mock_random.random_module.choice.assert_has_calls(8 * [call(data)]) 39 | 40 | # Option 2: Mocking a specific function 41 | 42 | @fixture # type: ignore 43 | def mock_random_choice(): 44 | expected_resample_data = [ 45 | 23, 29, 31, 37, 41, 43, 47, 53] 46 | mock_choice=Mock( 47 | name='mock random.choice()', 48 | side_effect=expected_resample_data) 49 | return SimpleNamespace(**locals()) 50 | 51 | def test_resample_choice(mock_random_choice, monkeypatch): 52 | monkeypatch.setattr( 53 | Chapter_11.ch11_r09.random, 54 | "choice", 55 | mock_random_choice.mock_choice) 56 | 57 | data = [2, 3, 5, 7, 11, 13, 17, 19] 58 | resample_data = list(Chapter_11.ch11_r09.resample(data, 8)) 59 | 60 | assert ( 61 | resample_data 62 | == mock_random_choice.expected_resample_data 63 | ) 64 | mock_random_choice.mock_choice.assert_has_calls(8 * [call(data)]) 65 | 66 | # Using a sentinel 67 | 68 | def test_resample_2(monkeypatch): 69 | mock_choice = Mock( 70 | name='mock random.choice()', 71 | side_effect=lambda x: x 72 | ) 73 | monkeypatch.setattr( 74 | Chapter_11.ch11_r09.random, 75 | "choice", 76 | mock_choice) 77 | 78 | resample_data = list(Chapter_11.ch11_r09.resample( 79 | sentinel.POPULATION, 8)) 80 | 81 | assert resample_data == [sentinel.POPULATION]*8 82 | mock_choice.assert_has_calls( 83 | 8 * [call(sentinel.POPULATION)]) 84 | -------------------------------------------------------------------------------- /Chapter_11/test_ch11_r09_ut.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 11, recipe 9. Testing things that involve randomness 4 | Unittest Variant. 5 | """ 6 | 7 | import unittest 8 | from unittest.mock import Mock, patch, call 9 | import Chapter_11.ch11_r09 10 | 11 | 12 | class GIVEN_resample_WHEN_evaluated_THEN_fair(unittest.TestCase): 13 | def setUp(self): 14 | self.data = [2, 3, 5, 7, 11, 13, 17, 19] 15 | self.expected_resample_data = [23, 29, 31, 37, 41, 43, 47, 53] 16 | self.mock_random = Mock(choice=Mock(side_effect=self.expected_resample_data)) 17 | 18 | def runTest(self): 19 | with patch("Chapter_11.ch11_r09.random", self.mock_random): 20 | resample_data = list(Chapter_11.ch11_r09.resample(self.data, 8)) 21 | 22 | self.assertListEqual(self.expected_resample_data, resample_data) 23 | 24 | self.mock_random.choice.assert_has_calls(8 * [call(self.data)]) 25 | -------------------------------------------------------------------------------- /Chapter_12/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Modern-Python-Cookbook-Second-Edition/0182ee7f17ae58cff2775290547321ebed443ae3/Chapter_12/__init__.py -------------------------------------------------------------------------------- /Chapter_12/ch12_r01.py: -------------------------------------------------------------------------------- 1 | """ 2 | openapi: 3.0.3 3 | info: 4 | title: Python Cookbook Chapter 12, recipe 1. 5 | description: Using the Flask framework for RESTful APIs 6 | version: "1.0" 7 | servers: 8 | - url: "http://127.0.0.1:5000/dealer" 9 | paths: 10 | /hand: 11 | get: 12 | parameters: 13 | - name: cards 14 | in: query 15 | required: false 16 | style: form 17 | schema: 18 | type: string 19 | explode: false 20 | responses: 21 | "200": 22 | description: One hand of cards with a size given by the hand value in the query string 23 | content: 24 | application/json: 25 | schema: 26 | type: array 27 | items: 28 | type: object 29 | properties: 30 | __class__: 31 | type: string 32 | __init__: 33 | type: object 34 | properties: 35 | rank: 36 | type: integer 37 | suit: 38 | type: string 39 | components: {} 40 | """ 41 | import random 42 | import os 43 | from typing import Optional 44 | from flask import Flask, jsonify, request, abort, Response 45 | from http import HTTPStatus 46 | 47 | from Chapter_12.card_model import Card, Deck 48 | 49 | dealer = Flask("dealer") 50 | 51 | deck: Optional[Deck] = None 52 | 53 | 54 | def get_deck() -> Deck: 55 | global deck 56 | if deck is None: 57 | random.seed(os.environ.get("DEAL_APP_SEED")) 58 | deck = Deck() 59 | return deck 60 | 61 | 62 | @dealer.before_request 63 | def check_json() -> Optional[Response]: 64 | if "json" in request.headers.get("Accept", "*/*"): 65 | return None 66 | if "json" == request.args.get("$format", "html"): 67 | return None 68 | print(f"Missing $format=json in {request.args}") 69 | return abort(HTTPStatus.BAD_REQUEST) 70 | 71 | 72 | @dealer.route("/dealer/hand") 73 | def deal() -> Response: 74 | try: 75 | hand_size = int(request.args.get("cards", 5)) 76 | assert 1 <= hand_size < 53 77 | except Exception as ex: 78 | abort(HTTPStatus.BAD_REQUEST) 79 | deck = get_deck() 80 | cards = deck.deal(hand_size) 81 | response = jsonify([card.serialize() for card in cards]) 82 | return response 83 | 84 | 85 | if __name__ == "__main__": 86 | dealer.run(use_reloader=True, threaded=False) 87 | 88 | """ 89 | https://editor.swagger.io/ 90 | 91 | Start with this to force a particular seed to get a consistent result. 92 | :: 93 | 94 | DEAL_APP_SEED=42 PYTHONPATH=. python Chapter_12.ch12_r01.py 95 | 96 | Note the --header for the accept is required, as are the quotes to stop zsh from looking at the ? 97 | :: 98 | 99 | curl 'http://127.0.0.1:5000/dealer/hand?cards=5' --header accept:application/json 100 | 101 | Response:: 102 | 103 | [{"__class__":"Card","rank":10,"suit":"\u2661"},{"__class__":"Card","rank":4,"suit":"\u2661"},{"__class__":"Card","rank":7,"suit":"\u2660"},{"__class__":"Card","rank":11,"suit":"\u2662"},{"__class__":"Card","rank":12,"suit":"\u2661"}] 104 | """ 105 | -------------------------------------------------------------------------------- /Chapter_12/ch12_r03.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2 | 3 | Chapter 12, recipe 3, Making REST requests with urllib. 4 | """ 5 | 6 | import urllib.request 7 | import urllib.parse 8 | import json 9 | from typing import Dict, Any 10 | from openapi_spec_validator import validate_spec # type: ignore 11 | 12 | 13 | def get_openapi_spec() -> Dict[str, Any]: 14 | """Is the OpenAPI specification valid? 15 | """ 16 | with urllib.request.urlopen( 17 | "http://127.0.0.1:5000/dealer/openapi.json" 18 | ) as spec_request: 19 | openapi_spec = json.load(spec_request) 20 | validate_spec(openapi_spec) 21 | print("openapi.json is valid") 22 | return openapi_spec 23 | 24 | 25 | def query_build_1() -> None: 26 | """Build and execute a query in pieces.""" 27 | query = {"hand": 5} 28 | full_url = urllib.parse.ParseResult( 29 | scheme="http", 30 | netloc="127.0.0.1:5000", 31 | path="/dealer" + "/hand", 32 | params="", 33 | query=urllib.parse.urlencode(query), 34 | fragment="", 35 | ) 36 | 37 | request2 = urllib.request.Request( 38 | url=urllib.parse.urlunparse(full_url), 39 | method="GET", 40 | headers={"Accept": "application/json",}, 41 | ) 42 | 43 | with urllib.request.urlopen(request2) as response: 44 | print(response.getcode()) 45 | print(response.headers) 46 | print(json.loads(response.read().decode("utf-8"))) 47 | 48 | 49 | def query_build_2(openapi_spec: Dict[str, Any]) -> None: 50 | """Simpler alternative from OpenAPI Spec""" 51 | query = [("cards", 2), ("cards", 1), ("cards", 1), ("cards", 1)] 52 | query_text = urllib.parse.urlencode(query) 53 | request3 = urllib.request.Request( 54 | url=f"{openapi_spec['servers'][0]['url']}/hands?{query_text}", 55 | method="GET", 56 | headers={"Accept": "application/json",}, 57 | ) 58 | with urllib.request.urlopen(request3) as response: 59 | print(response.getcode()) 60 | print(response.headers) 61 | print(json.loads(response.read().decode("utf-8"))) 62 | 63 | 64 | if __name__ == "__main__": 65 | print("Be sure the servar was started before running this.") 66 | print("For example, DEAL_APP_SEED=42 PYTHONPATH=. python Chapter_12/ch12_r02.py") 67 | spec = get_openapi_spec() 68 | query_build_1() 69 | query_build_2(spec) 70 | -------------------------------------------------------------------------------- /Chapter_12/test_ch12_all_integration.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Tests for all ch12_r??_client and ch12_r??_server pairs 4 | 5 | This requires ``demo.cert`` and ``demo.key`` in the local working directory. 6 | 7 | """ 8 | import os 9 | import subprocess 10 | import sys 11 | import time 12 | import pytest # type: ignore 13 | from pytest import * # type: ignore 14 | 15 | client_server_pairs = [ 16 | ("Chapter_12/ch12_r04_client.py", "Chapter_12/ch12_r04_server.py"), 17 | ("Chapter_12/ch12_r05_client.py", "Chapter_12/ch12_r05_server.py"), 18 | ("Chapter_12/ch12_r06_client.py", "Chapter_12/ch12_r06_server.py"), 19 | ] 20 | 21 | @fixture(params=client_server_pairs, ids=lambda x:f"{x[0]}-{x[1]}") # type: ignore 22 | def client_server(request): 23 | client, server = request.param 24 | server_process = subprocess.Popen(["python", server]) 25 | print(f"Starting {server_process.pid}") 26 | time.sleep(0.5) # Pause to let the server start. 27 | 28 | yield client, server 29 | 30 | time.sleep(0.25) 31 | server_process.terminate() 32 | time.sleep(0.25) # Pause while the server finishes. 33 | server_process.wait() 34 | assert server_process.returncode == 0, f"Server Exit Status {server_process.returncode}" 35 | 36 | @pytest.mark.ssl 37 | def test_client_main(client_server, tmp_path): 38 | client, server = client_server 39 | cache = tmp_path/"log" 40 | with cache.open('w') as log_output: 41 | client_process = subprocess.run( 42 | ["python", client], 43 | text=True, 44 | ) 45 | print(cache.read_text(), file=sys.stdout) 46 | assert client_process.returncode == 0 47 | 48 | -------------------------------------------------------------------------------- /Chapter_12/test_ch12_r01.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Tests for ch12_r01 4 | """ 5 | import json 6 | from unittest.mock import Mock 7 | import Chapter_12.ch12_r01 8 | from pytest import * # type: ignore 9 | 10 | 11 | @fixture # type: ignore 12 | def dealer_client(monkeypatch): 13 | monkeypatch.setenv("DEAL_APP_SEED", "42") 14 | app = Chapter_12.ch12_r01.dealer 15 | return app.test_client() 16 | 17 | 18 | def test_deal_cards(dealer_client): 19 | response = dealer_client.get( 20 | path="/dealer/hand", 21 | query_string={"cards": 5}, 22 | headers={"Accept": "application/json"}, 23 | ) 24 | assert response.status_code == 200 25 | assert response.json == [ 26 | {"__class__": "Card", "__init__": {"rank": 10, "suit": "♡"}}, 27 | {"__class__": "Card", "__init__": {"rank": 4, "suit": "♡"}}, 28 | {"__class__": "Card", "__init__": {"rank": 7, "suit": "♠"}}, 29 | {"__class__": "Card", "__init__": {"rank": 11, "suit": "♢"}}, 30 | {"__class__": "Card", "__init__": {"rank": 12, "suit": "♡"}}, 31 | ] 32 | -------------------------------------------------------------------------------- /Chapter_12/test_ch12_r02.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Tests for ch12_r02 4 | """ 5 | import json 6 | from unittest.mock import Mock 7 | import Chapter_12.ch12_r02 8 | 9 | from pytest import * # type: ignore 10 | import yaml 11 | 12 | @fixture # type: ignore 13 | def dealer_client(monkeypatch): 14 | monkeypatch.setenv("DEAL_APP_SEED", "42") 15 | app = Chapter_12.ch12_r02.dealer 16 | return app.test_client() 17 | 18 | 19 | def test_deal_cards(dealer_client): 20 | response = dealer_client.get( 21 | path="/dealer/hand", 22 | query_string={"cards": 5}, 23 | headers={"Accept": "application/json"}, 24 | ) 25 | assert response.status_code == 200 26 | assert response.json == [ 27 | {"__class__": "Card", "__init__": {"rank": 10, "suit": "♡"}}, 28 | {"__class__": "Card", "__init__": {"rank": 4, "suit": "♡"}}, 29 | {"__class__": "Card", "__init__": {"rank": 7, "suit": "♠"}}, 30 | {"__class__": "Card", "__init__": {"rank": 11, "suit": "♢"}}, 31 | {"__class__": "Card", "__init__": {"rank": 12, "suit": "♡"}}, 32 | ] 33 | 34 | 35 | def test_openapi_yaml(dealer_client): 36 | response = dealer_client.get( 37 | path="/dealer/openapi.yaml", 38 | headers={"Accept": "application/yaml"}, 39 | ) 40 | assert response.status_code == 200 41 | expected = Chapter_12.ch12_r02.specification 42 | assert expected == yaml.load(response.data, Loader=yaml.SafeLoader) 43 | 44 | def test_openapi_json(dealer_client): 45 | response = dealer_client.get( 46 | path="/dealer/openapi.json", 47 | headers={"Accept": "application/json"}, 48 | ) 49 | assert response.status_code == 200 50 | expected = Chapter_12.ch12_r02.specification 51 | assert expected == response.json 52 | 53 | -------------------------------------------------------------------------------- /Chapter_12/test_ch12_r03.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Tests for ch12_r03 4 | """ 5 | import os 6 | import re 7 | from pathlib import Path 8 | import subprocess 9 | import time 10 | from pytest import * # type: ignore 11 | 12 | import Chapter_12.ch12_r03 13 | 14 | @fixture(scope="module") # type: ignore 15 | def ch12_r02_server(): 16 | """Start and stop the server.""" 17 | env = os.environ.copy() 18 | env['PYTHONPATH'] = str(Path(__file__).parent.parent) 19 | env['DEAL_APP_SEED'] = '42' 20 | server = subprocess.Popen(["python", "Chapter_12/ch12_r02.py"], env=env) 21 | time.sleep(0.5) # Pause to let the server start. 22 | 23 | yield server # allow the test to run 24 | 25 | time.sleep(0.25) 26 | server.terminate() 27 | time.sleep(0.25) # Pause while the server finishes. 28 | server.wait() 29 | # server.kill() 30 | assert server.returncode == 0, f"Server Exit Status {server_process.returncode}" 31 | 32 | def test_get_spec(ch12_r02_server, capsys): 33 | spec = Chapter_12.ch12_r03.get_openapi_spec() 34 | assert spec['info']['title'] == 'Python Cookbook Chapter 12, recipe 2.' 35 | out, err = capsys.readouterr() 36 | assert out == "openapi.json is valid\n" 37 | 38 | header_pattern = re.compile(r"[\w-]+: .*?") 39 | 40 | def test_query_build_1(ch12_r02_server, capsys): 41 | Chapter_12.ch12_r03.query_build_1() 42 | out, err = capsys.readouterr() 43 | status_headers, _, body = out.partition("\n\n") 44 | status, *headers = status_headers.splitlines() 45 | assert status == "200" 46 | assert all(header_pattern.match(h) for h in headers) 47 | assert body == "\n[{'__class__': 'Card', '__init__': {'rank': 10, 'suit': '♡'}}, {'__class__': 'Card', '__init__': {'rank': 4, 'suit': '♡'}}, {'__class__': 'Card', '__init__': {'rank': 7, 'suit': '♠'}}, {'__class__': 'Card', '__init__': {'rank': 11, 'suit': '♢'}}, {'__class__': 'Card', '__init__': {'rank': 12, 'suit': '♡'}}]\n" 48 | 49 | def test_query_build_2(ch12_r02_server, capsys): 50 | spec = Chapter_12.ch12_r03.get_openapi_spec() 51 | Chapter_12.ch12_r03.query_build_2(spec) 52 | out, err = capsys.readouterr() 53 | status_headers, _, body = out.partition("\n\n") 54 | spec_response, status, *headers = status_headers.splitlines() 55 | assert spec_response == "openapi.json is valid" 56 | assert status == "200" 57 | assert all(header_pattern.match(h) for h in headers) 58 | assert body == "\n[{'cards': [{'__class__': 'Card', '__init__': {'rank': 3, 'suit': '♣'}}, {'__class__': 'Card', '__init__': {'rank': 10, 'suit': '♠'}}], 'hand': 0}, {'cards': [{'__class__': 'Card', '__init__': {'rank': 9, 'suit': '♠'}}], 'hand': 1}, {'cards': [{'__class__': 'Card', '__init__': {'rank': 13, 'suit': '♣'}}], 'hand': 2}, {'cards': [{'__class__': 'Card', '__init__': {'rank': 5, 'suit': '♣'}}], 'hand': 3}]\n" 59 | -------------------------------------------------------------------------------- /Chapter_12/test_ch12_r04_server.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Tests for ch12_r04_server 4 | """ 5 | import json 6 | from unittest.mock import Mock 7 | import Chapter_12.ch12_r04_server 8 | from pytest import * # type: ignore 9 | 10 | 11 | @fixture # type: ignore 12 | def dealer_client(monkeypatch): 13 | monkeypatch.setenv("DEAL_APP_SEED", "42") 14 | app = Chapter_12.ch12_r04_server.dealer 15 | return app.test_client() 16 | 17 | 18 | def test_openapi_spec(dealer_client): 19 | spec_response = dealer_client.get("/dealer/openapi.json") 20 | assert ( 21 | spec_response.get_json()["info"]["title"] 22 | == "Python Cookbook Chapter 12, recipe 4." 23 | ) 24 | 25 | 26 | def test_deal_cards_sequence(dealer_client): 27 | # Typical 28 | # response1 = dealer_client.post("/dealer/decks", json={'decks': 6}, headers={'Accept': "application/json"}) 29 | # In this specific case 30 | response1 = dealer_client.post( 31 | path="/dealer/decks", 32 | query_string={"decks": 6}, 33 | headers={"Accept": "application/json"}, 34 | ) 35 | assert response1.status_code == 201 36 | response_document = response1.get_json() 37 | assert response_document["status"] == "ok" 38 | deck_url = response1.headers["Location"] 39 | deck_id = response_document["id"] 40 | 41 | response2 = dealer_client.get(deck_url, headers={"Accept": "application/json"}) 42 | assert response2.status_code == 200 43 | assert len(response2.get_json()) == 6 * 52 44 | 45 | response3 = dealer_client.get( 46 | path=f"/dealer/decks/{deck_id}/hands", 47 | query_string={"cards": 5}, 48 | headers={"Accept": "application/json"}, 49 | ) 50 | assert response3.status_code == 200 51 | assert response3.json == [ 52 | { 53 | "cards": [ 54 | {"__class__": "Card", "__init__": {"rank": 10, "suit": "♡"}}, 55 | {"__class__": "Card", "__init__": {"rank": 1, "suit": "♠"}}, 56 | {"__class__": "Card", "__init__": {"rank": 9, "suit": "♡"}}, 57 | {"__class__": "Card", "__init__": {"rank": 11, "suit": "♢"}}, 58 | {"__class__": "Card", "__init__": {"rank": 5, "suit": "♡"}}, 59 | ], 60 | "hand": 0, 61 | } 62 | ] 63 | 64 | def test_deal_bad_make_deck(dealer_client): 65 | response = dealer_client.post( 66 | path="/dealer/decks", 67 | query_string={"decks": "not a number!"}, 68 | headers={"Accept": "application/json"}, 69 | ) 70 | assert response.status_code == 400 71 | assert response.get_json() is None 72 | assert b"Bad Request" in response.data 73 | 74 | 75 | def test_deal_bad_get_hands(dealer_client): 76 | deck_id = "definitely doesn't exist" 77 | response = dealer_client.get( 78 | path=f"/dealer/decks/{deck_id}/hands", 79 | query_string={"cards": 5}, 80 | headers={"Accept": "application/json"}, 81 | ) 82 | assert response.status_code == 404 83 | assert response.get_json() is None 84 | assert b"Not Found" in response.data 85 | assert b"deck "definitely doesn't exist" not found" in response.data 86 | -------------------------------------------------------------------------------- /Chapter_13/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Modern-Python-Cookbook-Second-Edition/0182ee7f17ae58cff2775290547321ebed443ae3/Chapter_13/__init__.py -------------------------------------------------------------------------------- /Chapter_13/ch13_r01.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2 | 3 | Chapter 13, recipe 1, Finding configuration files. 4 | """ 5 | from pathlib import Path 6 | import collections 7 | from typing import TextIO, Dict, Any, ChainMap 8 | 9 | 10 | def load_config_file(config_path: Path) -> Dict[str, Any]: # type: ignore[empty-body] 11 | """Loads a configuration mapping object with contents 12 | of a given file. 13 | 14 | :param config_path: Path to be read. 15 | :returns: mapping with configuration parameter values 16 | """ 17 | # Details omitted. 18 | 19 | 20 | def get_config() -> ChainMap[str, Any]: 21 | system_path = Path("/etc") / "profile" 22 | local_paths = [ 23 | Path.home() / ".bash_profile", 24 | Path.home() / ".bash_login", 25 | Path.home() / ".profile", 26 | ] 27 | 28 | configuration_items = [ 29 | dict( 30 | some_setting="Default Value", 31 | another_setting="Another Default", 32 | some_option="Built-In Choice", 33 | ) 34 | ] 35 | 36 | if system_path.exists(): 37 | configuration_items.append( 38 | load_config_file(system_path)) 39 | 40 | for config_path in local_paths: 41 | if config_path.exists(): 42 | configuration_items.append( 43 | load_config_file(config_path)) 44 | break 45 | 46 | configuration = collections.ChainMap( 47 | *reversed(configuration_items)) 48 | return configuration 49 | -------------------------------------------------------------------------------- /Chapter_13/ch13_r02.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2 | 3 | Chapter 13, recipe 2, Using YAML for configuration files. 4 | """ 5 | from pathlib import Path 6 | from typing import Dict, Any 7 | import yaml 8 | 9 | 10 | def load_config_file(config_path: Path) -> Dict[str, Any]: 11 | """Loads a configuration mapping object with contents 12 | of a given file. 13 | 14 | :param config_path: Path to be read. 15 | :returns: mapping with configuration parameter values 16 | """ 17 | with config_path.open() as config_file: 18 | document = yaml.load( 19 | config_file, Loader=yaml.SafeLoader) 20 | return document 21 | 22 | 23 | # Further Examples. 24 | # Uses of UnsafeLoader 25 | 26 | class Card: 27 | def __init__(self, rank: int, suit: str) -> None: 28 | self.rank = rank 29 | self.suit = suit 30 | 31 | def __repr__(self) -> str: 32 | return f"{self.rank} {self.suit}" 33 | 34 | 35 | card_text = """ 36 | !!python/object/apply:Chapter_13.ch13_r02.Card 37 | kwds: 38 | rank: 7 39 | suit: ♣︎ 40 | """ 41 | 42 | 43 | test_load_card = """ 44 | >>> yaml.load(card_text, Loader=yaml.UnsafeLoader) 45 | 7 ♣︎ 46 | """ 47 | 48 | import collections 49 | 50 | od_text = """ 51 | !!python/object/apply:collections.OrderedDict 52 | args: 53 | - !!omap 54 | - key1: string value 55 | - numerator: 355 56 | - denominator: 113 57 | """ 58 | 59 | test_load_OrderedDict = """ 60 | >>> yaml.load(od_text, Loader=yaml.UnsafeLoader) 61 | OrderedDict([('key1', 'string value'), ('numerator', 355), ('denominator', 113)]) 62 | 63 | """ 64 | 65 | __test__ = { 66 | n: v for n, v in locals().items() if n.startswith("test_") 67 | } 68 | -------------------------------------------------------------------------------- /Chapter_13/ch13_r03.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2 | 3 | Chapter 13, recipe 3, Using Python for configuration files 4 | """ 5 | 6 | from pathlib import Path 7 | from typing import Dict, Any, cast 8 | 9 | 10 | def load_config_file(config_path: Path) -> Dict[str, Any]: 11 | """Loads a configuration mapping object with contents 12 | of a given file. 13 | 14 | :param config_path: Path to be read. 15 | :returns: mapping with configuration parameter values 16 | """ 17 | code = compile( 18 | config_path.read_text(), 19 | config_path.name, 20 | "exec") 21 | locals: Dict[str, Any] = {} 22 | exec(code, {"__builtins__": __builtins__}, locals) 23 | return locals 24 | 25 | 26 | from pathlib import Path 27 | import platform 28 | import os 29 | import sys 30 | 31 | def load_config_file_xtra(config_path: Path) -> Dict[str, Any]: 32 | """Loads a configuration mapping object with contents 33 | of a given file. The execution context includes a selected 34 | portion of the standard library, include the :class:`pathlib.Path` class. 35 | 36 | :param config_path: Path to be read. 37 | :returns: mapping with configuration parameter values 38 | """ 39 | def not_allowed(*arg, **kw) -> None: 40 | raise RuntimeError("Operation not allowed") 41 | 42 | code = compile( 43 | config_path.read_text(), 44 | config_path.name, 45 | "exec") 46 | safe_builtins = cast(Dict[str, Any], __builtins__).copy() 47 | for name in ("eval", "exec", "compile", "__import__"): 48 | safe_builtins[name] = not_allowed 49 | globals = { 50 | "__builtins__": safe_builtins, 51 | "Path": Path, 52 | "platform": platform, 53 | "environ": os.environ.copy(), 54 | } 55 | locals: Dict[str, Any] = {} 56 | exec(code, globals, locals) 57 | return locals 58 | 59 | 60 | __test__ = { 61 | n: v for n, v in locals().items() if n.startswith("test_") 62 | } 63 | -------------------------------------------------------------------------------- /Chapter_13/ch13_r05a.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2 | 3 | Chapter 13, recipe 5, Designing scripts for composition. 4 | class-based design alternative. 5 | """ 6 | import random 7 | import yaml 8 | import collections 9 | from pathlib import Path 10 | import argparse 11 | import os 12 | import sys 13 | from typing import NamedTuple, List, Iterable, Tuple, Counter, Iterator, Optional 14 | 15 | 16 | class CrapsSimulator: 17 | def __init__(self, /, seed: Optional[int] = None) -> None: 18 | self.rng = random.Random(seed) 19 | self.faces: List[int] 20 | self.total: int 21 | 22 | def roll(self, n: int = 2) -> int: 23 | self.faces = list(self.rng.randint(1, 6) for _ in range(n)) 24 | self.total = sum(self.faces) 25 | return self.total 26 | 27 | def craps_game(self) -> List[List[int]]: 28 | self.roll() 29 | if self.total in [2, 3, 12]: 30 | return [self.faces] 31 | elif self.total in [7, 11]: 32 | return [self.faces] 33 | elif self.total in [4, 5, 6, 8, 9, 10]: 34 | point, sequence = self.total, [self.faces] 35 | self.roll() 36 | while self.total not in [7, point]: 37 | sequence.append(self.faces) 38 | self.roll() 39 | sequence.append(self.faces) 40 | return sequence 41 | else: 42 | raise Exception("Horrifying Logic Bug") 43 | 44 | def roll_iter(self, total_games: int) -> Iterator[List[List[int]]]: 45 | for i in range(total_games): 46 | sequence = self.craps_game() 47 | yield sequence 48 | 49 | 50 | test_sim_class = """ 51 | >>> sim = CrapsSimulator(seed=42) 52 | >>> list(sim.roll_iter(12)) # doctest: +NORMALIZE_WHITESPACE 53 | [[[6, 1]], 54 | [[1, 6]], 55 | [[3, 2], [2, 2], [6, 1]], 56 | [[6, 6]], 57 | [[5, 1], [5, 4], [1, 1], [1, 2], [2, 5]], 58 | [[5, 1], [5, 2]], 59 | [[6, 6]], 60 | [[6, 5]], 61 | [[4, 2], [4, 5], [3, 1], [2, 6], [4, 3]], 62 | [[3, 2], [2, 3]], 63 | [[1, 1]], 64 | [[4, 1], [3, 3], [5, 3], [1, 6]]] 65 | """ 66 | 67 | 68 | __test__ = {n: v for n, v in locals().items() if n.startswith("test_")} 69 | -------------------------------------------------------------------------------- /Chapter_13/confirm_destination.py: -------------------------------------------------------------------------------- 1 | """ 2 | Confirm how logging is configured. 3 | 4 | It's not always obvious. A "spike" like this can help understand 5 | the current logging configuration 6 | """ 7 | import logging 8 | 9 | def main(): 10 | logger = logging.getLogger("main") 11 | logger.debug("debug") 12 | logger.info("info") 13 | logger.warning("warning") 14 | logger.error("error") 15 | 16 | if __name__ == "__main__": 17 | logging.basicConfig(level=logging.INFO) 18 | main() 19 | logging.shutdown() 20 | -------------------------------------------------------------------------------- /Chapter_13/examples.txt: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2 | 3 | Chapter 13, Examples from the text. 4 | 5 | • Finding configuration files 6 | • Using YAML for configuration files 7 | • Using Python for configuration files 8 | • Using class-as-namespace for configuration values 9 | • Designing scripts for composition 10 | • Using logging for control and audit output 11 | 12 | """ 13 | 14 | # Finding configuration files 15 | 16 | >>> import collections 17 | >>> config = collections.ChainMap( 18 | ... {'another_setting': 2}, 19 | ... {'some_setting': 1}, 20 | ... {'some_setting': 'Default Value', 21 | ... 'another_setting': 'Another Default', 22 | ... 'some_option': 'Built-In Choice'}) 23 | 24 | 25 | >>> config['another_setting'] 26 | 2 27 | >>> config['some_setting'] 28 | 1 29 | >>> config['some_option'] 30 | 'Built-In Choice' 31 | 32 | 33 | # Using YAML for configuration files 34 | 35 | 36 | >>> import yaml 37 | >>> yaml_text = ''' 38 | ... --- 39 | ... id: 1 40 | ... text: "Some Words." 41 | ... --- 42 | ... id: 2 43 | ... text: "Different Words." 44 | ... ''' 45 | 46 | >>> document_iterator = yaml.load_all(yaml_text, Loader=yaml.SafeLoader) 47 | >>> document_1 = next(document_iterator) 48 | >>> document_1['id'] 49 | 1 50 | >>> document_2 = next(document_iterator) 51 | >>> document_2['text'] 52 | 'Different Words.' 53 | 54 | 55 | >>> mapping_text = ''' 56 | ... ? !!python/tuple ["a", "b"] 57 | ... : "value" 58 | ... ''' 59 | >>> yaml.load(mapping_text, Loader=yaml.UnsafeLoader) 60 | {('a', 'b'): 'value'} 61 | 62 | 63 | >>> import yaml 64 | >>> set_text = ''' 65 | ... document: 66 | ... id: 3 67 | ... data_values: 68 | ... !!set 69 | ... ? some 70 | ... ? more 71 | ... ? words 72 | ... ''' 73 | 74 | >>> some_document = yaml.load(set_text, Loader=yaml.SafeLoader) 75 | >>> some_document['document']['id'] 76 | 3 77 | >>> some_document['document']['data_values'] == {'some', 'more', 'words'} 78 | True 79 | 80 | 81 | >>> import yaml 82 | >>> yaml_text = ''' 83 | ... !!omap 84 | ... - key1: string value 85 | ... - numerator: 355 86 | ... - denominator: 113 87 | ... ''' 88 | 89 | 90 | >>> yaml.load(yaml_text, Loader=yaml.SafeLoader) 91 | [('key1', 'string value'), ('numerator', 355), ('denominator', 113)] 92 | 93 | 94 | 95 | 96 | # Using class-as-namespace for configuration values 97 | 98 | >>> from pprint import pprint 99 | >>> import Chapter_13.ch13_r04 100 | >>> configuration = Chapter_13.ch13_r04.load_config_module( 101 | ... 'Chapter_13.settings.Chesapeake') 102 | >>> configuration.__doc__.strip() 103 | 'Weather for Chesapeake Bay' 104 | >>> configuration.query 105 | {'mz': ['ANZ532']} 106 | >>> configuration.url['netloc'] 107 | 'forecast.weather.gov' 108 | 109 | >>> print(configuration) 110 | 111 | >>> pprint(vars(configuration)) 112 | mappingproxy({'__doc__': '\n Weather for Chesapeake Bay\n ', 113 | '__module__': 'Chapter_13.settings', 114 | 'query': {'mz': ['ANZ532']}}) 115 | -------------------------------------------------------------------------------- /Chapter_13/settings.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2 | 3 | Chapter 13, recipe 4, Using class-as-namespace for configuration values. 4 | Settings. 5 | """ 6 | 7 | 8 | class Configuration: 9 | """ 10 | Generic Configuration 11 | """ 12 | 13 | url = {"scheme": "http", "netloc": "forecast.weather.gov", "path": "/shmrn.php"} 14 | 15 | 16 | class Bahamas(Configuration): 17 | """ 18 | Weather forecast for Offshore including the Bahamas 19 | """ 20 | 21 | query = {"mz": ["AMZ117", "AMZ080"]} 22 | 23 | 24 | class Chesapeake(Configuration): 25 | """ 26 | Weather for Chesapeake Bay 27 | """ 28 | 29 | query = {"mz": ["ANZ532"]} 30 | -------------------------------------------------------------------------------- /Chapter_13/test_ch13_r01.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2 | 3 | Chapter 13, recipe 1. 4 | """ 5 | from pathlib import Path 6 | from pytest import * # type: ignore 7 | from unittest.mock import Mock, patch, mock_open, MagicMock, call 8 | import Chapter_13.ch13_r01 9 | 10 | 11 | @fixture # type: ignore 12 | def mock_path(monkeypatch, tmpdir): 13 | mocked_class = Mock( 14 | wraps=Path, 15 | return_value=Path(tmpdir / "etc"), 16 | home=Mock(return_value=Path(tmpdir / "home")), 17 | ) 18 | monkeypatch.setattr(Chapter_13.ch13_r01, "Path", mocked_class) 19 | 20 | (tmpdir / "etc").mkdir() 21 | (tmpdir / "etc" / "profile").write_text( 22 | "exists", encoding="utf-8") 23 | (tmpdir / "home").mkdir() 24 | (tmpdir / "home" / ".profile").write_text( 25 | "exists", encoding="utf-8") 26 | return mocked_class 27 | 28 | 29 | @fixture # type: ignore 30 | def mock_load_config(monkeypatch): 31 | mocked_load_config_file = Mock(return_value={}) 32 | monkeypatch.setattr( 33 | Chapter_13.ch13_r01, 34 | "load_config_file", 35 | mocked_load_config_file 36 | ) 37 | return mocked_load_config_file 38 | 39 | 40 | def test_get_config(mock_load_config, mock_path): 41 | config = Chapter_13.ch13_r01.get_config() 42 | 43 | assert mock_path.mock_calls == [ 44 | call("/etc"), 45 | call.home(), 46 | call.home(), 47 | call.home(), 48 | ] 49 | assert mock_load_config.mock_calls == [ 50 | call(mock_path.return_value / "profile"), 51 | call(mock_path.home.return_value / ".profile"), 52 | ] 53 | -------------------------------------------------------------------------------- /Chapter_13/test_ch13_r01_ut.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2 | 3 | Chapter 13, recipe 1. 4 | """ 5 | from pathlib import Path 6 | import unittest 7 | from unittest.mock import Mock, patch, mock_open, MagicMock, call 8 | import Chapter_13.ch13_r01 9 | 10 | 11 | class GIVEN_get_config_WHEN_load_THEN_overrides_found(unittest.TestCase): 12 | def setUp(self): 13 | self.system_exist = Mock( 14 | name="mock Path('/etc').exist() == True", 15 | exists=Mock(return_value=True), 16 | open=mock_open(), 17 | ) 18 | self.mock_system_path = Mock( 19 | name='mock Path("/etc")', 20 | __truediv__=Mock(return_value=self.system_exist), 21 | ) 22 | 23 | self.exist = Mock( 24 | name="mock Path.home().exist() == True", 25 | exists=Mock(return_value=True), 26 | open=mock_open(), 27 | ) 28 | self.not_exist = Mock( 29 | name="mock Path.home().exist() == False", 30 | exists=Mock(return_value=False) 31 | ) 32 | self.mock_home_path = Mock( 33 | name="mock Path.home()", 34 | __truediv__=Mock(side_effect=[self.not_exist, self.exist, self.exist]), 35 | ) 36 | 37 | self.mock_path = Mock( 38 | name="mock Path class", 39 | return_value=self.mock_system_path, # Path("/etc") 40 | home=Mock(return_value=self.mock_home_path), # Path.home() 41 | ) 42 | self.mock_load_config_file = Mock( 43 | name="mock_load_config_file", 44 | side_effect=[{"some_setting": 1}, {"another_setting": 2}], 45 | ) 46 | 47 | def runTest(self): 48 | with patch("Chapter_13.ch13_r01.Path", self.mock_path), patch( 49 | "Chapter_13.ch13_r01.load_config_file", self.mock_load_config_file 50 | ): 51 | config = Chapter_13.ch13_r01.get_config() 52 | # print(config) 53 | self.assertEqual(2, config["another_setting"]) 54 | self.assertEqual(1, config["some_setting"]) 55 | self.assertEqual("Built-In Choice", config["some_option"]) 56 | 57 | # print(self.mock_load.mock_calls) 58 | self.mock_load_config_file.assert_has_calls( 59 | [call(self.system_exist), call(self.exist)] 60 | ) 61 | 62 | # print(self.mock_expanded_home_path.mock_calls) 63 | self.mock_home_path.assert_has_calls( 64 | [ 65 | call.__truediv__(".bash_profile"), 66 | call.__truediv__(".bash_login"), 67 | call.__truediv__(".profile"), 68 | ] 69 | ) 70 | 71 | # print(self.mock_path.mock_calls) 72 | self.mock_path.assert_has_calls( 73 | [call("/etc"), call.home(), call.home(), call.home()] 74 | ) 75 | 76 | self.exist.assert_has_calls([call.exists()]) 77 | self.system_exist.assert_has_calls([call.exists()]) 78 | 79 | -------------------------------------------------------------------------------- /Chapter_13/test_ch13_r02.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2 | 3 | Chapter 13, recipe 2, Using YAML for configuration files. 4 | """ 5 | from pathlib import Path 6 | from pytest import * # type: ignore 7 | import Chapter_13.ch13_r02 8 | 9 | 10 | config_text = """ 11 | query: 12 | mz: 13 | - ANZ532 14 | - AMZ117 15 | - AMZ080 16 | url: 17 | scheme: http 18 | netloc: forecast.weather.gov 19 | path: shmrn.php 20 | description: > 21 | Weather forecast for Offshore including the Bahamas 22 | """ 23 | 24 | 25 | @fixture # type: ignore 26 | def config_yaml_file(tmpdir): 27 | filename = tmpdir/"config.yaml" 28 | filename.write_text(config_text, encoding='utf-8') 29 | return filename 30 | 31 | 32 | def test_load_config_simple(config_yaml_file): 33 | expected = { 34 | 'description': 'Weather forecast for Offshore including the Bahamas\n', 35 | 'query': {'mz': ['ANZ532', 'AMZ117', 'AMZ080']}, 36 | 'url': { 37 | 'netloc': 'forecast.weather.gov', 38 | 'path': 'shmrn.php', 39 | 'scheme': 'http' 40 | } 41 | } 42 | 43 | config = Chapter_13.ch13_r02.load_config_file( 44 | Path(config_yaml_file) 45 | ) 46 | 47 | assert expected == config 48 | -------------------------------------------------------------------------------- /Chapter_13/test_ch13_r03.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2 | 3 | Chapter 13, recipe 3, Using Python for configuration files 4 | """ 5 | from pathlib import Path 6 | from textwrap import dedent 7 | from pytest import * # type: ignore 8 | import Chapter_13.ch13_r03 9 | 10 | 11 | @fixture # type: ignore 12 | def settings_py_file(tmpdir): 13 | filename = tmpdir/"settings.py" 14 | content = dedent(''' 15 | """Weather forecast for Offshore including the Bahamas 16 | """ 17 | query = {'mz': ['ANZ532', 'AMZ117', 'AMZ080']} 18 | url = { 19 | 'scheme': 'http', 20 | 'netloc': 'forecast.weather.gov', 21 | 'path': '/shmrn.php' 22 | } 23 | ''') 24 | filename.write_text(content, encoding='utf-8') 25 | return filename 26 | 27 | def test_load_config_file(settings_py_file): 28 | settings = Chapter_13.ch13_r03.load_config_file(Path(settings_py_file)) 29 | expected = { 30 | '__doc__': 'Weather forecast for Offshore including the Bahamas\n', 31 | 'query': {'mz': ['ANZ532', 'AMZ117', 'AMZ080']}, 32 | 'url': { 33 | 'netloc': 'forecast.weather.gov', 34 | 'path': '/shmrn.php', 35 | 'scheme': 'http'}} 36 | assert expected == settings 37 | 38 | 39 | @fixture # type: ignore 40 | def settings_extra_py_file(tmpdir): 41 | filename = tmpdir/"settings.py" 42 | content = dedent(''' 43 | """Config with related paths""" 44 | if environ.get("APP_ENV", "production"): 45 | base = Path('/var/app/') 46 | else: 47 | base = Path.cwd("var") 48 | log = base/'log' 49 | out = base/'out' 50 | ''') 51 | filename.write_text(content, encoding='utf-8') 52 | return filename 53 | 54 | def test_load_config_file_path(settings_extra_py_file): 55 | settings = Chapter_13.ch13_r03.load_config_file_xtra(Path(settings_extra_py_file)) 56 | expected = { 57 | '__doc__': 'Config with related paths', 58 | 'base': Path('/var/app'), 59 | 'log': Path('/var/app/log'), 60 | 'out': Path('/var/app/out')} 61 | assert expected == settings 62 | 63 | 64 | 65 | @fixture # type: ignore 66 | def settings_invalid_py_file(tmpdir): 67 | filename = tmpdir/"settings.py" 68 | content = dedent(''' 69 | """Config with related paths""" 70 | import os 71 | exec("os.system('shutdown')") 72 | ''') 73 | filename.write_text(content, encoding='utf-8') 74 | return filename 75 | 76 | def test_load_config_file_invalid(settings_invalid_py_file): 77 | with raises(RuntimeError) as exc_info: 78 | settings = Chapter_13.ch13_r03.load_config_file_xtra(Path(settings_invalid_py_file)) 79 | assert exc_info.type == RuntimeError 80 | assert exc_info.value.args == ("Operation not allowed",) 81 | 82 | -------------------------------------------------------------------------------- /Chapter_13/test_ch13_r04.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2 | 3 | Chapter 13, recipe 3, Using class-as-namespace for configuration values 4 | """ 5 | from pathlib import Path 6 | from textwrap import dedent 7 | from pytest import * # type: ignore 8 | import Chapter_13.ch13_r04 9 | 10 | 11 | def test_load_config_file(): 12 | settings_path = Path('Chapter_13/settings.py') 13 | configuration = Chapter_13.ch13_r04.load_config_file( 14 | settings_path, 'Chesapeake') 15 | assert configuration.__doc__.strip() == 'Weather for Chesapeake Bay' 16 | assert configuration.query == {'mz': ['ANZ532']} 17 | assert configuration.url['netloc'] == 'forecast.weather.gov' 18 | 19 | 20 | def test_load_config_module(): 21 | configuration = Chapter_13.ch13_r04.load_config_module('Chapter_13.settings.Chesapeake') 22 | assert configuration.__doc__.strip() == 'Weather for Chesapeake Bay' 23 | assert str(configuration) == "" 24 | assert dict(vars(configuration)) == { 25 | '__doc__': '\n Weather for Chesapeake Bay\n ', 26 | '__module__': 'Chapter_13.settings', 27 | 'query': {'mz': ['ANZ532']} 28 | } 29 | -------------------------------------------------------------------------------- /Chapter_13/test_ch13_r05.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2 | 3 | Chapter 13, recipe 5. Designing scripts for composition 4 | """ 5 | import yaml 6 | from pytest import * # type: ignore 7 | import Chapter_13.ch13_r05 8 | import random 9 | 10 | def test_roll_iter(): 11 | actual = list(Chapter_13.ch13_r05.roll_iter(12, seed=42)) 12 | expected = [ 13 | [[6, 1]], 14 | [[1, 6]], 15 | [[3, 2], [2, 2], [6, 1]], 16 | [[6, 6]], 17 | [[5, 1], [5, 4], [1, 1], [1, 2], [2, 5]], 18 | [[5, 1], [5, 2]], 19 | [[6, 6]], 20 | [[6, 5]], 21 | [[4, 2], [4, 5], [3, 1], [2, 6], [4, 3]], 22 | [[3, 2], [2, 3]], 23 | [[1, 1]], 24 | [[4, 1], [3, 3], [5, 3], [1, 6]] 25 | ] 26 | assert expected == actual 27 | 28 | def test_craps_game(tmpdir, monkeypatch): 29 | (tmpdir / "data").mkdir() 30 | tmp_output_path = tmpdir / "data" / "ch13_r05_test.yaml" 31 | monkeypatch.setenv("RANDOMSEED", "2") 32 | options = Chapter_13.ch13_r05.get_options( 33 | ["--samples", "10", "--output", str(tmp_output_path)] 34 | ) 35 | face_count = Chapter_13.ch13_r05.write_rolls( 36 | options.output_path, 37 | Chapter_13.ch13_r05.roll_iter(options.samples, options.seed), 38 | ) 39 | assert face_count == { 40 | 8: 8, 41 | 7: 6, 42 | 10: 5, 43 | 4: 3, 44 | 6: 3, 45 | 9: 3, 46 | 2: 2, 47 | 3: 1, 48 | 5: 1, 49 | 11: 1, 50 | 12: 1, 51 | } 52 | results = list( 53 | yaml.load_all( 54 | tmp_output_path.read_text(encoding="utf-8"), Loader=yaml.SafeLoader 55 | ) 56 | ) 57 | assert results == [ 58 | [[1, 1]], 59 | [[1, 3], [2, 6], [6, 3], [3, 5], [2, 5]], 60 | [[1, 5], [6, 2], [4, 6], [4, 6], [5, 3], [5, 4], [5, 3], [1, 1], [3, 4]], 61 | [[3, 4]], 62 | [[4, 5], [2, 5]], 63 | [[2, 2], [2, 1], [2, 3], [2, 2]], 64 | [[5, 5], [3, 5], [6, 5], [2, 4], [4, 6]], 65 | [[5, 3], [5, 3]], 66 | [[3, 4]], 67 | [[2, 4], [6, 6], [4, 6], [5, 2]], 68 | ] 69 | -------------------------------------------------------------------------------- /Chapter_13/test_ch13_r05_ut.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2 | 3 | Chapter 13, recipe 5. Designing scripts for composition 4 | """ 5 | from pathlib import Path 6 | import os 7 | import unittest 8 | import yaml 9 | import Chapter_13.ch13_r05 10 | 11 | 12 | class GIVEN_ch13_r05_WHEN_run_app_THEN_output(unittest.TestCase): 13 | def setUp(self): 14 | self.data_path = Path("data/ch13_r05_test.yaml") 15 | if self.data_path.exists(): 16 | self.data_path.unlink() 17 | 18 | def runTest(self): 19 | os.environ["RANDOMSEED"] = "2" 20 | options = Chapter_13.ch13_r05.get_options( 21 | ["--samples", "10", "--output", "data/ch13_r05_test.yaml"] 22 | ) 23 | face_count = Chapter_13.ch13_r05.write_rolls( 24 | options.output_path, 25 | Chapter_13.ch13_r05.roll_iter(options.samples, options.seed), 26 | ) 27 | self.assertDictEqual( 28 | {8: 8, 7: 6, 10: 5, 4: 3, 6: 3, 9: 3, 2: 2, 3: 1, 5: 1, 11: 1, 12: 1}, 29 | face_count, 30 | ) 31 | results = list( 32 | yaml.load_all(self.data_path.read_text(), Loader=yaml.SafeLoader) 33 | ) 34 | self.assertListEqual( 35 | [ 36 | [[1, 1]], 37 | [[1, 3], [2, 6], [6, 3], [3, 5], [2, 5]], 38 | [ 39 | [1, 5], 40 | [6, 2], 41 | [4, 6], 42 | [4, 6], 43 | [5, 3], 44 | [5, 4], 45 | [5, 3], 46 | [1, 1], 47 | [3, 4], 48 | ], 49 | [[3, 4]], 50 | [[4, 5], [2, 5]], 51 | [[2, 2], [2, 1], [2, 3], [2, 2]], 52 | [[5, 5], [3, 5], [6, 5], [2, 4], [4, 6]], 53 | [[5, 3], [5, 3]], 54 | [[3, 4]], 55 | [[2, 4], [6, 6], [4, 6], [5, 2]], 56 | ], 57 | results, 58 | ) 59 | -------------------------------------------------------------------------------- /Chapter_14/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Modern-Python-Cookbook-Second-Edition/0182ee7f17ae58cff2775290547321ebed443ae3/Chapter_14/__init__.py -------------------------------------------------------------------------------- /Chapter_14/ch14_r04.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2 | 3 | Chapter 14, recipe 4, Wrapping and combining CLI applications 4 | 5 | This uses an explicit `python` command 6 | so Chapter_13/ch13_r05.py does not have to be marked executable. 7 | """ 8 | import argparse 9 | from pathlib import Path 10 | import subprocess 11 | import sys 12 | from typing import List, Optional 13 | 14 | 15 | def get_options( 16 | argv: Optional[List[str]] = None 17 | ) -> argparse.Namespace: 18 | if argv is None: 19 | argv = sys.argv[1:] 20 | parser = argparse.ArgumentParser() 21 | parser.add_argument("directory", type=Path) 22 | parser.add_argument("games", type=int) 23 | options = parser.parse_args(argv) 24 | return options 25 | 26 | 27 | def make_files(directory: Path, files: int = 100) -> None: 28 | """Create sample data files.""" 29 | for n in range(files): 30 | filename = directory / f"game_{n}.yaml" 31 | command = [ 32 | "python", # Can be removed if the app is executable 33 | "Chapter_13/ch13_r05.py", 34 | "--samples", 35 | "10", 36 | "--output", 37 | str(filename), 38 | ] 39 | subprocess.run(command, check=True) 40 | 41 | 42 | def make_files_clean(directory: Path, files: int = 100) -> None: 43 | """Create sample data files, with cleanup after a failure.""" 44 | try: 45 | make_files(directory, files) 46 | except subprocess.CalledProcessError as ex: 47 | # Remove any files. 48 | for partial in directory.glob("game_*.yaml"): 49 | partial.unlink() 50 | raise 51 | 52 | 53 | def main() -> None: 54 | options = get_options() 55 | make_files_clean(options.directory, options.games) 56 | 57 | 58 | if __name__ == "__main__": 59 | main() 60 | -------------------------------------------------------------------------------- /Chapter_14/examples.txt: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2 | 3 | Chapter 14, Examples from the text. 4 | 5 | • Combining two applications into one 6 | • Combining many applications using the Command design pattern 7 | • Managing arguments and configuration in composite applications 8 | • Wrapping and combining CLI applications 9 | • Wrapping a program and checking the output 10 | • Controlling complex sequences of steps 11 | 12 | """ 13 | -------------------------------------------------------------------------------- /Chapter_14/test_ch14_r02.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2 | 3 | Chapter 14, recipe 3, Combining many applications using the Command design pattern 4 | """ 5 | import argparse 6 | from pathlib import Path 7 | from unittest.mock import Mock, call, sentinel 8 | from pytest import * # type: ignore 9 | 10 | import Chapter_14.ch14_r02 11 | 12 | 13 | def test_command(): 14 | options = argparse.Namespace() 15 | cmd = Chapter_14.ch14_r02.Command() 16 | cmd.execute(options) 17 | 18 | 19 | @fixture # type: ignore 20 | def mock_ch13_r05(): 21 | mock_roll_iter = Mock(return_value=sentinel.DATA) 22 | mock_write_rolls = Mock() 23 | mock_module = Mock(roll_iter=mock_roll_iter, write_rolls=mock_write_rolls) 24 | return mock_module 25 | 26 | 27 | def test_simulate(mock_ch13_r05, monkeypatch, capsys, tmpdir): 28 | target = tmpdir / "game_file.yaml" 29 | monkeypatch.setattr(Chapter_14.ch14_r02, 'ch13_r05', mock_ch13_r05) 30 | options = argparse.Namespace( 31 | game_file=target, 32 | games=sentinel.GAMES, 33 | seed=sentinel.SEED, 34 | ) 35 | 36 | cmd = Chapter_14.ch14_r02.Simulate() 37 | cmd.execute(options) 38 | 39 | assert mock_ch13_r05.roll_iter.mock_calls == [call(sentinel.GAMES, sentinel.SEED)] 40 | assert mock_ch13_r05.write_rolls.mock_calls == [call(Path(target), sentinel.DATA)] 41 | out, err = capsys.readouterr() 42 | assert out.splitlines() == [ 43 | f"Created {target}" 44 | ] 45 | 46 | 47 | @fixture # type: ignore 48 | def mock_ch13_r06(): 49 | mock_process_all_files = Mock() 50 | mock_module = Mock(process_all_files=mock_process_all_files) 51 | return mock_module 52 | 53 | 54 | def test_summarize(mock_ch13_r06, monkeypatch, capsys, tmpdir): 55 | target = tmpdir / "summary_file.yaml" 56 | game_file_1 = tmpdir / "game_file_1.yaml" 57 | game_file_2 = tmpdir / "game_file_2.yaml" 58 | monkeypatch.setattr(Chapter_14.ch14_r02, 'ch13_r06', mock_ch13_r06) 59 | options = argparse.Namespace( 60 | summary_file=target, 61 | game_files=[game_file_1, game_file_2] 62 | ) 63 | 64 | cmd = Chapter_14.ch14_r02.Summarize() 65 | cmd.execute(options) 66 | 67 | assert len(mock_ch13_r06.process_all_files.mock_calls) == 1 68 | process_args = mock_ch13_r06.process_all_files.mock_calls[0].args 69 | assert process_args[1] == [game_file_1, game_file_2] 70 | 71 | 72 | def test_simsum(mock_ch13_r05, mock_ch13_r06, monkeypatch, capsys, tmpdir): 73 | monkeypatch.setattr(Chapter_14.ch14_r02, 'ch13_r06', mock_ch13_r06) 74 | monkeypatch.setattr(Chapter_14.ch14_r02, 'ch13_r05', mock_ch13_r05) 75 | options = argparse.Namespace( 76 | games=sentinel.GAMES, 77 | summary_file=tmpdir/"summary.yaml", 78 | seed=sentinel.SEED 79 | ) 80 | 81 | cmd = Chapter_14.ch14_r02.SimSum() 82 | cmd.execute(options) 83 | 84 | assert mock_ch13_r05.roll_iter.mock_calls == [call(sentinel.GAMES, sentinel.SEED)] 85 | assert mock_ch13_r05.write_rolls.mock_calls == [call(cmd.intermediate, sentinel.DATA)] 86 | out, err = capsys.readouterr() 87 | assert out.splitlines() == [ 88 | f"Created {str(cmd.intermediate)}" 89 | ] 90 | assert len(mock_ch13_r06.process_all_files.mock_calls) == 1 91 | process_args = mock_ch13_r06.process_all_files.mock_calls[0].args 92 | assert process_args[1] == [Path(cmd.intermediate)] 93 | -------------------------------------------------------------------------------- /Chapter_14/test_ch14_r03.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2 | 3 | Chapter 14, recipe 3, Managing arguments and configuration in composite applications 4 | """ 5 | import yaml 6 | import Chapter_14.ch14_r03 7 | 8 | def test_simulate(monkeypatch, tmpdir): 9 | options = Chapter_14.ch14_r03.get_options( 10 | ["simulate", "-g", "5", "-o", str(tmpdir/"x.yaml"), "--seed", "42"] 11 | ) 12 | assert options.command == Chapter_14.ch14_r03.Simulate 13 | 14 | cmd_instance = options.command() 15 | cmd_instance.execute(options) 16 | 17 | body = (tmpdir/"x.yaml").read_text(encoding="utf-8") 18 | sim_results = list(yaml.load_all(body, Loader=yaml.SafeLoader)) 19 | expected = [ 20 | [[4, 4], [6, 3], [1, 1], [1, 5], [4, 2], [2, 4], [3, 4]], 21 | [[2, 2], [3, 3], [6, 3], [6, 1]], 22 | [[4, 5], [6, 6], [6, 1]], 23 | [[2, 6], [2, 6]], 24 | [[6, 3], [1, 5], [6, 5], [2, 3], [4, 1], [3, 2], [1, 4], 25 | [5, 3], [2, 2], [1, 2], [2, 6], [2, 2], [4, 3]] 26 | ] 27 | assert expected == sim_results 28 | 29 | 30 | def test_simulate_summarize(monkeypatch, tmpdir): 31 | options_1 = Chapter_14.ch14_r03.get_options( 32 | ["simulate", "-g", "5", "-o", str(tmpdir/"x.yaml"), "--seed", "42"] 33 | ) 34 | assert options_1.command == Chapter_14.ch14_r03.Simulate 35 | 36 | cmd_instance = options_1.command() 37 | cmd_instance.execute(options_1) 38 | 39 | body = (tmpdir/"x.yaml").read_text(encoding="utf-8") 40 | sim_results = list(yaml.load_all(body, Loader=yaml.SafeLoader)) 41 | expected = [ 42 | [[4, 4], [6, 3], [1, 1], [1, 5], [4, 2], [2, 4], [3, 4]], 43 | [[2, 2], [3, 3], [6, 3], [6, 1]], 44 | [[4, 5], [6, 6], [6, 1]], 45 | [[2, 6], [2, 6]], 46 | [[6, 3], [1, 5], [6, 5], [2, 3], [4, 1], [3, 2], [1, 4], 47 | [5, 3], [2, 2], [1, 2], [2, 6], [2, 2], [4, 3]] 48 | ] 49 | assert expected == sim_results 50 | 51 | options_2 = Chapter_14.ch14_r03.get_options( 52 | ["summarize", "-o", str(tmpdir/"y.yaml"), str(tmpdir/"x.yaml")] 53 | ) 54 | assert options_2.command == Chapter_14.ch14_r03.Summarize 55 | 56 | cmd_instance = options_2.command() 57 | cmd_instance.execute(options_2) 58 | body = (tmpdir/"y.yaml").read_text(encoding="utf-8") 59 | sum_results = list(yaml.load_all(body, Loader=yaml.UnsafeLoader)) 60 | expected = [ 61 | {('loss', 3): 1, 62 | ('loss', 4): 1, 63 | ('loss', 7): 1, 64 | ('loss', 13): 1, 65 | ('win', 2): 1} 66 | ] 67 | assert expected == sum_results 68 | 69 | 70 | def test_simsumm(monkeypatch, tmpdir, capsys): 71 | options_1 = Chapter_14.ch14_r03.get_options( 72 | ["simsum", "-g", "5", "-o", str(tmpdir/"y.yaml"), "--seed", "42"] 73 | ) 74 | assert options_1.command == Chapter_14.ch14_r03.SimSum 75 | 76 | cmd_instance = options_1.command() 77 | cmd_instance.execute(options_1) 78 | 79 | expected = [ 80 | {('loss', 3): 1, 81 | ('loss', 4): 1, 82 | ('loss', 7): 1, 83 | ('loss', 13): 1, 84 | ('win', 2): 1} 85 | ] 86 | out, err = capsys.readouterr() 87 | assert out.splitlines() == [ 88 | "Counter({('loss', 7): 1, ('loss', 4): 1, ('loss', 3): 1, ('win', 2): 1, ('loss', 13): 1})" 89 | ] 90 | -------------------------------------------------------------------------------- /Chapter_15/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Modern-Python-Cookbook-Second-Edition/0182ee7f17ae58cff2775290547321ebed443ae3/Chapter_15/__init__.py -------------------------------------------------------------------------------- /Chapter_15/ch15_r03.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 15, recipe 3, Computing the coefficient of correlation 4 | """ 5 | 6 | from math import sqrt 7 | from typing import Iterable, TypedDict, List 8 | 9 | class Point(TypedDict): 10 | x: float 11 | y: float 12 | 13 | class Series(TypedDict): 14 | series: str 15 | data: List[Point] 16 | 17 | def get_series(data: List[Series], series_name: str) -> Series: 18 | series = next( 19 | s for s in data 20 | if s["series"] == series_name 21 | ) 22 | return series 23 | 24 | def correlation(series: List[Point]) -> float: 25 | sumxy = sum(p["x"] * p["y"] for p in series) 26 | sumx = sum(p["x"] for p in series) 27 | sumy = sum(p["y"] for p in series) 28 | sumx2 = sum(p["x"] ** 2 for p in series) 29 | sumy2 = sum(p["y"] ** 2 for p in series) 30 | n = sum(1 for p in series) 31 | 32 | r = (n * sumxy - sumx * sumy) / ( 33 | sqrt(n * sumx2 - sumx ** 2) * sqrt(n * sumy2 - sumy ** 2) 34 | ) 35 | return r 36 | 37 | 38 | def corr2(series: List[Point]) -> float: 39 | sumx = sumy = sumxy = sumx2 = sumy2 = n = 0.0 40 | for point in series: 41 | x, y = point["x"], point["y"] 42 | n += 1 43 | sumx += x 44 | sumy += y 45 | sumxy += x * y 46 | sumx2 += x ** 2 47 | sumy2 += y ** 2 48 | 49 | r = (n * sumxy - sumx * sumy) / ( 50 | sqrt(n * sumx2 - sumx ** 2) * sqrt(n * sumy2 - sumy ** 2) 51 | ) 52 | return r 53 | 54 | 55 | from pathlib import Path 56 | import json 57 | 58 | source_path = Path("data") / "anscombe.json" 59 | test_correlation = """ 60 | >>> data = json.loads(source_path.read_text()) 61 | >>> for series in data: 62 | ... r = correlation(series['data']) 63 | ... print(series['series'], 'r=', round(r, 2)) 64 | I r= 0.82 65 | II r= 0.82 66 | III r= 0.82 67 | IV r= 0.82 68 | 69 | """ 70 | 71 | test_corr2 = """ 72 | >>> data = json.loads(source_path.read_text()) 73 | >>> for series in data: 74 | ... r = corr2(series['data']) 75 | ... print(f"{series['series']:>3s}, r={r:.2f}") 76 | I, r=0.82 77 | II, r=0.82 78 | III, r=0.82 79 | IV, r=0.82 80 | 81 | """ 82 | 83 | def main(): 84 | data: List[Series] = json.loads( 85 | source_path.read_text(), 86 | ) 87 | for series in data: 88 | r = correlation(series["data"]) 89 | print( 90 | f"{series['series']:>3s}, r={r:.3f}" 91 | ) 92 | 93 | test_main = """ 94 | >>> main() 95 | I, r=0.816 96 | II, r=0.816 97 | III, r=0.816 98 | IV, r=0.817 99 | """ 100 | 101 | __test__ = {n: v for n, v in locals().items() if n.startswith("test_")} 102 | 103 | if __name__ == "__main__": 104 | main() 105 | -------------------------------------------------------------------------------- /Chapter_15/ch15_r04.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | Chapter 15, recipe 4, Computing regression parameters 4 | """ 5 | 6 | import statistics 7 | from typing import Iterable, TypedDict, List, NamedTuple 8 | from Chapter_15.ch15_r03 import correlation, Point 9 | 10 | 11 | class Regression(NamedTuple): 12 | alpha: float 13 | beta: float 14 | 15 | def regression(series: List[Point]) -> Regression: 16 | 17 | m_x = statistics.mean(p["x"] for p in series) 18 | m_y = statistics.mean(p["y"] for p in series) 19 | s_x = statistics.stdev(p["x"] for p in series) 20 | s_y = statistics.stdev(p["y"] for p in series) 21 | r_xy = correlation(series) 22 | 23 | b = r_xy * s_y / s_x 24 | a = m_y - b * m_x 25 | return Regression(a, b) 26 | 27 | 28 | from math import sqrt 29 | 30 | 31 | def regr2(series: Iterable[Point]) -> Regression: 32 | sumx = sumy = sumxy = sumx2 = sumy2 = n = 0.0 33 | for point in series: 34 | x, y = point["x"], point["y"] 35 | n += 1 36 | sumx += x 37 | sumy += y 38 | sumxy += x * y 39 | sumx2 += x ** 2 40 | sumy2 += y ** 2 41 | m_x = sumx / n 42 | m_y = sumy / n 43 | s_x = sqrt((n * sumx2 - sumx ** 2) / (n * (n - 1))) 44 | s_y = sqrt((n * sumy2 - sumy ** 2) / (n * (n - 1))) 45 | r_xy = (n * sumxy - sumx * sumy) / ( 46 | sqrt(n * sumx2 - sumx ** 2) * sqrt(n * sumy2 - sumy ** 2) 47 | ) 48 | b = r_xy * s_y / s_x 49 | a = m_y - b * m_x 50 | return Regression(a, b) 51 | 52 | 53 | from pathlib import Path 54 | import json 55 | 56 | source_path = Path("data") / "anscombe.json" 57 | 58 | test_regression = """ 59 | >>> data = json.loads(source_path.read_text()) 60 | >>> for series in data: 61 | ... a, b = regression(series['data']) 62 | ... print( 63 | ... f"{series['series']:>3s} " 64 | ... f"y={round(a, 2)}+{round(b,3)}*x" 65 | ... ) 66 | I y=3.0+0.5*x 67 | II y=3.0+0.5*x 68 | III y=3.0+0.5*x 69 | IV y=3.0+0.5*x 70 | 71 | """ 72 | 73 | test_regr2 = """ 74 | >>> data = json.loads(source_path.read_text()) 75 | >>> for series in data: 76 | ... a, b = regr2(series['data']) 77 | ... print(f"{series['series']:>3s} y={round(a, 2)}+{round(b,3)}*x") 78 | I y=3.0+0.5*x 79 | II y=3.0+0.5*x 80 | III y=3.0+0.5*x 81 | IV y=3.0+0.5*x 82 | 83 | """ 84 | 85 | __test__ = {n: v for n, v in locals().items() if n.startswith("test_")} 86 | -------------------------------------------------------------------------------- /Chapter_15/collector_expected_values.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2nd ed. 2 | 3 | This computes the expected values for the a simulation. 4 | 5 | See 6 | 7 | http://www.brynmawr.edu/math/people/anmyers/PAPERS/SIGEST_Coupons.pdf 8 | 9 | and 10 | 11 | https://en.wikipedia.org/wiki/Stirling_numbers_of_the_second_kind 12 | 13 | and 14 | 15 | https://en.wikipedia.org/wiki/Binomial_coefficient 16 | """ 17 | 18 | from math import factorial 19 | 20 | 21 | def expected(n: int, population: int = 8) -> float: 22 | r""" 23 | What is the probability p(n, d) that exactly n boxes of cereal will 24 | have to be purchased in order to obtain, for the first time, 25 | a complete collection of at least one of each of the d kinds of souvenir 26 | coupons? 27 | 28 | .. math:: 29 | 30 | p(n, d) = \frac{d!}{d^n} \lbrace\textstyle{ n-1 \atop d-1 }\rbrace 31 | 32 | >>> round(expected(17, 8), 4) 33 | 0.0588 34 | """ 35 | return factorial(population) / population ** n * stirling2(n - 1, population - 1) 36 | 37 | 38 | def binom(n: int, k: int) -> int: 39 | r""" 40 | 41 | .. math:: 42 | 43 | \binom n k = \frac{n!}{k!\,(n-k)!} \quad \text{for }\ 0\leq k\leq n 44 | 45 | >>> binom(24, 12) 46 | 2704156 47 | """ 48 | return factorial(n) // (factorial(k) * factorial(n - k)) 49 | 50 | 51 | def stirling2(n: int, k: int) -> float: 52 | r""" 53 | 54 | The Stirling numbers of the second kind, 55 | written S(n,k) or :math:`\lbrace\textstyle{n\atop k}\rbrace` 56 | count the number of ways to partition a set of n labelled objects 57 | into k nonempty unlabelled subsets. 58 | 59 | .. math:: 60 | 61 | \lbrace\textstyle{n\atop n}\rbrace = 1 \\ 62 | \lbrace\textstyle{n\atop 1}\rbrace = 1 \\ 63 | \lbrace\textstyle{n\atop k}\rbrace = k 64 | \lbrace\textstyle{n-1 \atop k}\rbrace + \lbrace\textstyle{n-1 \atop k-1}\rbrace 65 | 66 | Or 67 | 68 | .. math:: 69 | 70 | \left\{ {n \atop k}\right\} = \frac{1}{k!}\sum_{j=0}^{k} (-1)^{k-j} \binom{k}{j} j^n 71 | 72 | >>> stirling2(8, 8) 73 | 1.0 74 | >>> stirling2(8, 1) 75 | 1.0 76 | >>> stirling2(5, 3) 77 | 25.0 78 | """ 79 | 80 | return ( 81 | 1 82 | / factorial(k) 83 | * sum( 84 | (-1 if (k - j) % 2 else 1) * binom(k, j) * j ** n for j in range(0, k + 1) 85 | ) 86 | ) 87 | 88 | 89 | if __name__ == "__main__": 90 | 91 | for i in range(8, 30): 92 | print(i, expected(i, 8)) 93 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | **************************************** 2 | Modern Python Cookbook - Second Edition 3 | Modern Python Cookbook - Second Edition 4 | **************************************** 5 | 6 | This is the code repository for `Modern Python Cookbook - Second Edition `_, published by `Packt `_. It contains all the supporting project files necessary to work through the book from start to finish. 7 | 8 | About the Book 9 | =============== 10 | This book comes with over 133 recipes on the latest version of ``Python 3.9``, that will touch upon all necessary Python concepts related to data structures, OOP, functional, and statistical programming to get acquainted with nuances of Python syntax and how to effectively take advantage of it. 11 | By the end of this Python book, you will be equipped with the knowledge of testing, web services, configuration and application integration tips and tricks. You will be armed with the knowledge of creating applications with flexible logging, powerful configuration, and command-line options, automated unit tests, and good documentation. 12 | 13 | 14 | 15 | 16 | 17 | Instructions and Navigation 18 | ============================= 19 | All of the code is organized into folders. Each folder is numbered chapterwise ``Chapter_03`` and further inside recipewise ``Ch03_r01.py``, some execution is also shown in the ``example.txt`` file. 20 | 21 | Installation and Setup 22 | *********************** 23 | 24 | 1. Install either Miniconda or Anaconda. 25 | 26 | 2. Create the book's environment. 27 | 28 | :: 29 | 30 | conda create -n cookbook python=3.9 31 | 32 | Note that python >= 3.9 is required for all of the examples. 33 | 34 | 3. Activate the environment. 35 | 36 | :: 37 | 38 | conda activate cookbook 39 | 40 | 3. Install the required components. This will download and install all of the 41 | packages listed in the ``requirements.txt`` file. 42 | 43 | :: 44 | 45 | python -m pip install --requirement requirements.txt 46 | 47 | 4. Run the **tox** tool to test all of the modules. 48 | 49 | :: 50 | 51 | tox 52 | 53 | 5. No Internet. 54 | 55 | - ``tox`` (or ``tox -e py39,network``) will run all tests. 56 | 57 | - ``tox -e py39`` will run the subset of tests that do not need an internet connection. 58 | 59 | 6. Windows. 60 | 61 | The Chapter_05 examples make OS requests that work for Darwin and Linux, 62 | but are not designed to work for Windows. In this chapter's examples, also, 63 | you might need to use ``#doctest: +SKIP`` to skip over tests that are not relevant 64 | for your specific OS. 65 | 66 | 7. To run the Chapter 12 tests that require the proper SSL setup. 67 | 68 | :: 69 | 70 | tox -e ssl 71 | 72 | To run a specific example as a main program, be sure to set the ``PYTHONPATH`` environment variable. 73 | 74 | :: 75 | 76 | PYTHONPATH=. python Chapter_13/ch13_r06.py data/ch13*.yaml 77 | 78 | To run a number of main program demos, use the following tox environment 79 | 80 | :: 81 | 82 | tox -e main 83 | 84 | Related Products 85 | ================= 86 | - `Python Machine Learning - Third Edition `_ 87 | 88 | - `Python Automation Cookbook - Second Edition `_ 89 | -------------------------------------------------------------------------------- /anscome_json.py: -------------------------------------------------------------------------------- 1 | """Convert the CSV version of the Anscombe data 2 | into a more useful JSON document. 3 | """ 4 | from pathlib import Path 5 | import csv 6 | import collections 7 | import json 8 | 9 | if __name__ == "__main__": 10 | 11 | source_path = Path('anscombe_raw.csv') 12 | with source_path.open() as source_file: 13 | reader = csv.reader(source_file) 14 | h1 = next(reader) 15 | h2 = next(reader) 16 | series_data = collections.defaultdict(list) 17 | for row in reader: 18 | for series, x_col, y_col in ( 19 | ('I', 1, 2), ('II', 3, 4), ('III', 5, 6), 20 | ('IV', 7, 8)): 21 | series_data[series].append( 22 | collections.OrderedDict([ 23 | ('x', float(row[x_col])), 24 | ('y', float(row[y_col]))] 25 | ) 26 | ) 27 | 28 | document = [ 29 | OrderedDict([('series', series), ('data', series_data[series])]) 30 | for series in ('I', 'II', 'III', 'IV') 31 | ] 32 | 33 | print(json.dumps(document, indent=2)) 34 | -------------------------------------------------------------------------------- /cert.conf: -------------------------------------------------------------------------------- 1 | # demo_cert.conf 2 | # openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout demo.key -out demo.cert -config cert.conf 3 | 4 | [req] 5 | distinguished_name = req_distinguished_name 6 | x509_extensions = v3_req 7 | prompt = no 8 | 9 | [req_distinguished_name] 10 | countryName = "US" 11 | stateOrProvinceName = "Nevada" 12 | localityName = "Las Vegas" 13 | organizationName = "ItMayBeAHack" 14 | organizationalUnitName = "Chapter 11" 15 | commonName = "www.yourdomain.com" 16 | 17 | # req_extensions 18 | [ v3_req ] 19 | # http://www.openssl.org/docs/apps/x509v3_config.html 20 | subjectAltName = IP:127.0.0.1 21 | -------------------------------------------------------------------------------- /data/ch07_r13.csv: -------------------------------------------------------------------------------- 1 | start_lat,start_lon,end_lat,end_lon,distance 2 | 38.9784,-76.4922,38.3185,-76.4541,40 3 | 38.3185,-76.4541,37.5531,-76.3403,46 4 | 37.5531,-76.3403,36.8443,-76.2922,43 5 | -------------------------------------------------------------------------------- /data/ch07_r13.csv.bz2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Modern-Python-Cookbook-Second-Edition/0182ee7f17ae58cff2775290547321ebed443ae3/data/ch07_r13.csv.bz2 -------------------------------------------------------------------------------- /data/ch10_file1.yaml: -------------------------------------------------------------------------------- 1 | # Chapter 9. Example YAML file 1 2 | name: 3 | "data/ch09_file1.yaml" 4 | purpose: 5 | "An example parameter file" 6 | -------------------------------------------------------------------------------- /data/ch10_file2.yaml: -------------------------------------------------------------------------------- 1 | # Chapter 9. Example YAML file 2 2 | name: 3 | "data/ch09_file2.yaml" 4 | purpose: 5 | "A second parameter file" 6 | param: 7 | 42 8 | finn: 9 | FN-2187 10 | -------------------------------------------------------------------------------- /data/ch10_r10.log: -------------------------------------------------------------------------------- 1 | [2019-11-12 18:59:30,026] INFO in ch09_r10: Sample Message One 2 | [2019-11-12 18:59:30,026] DEBUG in ch09_r10: Debugging 3 | [2019-11-12 18:59:30,026] WARNING in ch09_r10: Something might have gone wrong 4 | -------------------------------------------------------------------------------- /data/ch12_r05_test.yaml: -------------------------------------------------------------------------------- 1 | --- [[1, 1]] 2 | --- [[1, 3], [2, 6], [6, 3], [3, 5], [2, 5]] 3 | --- [[1, 5], [6, 2], [4, 6], [4, 6], [5, 3], [5, 4], [5, 3], [1, 1], [3, 4]] 4 | --- [[3, 4]] 5 | --- [[4, 5], [2, 5]] 6 | --- [[2, 2], [2, 1], [2, 3], [2, 2]] 7 | --- [[5, 5], [3, 5], [6, 5], [2, 4], [4, 6]] 8 | --- [[5, 3], [5, 3]] 9 | --- [[3, 4]] 10 | --- [[2, 4], [6, 6], [4, 6], [5, 2]] 11 | -------------------------------------------------------------------------------- /data/ch13_r05_test.yaml: -------------------------------------------------------------------------------- 1 | --- [[1, 1]] 2 | --- [[1, 3], [2, 6], [6, 3], [3, 5], [2, 5]] 3 | --- [[1, 5], [6, 2], [4, 6], [4, 6], [5, 3], [5, 4], [5, 3], [1, 1], [3, 4]] 4 | --- [[3, 4]] 5 | --- [[4, 5], [2, 5]] 6 | --- [[2, 2], [2, 1], [2, 3], [2, 2]] 7 | --- [[5, 5], [3, 5], [6, 5], [2, 4], [4, 6]] 8 | --- [[5, 3], [5, 3]] 9 | --- [[3, 4]] 10 | --- [[2, 4], [6, 6], [4, 6], [5, 2]] 11 | -------------------------------------------------------------------------------- /data/craps.csv: -------------------------------------------------------------------------------- 1 | final,least,most 2 | 5,0,6 3 | -3,-4,0 4 | -1,-3,1 5 | 3,0,4 6 | -------------------------------------------------------------------------------- /data/ex2_r12.csv: -------------------------------------------------------------------------------- 1 | column,data,heading 2 | 2,3,5 3 | -------------------------------------------------------------------------------- /data/extra_detail.log: -------------------------------------------------------------------------------- 1 | Some detailed output 2 | -------------------------------------------------------------------------------- /data/extract_20170704010203.json: -------------------------------------------------------------------------------- 1 | { 2 | "primes": [ 3 | 2, 4 | 3, 5 | 5, 6 | 7, 7 | 11, 8 | 13, 9 | 17, 10 | 19 11 | ] 12 | } -------------------------------------------------------------------------------- /data/fuel.csv: -------------------------------------------------------------------------------- 1 | date,engine on,fuel height 2 | ,engine off,fuel height 3 | ,Other notes, 4 | ,, 5 | 10/25/13,08:24:00 AM,29 6 | ,01:15:00 PM,27 7 | ,calm seas -- anchor solomon's island, 8 | 10/26/13,09:12:00 AM,27 9 | ,06:25:00 PM,22 10 | ,choppy -- anchor in jackson's creek, 11 | -------------------------------------------------------------------------------- /data/fuel.ods: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Modern-Python-Cookbook-Second-Edition/0182ee7f17ae58cff2775290547321ebed443ae3/data/fuel.ods -------------------------------------------------------------------------------- /data/fuel2.csv: -------------------------------------------------------------------------------- 1 | date,engine on,fuel height on,engine off,fuel height off 2 | 10/25/13,08:24:00,29,13:15:00,27 3 | 10/26/13,09:12:00,27,18:25:00,22 4 | 10/28/13,13:21:00,22,06:25:00,14 5 | -------------------------------------------------------------------------------- /data/game_0.yaml: -------------------------------------------------------------------------------- 1 | --- [[3, 3], [3, 4]] 2 | --- [[2, 1]] 3 | --- [[3, 2], [3, 1], [4, 4], [4, 1]] 4 | --- [[6, 5]] 5 | --- [[1, 4], [4, 6], [4, 2], [4, 5], [3, 3], [4, 5], [6, 5], [5, 4], [6, 6], [2, 6], 6 | [3, 6], [5, 3], [4, 6], [2, 4], [2, 2], [4, 3]] 7 | --- [[4, 4], [5, 3]] 8 | --- [[2, 5]] 9 | --- [[6, 4], [3, 6], [3, 3], [2, 3], [3, 4]] 10 | --- [[5, 3], [6, 1]] 11 | --- [[5, 4], [6, 4], [2, 3], [3, 4]] 12 | -------------------------------------------------------------------------------- /data/game_1.yaml: -------------------------------------------------------------------------------- 1 | --- [[3, 5], [5, 5], [1, 6]] 2 | --- [[4, 1], [4, 6], [2, 5]] 3 | --- [[5, 1], [4, 2]] 4 | --- [[3, 1], [1, 5], [3, 6], [2, 4], [3, 5], [1, 4], [5, 5], [6, 2], [5, 2]] 5 | --- [[4, 1], [1, 1], [6, 2], [5, 6], [6, 3], [2, 3]] 6 | --- [[6, 6]] 7 | --- [[1, 2]] 8 | --- [[3, 2], [2, 4], [6, 3], [1, 4]] 9 | --- [[4, 4], [1, 5], [5, 1], [1, 6]] 10 | --- [[3, 3], [1, 6]] 11 | -------------------------------------------------------------------------------- /data/output.csv: -------------------------------------------------------------------------------- 1 | Heading1,Heading2 2 | 355,113 3 | -------------------------------------------------------------------------------- /data/output.csv.old: -------------------------------------------------------------------------------- 1 | Heading1,Heading2 2 | 355,113 3 | -------------------------------------------------------------------------------- /data/quotient.csv: -------------------------------------------------------------------------------- 1 | numerator,denominator 2 | 355,113 3 | 87,32 4 | -------------------------------------------------------------------------------- /data/quotient.csv.old: -------------------------------------------------------------------------------- 1 | numerator,denominator 2 | 87,32 3 | -------------------------------------------------------------------------------- /data/race_result.json: -------------------------------------------------------------------------------- 1 | { 2 | "teams": [ 3 | { 4 | "name": "Abu Dhabi Ocean Racing", 5 | "position": [ 6 | 1, 7 | 3, 8 | 2, 9 | 2, 10 | 1, 11 | 2, 12 | 5, 13 | 3, 14 | 5 15 | ] 16 | }, 17 | { 18 | "name": "Team Brunel", 19 | "position": [ 20 | 3, 21 | 1, 22 | 5, 23 | 5, 24 | 4, 25 | 3, 26 | 1, 27 | 5, 28 | 2 29 | ] 30 | }, 31 | { 32 | "name": "Dongfeng Race Team", 33 | "position": [ 34 | 2, 35 | 2, 36 | 1, 37 | 3, 38 | null, 39 | 1, 40 | 4, 41 | 7, 42 | 4 43 | ] 44 | }, 45 | { 46 | "name": "MAPFRE", 47 | "position": [ 48 | 7, 49 | 4, 50 | 4, 51 | 1, 52 | 2, 53 | 4, 54 | 2, 55 | 4, 56 | 3 57 | ] 58 | }, 59 | { 60 | "name": "Team Alvimedica", 61 | "position": [ 62 | 5, 63 | null, 64 | 3, 65 | 4, 66 | 3, 67 | 5, 68 | 3, 69 | 6, 70 | 1 71 | ] 72 | }, 73 | { 74 | "name": "Team SCA", 75 | "position": [ 76 | 6, 77 | 6, 78 | 6, 79 | 6, 80 | 5, 81 | 6, 82 | 6, 83 | 1, 84 | 7 85 | ] 86 | }, 87 | { 88 | "name": "Team Vestas Wind", 89 | "position": [ 90 | 4, 91 | null, 92 | null, 93 | null, 94 | null, 95 | null, 96 | null, 97 | 2, 98 | 6 99 | ] 100 | } 101 | ], 102 | "legs": [ 103 | "ALICANTE - CAPE TOWN", 104 | "CAPE TOWN - ABU DHABI", 105 | "ABU DHABI - SANYA", 106 | "SANYA - AUCKLAND", 107 | "AUCKLAND - ITAJA\u00cd", 108 | "ITAJA\u00cd - NEWPORT", 109 | "NEWPORT - LISBON", 110 | "LISBON - LORIENT", 111 | "LORIENT - GOTHENBURG" 112 | ] 113 | } 114 | -------------------------------------------------------------------------------- /data/sample.csv: -------------------------------------------------------------------------------- 1 | date,level,module,message 2 | "2016-06-15 17:57:54,715",INFO,ch10_r10,Sample Message One 3 | "2016-06-15 17:57:54,715",DEBUG,ch10_r10,Debugging 4 | "2016-06-15 17:57:54,715",WARNING,ch10_r10,Something might have gone wrong 5 | -------------------------------------------------------------------------------- /data/sample.log: -------------------------------------------------------------------------------- 1 | [2016-06-15 17:57:54,715] INFO in ch10_r10: Sample Message One 2 | [2016-06-15 17:57:54,715] DEBUG in ch10_r10: Debugging 3 | [2016-06-15 17:57:54,715] WARNING in ch10_r10: Something might have gone wrong 4 | -------------------------------------------------------------------------------- /data/some_file.txt: -------------------------------------------------------------------------------- 1 | You drew 🀀 2 | -------------------------------------------------------------------------------- /data/ssl.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDHDCCAoWgAwIBAgIJAMvSwcj2/2IcMA0GCSqGSIb3DQEBBQUAMGgxCzAJBgNV 3 | BAYTAlVTMQswCQYDVQQIEwJWQTEVMBMGA1UEChMMSXRNYXlCZUFIYWNrMRQwEgYD 4 | VQQDEwtTdGV2ZW4gTG90dDEfMB0GCSqGSIb3DQEJARYQc19sb3R0QHlhaG9vLmNv 5 | bTAeFw0xNjA3MzAxODI2NTBaFw0xNzA3MzAxODI2NTBaMGgxCzAJBgNVBAYTAlVT 6 | MQswCQYDVQQIEwJWQTEVMBMGA1UEChMMSXRNYXlCZUFIYWNrMRQwEgYDVQQDEwtT 7 | dGV2ZW4gTG90dDEfMB0GCSqGSIb3DQEJARYQc19sb3R0QHlhaG9vLmNvbTCBnzAN 8 | BgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA6uQFmp4NIjUYIIstAwD+XtZRK7KDQL1W 9 | V222gmo/1pb9uuEfChEU5dCycbXQU5AiydeS3l/mEnXDSwUazW+9h0wFY8d5ci9I 10 | XVNk7TrMUfVbLCLIBsfZFATGKMs0ZaH2ETdEIWfIAxMs4w5PdsgG4R590Se6npr2 11 | U/6g9HMDnDMCAwEAAaOBzTCByjAdBgNVHQ4EFgQUYPlWXKzNfiSH84MyV7tBjdll 12 | feQwgZoGA1UdIwSBkjCBj4AUYPlWXKzNfiSH84MyV7tBjdllfeShbKRqMGgxCzAJ 13 | BgNVBAYTAlVTMQswCQYDVQQIEwJWQTEVMBMGA1UEChMMSXRNYXlCZUFIYWNrMRQw 14 | EgYDVQQDEwtTdGV2ZW4gTG90dDEfMB0GCSqGSIb3DQEJARYQc19sb3R0QHlhaG9v 15 | LmNvbYIJAMvSwcj2/2IcMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEA 16 | tlpKktiXiEbjjQxrhUklxr31Zar/I5kwL0buK8fY1wy2+tnX8qfY4MLxPGNmBvMV 17 | vznrvve291fQIKJEKZlnu07n+3yCgRo9zezg7UUoemX3ncoqyH41UJ3nKY3NErLn 18 | ozdLWF2URBTRQ73U1BinaGHvXXv4aOOKliHCK4ZdXGc= 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /data/ssl.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXgIBAAKBgQDq5AWang0iNRggiy0DAP5e1lErsoNAvVZXbbaCaj/Wlv264R8K 3 | ERTl0LJxtdBTkCLJ15LeX+YSdcNLBRrNb72HTAVjx3lyL0hdU2TtOsxR9VssIsgG 4 | x9kUBMYoyzRlofYRN0QhZ8gDEyzjDk92yAbhHn3RJ7qemvZT/qD0cwOcMwIDAQAB 5 | AoGBAMuE5pYhe5+4AT+xVz7TDZN+frYt5iuh4b2/AfPdu5JjIVVa2VoPktvNB9Ae 6 | iHLe4EWi0056KQ61mpDVy3NU7hEzb6+mZEMERRNCr+CkOjNV4zS83AIFjo/0q1Hu 7 | wu/vFHedK8GKjF9KzvmbKRcUafk9K49QWwJLqvF7pIT1GIUBAkEA+ubjqQul3zFO 8 | 4zj9GfJ42tq6L9VLFlMhYcUeQ1VYkl4D/4InxTF05rJFe2zK0Qm/n2GR93JuZMiV 9 | uEoxkPn8uwJBAO+p2PWVgD8HuvH/M1Z4EuG0Hdd2Tw3Z0ElvqdO7tjQlQgMDZ9Nu 10 | eikYtg2kxYLZlHG+fWK6gdYJsIowYkDnYukCQHT5FAaJ0ak+ucKhnQ5txv5xnwAR 11 | 9tHPq+6DEdrefU6v2jlTGuuKZS/wyQgD7oKKgvXxbTXp+aGvCm5C2Nz4T2UCQQDo 12 | uP6j4wuIqSIAkfaxFaIIJ5X+vrSfV43pcZPwtcuFMVS7hlgQuKvJMmS+NO1SVaPP 13 | G9G3yDIBvKEAX2FZcxbZAkEAmc5NAPuhFzuopxb0VN9viBrRxqx2Zv2P6owYX7xP 14 | XEwEWf8p+MFrOmEijN04w4J4qu2H3XOks+uacEfnrVmg5Q== 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /data/sum.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | ? !!python/tuple 3 | - loss 4 | - 1 5 | : 10 6 | ? !!python/tuple 7 | - loss 8 | - 2 9 | : 10 10 | ? !!python/tuple 11 | - loss 12 | - 3 13 | : 7 14 | ? !!python/tuple 15 | - loss 16 | - 4 17 | : 8 18 | ? !!python/tuple 19 | - loss 20 | - 5 21 | : 4 22 | ? !!python/tuple 23 | - loss 24 | - 6 25 | : 5 26 | ? !!python/tuple 27 | - loss 28 | - 8 29 | : 1 30 | ? !!python/tuple 31 | - loss 32 | - 11 33 | : 1 34 | ? !!python/tuple 35 | - loss 36 | - 14 37 | : 1 38 | ? !!python/tuple 39 | - loss 40 | - 17 41 | : 2 42 | ? !!python/tuple 43 | - win 44 | - 1 45 | : 22 46 | ? !!python/tuple 47 | - win 48 | - 2 49 | : 10 50 | ? !!python/tuple 51 | - win 52 | - 3 53 | : 3 54 | ? !!python/tuple 55 | - win 56 | - 4 57 | : 8 58 | ? !!python/tuple 59 | - win 60 | - 5 61 | : 2 62 | ? !!python/tuple 63 | - win 64 | - 6 65 | : 2 66 | ? !!python/tuple 67 | - win 68 | - 7 69 | : 2 70 | ? !!python/tuple 71 | - win 72 | - 8 73 | : 2 74 | -------------------------------------------------------------------------------- /data/summary_log.csv: -------------------------------------------------------------------------------- 1 | timestamp,levelname,module,message 2 | "2016-06-15 17:57:54,715",INFO,ch09_r10,Sample Message One 3 | "2016-06-15 17:57:54,715",DEBUG,ch09_r10,Debugging 4 | "2016-06-15 17:57:54,715",WARNING,ch09_r10,Something might have gone wrong 5 | "2019-11-12 18:59:30,026",INFO,ch09_r10,Sample Message One 6 | "2019-11-12 18:59:30,026",DEBUG,ch09_r10,Debugging 7 | "2019-11-12 18:59:30,026",WARNING,ch09_r10,Something might have gone wrong 8 | -------------------------------------------------------------------------------- /data/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "title": "dealer", 5 | "version": "1.0" 6 | }, 7 | "schemes": "http", 8 | "host": "127.0.0.1:5000", 9 | "basePath": "/dealer", 10 | "consumes": "application/json", 11 | "produces": "application/json", 12 | "paths": { 13 | "/hands": { 14 | "get": { 15 | "parameters": [ 16 | {"name": "cards", 17 | "in": "query", 18 | "description": "number of cards in each hand", 19 | "type": "array", "items": {"type": "integer"}, 20 | "collectionFormat": "multi", 21 | "default": [13, 13, 13, 13] 22 | } 23 | ], 24 | "responses": { 25 | "200": { 26 | "description": "one hand of cards for each `hand` value in the query string" 27 | } 28 | } 29 | } 30 | }, 31 | "/hand": { 32 | "get": { 33 | "parameters": [ 34 | {"name": "cards", "in": "query", "type": "integer", "default": 5} 35 | ], 36 | "responses": { 37 | "200": { 38 | "description": "One hand of cards with a size given by the `hand` value in the query string" 39 | } 40 | } 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /data/waypoints.csv: -------------------------------------------------------------------------------- 1 | lat,lon,date,time 2 | 32.8321666666667,-79.9338333333333,2012-11-27,09:15:00 3 | 31.6714833333333,-80.93325,2012-11-28,00:00:00 4 | 30.7171666666667,-81.5525,2012-11-28,11:35:00 5 | -------------------------------------------------------------------------------- /data/wc.csv: -------------------------------------------------------------------------------- 1 | ,0,-5,-10,-15,-20,-25,-30,-35,-40 2 | 0,13.12,10.0125,6.9049999999999985,3.7974999999999977,0.6899999999999977,-2.417500000000002,-5.525000000000004,-8.632500000000002,-11.740000000000004 3 | 2,0.41644524011885764,-4.906076511109319,-10.228598262337496,-15.551120013565672,-20.87364176479385,-26.196163516022025,-31.518685267250206,-36.84120701847838,-42.16372876970655 4 | 4,-1.0735183410113276,-6.655833654208774,-12.23814896740622,-17.820464280603666,-23.402779593801114,-28.985094906998558,-34.56741022019601,-40.14972553339345,-45.73204084659089 5 | 6,-2.0248391361124742,-7.773028985483009,-13.521218834853546,-19.269408684224082,-25.017598533594615,-30.76578838296515,-36.51397823233569,-42.26216808170622,-48.010357931076754 6 | 8,-2.7382354864041094,-8.610814804943788,-14.483394123483468,-20.35597344202315,-26.228552760562827,-32.1011320791025,-37.973711397642184,-43.84629071618187,-49.71887003472155 7 | 10,-3.314650193381196,-9.287734538884997,-15.2608188843888,-21.233903229892604,-27.206987575396404,-33.180071920900204,-39.15315626640401,-45.126240611907804,-51.09932495741161 8 | 12,-3.801133974951261,-9.859042779290828,-15.916951583630397,-21.97486038796996,-28.03276919230953,-34.0906779966491,-40.14858680098867,-46.20649560532823,-52.2644044096678 9 | 14,-4.223667956524329,-10.355250781837388,-16.486833607150448,-22.61841643246351,-28.749999257776565,-34.881582083089626,-41.013164908402686,-47.14474773371575,-53.27633055902881 10 | 16,-4.5982025414797665,-10.795090099833649,-16.991977658187533,-23.188865216541416,-29.385752774895295,-35.582640333249174,-41.77952789160307,-47.976415449956946,-54.173303008310825 11 | 18,-4.935273251789413,-11.190933253695526,-17.44659325560164,-23.702253257507756,-29.95791325941387,-36.21357326131998,-42.4692332632261,-48.72489326513221,-54.98055326703833 12 | -------------------------------------------------------------------------------- /data/wc1.csv: -------------------------------------------------------------------------------- 1 | ,0,-5,-10,-15,-20,-25,-30,-35,-40 2 | 0.0,13.12,10.0125,6.9049999999999985,3.7974999999999977,0.6899999999999977,-2.417500000000002,-5.525000000000004,-8.632500000000002,-11.740000000000004 3 | 2.0,0.41644524011885764,-4.906076511109319,-10.228598262337496,-15.551120013565672,-20.87364176479385,-26.196163516022025,-31.518685267250206,-36.84120701847838,-42.16372876970655 4 | 4.0,-1.0735183410113276,-6.655833654208774,-12.23814896740622,-17.820464280603666,-23.402779593801114,-28.985094906998558,-34.56741022019601,-40.14972553339345,-45.73204084659089 5 | 6.0,-2.0248391361124742,-7.773028985483009,-13.521218834853546,-19.269408684224082,-25.017598533594615,-30.76578838296515,-36.51397823233569,-42.26216808170622,-48.010357931076754 6 | 8.0,-2.7382354864041094,-8.610814804943788,-14.483394123483468,-20.35597344202315,-26.228552760562827,-32.1011320791025,-37.973711397642184,-43.84629071618187,-49.71887003472155 7 | 10.0,-3.314650193381196,-9.287734538884997,-15.2608188843888,-21.233903229892604,-27.206987575396404,-33.180071920900204,-39.15315626640401,-45.126240611907804,-51.09932495741161 8 | 12.0,-3.801133974951261,-9.859042779290828,-15.916951583630397,-21.97486038796996,-28.03276919230953,-34.0906779966491,-40.14858680098867,-46.20649560532823,-52.2644044096678 9 | 14.0,-4.223667956524329,-10.355250781837388,-16.486833607150448,-22.61841643246351,-28.749999257776565,-34.881582083089626,-41.013164908402686,-47.14474773371575,-53.27633055902881 10 | 16.0,-4.5982025414797665,-10.795090099833649,-16.991977658187533,-23.188865216541416,-29.385752774895295,-35.582640333249174,-41.77952789160307,-47.976415449956946,-54.173303008310825 11 | 18.0,-4.935273251789413,-11.190933253695526,-17.44659325560164,-23.702253257507756,-29.95791325941387,-36.21357326131998,-42.4692332632261,-48.72489326513221,-54.98055326703833 12 | -------------------------------------------------------------------------------- /data/x.yaml: -------------------------------------------------------------------------------- 1 | --- [[3, 6], [2, 3], [6, 3]] 2 | --- [[4, 5], [4, 2], [1, 1], [2, 1], [4, 6], [3, 2], [3, 4]] 3 | --- [[4, 5], [6, 6], [4, 5]] 4 | --- [[6, 4], [6, 5], [3, 2], [6, 2], [2, 4], [4, 1], [3, 1], [3, 3], [4, 5], [1, 2], 5 | [2, 1], [6, 5], [2, 6], [5, 1], [2, 2], [3, 2], [3, 4]] 6 | --- [[2, 5]] 7 | --- [[6, 2], [1, 6]] 8 | --- [[3, 1], [4, 2], [1, 5], [4, 6], [3, 5], [2, 3], [1, 6]] 9 | --- [[6, 1]] 10 | --- [[6, 1]] 11 | --- [[4, 3]] 12 | -------------------------------------------------------------------------------- /data/x12.yaml: -------------------------------------------------------------------------------- 1 | --- [[6, 1]] 2 | --- [[1, 6]] 3 | --- [[3, 2], [2, 2], [6, 1]] 4 | --- [[6, 6]] 5 | --- [[5, 1], [5, 4], [1, 1], [1, 2], [2, 5]] 6 | --- [[5, 1], [5, 2]] 7 | --- [[6, 6]] 8 | --- [[6, 5]] 9 | --- [[4, 2], [4, 5], [3, 1], [2, 6], [4, 3]] 10 | --- [[3, 2], [2, 3]] 11 | --- [[1, 1]] 12 | --- [[4, 1], [3, 3], [5, 3], [1, 6]] 13 | --- [[4, 5], [1, 4], [1, 5], [3, 6]] 14 | --- [[5, 3], [5, 2]] 15 | --- [[6, 1]] 16 | --- [[1, 6]] 17 | --- [[2, 3], [1, 2], [1, 4]] 18 | --- [[3, 4]] 19 | --- [[6, 3], [2, 3], [3, 2], [6, 3]] 20 | --- [[6, 6]] 21 | --- [[6, 1]] 22 | --- [[5, 6]] 23 | --- [[2, 5]] 24 | --- [[6, 2], [2, 4], [4, 3]] 25 | --- [[6, 6]] 26 | --- [[5, 2]] 27 | --- [[6, 3], [1, 2], [1, 3], [4, 3]] 28 | --- [[1, 2]] 29 | --- [[5, 6]] 30 | --- [[3, 2], [6, 4], [4, 6], [4, 2], [3, 2]] 31 | --- [[2, 6], [5, 5], [3, 6], [5, 4], [5, 4], [3, 2], [2, 5]] 32 | --- [[4, 1], [1, 1], [2, 6], [2, 6], [4, 5], [1, 4]] 33 | --- [[4, 5], [4, 5]] 34 | --- [[3, 5], [1, 6]] 35 | --- [[6, 1]] 36 | --- [[6, 5]] 37 | --- [[3, 6], [3, 1], [3, 4]] 38 | --- [[2, 4], [1, 6]] 39 | --- [[6, 3], [5, 2]] 40 | --- [[5, 1], [6, 3], [6, 5], [5, 2]] 41 | --- [[2, 3], [2, 5]] 42 | --- [[5, 1], [5, 3], [4, 1], [1, 3], [3, 2], [1, 2], [5, 1]] 43 | --- [[1, 6]] 44 | --- [[4, 1], [5, 2]] 45 | --- [[2, 6], [4, 5], [2, 3], [5, 5], [4, 2], [5, 6], [6, 2]] 46 | --- [[6, 3], [4, 6], [6, 3]] 47 | --- [[4, 5], [4, 1], [2, 2], [1, 3], [1, 5], [5, 2]] 48 | --- [[5, 2]] 49 | --- [[1, 1]] 50 | --- [[6, 6]] 51 | --- [[1, 2]] 52 | --- [[1, 1]] 53 | --- [[3, 1], [5, 2]] 54 | --- [[3, 6], [4, 2], [5, 2]] 55 | --- [[6, 5]] 56 | --- [[5, 4], [2, 4], [4, 2], [1, 1], [6, 4], [3, 4]] 57 | --- [[4, 4], [6, 1]] 58 | --- [[6, 6]] 59 | --- [[6, 1]] 60 | --- [[1, 4], [6, 3], [1, 2], [2, 2], [5, 4], [2, 4], [2, 3]] 61 | --- [[4, 2], [1, 4], [5, 1]] 62 | --- [[1, 6]] 63 | --- [[5, 1], [1, 2], [2, 4]] 64 | --- [[4, 4], [2, 4], [1, 2], [4, 1], [4, 3]] 65 | --- [[4, 3]] 66 | --- [[4, 6], [6, 5], [6, 6], [4, 2], [2, 3], [2, 1], [5, 6], [5, 1], [6, 3], [1, 1], 67 | [5, 4], [5, 5]] 68 | --- [[2, 1]] 69 | --- [[5, 1], [2, 1], [5, 1]] 70 | --- [[6, 2], [4, 1], [5, 2]] 71 | --- [[5, 5], [1, 5], [1, 4], [6, 5], [5, 5]] 72 | --- [[3, 3], [2, 6], [6, 3], [2, 3], [4, 2]] 73 | --- [[6, 6]] 74 | --- [[3, 4]] 75 | --- [[3, 1], [1, 4], [5, 5], [1, 1], [5, 2]] 76 | --- [[5, 3], [2, 3], [1, 2], [3, 3], [2, 4], [5, 6], [3, 5]] 77 | --- [[6, 5]] 78 | --- [[1, 6]] 79 | --- [[5, 3], [6, 1]] 80 | --- [[2, 3], [1, 1], [6, 5], [2, 3]] 81 | --- [[3, 5], [2, 6]] 82 | --- [[3, 2], [6, 6], [3, 5], [4, 3]] 83 | --- [[1, 1]] 84 | --- [[6, 4], [3, 1], [1, 3], [2, 6], [3, 2], [6, 4]] 85 | --- [[5, 6]] 86 | --- [[4, 5], [1, 1], [1, 6]] 87 | --- [[2, 5]] 88 | --- [[1, 3], [5, 5], [2, 4], [2, 1], [3, 3], [1, 3]] 89 | --- [[2, 6], [2, 6]] 90 | --- [[1, 3], [5, 4], [5, 6], [2, 2]] 91 | --- [[2, 2], [4, 1], [2, 6], [3, 4]] 92 | --- [[6, 6]] 93 | --- [[2, 3], [2, 6], [1, 4]] 94 | --- [[1, 4], [2, 2], [4, 3]] 95 | --- [[3, 2], [2, 1], [6, 2], [4, 3]] 96 | --- [[3, 1], [3, 3], [6, 5], [4, 6], [5, 3], [1, 1], [3, 2], [5, 3], [1, 1], [5, 4], 97 | [3, 6], [3, 4]] 98 | --- [[5, 5], [1, 4], [5, 2]] 99 | --- [[3, 1], [6, 4], [1, 5], [5, 6], [6, 6], [6, 6], [2, 3], [4, 1], [6, 3], [5, 3], 100 | [6, 1]] 101 | --- [[6, 3], [5, 3], [6, 4], [3, 4]] 102 | --- [[6, 3], [5, 2]] 103 | --- [[2, 4], [6, 4], [6, 6], [2, 5]] 104 | -------------------------------------------------------------------------------- /data/x13.yaml: -------------------------------------------------------------------------------- 1 | --- [[6, 1]] 2 | --- [[1, 6]] 3 | --- [[3, 2], [2, 2], [6, 1]] 4 | --- [[6, 6]] 5 | --- [[5, 1], [5, 4], [1, 1], [1, 2], [2, 5]] 6 | --- [[5, 1], [5, 2]] 7 | --- [[6, 6]] 8 | --- [[6, 5]] 9 | --- [[4, 2], [4, 5], [3, 1], [2, 6], [4, 3]] 10 | --- [[3, 2], [2, 3]] 11 | --- [[1, 1]] 12 | --- [[4, 1], [3, 3], [5, 3], [1, 6]] 13 | --- [[4, 5], [1, 4], [1, 5], [3, 6]] 14 | --- [[5, 3], [5, 2]] 15 | --- [[6, 1]] 16 | --- [[1, 6]] 17 | --- [[2, 3], [1, 2], [1, 4]] 18 | --- [[3, 4]] 19 | --- [[6, 3], [2, 3], [3, 2], [6, 3]] 20 | --- [[6, 6]] 21 | --- [[6, 1]] 22 | --- [[5, 6]] 23 | --- [[2, 5]] 24 | --- [[6, 2], [2, 4], [4, 3]] 25 | --- [[6, 6]] 26 | --- [[5, 2]] 27 | --- [[6, 3], [1, 2], [1, 3], [4, 3]] 28 | --- [[1, 2]] 29 | --- [[5, 6]] 30 | --- [[3, 2], [6, 4], [4, 6], [4, 2], [3, 2]] 31 | --- [[2, 6], [5, 5], [3, 6], [5, 4], [5, 4], [3, 2], [2, 5]] 32 | --- [[4, 1], [1, 1], [2, 6], [2, 6], [4, 5], [1, 4]] 33 | --- [[4, 5], [4, 5]] 34 | --- [[3, 5], [1, 6]] 35 | --- [[6, 1]] 36 | --- [[6, 5]] 37 | --- [[3, 6], [3, 1], [3, 4]] 38 | --- [[2, 4], [1, 6]] 39 | --- [[6, 3], [5, 2]] 40 | --- [[5, 1], [6, 3], [6, 5], [5, 2]] 41 | --- [[2, 3], [2, 5]] 42 | --- [[5, 1], [5, 3], [4, 1], [1, 3], [3, 2], [1, 2], [5, 1]] 43 | --- [[1, 6]] 44 | --- [[4, 1], [5, 2]] 45 | --- [[2, 6], [4, 5], [2, 3], [5, 5], [4, 2], [5, 6], [6, 2]] 46 | --- [[6, 3], [4, 6], [6, 3]] 47 | --- [[4, 5], [4, 1], [2, 2], [1, 3], [1, 5], [5, 2]] 48 | --- [[5, 2]] 49 | --- [[1, 1]] 50 | --- [[6, 6]] 51 | --- [[1, 2]] 52 | --- [[1, 1]] 53 | --- [[3, 1], [5, 2]] 54 | --- [[3, 6], [4, 2], [5, 2]] 55 | --- [[6, 5]] 56 | --- [[5, 4], [2, 4], [4, 2], [1, 1], [6, 4], [3, 4]] 57 | --- [[4, 4], [6, 1]] 58 | --- [[6, 6]] 59 | --- [[6, 1]] 60 | --- [[1, 4], [6, 3], [1, 2], [2, 2], [5, 4], [2, 4], [2, 3]] 61 | --- [[4, 2], [1, 4], [5, 1]] 62 | --- [[1, 6]] 63 | --- [[5, 1], [1, 2], [2, 4]] 64 | --- [[4, 4], [2, 4], [1, 2], [4, 1], [4, 3]] 65 | --- [[4, 3]] 66 | --- [[4, 6], [6, 5], [6, 6], [4, 2], [2, 3], [2, 1], [5, 6], [5, 1], [6, 3], [1, 1], 67 | [5, 4], [5, 5]] 68 | --- [[2, 1]] 69 | --- [[5, 1], [2, 1], [5, 1]] 70 | --- [[6, 2], [4, 1], [5, 2]] 71 | --- [[5, 5], [1, 5], [1, 4], [6, 5], [5, 5]] 72 | --- [[3, 3], [2, 6], [6, 3], [2, 3], [4, 2]] 73 | --- [[6, 6]] 74 | --- [[3, 4]] 75 | --- [[3, 1], [1, 4], [5, 5], [1, 1], [5, 2]] 76 | --- [[5, 3], [2, 3], [1, 2], [3, 3], [2, 4], [5, 6], [3, 5]] 77 | --- [[6, 5]] 78 | --- [[1, 6]] 79 | --- [[5, 3], [6, 1]] 80 | --- [[2, 3], [1, 1], [6, 5], [2, 3]] 81 | --- [[3, 5], [2, 6]] 82 | --- [[3, 2], [6, 6], [3, 5], [4, 3]] 83 | --- [[1, 1]] 84 | --- [[6, 4], [3, 1], [1, 3], [2, 6], [3, 2], [6, 4]] 85 | --- [[5, 6]] 86 | --- [[4, 5], [1, 1], [1, 6]] 87 | --- [[2, 5]] 88 | --- [[1, 3], [5, 5], [2, 4], [2, 1], [3, 3], [1, 3]] 89 | --- [[2, 6], [2, 6]] 90 | --- [[1, 3], [5, 4], [5, 6], [2, 2]] 91 | --- [[2, 2], [4, 1], [2, 6], [3, 4]] 92 | --- [[6, 6]] 93 | --- [[2, 3], [2, 6], [1, 4]] 94 | --- [[1, 4], [2, 2], [4, 3]] 95 | --- [[3, 2], [2, 1], [6, 2], [4, 3]] 96 | --- [[3, 1], [3, 3], [6, 5], [4, 6], [5, 3], [1, 1], [3, 2], [5, 3], [1, 1], [5, 4], 97 | [3, 6], [3, 4]] 98 | --- [[5, 5], [1, 4], [5, 2]] 99 | --- [[3, 1], [6, 4], [1, 5], [5, 6], [6, 6], [6, 6], [2, 3], [4, 1], [6, 3], [5, 3], 100 | [6, 1]] 101 | --- [[6, 3], [5, 3], [6, 4], [3, 4]] 102 | --- [[6, 3], [5, 2]] 103 | --- [[2, 4], [6, 4], [6, 6], [2, 5]] 104 | -------------------------------------------------------------------------------- /data/y.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | ? !!python/tuple [loss, 1] 3 | : 9 4 | ? !!python/tuple [loss, 2] 5 | : 11 6 | ? !!python/tuple [loss, 3] 7 | : 5 8 | ? !!python/tuple [loss, 4] 9 | : 4 10 | ? !!python/tuple [loss, 5] 11 | : 4 12 | ? !!python/tuple [loss, 6] 13 | : 2 14 | ? !!python/tuple [loss, 7] 15 | : 2 16 | ? !!python/tuple [loss, 9] 17 | : 1 18 | ? !!python/tuple [loss, 10] 19 | : 1 20 | ? !!python/tuple [loss, 12] 21 | : 1 22 | ? !!python/tuple [win, 1] 23 | : 28 24 | ? !!python/tuple [win, 2] 25 | : 8 26 | ? !!python/tuple [win, 3] 27 | : 8 28 | ? !!python/tuple [win, 4] 29 | : 4 30 | ? !!python/tuple [win, 5] 31 | : 3 32 | ? !!python/tuple [win, 6] 33 | : 3 34 | ? !!python/tuple [win, 7] 35 | : 1 36 | ? !!python/tuple [win, 9] 37 | : 2 38 | ? !!python/tuple [win, 12] 39 | : 2 40 | ? !!python/tuple [win, 15] 41 | : 1 42 | -------------------------------------------------------------------------------- /data/y12.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | ? !!python/tuple 3 | - loss 4 | - 1 5 | : 15 6 | ? !!python/tuple 7 | - loss 8 | - 2 9 | : 11 10 | ? !!python/tuple 11 | - loss 12 | - 3 13 | : 8 14 | ? !!python/tuple 15 | - loss 16 | - 4 17 | : 8 18 | ? !!python/tuple 19 | - loss 20 | - 5 21 | : 4 22 | ? !!python/tuple 23 | - loss 24 | - 6 25 | : 2 26 | ? !!python/tuple 27 | - loss 28 | - 7 29 | : 1 30 | ? !!python/tuple 31 | - loss 32 | - 11 33 | : 1 34 | ? !!python/tuple 35 | - loss 36 | - 12 37 | : 1 38 | ? !!python/tuple 39 | - win 40 | - 1 41 | : 24 42 | ? !!python/tuple 43 | - win 44 | - 2 45 | : 4 46 | ? !!python/tuple 47 | - win 48 | - 3 49 | : 6 50 | ? !!python/tuple 51 | - win 52 | - 4 53 | : 4 54 | ? !!python/tuple 55 | - win 56 | - 5 57 | : 3 58 | ? !!python/tuple 59 | - win 60 | - 6 61 | : 3 62 | ? !!python/tuple 63 | - win 64 | - 7 65 | : 4 66 | ? !!python/tuple 67 | - win 68 | - 12 69 | : 1 70 | -------------------------------------------------------------------------------- /data/y13.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | ? !!python/tuple 3 | - loss 4 | - 1 5 | : 15 6 | ? !!python/tuple 7 | - loss 8 | - 2 9 | : 11 10 | ? !!python/tuple 11 | - loss 12 | - 3 13 | : 8 14 | ? !!python/tuple 15 | - loss 16 | - 4 17 | : 8 18 | ? !!python/tuple 19 | - loss 20 | - 5 21 | : 4 22 | ? !!python/tuple 23 | - loss 24 | - 6 25 | : 2 26 | ? !!python/tuple 27 | - loss 28 | - 7 29 | : 1 30 | ? !!python/tuple 31 | - loss 32 | - 11 33 | : 1 34 | ? !!python/tuple 35 | - loss 36 | - 12 37 | : 1 38 | ? !!python/tuple 39 | - win 40 | - 1 41 | : 24 42 | ? !!python/tuple 43 | - win 44 | - 2 45 | : 4 46 | ? !!python/tuple 47 | - win 48 | - 3 49 | : 6 50 | ? !!python/tuple 51 | - win 52 | - 4 53 | : 4 54 | ? !!python/tuple 55 | - win 56 | - 5 57 | : 3 58 | ? !!python/tuple 59 | - win 60 | - 6 61 | : 3 62 | ? !!python/tuple 63 | - win 64 | - 7 65 | : 4 66 | ? !!python/tuple 67 | - win 68 | - 12 69 | : 1 70 | -------------------------------------------------------------------------------- /hint_game.py: -------------------------------------------------------------------------------- 1 | """Python Cookbook 2 | """ 3 | import random 4 | from collections import Counter 5 | from math import log 6 | 7 | LO = 1 8 | HI = 12 9 | 10 | def hinter(target, summary): 11 | count= 1 12 | guess = int(input("Enter an initial guess: ")) 13 | while guess != target: 14 | count += 1 15 | if guess < LO or guess > HI: 16 | print("Keep to the range", LO, "to", HI) 17 | elif guess < target: 18 | print("Too low") 19 | elif guess > target: 20 | print("Too high") 21 | else: 22 | raise Exception("Design Error") 23 | guess = int(input("Enter your next guess: ")) 24 | print("Correct!") 25 | print(count, "tries") 26 | print() 27 | summary[count] += 1 28 | 29 | if __name__ == "__main__": 30 | frequency = Counter() 31 | hinter(random.randint(LO, HI), frequency) 32 | again = input("Again? ").lower() 33 | while again.startswith('y'): 34 | hinter(random.randint(LO, HI), frequency) 35 | again = input("Again? ").lower() 36 | 37 | total = sum(frequency[count]*count for count in frequency) 38 | count = sum(frequency[count] for count in frequency) 39 | print("Your performance") 40 | print(frequency) 41 | print("avg =", total/count) 42 | print("ideally", log(HI-LO+1, 2)) 43 | -------------------------------------------------------------------------------- /renamer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Rename a chapter's examples. This does *not* look inside the files for other changes. 3 | Those changes are fraught with complexities. 4 | 5 | Some ideas... 6 | 7 | - "Chapter \d+" -> "Chapter m" 8 | 9 | - "Chapter_\d\d" -> "Chapter_\d\d" 10 | 11 | - "Chapter_\d\d.ch\d\d" -> "Chapter_\d\d.\d\d" 12 | """ 13 | from pathlib import Path 14 | import re 15 | from typing import Callable, Iterator 16 | 17 | 18 | def glob_rename(base: Path, pattern: str, transform: Callable[[str], str]) -> Iterator[Callable[[], None]]: 19 | """Emits closures that handle the real work.""" 20 | for path in base.glob(pattern): 21 | target = Path(transform(str(path))) 22 | print(f"mv {path} {target}") 23 | yield lambda: path.rename(target) 24 | 25 | 26 | def search(base: Path, old_name: str) -> None: 27 | for path in sorted(base.glob("*")): 28 | if not path.is_file(): 29 | continue 30 | with path.open() as source: 31 | for n, line in enumerate(source, start=1): 32 | if re.search(old_name, line): 33 | print(f"{path.name}:{n:3d}:{line.rstrip()}") 34 | 35 | 36 | def rename(base: Path, old_name: str, new_name: str, dry_run: bool=True) -> None: 37 | for c in glob_rename(base, f"{old_name}*", lambda n: n.replace(old_name, new_name)): 38 | if not dry_run: 39 | c() 40 | for c in glob_rename(base, f"test_{old_name}*", lambda n: n.replace(old_name, new_name)): 41 | if not dry_run: 42 | c() 43 | 44 | 45 | if __name__ == "__main__": 46 | print("All done. Do not rerun.") 47 | # search(Path.cwd() / "Chapter_12", "ch12") 48 | # Refactoring Chapter 4... 49 | # rename(Path.cwd() / "Chapter_12", "ch12", "ch13", dry_run=False) 50 | # rename(Path.cwd() / "Chapter_11", "ch11", "ch12", dry_run=False) 51 | # rename(Path.cwd() / "Chapter_10", "ch10", "ch11", dry_run=False) 52 | # rename(Path.cwd() / "Chapter_09", "ch09", "ch10", dry_run=False) 53 | # rename(Path.cwd() / "Chapter_08", "ch08", "ch09", dry_run=False) 54 | # rename(Path.cwd() / "Chapter_07", "ch07", "ch08", dry_run=False) 55 | # rename(Path.cwd() / "Chapter_06", "ch06", "ch07", dry_run=False) 56 | # rename(Path.cwd() / "Chapter_05", "ch05", "ch06", dry_run=False) 57 | # rename(Path.cwd() / "Chapter_04B", "ch04", "ch05", dry_run=False) 58 | -------------------------------------------------------------------------------- /requirements.in: -------------------------------------------------------------------------------- 1 | beautifulsoup4 2 | flask 3 | jinja2 4 | jsonschema 5 | lxml 6 | openapi-spec-validator 7 | pyyaml 8 | types-PyYAML 9 | requests 10 | types-requests 11 | sqlalchemy 12 | werkzeug 13 | tox 14 | pytest 15 | mypy 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.9 3 | # by the following command: 4 | # 5 | # pip-compile 6 | # 7 | attrs==23.1.0 8 | # via 9 | # jsonschema 10 | # referencing 11 | beautifulsoup4==4.12.2 12 | # via -r requirements.in 13 | blinker==1.6.2 14 | # via flask 15 | cachetools==5.3.1 16 | # via tox 17 | certifi==2023.7.22 18 | # via requests 19 | chardet==5.1.0 20 | # via tox 21 | charset-normalizer==3.2.0 22 | # via requests 23 | click==8.1.6 24 | # via flask 25 | colorama==0.4.6 26 | # via tox 27 | distlib==0.3.7 28 | # via virtualenv 29 | filelock==3.12.2 30 | # via 31 | # tox 32 | # virtualenv 33 | flask==2.3.2 34 | # via -r requirements.in 35 | greenlet==2.0.2 36 | # via sqlalchemy 37 | idna==3.4 38 | # via requests 39 | iniconfig==2.0.0 40 | # via pytest 41 | itsdangerous==2.1.2 42 | # via flask 43 | jinja2==3.1.2 44 | # via 45 | # -r requirements.in 46 | # flask 47 | jsonschema==4.18.4 48 | # via 49 | # -r requirements.in 50 | # openapi-schema-validator 51 | # openapi-spec-validator 52 | jsonschema-spec==0.2.3 53 | # via openapi-spec-validator 54 | jsonschema-specifications==2023.7.1 55 | # via 56 | # jsonschema 57 | # openapi-schema-validator 58 | lazy-object-proxy==1.9.0 59 | # via openapi-spec-validator 60 | lxml==4.9.3 61 | # via -r requirements.in 62 | markupsafe==2.1.3 63 | # via 64 | # jinja2 65 | # werkzeug 66 | mypy==1.4.1 67 | # via -r requirements.in 68 | mypy-extensions==1.0.0 69 | # via mypy 70 | openapi-schema-validator==0.6.0 71 | # via openapi-spec-validator 72 | openapi-spec-validator==0.6.0 73 | # via -r requirements.in 74 | packaging==23.1 75 | # via 76 | # pyproject-api 77 | # pytest 78 | # tox 79 | pathable==0.4.3 80 | # via jsonschema-spec 81 | platformdirs==3.9.1 82 | # via 83 | # tox 84 | # virtualenv 85 | pluggy==1.2.0 86 | # via 87 | # pytest 88 | # tox 89 | pyproject-api==1.5.3 90 | # via tox 91 | pytest==7.4.0 92 | # via -r requirements.in 93 | pyyaml==6.0.1 94 | # via 95 | # -r requirements.in 96 | # jsonschema-spec 97 | referencing==0.29.3 98 | # via 99 | # jsonschema 100 | # jsonschema-spec 101 | # jsonschema-specifications 102 | requests==2.31.0 103 | # via 104 | # -r requirements.in 105 | # jsonschema-spec 106 | rfc3339-validator==0.1.4 107 | # via openapi-schema-validator 108 | rpds-py==0.9.2 109 | # via 110 | # jsonschema 111 | # referencing 112 | six==1.16.0 113 | # via rfc3339-validator 114 | soupsieve==2.4.1 115 | # via beautifulsoup4 116 | sqlalchemy==2.0.19 117 | # via -r requirements.in 118 | tox==4.6.4 119 | # via -r requirements.in 120 | types-pyyaml==6.0.12.11 121 | # via -r requirements.in 122 | types-requests==2.31.0.2 123 | # via -r requirements.in 124 | types-urllib3==1.26.25.14 125 | # via types-requests 126 | typing-extensions==4.7.1 127 | # via 128 | # mypy 129 | # sqlalchemy 130 | urllib3==2.0.7 131 | # via requests 132 | virtualenv==20.24.2 133 | # via tox 134 | werkzeug==2.3.6 135 | # via 136 | # -r requirements.in 137 | # flask 138 | -------------------------------------------------------------------------------- /route.csv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Modern-Python-Cookbook-Second-Edition/0182ee7f17ae58cff2775290547321ebed443ae3/route.csv -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | export CH10_API_KEY=8160ecb2-c731-482f-adaf-eb3bd89b95b7 2 | -------------------------------------------------------------------------------- /size.py: -------------------------------------------------------------------------------- 1 | """ 2 | Size of the book's code base. 3 | 4 | Include all non-blank lines in .py files. 5 | 6 | Also include all non-blank lines of the examples.txt files. 7 | """ 8 | from pathlib import Path 9 | from typing import Iterator, NamedTuple 10 | 11 | class SampleCode(NamedTuple): 12 | path: Path 13 | lines: int 14 | 15 | def module_iter(base: Path = Path.cwd()) -> Iterator: 16 | for p in base.glob("Chapter_*/*.py"): 17 | lines = list(filter(None, p.read_text().splitlines())) 18 | yield SampleCode(p, len(lines)) 19 | 20 | def example_iter(base: Path = Path.cwd()) -> Iterator: 21 | for p in base.glob("Chapter_*/examples.txt"): 22 | lines = list(filter(None, p.read_text().splitlines())) 23 | yield SampleCode(p, len(lines)) 24 | 25 | if __name__ == "__main__": 26 | book = list(module_iter()) + list(example_iter()) 27 | sample_files = sum(1 for s in book) 28 | lines_of_code = sum(s.lines for s in book) 29 | print(f"{lines_of_code:,d} lines of code") 30 | print(f"{sample_files:,d} files") 31 | -------------------------------------------------------------------------------- /temp/anscombe_raw.csv: -------------------------------------------------------------------------------- 1 | Data-Set:,I,,II,,III,,IV,,, 2 | ,X,Y,X,Y,X,Y,X,Y,, 3 | ,10,8.04,10,9.14,10,7.46,8,6.58,, 4 | ,8,6.95,8,8.14,8,6.77,8,5.76,, 5 | ,13,7.58,13,8.74,13,12.74,8,7.71,, 6 | ,9,8.81,9,8.77,9,7.11,8,8.84,, 7 | ,11,8.33,11,9.26,11,7.81,8,8.47,, 8 | ,14,9.96,14,8.1,14,8.84,8,7.04,, 9 | ,6,7.24,6,6.13,6,6.08,8,5.25,, 10 | ,4,4.26,4,3.1,4,5.39,19,12.5,, 11 | ,12,10.84,12,9.13,12,8.15,8,5.56,, 12 | ,7,4.82,7,7.26,7,6.42,8,7.91,, 13 | ,5,5.68,5,4.74,5,5.73,8,6.89,, 14 | -------------------------------------------------------------------------------- /temp/sample.csv: -------------------------------------------------------------------------------- 1 | date,level,module,message 2 | -------------------------------------------------------------------------------- /temp/some_file.txt: -------------------------------------------------------------------------------- 1 | You drew 🀀 2 | --------------------------------------------------------------------------------