├── .gitignore ├── LICENSE ├── README.md ├── code_examples ├── __init__.py ├── chapter1 │ ├── adjust_recipe.py │ ├── adjust_recipe_improved.py │ ├── adjust_recipe_wrong.py │ ├── cookbooks.py │ ├── cookbooks_counter.py │ ├── cookbooks_defaultdict.py │ ├── iteration.py │ └── run_tests.sh ├── chapter10 │ ├── person_construction.py │ ├── pizza_maker.py │ ├── pizza_maker_invariant.py │ ├── pizza_maker_method.py │ └── run_tests.sh ├── chapter11 │ ├── grocery_app.py │ └── run_tests.sh ├── chapter12 │ ├── duck_type.py │ ├── restaurant.py │ ├── run_tests.sh │ └── square.py ├── chapter13 │ ├── iterator.py │ ├── load_restaurant.py │ ├── restaurant.py │ ├── run_tests.sh │ ├── runtime_checking.py │ ├── splittable.py │ ├── splittable_inheritance.py │ └── splittable_protocol.py ├── chapter14 │ ├── missing.yaml │ ├── read_restaurant.py │ ├── restaurant.yaml │ ├── restaurant_pydantic.py │ ├── run_tests.sh │ ├── simple_model.py │ ├── test_cases.txt │ ├── validators.py │ ├── validators_2.py │ ├── validators_custom.py │ └── wrong_type.yaml ├── chapter15 │ ├── restaurant_notification.py │ └── run_tests.sh ├── chapter17 │ ├── call_repeat.py │ ├── recommendation.py │ ├── recommendation_improved.py │ └── run_tests.sh ├── chapter18 │ ├── run_tests.sh │ └── rxpy.py ├── chapter19 │ ├── pluggable.py │ └── run_tests.sh ├── chapter2 │ ├── double_items.py │ ├── memory_value.py │ ├── print_items.py │ ├── run_tests.sh │ └── types_example.py ├── chapter20 │ ├── hotdog.py │ ├── hotdog_checker.py │ ├── lint_example.py │ ├── run_tests.sh │ └── whitespace_checker.py ├── chapter21 │ ├── run_tests.sh │ ├── test_aaa_test.py │ ├── test_basic_pytest.py │ ├── test_context_manager.py │ ├── test_custom_hamcrest.py │ ├── test_extracted.py │ ├── test_fixture with_cleanup.py │ ├── test_fixture.py │ ├── test_fixture_with_message.py │ ├── test_hamcrest.py │ └── test_parameterized.py ├── chapter22 │ ├── features │ │ ├── food.feature │ │ ├── steps │ │ │ └── steps.py │ │ └── table_driven.feature │ └── regex │ │ ├── food.feature │ │ └── steps │ │ └── step.py ├── chapter23 │ ├── test_basic_hypothesis.py │ ├── test_composite_strategies.py │ └── test_hypothesis_stateful.py ├── chapter24 │ ├── calorie_tracker.py │ └── test_calorie_tracker.py ├── chapter3 │ ├── find_workers.py │ ├── invalid │ │ ├── close_kitchen.py │ │ ├── invalid_example1.py │ │ ├── invalid_example2.py │ │ ├── invalid_example3.py │ │ └── invalid_type.py │ ├── run_tests.sh │ └── variable_annotation.py ├── chapter4 │ ├── create_hot_dog.py │ ├── create_hot_dog_defensive.py │ ├── create_hot_dog_union.py │ ├── invalid │ │ ├── dispense_bun.py │ │ ├── final.py │ │ ├── hotdog_invalid.py │ │ ├── literals.py │ │ ├── newtype.py │ │ └── union_hotdog.py │ ├── product_type.py │ ├── run_tests.sh │ └── sum_type.py ├── chapter5 │ ├── abc.py │ ├── count_authors.py │ ├── generic.py │ ├── graph.py │ ├── invalid │ │ └── graph.py │ ├── overriding_dict.py │ ├── print_items.py │ ├── reverse.py │ ├── run_tests.sh │ ├── typeddict.py │ └── userdict.py ├── chapter6 │ ├── .pyre_configuration │ ├── insecure.py │ └── stubs │ │ └── taint │ │ ├── general.pysa │ │ └── taint.config ├── chapter7 │ ├── __init__.py │ ├── automated_recipe_maker.py │ ├── main.py │ ├── pasta_with_sausage.py │ └── run_tests.sh ├── chapter8 │ ├── allergen.py │ ├── allergen_flag.py │ ├── auto_enum.py │ ├── auto_enum_generate.py │ ├── liquid_measure.py │ ├── liquid_measure_intenum.py │ ├── mother_sauces.py │ ├── mother_sauces_alias.py │ ├── mother_sauces_bad.py │ ├── mother_sauces_unique.py │ └── run_tests.sh ├── chapter9 │ ├── fraction.py │ ├── frozen_recipe.py │ ├── namedtuple.py │ ├── nutritional_info.py │ ├── recipe.py │ └── run_tests.sh └── plugin │ ├── setup.py │ └── ultimate_kitchen_assistant │ ├── __init__.py │ ├── pasta_maker.py │ ├── plugin_spec.py │ └── tex_mex.py ├── mypy.ini ├── requirements.txt └── run_tests.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | .watchmanconfig 132 | 133 | 134 | *monkeytype* 135 | .pytype 136 | 137 | # pylint chapter example 138 | error.txt 139 | 140 | # behave chapter reports 141 | reports/ 142 | 143 | # mutation testing 144 | .mutmut-cache 145 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Pat Viafore 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RobustPython 2 | Code Examples for Robust Python book 3 | 4 | Note that to get many of the examples use dummy types and data to not take away from the book example. 5 | 6 | For example, complex types might be aliased as a string and instances are just random snippets of text. 7 | Additionally, functions might just return hardcoded values. 8 | 9 | The meat of the book examples are unchanged from the text, however. Feel free to stick in a `breakpoint()` 10 | in the code to further understand how it works. 11 | -------------------------------------------------------------------------------- /code_examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pviafore/RobustPython/dafb95d801dff2c8ff7856ba46d3c052d54e0033/code_examples/__init__.py -------------------------------------------------------------------------------- /code_examples/chapter1/adjust_recipe.py: -------------------------------------------------------------------------------- 1 | # Take a meal recipe and change the number of servings 2 | # by adjusting each ingredient 3 | # A recipe's first element is the number of servings, and the remainder 4 | # of elements is (name, amount, unit), such as ("flour", 1.5, "cup") 5 | 6 | def adjust_recipe(recipe, servings): 7 | new_recipe = [servings] 8 | old_servings = recipe[0] 9 | factor = servings / old_servings 10 | recipe.pop(0) 11 | while recipe: 12 | ingredient, amount, unit = recipe.pop(0) 13 | # please only use numbers that will be easily measurable 14 | new_recipe.append((ingredient, amount * factor, unit)) 15 | return new_recipe 16 | 17 | 18 | def test_adjust_recipe(): 19 | old_recipe = [2, ("flour", 1.5, "cups")] 20 | adjusted = adjust_recipe(old_recipe, 4) 21 | assert [4, ("flour", 3, "cups")] == adjusted 22 | assert old_recipe == [] 23 | 24 | 25 | test_adjust_recipe() 26 | -------------------------------------------------------------------------------- /code_examples/chapter1/adjust_recipe_improved.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | from dataclasses import dataclass 4 | from fractions import Fraction 5 | from typing import List 6 | 7 | @dataclass 8 | class Ingredient: 9 | name: str 10 | amount: Fraction 11 | units: str 12 | 13 | def adjust_proportion(self, factor: Fraction): 14 | self.amount *= factor 15 | 16 | @dataclass 17 | class Recipe: 18 | servings: int 19 | ingredients: list[Ingredient] 20 | 21 | def clear_ingredients(self): 22 | self.ingredients.clear() 23 | 24 | def get_ingredients(self): 25 | return self.ingredients 26 | 27 | # Take a meal recipe and change the number of servings 28 | # recipe is a Recipe class 29 | def adjust_recipe(recipe, servings): 30 | # create a copy of the ingredients 31 | new_ingredients = list(recipe.get_ingredients()) 32 | recipe.clear_ingredients() 33 | for ingredient in new_ingredients: 34 | ingredient.adjust_proportion(Fraction(servings, recipe.servings)) 35 | return Recipe(servings, new_ingredients) 36 | 37 | 38 | def test_adjust_recipe(): 39 | old_recipe = Recipe(2, [Ingredient('flour', 1.5, 'cups')]) 40 | adjusted = adjust_recipe(old_recipe, 4) 41 | assert Recipe(4, [Ingredient('flour', 3, 'cups')]) == adjusted 42 | assert old_recipe.ingredients == [] 43 | 44 | 45 | test_adjust_recipe() 46 | -------------------------------------------------------------------------------- /code_examples/chapter1/adjust_recipe_wrong.py: -------------------------------------------------------------------------------- 1 | # Take a meal recipe and change the number of servings 2 | # by adjusting each ingredient 3 | # A recipe's first element is the number of servings, and the remainder 4 | # of elements is (name, amount, unit), such as ("flour", 1.5, "cup") 5 | def adjust_recipe(recipe, servings): 6 | old_servings = recipe.pop(0) 7 | factor = servings / old_servings 8 | new_recipe = {ingredient: (amount*factor, unit) 9 | for ingredient, amount, unit in recipe} 10 | new_recipe["servings"] = servings 11 | return new_recipe 12 | 13 | 14 | def test_adjust_recipe(): 15 | old_recipe = [2, ("flour", 1.5, "cups")] 16 | adjusted = adjust_recipe(old_recipe, 4) 17 | assert {"servings": 4, "flour": (3, "cups")} == adjusted 18 | 19 | # THIS IS WRONG BEHAVIOR, we should have emptied the list 20 | assert old_recipe != [] 21 | 22 | 23 | test_adjust_recipe() 24 | -------------------------------------------------------------------------------- /code_examples/chapter1/cookbooks.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import List 3 | 4 | @dataclass 5 | class Cookbook: 6 | author: str 7 | 8 | def create_author_count_mapping(cookbooks: list[Cookbook]): 9 | counter = {} 10 | for cookbook in cookbooks: 11 | if cookbook.author not in counter: 12 | counter[cookbook.author] = 0 13 | counter[cookbook.author] += 1 14 | return counter 15 | 16 | def test_create_author_count(): 17 | cookbooks = [Cookbook('Pat Viafore'), Cookbook('Pat Viafore'), Cookbook('J. Kenji Lopez-Alt')] 18 | assert create_author_count_mapping(cookbooks) == { 19 | 'Pat Viafore': 2, 20 | 'J. Kenji Lopez-Alt': 1 21 | } 22 | 23 | test_create_author_count() 24 | -------------------------------------------------------------------------------- /code_examples/chapter1/cookbooks_counter.py: -------------------------------------------------------------------------------- 1 | from collections import Counter 2 | from dataclasses import dataclass 3 | from typing import List 4 | 5 | @dataclass 6 | class Cookbook: 7 | author: str 8 | 9 | def create_author_count_mapping(cookbooks: list[Cookbook]): 10 | return Counter(book.author for book in cookbooks) 11 | 12 | def test_create_author_count(): 13 | cookbooks = [Cookbook('Pat Viafore'), Cookbook('Pat Viafore'), Cookbook('J. Kenji Lopez-Alt')] 14 | assert create_author_count_mapping(cookbooks) == { 15 | 'Pat Viafore': 2, 16 | 'J. Kenji Lopez-Alt': 1 17 | } 18 | 19 | test_create_author_count() 20 | -------------------------------------------------------------------------------- /code_examples/chapter1/cookbooks_defaultdict.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from dataclasses import dataclass 3 | from typing import Dict, List 4 | 5 | @dataclass 6 | class Cookbook: 7 | author: str 8 | 9 | def create_author_count_mapping(cookbooks: list[Cookbook]): 10 | counter: dict[str, int] = defaultdict(lambda: 0) 11 | for cookbook in cookbooks: 12 | counter[cookbook.author] += 1 13 | return counter 14 | 15 | def test_create_author_count(): 16 | cookbooks = [Cookbook('Pat Viafore'), Cookbook('Pat Viafore'), Cookbook('J. Kenji Lopez-Alt')] 17 | assert create_author_count_mapping(cookbooks) == { 18 | 'Pat Viafore': 2, 19 | 'J. Kenji Lopez-Alt': 1 20 | } 21 | 22 | test_create_author_count() 23 | -------------------------------------------------------------------------------- /code_examples/chapter1/iteration.py: -------------------------------------------------------------------------------- 1 | text = "This is some generic text" 2 | index = 0 3 | while index < len(text): 4 | print(text[index]) 5 | index += 1 6 | 7 | 8 | for character in text: 9 | print(character) 10 | 11 | print("\n".join(text)) 12 | -------------------------------------------------------------------------------- /code_examples/chapter1/run_tests.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash -eu 2 | python code_examples/chapter1/adjust_recipe.py 3 | python code_examples/chapter1/adjust_recipe_wrong.py 4 | python code_examples/chapter1/adjust_recipe_improved.py 5 | python code_examples/chapter1/cookbooks.py 6 | python code_examples/chapter1/cookbooks_counter.py 7 | python code_examples/chapter1/cookbooks_defaultdict.py 8 | 9 | python code_examples/chapter1/iteration.py 10 | 11 | # will fail 1 file until python 3.9 is released and mypy supports | operator 12 | mypy code_examples/chapter1/*.py 13 | 14 | echo "All Chapter 1 Tests Passed" 15 | -------------------------------------------------------------------------------- /code_examples/chapter10/person_construction.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | class Person: 4 | name: str = "" 5 | years_experience: int = 0 6 | address: str = "" 7 | 8 | pat = Person() 9 | pat.name = "Pat" 10 | 11 | try: 12 | pat = Person("Pat", 13, "123 Fake St.") # type: ignore 13 | assert False 14 | except TypeError: 15 | pass 16 | 17 | assert pat.name == "Pat" 18 | 19 | pat_dict = { 20 | "name": "", 21 | "years_experience": 0, 22 | "address": "" 23 | } 24 | 25 | @dataclass 26 | class PersonDataclass(): 27 | name: str = "" 28 | years_experience: int = 0 29 | address: str = "" 30 | 31 | 32 | class Person: # type: ignore 33 | def __init__(self, 34 | name: str, 35 | years_experience: int, 36 | address: str): 37 | self.name = name 38 | self.years_experience = years_experience 39 | self.address = address 40 | 41 | pat = Person("Pat", 13, "123 Fake St.") # type: ignore 42 | -------------------------------------------------------------------------------- /code_examples/chapter10/pizza_maker.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | def is_sauce(t): 4 | return t in ['Tomato Sauce', "Olive Oil"] 5 | 6 | class PizzaSpecification: 7 | def __init__(self, 8 | dough_radius_in_inches: int, 9 | toppings: list[str]): 10 | assert 6 <= dough_radius_in_inches <= 12, \ 11 | 'Dough must be between 6 and 12 inches' 12 | sauces = [t for t in toppings if is_sauce(t)] 13 | assert len(sauces) < 2, \ 14 | 'Can only have at most one sauce' 15 | 16 | self.dough_radius_in_inches = dough_radius_in_inches 17 | sauce = sauces[:1] 18 | self.toppings = sauce + \ 19 | [t for t in toppings if not is_sauce(t)] 20 | 21 | PizzaSpecification(10, ['Tomato Sauce', 'Basil']) 22 | 23 | 24 | import contextlib 25 | 26 | @contextlib.contextmanager 27 | def create_pizza_specification(dough_radius_in_inches: int, 28 | toppings): 29 | pizza_spec = PizzaSpecification(dough_radius_in_inches, toppings) 30 | yield pizza_spec 31 | assert 6 <= pizza_spec.dough_radius_in_inches <= 12 32 | sauces = [t for t in pizza_spec.toppings if is_sauce(t)] 33 | assert len(sauces) < 2 34 | if sauces: 35 | assert pizza_spec.toppings[0] == sauces[0] 36 | 37 | # check that we assert order of all non sauces 38 | # keep in mind, no invariant is specified that we can't add 39 | # toppings at a later date, so we only check against what was 40 | # passed in 41 | non_sauces = [t for t in pizza_spec.toppings if t not in sauces] 42 | expected_non_sauces = [t for t in toppings if t not in sauces] 43 | for expected, actual in zip(expected_non_sauces, non_sauces): 44 | assert expected == actual 45 | 46 | 47 | def test_pizza_operations(): 48 | with create_pizza_specification(8, ["Tomato Sauce", "Peppers"]) \ 49 | as pizza_spec: 50 | 51 | assert pizza_spec.toppings == ["Tomato Sauce", "Peppers"] 52 | 53 | test_pizza_operations 54 | 55 | 56 | pizza_spec = PizzaSpecification(dough_radius_in_inches=8, 57 | toppings=['Olive Oil', 58 | 'Garlic', 59 | 'Sliced Roma Tomatoes', 60 | 'Mozzarella']) 61 | 62 | pizza_spec.dough_radius_in_inches = 100 # BAD! 63 | assert pizza_spec.dough_radius_in_inches == 100 64 | pizza_spec.toppings.append('Tomato Sauce') # Second Sauce, oh no 65 | -------------------------------------------------------------------------------- /code_examples/chapter10/pizza_maker_invariant.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | def is_sauce(t): 4 | return t in ['Tomato Sauce', "Olive Oil"] 5 | 6 | class PizzaSpecification: 7 | def __init__(self, 8 | dough_radius_in_inches: int, 9 | toppings: list[str]): 10 | assert 6 <= dough_radius_in_inches <= 12, \ 11 | 'Dough must be between 6 and 12 inches' 12 | sauces = [t for t in toppings if is_sauce(t)] 13 | assert len(sauces) < 2, \ 14 | 'Can only have at most one sauce' 15 | 16 | self.__dough_radius_in_inches = dough_radius_in_inches # <1> 17 | sauce = sauces[:1] 18 | self.__toppings = sauce + \ 19 | [t for t in toppings if not is_sauce(t)] # <2> 20 | 21 | pizza_spec = PizzaSpecification(dough_radius_in_inches=8, 22 | toppings=['Olive Oil', 23 | 'Garlic', 24 | 'Sliced Roma Tomatoes', 25 | 'Mozzarella']) 26 | 27 | 28 | pizza_spec = PizzaSpecification(dough_radius_in_inches=8, 29 | toppings=['Olive Oil', 30 | 'Garlic', 31 | 'Sliced Roma Tomatoes', 32 | 'Mozzarella']) 33 | 34 | try: 35 | pizza_spec.__toppings.append('Tomato Sauce') # OOPS 36 | assert False 37 | except AttributeError: 38 | pass 39 | 40 | assert pizza_spec.__dict__ == { '_PizzaSpecification__toppings': ['Olive Oil', 41 | 'Garlic', 42 | 'Sliced Roma Tomatoes', 43 | 'Mozzarella'], 44 | '_PizzaSpecification__dough_radius_in_inches': 8 45 | } 46 | 47 | 48 | pizza_spec._PizzaSpecification__dough_radius_in_inches = 100 # type: ignore 49 | assert pizza_spec._PizzaSpecification__dough_radius_in_inches == 100 # type: ignore 50 | -------------------------------------------------------------------------------- /code_examples/chapter10/pizza_maker_method.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | def is_sauce(t): 4 | return t in ['Tomato Sauce', "Olive Oil"] 5 | 6 | class PizzaException(RuntimeError): 7 | pass 8 | 9 | class PizzaSpecification: 10 | def __init__(self, 11 | dough_radius_in_inches: int, 12 | toppings: list[str]): 13 | assert 6 <= dough_radius_in_inches <= 12, \ 14 | 'Dough must be between 6 and 12 inches' 15 | 16 | self.__dough_radius_in_inches = dough_radius_in_inches 17 | self.__toppings: list[str] = [] 18 | for topping in toppings: 19 | self.add_topping(topping) # <1> 20 | 21 | 22 | def add_topping(self, topping: str): # <2> 23 | ''' 24 | Add a topping to the pizza 25 | All rules for pizza construction (one sauce, no sauce above 26 | cheese, etc.) still apply. 27 | ''' 28 | if (is_sauce(topping) and 29 | any(t for t in self.__toppings if is_sauce(t))): 30 | raise PizzaException('Pizza may only have one sauce') 31 | 32 | if is_sauce(topping): 33 | self.__toppings.insert(0, topping) 34 | else: 35 | self.__toppings.append(topping) 36 | -------------------------------------------------------------------------------- /code_examples/chapter10/run_tests.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash -eu 2 | PYTHONPATH=. python code_examples/chapter10/person_construction.py 3 | PYTHONPATH=. python code_examples/chapter10/pizza_maker.py 4 | PYTHONPATH=. python code_examples/chapter10/pizza_maker_invariant.py 5 | PYTHONPATH=. python code_examples/chapter10/pizza_maker_method.py 6 | 7 | mypy code_examples/chapter10/*.py 8 | 9 | echo "All Chapter 10 Tests Passed" 10 | -------------------------------------------------------------------------------- /code_examples/chapter11/grocery_app.py: -------------------------------------------------------------------------------- 1 | import decimal 2 | from dataclasses import dataclass 3 | from enum import auto, Enum 4 | from typing import Dict, Iterable, List, Tuple 5 | 6 | class ImperialMeasure(Enum): 7 | TEASPOON = auto() 8 | TABLESPOON = auto() 9 | CUP = auto() 10 | 11 | @dataclass(frozen=True) 12 | class Ingredient: 13 | name: str 14 | brand: str 15 | amount: float = 1 16 | units: ImperialMeasure = ImperialMeasure.CUP 17 | 18 | def __add__(self, rhs: 'Ingredient'): 19 | # make sure we are adding the same ingredient 20 | assert (self.name, self.brand) == (rhs.name, rhs.brand) 21 | # build up conversion chart (lhs, rhs): multiplication factor 22 | conversion: dict[tuple[ImperialMeasure, ImperialMeasure], float] = { 23 | (ImperialMeasure.CUP, ImperialMeasure.CUP): 1, 24 | (ImperialMeasure.CUP, ImperialMeasure.TABLESPOON): 16, 25 | (ImperialMeasure.CUP, ImperialMeasure.TEASPOON): 48, 26 | (ImperialMeasure.TABLESPOON, ImperialMeasure.CUP): 1/16, 27 | (ImperialMeasure.TABLESPOON, ImperialMeasure.TABLESPOON): 1, 28 | (ImperialMeasure.TABLESPOON, ImperialMeasure.TEASPOON): 3, 29 | (ImperialMeasure.TEASPOON, ImperialMeasure.CUP): 1/48, 30 | (ImperialMeasure.TEASPOON, ImperialMeasure.TABLESPOON): 1/3, 31 | (ImperialMeasure.TEASPOON, ImperialMeasure.TEASPOON): 1 32 | } 33 | 34 | return Ingredient(rhs.name, 35 | rhs.brand, 36 | rhs.amount + self.amount * conversion[(rhs.units, self.units)], 37 | rhs.units) 38 | 39 | 40 | @dataclass 41 | class Recipe: 42 | name: str 43 | ingredients: list[Ingredient] 44 | servings: int 45 | 46 | from dataclasses import dataclass 47 | 48 | @dataclass(frozen=True) 49 | class Coordinates: 50 | lat: float 51 | lon: float 52 | 53 | @dataclass(frozen=True) 54 | class Store: 55 | coordinates: Coordinates 56 | name: str 57 | 58 | 59 | @dataclass 60 | class Item: 61 | name: str 62 | brand: str 63 | measure: ImperialMeasure 64 | price_in_cents: decimal.Decimal 65 | amount: float 66 | 67 | Inventory = dict[Store, list[Item]] 68 | 69 | spaghetti = Item( 70 | "Spaghetti", 71 | "Pat's Homemade", 72 | ImperialMeasure.CUP, 73 | amount=4, 74 | price_in_cents=decimal.Decimal(160) 75 | ) 76 | reserved_items: list[Item] = [] 77 | delivered_items: list[Item] = [] 78 | def get_grocery_inventory() -> Inventory: 79 | # reach out to APIs and populate the dictionary 80 | return { 81 | Store(Coordinates(0,0), "Pat's Market") : [spaghetti] 82 | } 83 | 84 | def reserve_items(store: Store, items: Iterable[Item]) -> bool: 85 | return True 86 | 87 | def unreserve_items(store: Store, items: Iterable[Item]) -> bool: 88 | return True 89 | 90 | def order_items(store: Store, items: Iterable[Item]) -> bool: 91 | return True 92 | 93 | 94 | from typing import Iterable, Optional, Set 95 | from copy import deepcopy 96 | class Order: 97 | ''' An Order class that represents a list of ingredients ''' 98 | def __init__(self, recipes: Iterable[Recipe]): 99 | self.__confirmed = False 100 | self.__ingredients: set[Ingredient] = set() 101 | for recipe in recipes: 102 | for ingredient in recipe.ingredients: 103 | self.add_ingredient(ingredient) 104 | 105 | def get_ingredients(self) -> list[Ingredient]: 106 | ''' Return a alphabetically sorted list of ingredients ''' 107 | # return a copy so that users won't inadvertently mess with 108 | # our internal data 109 | return sorted(deepcopy(self.__ingredients), 110 | key=lambda ing: ing.name) 111 | 112 | def _get_matching_ingredient(self, 113 | ingredient: Ingredient) -> Optional[Ingredient]: 114 | try: 115 | return next(ing for ing in self.__ingredients if 116 | ((ing.name, ing.brand) == 117 | (ingredient.name, ingredient.brand))) 118 | except StopIteration: 119 | return None 120 | 121 | def add_ingredient(self, ingredient: Ingredient): 122 | ''' adds the ingredient if it's not already added, 123 | or increases the amount if it has 124 | ''' 125 | target_ingredient = self._get_matching_ingredient(ingredient) 126 | if target_ingredient is None: 127 | # ingredient for the first time - add it 128 | self.__ingredients.add(ingredient) 129 | else: 130 | # add ingredient to existing set 131 | target_ingredient += ingredient 132 | 133 | def confirm(self): 134 | self.__confirmed = True 135 | 136 | def unconfirm(self): 137 | self.__confirmed = False 138 | 139 | def is_confirmed(self): 140 | return self.__confirmed 141 | 142 | def display_order(order: Order): 143 | pass 144 | def wait_for_user_order_confirmation(order: Order): 145 | order.confirm() 146 | pass 147 | 148 | class _GroceryList: 149 | def __init__(self, order: Order, grocery_inventory: Inventory): 150 | self.order = order 151 | self.inventory = grocery_inventory 152 | 153 | def is_confirmed(self): 154 | return True 155 | 156 | def order_and_unreserve_items(self): 157 | pass 158 | 159 | def reserve_items_from_stores(self): 160 | pass 161 | 162 | def unreserve_items(self): 163 | pass 164 | 165 | def has_reserved_items(self): 166 | pass 167 | 168 | def get_grocery_order(self) -> list[Item]: 169 | return [spaghetti] 170 | 171 | def wait_for_user_grocery_confirmation(grocery_list: _GroceryList): 172 | pass 173 | 174 | def deliver_ingredients(grocery_list: _GroceryList): 175 | global delivered_items 176 | delivered_items += grocery_list.get_grocery_order() 177 | 178 | from contextlib import contextmanager 179 | 180 | @contextmanager 181 | def create_grocery_list(order: Order, inventory: Inventory): 182 | grocery_list = _GroceryList(order, inventory) 183 | try: 184 | yield grocery_list 185 | finally: 186 | if grocery_list.has_reserved_items(): 187 | grocery_list.unreserve_items() 188 | 189 | def make_order(recipes): 190 | order = Order(recipes) 191 | # the user can make changes if needed 192 | display_order(order) 193 | wait_for_user_order_confirmation(order) 194 | if order.is_confirmed(): 195 | grocery_inventory = get_grocery_inventory() 196 | with create_grocery_list(order, grocery_inventory) as grocery_list: 197 | grocery_list.reserve_items_from_stores() 198 | wait_for_user_grocery_confirmation(grocery_list) 199 | if grocery_list.is_confirmed(): 200 | grocery_list.order_and_unreserve_items() 201 | deliver_ingredients(grocery_list) 202 | 203 | def test_order(): 204 | make_order([Recipe(name="Pasta", ingredients=[Ingredient( 205 | "Spaghetti", 206 | "Pat's Homemade", 207 | units=ImperialMeasure.CUP, 208 | amount=2)], servings=1)]) 209 | assert reserved_items == [] 210 | assert delivered_items == [spaghetti] 211 | 212 | if __name__ == "__main__": 213 | test_order() 214 | -------------------------------------------------------------------------------- /code_examples/chapter11/run_tests.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash -eu 2 | PYTHONPATH=. python code_examples/chapter11/grocery_app.py 3 | 4 | mypy code_examples/chapter11/*.py 5 | 6 | echo "All Chapter 11 Tests Passed" 7 | -------------------------------------------------------------------------------- /code_examples/chapter12/duck_type.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | def double(x): 4 | return x + x 5 | 6 | assert double(3) == 6 7 | assert double("abc") == "abcabc" 8 | -------------------------------------------------------------------------------- /code_examples/chapter12/restaurant.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | 4 | class Coordinates: 5 | pass 6 | 7 | class Employee: 8 | pass 9 | 10 | class Ingredient: 11 | pass 12 | 13 | class Menu: 14 | def contains(self, ingredient: Ingredient) -> bool: 15 | return True 16 | 17 | class Finances: 18 | pass 19 | 20 | class Dish: 21 | pass 22 | 23 | class RestaurantData: 24 | pass 25 | 26 | class Restaurant: 27 | def __init__(self, 28 | name: str, 29 | location: Coordinates, 30 | employees: list[Employee], 31 | inventory: list[Ingredient], 32 | menu: Menu, 33 | finances: Finances): 34 | self.name = name 35 | self.location = location, 36 | self.employees = employees 37 | self.inventory = inventory 38 | self.menu = menu 39 | self.finances = finances 40 | 41 | 42 | def transfer_employees(self, 43 | employees: list[Employee], 44 | restaurant: 'Restaurant'): 45 | pass 46 | 47 | def order_dish(self, dish: Dish): 48 | pass 49 | 50 | def add_inventory(self, ingredients: list[Ingredient], cost_in_cents: int): 51 | pass 52 | 53 | def register_hours_employee_worked(self, 54 | employee: Employee, 55 | minutes_worked: int): 56 | pass 57 | 58 | def get_restaurant_data(self) -> RestaurantData: 59 | return RestaurantData() 60 | 61 | def change_menu(self, menu: Menu): 62 | self.__menu = menu 63 | 64 | def move_location(self, new_location: Coordinates): 65 | pass 66 | 67 | class GPS: 68 | def get_coordinates(self) -> Coordinates: 69 | return Coordinates() 70 | 71 | def initialize_gps(): 72 | return GPS() 73 | 74 | def schedule_auto_driving_task(location: Coordinates): 75 | pass 76 | 77 | class FoodTruck(Restaurant): 78 | def __init__(self, 79 | name: str, 80 | location: Coordinates, 81 | employees: list[Employee], 82 | inventory: list[Ingredient], 83 | menu: Menu, 84 | finances: Finances): 85 | super().__init__( name, location, employees,inventory, menu, finances) 86 | self.__gps = initialize_gps() 87 | 88 | def move_location(self, new_location: Coordinates): 89 | # schedule a task to drive us to our new location 90 | schedule_auto_driving_task(new_location) 91 | super().move_location(new_location) 92 | 93 | def get_current_location(self) -> Coordinates: 94 | return self.__gps.get_coordinates() 95 | 96 | class PopUpStall(Restaurant): 97 | pass 98 | 99 | # this should all type check just fine 100 | food_truck = FoodTruck("Pat's Food Truck", Coordinates(), [], [], Menu(), Finances()) 101 | food_truck.order_dish(Dish()) 102 | food_truck.move_location(Coordinates()) 103 | 104 | def display_restaurant_data(restaurant: Restaurant): 105 | data = restaurant.get_restaurant_data() 106 | # ... snip drawing code here ... 107 | 108 | restaurants: list[Restaurant] = [food_truck] 109 | for restaurant in restaurants: 110 | display_restaurant_data(restaurant) 111 | 112 | 113 | class RestrictedMenuRestaurant(Restaurant): 114 | 115 | def __init__(self, 116 | name: str, 117 | location: Coordinates, 118 | employees: list[Employee], 119 | inventory: list[Ingredient], 120 | menu: Menu, 121 | finances: Finances, 122 | restricted_items: list[Ingredient]): 123 | super().__init__(name,location,employees,inventory,menu,finances) 124 | self.__restricted_items = restricted_items 125 | 126 | def change_menu(self, menu: Menu): 127 | if any(not menu.contains(ingredient) for ingredient in self.__restricted_items): 128 | # new menus MUST contain restricted ingredients 129 | return super().change_menu(menu) 130 | 131 | RestrictedMenuRestaurant("Name", Coordinates(), [], [], Menu(), Finances(), []).change_menu(Menu()) 132 | -------------------------------------------------------------------------------- /code_examples/chapter12/run_tests.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash -eu 2 | python code_examples/chapter12/restaurant.py 3 | python code_examples/chapter12/square.py 4 | python code_examples/chapter12/duck_type.py 5 | 6 | mypy code_examples/chapter12/*.py 7 | 8 | echo "All Chapter 12 Tests Passed" 9 | -------------------------------------------------------------------------------- /code_examples/chapter12/square.py: -------------------------------------------------------------------------------- 1 | class Rectangle: 2 | def __init__(self, height: int, width: int): 3 | self._height = height 4 | self._width = width 5 | 6 | def set_width(self, new_width): 7 | self._width = new_width 8 | 9 | def set_height(self, new_height): 10 | self._height = new_height 11 | 12 | def get_width(self) -> int: 13 | return self._width 14 | 15 | def get_height(self) -> int: 16 | return self._height 17 | 18 | class Square(Rectangle): 19 | def __init__(self, length: int): 20 | super().__init__(length, length) 21 | 22 | def set_side_length(self, new_length): 23 | super().set_width(new_length) 24 | super().set_height(new_length) 25 | 26 | def set_width(self, new_width): 27 | self.set_side_length(new_width) 28 | 29 | def set_height(self, new_height): 30 | self.set_side_length(new_height) 31 | 32 | def double_width(rectangle: Rectangle): 33 | old_height = rectangle.get_height() 34 | rectangle.set_width(rectangle.get_width() * 2) 35 | # check that the height is unchanged 36 | assert rectangle.get_height() == old_height 37 | 38 | try: 39 | double_width(Square(5)) 40 | raise RuntimeError("This should not pass") 41 | except AssertionError: 42 | print("Expected assert") 43 | -------------------------------------------------------------------------------- /code_examples/chapter13/iterator.py: -------------------------------------------------------------------------------- 1 | from random import shuffle 2 | from typing import MutableSequence, Iterator 3 | class ShuffleIterator: 4 | def __init__(self, sequence: MutableSequence): 5 | self.sequence = list(sequence) 6 | shuffle(self.sequence) 7 | 8 | def __iter__(self): 9 | return self 10 | 11 | def __next__(self): 12 | if not self.sequence: 13 | raise StopIteration 14 | return self.sequence.pop(0) 15 | 16 | my_list = [1, 2, 3, 4] 17 | iterator: Iterator = ShuffleIterator(my_list) 18 | 19 | assert {1,2,3,4} == {n for n in iterator} 20 | -------------------------------------------------------------------------------- /code_examples/chapter13/load_restaurant.py: -------------------------------------------------------------------------------- 1 | from typing import List, Protocol 2 | 3 | class Restaurant(Protocol): 4 | name: str 5 | address: str 6 | standard_lunch_entries: list[str] 7 | other_entries: list[str] 8 | 9 | def render_menu(self) -> str: 10 | ... 11 | 12 | def load_restaurant(restaurant: Restaurant): 13 | # code to load restaurant 14 | # ... 15 | pass 16 | 17 | import restaurant 18 | 19 | #mypy does not support modules as protocols yet 20 | load_restaurant(restaurant) # type: ignore 21 | -------------------------------------------------------------------------------- /code_examples/chapter13/restaurant.py: -------------------------------------------------------------------------------- 1 | name = "Chameleon Café" 2 | address = "123 Fake St." 3 | 4 | standard_lunch_entries = ['BLTSandwich'] 5 | other_entries = ['BLTSandwich'] 6 | 7 | def render_menu() -> str: 8 | # Code to render a menu 9 | return "BLAH" 10 | -------------------------------------------------------------------------------- /code_examples/chapter13/run_tests.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash -eu 2 | python code_examples/chapter13/splittable.py 3 | python code_examples/chapter13/splittable_inheritance.py 4 | python code_examples/chapter13/iterator.py 5 | python code_examples/chapter13/splittable_protocol.py 6 | python code_examples/chapter13/runtime_checking.py 7 | PYTHONPATH=code_examples/chapter13 python code_examples/chapter13/load_restaurant.py 8 | 9 | mypy code_examples/chapter13/*.py 10 | 11 | echo "All Chapter 13 Tests Passed" 12 | -------------------------------------------------------------------------------- /code_examples/chapter13/runtime_checking.py: -------------------------------------------------------------------------------- 1 | from typing import runtime_checkable, Protocol, Tuple 2 | 3 | @runtime_checkable 4 | class Splittable(Protocol): 5 | cost: int 6 | name: str 7 | 8 | def split_in_half(self) -> tuple['Splittable', 'Splittable']: 9 | ... 10 | 11 | class BLTSandwich: 12 | def __init__(self): 13 | self.cost = 6.95 14 | self.name = 'BLT' 15 | # This class handles a fully constructed BLT sandwich 16 | # ... 17 | 18 | def split_in_half(self) -> tuple['BLTSandwich', 'BLTSandwich']: 19 | # Instructions for how to split a sandwich in half 20 | # Cut along diagonal, wrap separately, etc. 21 | # Return two sandwiches in return 22 | return (BLTSandwich(), BLTSandwich()) 23 | 24 | assert isinstance(BLTSandwich(), Splittable) 25 | -------------------------------------------------------------------------------- /code_examples/chapter13/splittable.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | 3 | class BLTSandwich: 4 | def __init__(self): 5 | self.cost = 6.95 6 | self.name = 'BLT' 7 | # This class handles a fully constructed BLT sandwich 8 | # ... 9 | 10 | def split_in_half(self) -> tuple['BLTSandwich', 'BLTSandwich']: 11 | # Instructions for how to split a sandwich in half 12 | # Cut along diagonal, wrap separately, etc. 13 | # Return two sandwiches in return 14 | return (BLTSandwich(), BLTSandwich()) 15 | 16 | class Chili: 17 | def __init__(self): 18 | self.cost = 4.95 19 | self.name = 'Chili' 20 | # This class handles a fully loaded chili 21 | # ... 22 | 23 | def split_in_half(self) -> tuple['Chili', 'Chili']: 24 | # Instructions for how to split chili in half 25 | # Ladle into new container, add toppings 26 | # Return two cups of chili in return 27 | # ... 28 | return (Chili(), Chili()) 29 | 30 | class BaconCheeseburger: 31 | def __init__(self): 32 | self.cost = 11.95 33 | self.name = 'Bacon Cheeseburger' 34 | # This class handles a delicious Bacon Cheeseburger 35 | # ... 36 | 37 | # NOTE! no split_in_half method 38 | 39 | import math 40 | def split_dish(dish): 41 | dishes = dish.split_in_half() 42 | assert len(dishes) == 2 43 | for half_dish in dishes: 44 | half_dish.cost = math.ceil(half_dish.cost) / 2 45 | half_dish.name = "½ " + half_dish.name 46 | return dishes 47 | 48 | sandwich = BLTSandwich() 49 | dishes = split_dish(sandwich) 50 | assert dishes[0].cost == 3.5 51 | assert dishes[0].name == "½ BLT" 52 | assert dishes[0].cost == dishes[1].cost 53 | assert dishes[0].name == dishes[1].name 54 | -------------------------------------------------------------------------------- /code_examples/chapter13/splittable_inheritance.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | 3 | class Splittable: 4 | def __init__(self, cost, name): 5 | self.cost = cost 6 | self.name = name 7 | 8 | def split_in_half(self) -> tuple['Splittable', 'Splittable']: 9 | raise NotImplementedError("Must implement split in half") 10 | 11 | class BLTSandwich(Splittable): 12 | def __init__(self): 13 | self.cost = 6.95 14 | self.name = 'BLT' 15 | # This class handles a fully constructed BLT sandwich 16 | # ... 17 | 18 | def split_in_half(self) -> tuple['BLTSandwich', 'BLTSandwich']: 19 | # Instructions for how to split a sandwich in half 20 | # Cut along diagonal, wrap separately, etc. 21 | # Return two sandwiches in return 22 | return (BLTSandwich(), BLTSandwich()) 23 | 24 | class Chili(Splittable): 25 | def __init__(self): 26 | self.cost = 4.95 27 | self.name = 'Chili' 28 | # This class handles a fully loaded chili 29 | # ... 30 | 31 | def split_in_half(self) -> tuple['Chili', 'Chili']: 32 | # Instructions for how to split chili in half 33 | # Ladle into new container, add toppings 34 | # Return two cups of chili in return 35 | # ... 36 | return (Chili(), Chili()) 37 | 38 | import math 39 | def split_dish(dish): 40 | dishes = dish.split_in_half() 41 | assert len(dishes) == 2 42 | for half_dish in dishes: 43 | half_dish.cost = math.ceil(half_dish.cost) / 2 44 | half_dish.name = "½ " + half_dish.name 45 | return dishes 46 | 47 | sandwich = BLTSandwich() 48 | dishes = split_dish(sandwich) 49 | assert dishes[0].cost == 3.5 50 | assert dishes[0].name == "½ BLT" 51 | assert dishes[0].cost == dishes[1].cost 52 | assert dishes[0].name == dishes[1].name 53 | -------------------------------------------------------------------------------- /code_examples/chapter13/splittable_protocol.py: -------------------------------------------------------------------------------- 1 | from typing import Protocol, Tuple 2 | class Splittable(Protocol): 3 | cost: int 4 | name: str 5 | 6 | def split_in_half(self) -> tuple['Splittable', 'Splittable']: 7 | # this is a literal ellipsis to indicate a stub function 8 | ... 9 | 10 | class BLTSandwich: 11 | def __init__(self): 12 | self.cost = 6.95 13 | self.name = 'BLT' 14 | # This class handles a fully constructed BLT sandwich 15 | # ... 16 | 17 | def split_in_half(self) -> tuple['BLTSandwich', 'BLTSandwich']: 18 | # Instructions for how to split a sandwich in half 19 | # Cut along diagonal, wrap separately, etc. 20 | # Return two sandwiches in return 21 | return (BLTSandwich(), BLTSandwich()) 22 | 23 | class Chili: 24 | def __init__(self): 25 | self.cost = 4.95 26 | self.name = 'Chili' 27 | # This class handles a fully loaded chili 28 | # ... 29 | 30 | def split_in_half(self) -> tuple['Chili', 'Chili']: 31 | # Instructions for how to split chili in half 32 | # Ladle into new container, add toppings 33 | # Return two cups of chili in return 34 | # ... 35 | return (Chili(), Chili()) 36 | 37 | import math 38 | def split_dish(dish): 39 | dishes = dish.split_in_half() 40 | assert len(dishes) == 2 41 | for half_dish in dishes: 42 | half_dish.cost = math.ceil(half_dish.cost) / 2 43 | half_dish.name = "½ " + half_dish.name 44 | return dishes 45 | 46 | sandwich = BLTSandwich() 47 | dishes = split_dish(sandwich) 48 | assert dishes[0].cost == 3.5 49 | assert dishes[0].name == "½ BLT" 50 | assert dishes[0].cost == dishes[1].cost 51 | assert dishes[0].name == dishes[1].name 52 | -------------------------------------------------------------------------------- /code_examples/chapter14/missing.yaml: -------------------------------------------------------------------------------- 1 | name: Viafore's 2 | owner: Pat Viafore 3 | address: 123 Fake St. Fakington, FA 01234 4 | employees: 5 | - name: Pat Viafore 6 | position: Chef 7 | payment_details: 8 | bank_details: 9 | routing_number: "123456789" 10 | account_number: "123456789012" 11 | - name: Made-up McGee 12 | position: Server 13 | payment_details: 14 | bank_details: 15 | routing_number: "123456789" 16 | account_number: "123456789012" 17 | - name: Fabricated Frank 18 | position: Sous Chef 19 | payment_details: 20 | bank_details: 21 | routing_number: "123456789" 22 | account_number: "123456789012" 23 | - name: Illusory Ilsa 24 | position: Host 25 | payment_details: 26 | bank_details: 27 | routing_number: "123456789" 28 | account_number: "123456789012" 29 | dishes: 30 | - name: Pasta And Sausage 31 | price_in_cents: 1295 32 | description: Rigatoni and Sausage with a Tomato-Garlic-Basil Sauce 33 | - name: Pasta Bolognese 34 | price_in_cents: 1495 35 | description: Spaghetti with a rich Tomato and Beef Sauce 36 | - name: Caprese Salad 37 | price_in_cents: 795 38 | picture: caprese.png 39 | number_of_seats: 12 40 | to_go: true 41 | delivery: false 42 | -------------------------------------------------------------------------------- /code_examples/chapter14/read_restaurant.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | 3 | with open('code_examples/chapter14/restaurant.yaml') as yaml_file: 4 | restaurant = yaml.safe_load(yaml_file) 5 | 6 | assert restaurant == { 7 | "name": "Viafore's", 8 | "owner": "Pat Viafore", 9 | "address": "123 Fake St. Fakington, FA 01234", 10 | "employees": [{ 11 | "name": "Pat Viafore", 12 | "position": "Chef", 13 | "payment_details": { 14 | "bank_details": { 15 | "routing_number": '123456789', 16 | "account_number": '123456789012' 17 | } 18 | } 19 | }, 20 | { 21 | "name": "Made-up McGee", 22 | "position": "Server", 23 | "payment_details": { 24 | "bank_details": { 25 | "routing_number": '123456789', 26 | "account_number": '123456789012' 27 | } 28 | } 29 | }, 30 | { 31 | "name": "Fabricated Frank", 32 | "position": "Sous Chef", 33 | "payment_details": { 34 | "bank_details": { 35 | "routing_number": '123456789', 36 | "account_number": '123456789012' 37 | } 38 | } 39 | }, 40 | { 41 | "name": "Illusory Ilsa", 42 | "position": "Host", 43 | "payment_details": { 44 | "bank_details": { 45 | "routing_number": '123456789', 46 | "account_number": '123456789012' 47 | } 48 | } 49 | }], 50 | "dishes": [{ 51 | "name": "Pasta And Sausage", 52 | "price_in_cents": 1295, 53 | "description": "Rigatoni and Sausage with a Tomato-Garlic-Basil Sauce" 54 | }, 55 | { 56 | "name": "Pasta Bolognese", 57 | "price_in_cents": 1495, 58 | "description": "Spaghetti with a rich Tomato and Beef Sauce" 59 | }, 60 | { 61 | "name": "Caprese Salad", 62 | "price_in_cents": 795, 63 | "description": "Tomato, Buffalo Mozzarella and Basil", 64 | "picture": "caprese.png" 65 | }], 66 | 'number_of_seats': 12, 67 | "to_go": True, 68 | "delivery": False 69 | } 70 | 71 | from typing import Literal,TypedDict,Union 72 | class AccountAndRoutingNumber(TypedDict): 73 | account_number: str 74 | routing_number: str 75 | 76 | class BankDetails(TypedDict): 77 | bank_details: AccountAndRoutingNumber 78 | 79 | class Address(TypedDict): 80 | address: str 81 | 82 | AddressOrBankDetails = Union[Address, BankDetails] 83 | 84 | Position = Literal['Chef', 'Sous Chef', 'Host', 85 | 'Server', 'Delivery Driver'] 86 | 87 | class Dish(TypedDict): 88 | name: str 89 | price_in_cents: int 90 | description: str 91 | 92 | class DishWithOptionalPicture(Dish, TypedDict, total=False): 93 | picture: str 94 | 95 | class Employee(TypedDict): 96 | name: str 97 | position: Position 98 | payment_details: AddressOrBankDetails 99 | 100 | class Restaurant(TypedDict): 101 | name: str 102 | owner: str 103 | address: str 104 | employees: list[Employee] 105 | dishes: list[Dish] 106 | number_of_seats: int 107 | to_go: bool 108 | delivery: bool 109 | 110 | 111 | def load_restaurant(filename: str) -> Restaurant: 112 | with open(filename) as yaml_file: 113 | return yaml.safe_load(yaml_file) 114 | 115 | load_restaurant('code_examples/chapter14/restaurant.yaml') 116 | -------------------------------------------------------------------------------- /code_examples/chapter14/restaurant.yaml: -------------------------------------------------------------------------------- 1 | name: Viafore's 2 | owner: Pat Viafore 3 | address: 123 Fake St. Fakington, FA 01234 4 | employees: 5 | - name: Pat Viafore 6 | position: Chef 7 | payment_details: 8 | bank_details: 9 | routing_number: "123456789" 10 | account_number: "123456789012" 11 | - name: Made-up McGee 12 | position: Server 13 | payment_details: 14 | bank_details: 15 | routing_number: "123456789" 16 | account_number: "123456789012" 17 | - name: Fabricated Frank 18 | position: Sous Chef 19 | payment_details: 20 | bank_details: 21 | routing_number: "123456789" 22 | account_number: "123456789012" 23 | - name: Illusory Ilsa 24 | position: Host 25 | payment_details: 26 | bank_details: 27 | routing_number: "123456789" 28 | account_number: "123456789012" 29 | dishes: 30 | - name: Pasta And Sausage 31 | price_in_cents: 1295 32 | description: Rigatoni and Sausage with a Tomato-Garlic-Basil Sauce 33 | - name: Pasta Bolognese 34 | price_in_cents: 1495 35 | description: Spaghetti with a rich Tomato and Beef Sauce 36 | - name: Caprese Salad 37 | price_in_cents: 795 38 | description: Tomato, Buffalo Mozzarella, and Basil 39 | picture: caprese.png 40 | number_of_seats: 12 41 | to_go: true 42 | delivery: false 43 | -------------------------------------------------------------------------------- /code_examples/chapter14/restaurant_pydantic.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | from pydantic import ValidationError 3 | from pydantic.dataclasses import dataclass 4 | from typing import Literal, List, Optional, TypedDict, Union 5 | 6 | @dataclass 7 | class AccountAndRoutingNumber(): 8 | account_number: str 9 | routing_number: str 10 | 11 | @dataclass 12 | class BankDetails: 13 | bank_details: AccountAndRoutingNumber 14 | 15 | @dataclass 16 | class Address: 17 | address: str 18 | 19 | AddressOrBankDetails = Union[Address, BankDetails] 20 | 21 | Position = Literal['Chef', 'Sous Chef', 'Host', 22 | 'Server', 'Delivery Driver'] 23 | 24 | @dataclass 25 | class Dish: 26 | name: str 27 | price_in_cents: int 28 | description: str 29 | picture: Optional[str] = None 30 | 31 | @dataclass 32 | class Employee: 33 | name: str 34 | position: Position 35 | payment_details: AddressOrBankDetails 36 | 37 | @dataclass 38 | class Restaurant: 39 | name: str 40 | owner: str 41 | address: str 42 | employees: list[Employee] 43 | dishes: list[Dish] 44 | number_of_seats: int 45 | to_go: bool 46 | delivery: bool 47 | 48 | 49 | def load_restaurant(filename: str) -> Restaurant: 50 | with open(filename) as yaml_file: 51 | data = yaml.safe_load(yaml_file) 52 | return Restaurant(**data) 53 | 54 | try: 55 | restaurant = load_restaurant("code_examples/chapter14/missing.yaml") 56 | assert False, "Should have failed" 57 | except ValidationError: 58 | pass 59 | 60 | try: 61 | restaurant = load_restaurant("code_examples/chapter14/wrong_type.yaml") 62 | assert False, "Should have failed" 63 | except ValidationError: 64 | pass 65 | -------------------------------------------------------------------------------- /code_examples/chapter14/run_tests.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash -eu 2 | python code_examples/chapter14/read_restaurant.py 3 | python code_examples/chapter14/restaurant_pydantic.py 4 | python code_examples/chapter14/validators.py 5 | python code_examples/chapter14/validators_2.py 6 | python code_examples/chapter14/validators_custom.py 7 | python code_examples/chapter14/simple_model.py 8 | 9 | 10 | echo "All Chapter 14 Tests Passed" 11 | -------------------------------------------------------------------------------- /code_examples/chapter14/simple_model.py: -------------------------------------------------------------------------------- 1 | from pydantic.dataclasses import dataclass 2 | from pydantic import StrictInt 3 | @dataclass 4 | class Model: 5 | value: int 6 | 7 | Model(value="123") 8 | Model(value=5.5) 9 | 10 | from pydantic import StrictInt 11 | @dataclass 12 | class Model2: 13 | value: StrictInt 14 | 15 | from pydantic import ValidationError 16 | 17 | try: 18 | Model2(value="0023") 19 | assert False, "Model should have failed to parse" 20 | except ValidationError: 21 | pass 22 | -------------------------------------------------------------------------------- /code_examples/chapter14/test_cases.txt: -------------------------------------------------------------------------------- 1 | Name is zero characters 2 | Name is not a string 3 | Name is longer than 32 characters 4 | Name field is missing 5 | Name contains invalid characters 6 | Owners Name is empty 7 | Owner's name is missing 8 | Owner's name is an integer 9 | Address is not a valid address 10 | Address is empty 11 | Address is missing 12 | Address is an integer 13 | Employees is empty 14 | Employees is not a list 15 | Employees is a list of strings 16 | Employees is missing 17 | There is not 1 chef 18 | There is not 1 server 19 | Employees' name is blank 20 | Employee's name is missing 21 | Employee's position is missing 22 | Employees's name is an integer 23 | Employee's position is not an approved position 24 | Employee's position is an integer 25 | Employee does not have payment information 26 | Employee's bank payment information is a string 27 | Employee's address is not a valid location 28 | Employee's has payment information as a dictionary, but no bank details or address 29 | Employee's payment informations has both bank details and address 30 | Employee's address is missing 31 | Employee's address is blank 32 | Employee's address is a string 33 | Employee's routing number is a list 34 | Employee's routing number is missing 35 | Employee's routing number is not a routing number 36 | Employee's routing number is an integer that gets truncated 37 | Employee's routing number is blank 38 | Employee's account number is a list 39 | Employee's account number is missing 40 | Employee's account number is not an account number number 41 | Employee's account number is an integer that gets truncated 42 | Employee's account number is blank 43 | Dishes is missing 44 | Dishes is a string 45 | Less than three dishes 46 | Dish's name is blank 47 | Dish's name is an integer 48 | Dish's name is missing 49 | Dish's price is missing 50 | Dish's price is a set 51 | Dish's price is negative 52 | Dish's name is too long 53 | dish's description is blank 54 | Dish's description is missing 55 | Dish's description is an integer 56 | Dish's description is too long 57 | Dish's picture is an int 58 | Dish's picture is not a file on the filesystem 59 | Dish's picture is not a valid picture type 60 | Dish's do not have unique names 61 | Number of Seats is missing 62 | Number of Seats is a string 63 | Number of seats is negative 64 | To Go is missing 65 | To go is a string 66 | Delivery is missing 67 | Delivery is a string 68 | -------------------------------------------------------------------------------- /code_examples/chapter14/validators.py: -------------------------------------------------------------------------------- 1 | from pydantic.dataclasses import dataclass 2 | from pydantic import constr, PositiveInt, ValidationError 3 | from typing import Literal, Optional, Union 4 | @dataclass 5 | class AccountAndRoutingNumber: 6 | account_number: constr(min_length=9,max_length=9) 7 | routing_number: constr(min_length=8,max_length=12) 8 | 9 | 10 | @dataclass 11 | class BankDetails: 12 | bank_details: AccountAndRoutingNumber 13 | 14 | 15 | @dataclass 16 | class Address: 17 | address: constr(min_length=1) 18 | 19 | AddressOrBankDetails = Union[Address, BankDetails] 20 | 21 | Position = Literal['Chef', 'Sous Chef', 'Host', 22 | 'Server', 'Delivery Driver'] 23 | @dataclass 24 | class Employee: 25 | name: str 26 | position: Position 27 | payment_details: AddressOrBankDetails 28 | 29 | @dataclass 30 | class Dish: 31 | name: constr(min_length=1, max_length=16) 32 | price_in_cents: PositiveInt 33 | description: constr(min_length=1, max_length=80) 34 | picture: Optional[str] = None 35 | 36 | @dataclass 37 | class Restaurant: 38 | name: constr(regex=r'^[a-zA-Z0-9 ]*$', 39 | min_length=1, max_length=16) 40 | owner: constr(min_length=1) 41 | address: constr(min_length=1) 42 | employees: list[Employee] 43 | dishes: list[Dish] 44 | number_of_seats: PositiveInt 45 | to_go: bool 46 | delivery: bool 47 | 48 | try: 49 | restaurant = Restaurant(**{ 50 | 'name': 'Dine-n-Dash', 51 | 'owner': 'Pat Viafore', 52 | 'address': '123 Fake St.', 53 | 'employees': [], 54 | 'dishes': [], 55 | 'number_of_seats': -5, 56 | 'to_go': False, 57 | 'delivery': True 58 | }) 59 | assert False, "Should not have been able to construct Restaurant" 60 | except ValidationError: 61 | pass 62 | -------------------------------------------------------------------------------- /code_examples/chapter14/validators_2.py: -------------------------------------------------------------------------------- 1 | from pydantic.dataclasses import dataclass 2 | from pydantic import constr, PositiveInt, ValidationError 3 | from typing import List, Literal, Optional, Union 4 | @dataclass 5 | class AccountAndRoutingNumber: 6 | account_number: constr(min_length=9,max_length=9) 7 | routing_number: constr(min_length=8,max_length=12) 8 | 9 | 10 | @dataclass 11 | class BankDetails: 12 | bank_details: AccountAndRoutingNumber 13 | 14 | 15 | @dataclass 16 | class Address: 17 | address: constr(min_length=1) 18 | 19 | AddressOrBankDetails = Union[Address, BankDetails] 20 | 21 | Position = Literal['Chef', 'Sous Chef', 'Host', 22 | 'Server', 'Delivery Driver'] 23 | @dataclass 24 | class Employee: 25 | name: str 26 | position: Position 27 | 28 | @dataclass 29 | class Dish: 30 | name: constr(min_length=1, max_length=16) 31 | price_in_cents: PositiveInt 32 | description: constr(min_length=1, max_length=80) 33 | picture: Optional[str] = None 34 | 35 | 36 | @dataclass 37 | class Dish: 38 | name: constr(min_length=1, max_length=16) 39 | 40 | from pydantic import conlist,constr 41 | @dataclass 42 | class Restaurant: 43 | name: constr(regex=r'^[a-zA-Z0-9 ]*$', 44 | min_length=1, max_length=16) 45 | owner: constr(min_length=1) 46 | address: constr(min_length=1) 47 | employees: conlist(Employee, min_items=2) 48 | dishes: conlist(Dish, min_items=3) 49 | number_of_seats: PositiveInt 50 | to_go: bool 51 | delivery: bool 52 | 53 | try: 54 | restaurant = Restaurant(**{ 55 | 'name': 'Dine n Dash', 56 | 'owner': 'Pat Viafore', 57 | 'address': '123 Fake St.', 58 | 'employees': [Employee('Pat', 'Chef'), Employee('Joe', 'Server')], 59 | 'dishes': [Dish('abc'), Dish('def')], 60 | 'number_of_seats': 5, 61 | 'to_go': False, 62 | 'delivery': True 63 | }) 64 | assert False, "Should not have been able to construct Restaurant" 65 | except ValidationError: 66 | pass 67 | -------------------------------------------------------------------------------- /code_examples/chapter14/validators_custom.py: -------------------------------------------------------------------------------- 1 | from pydantic.dataclasses import dataclass 2 | from pydantic import constr, PositiveInt, ValidationError 3 | from typing import List, Literal, Optional, Union 4 | @dataclass 5 | class AccountAndRoutingNumber: 6 | account_number: constr(min_length=9,max_length=9) 7 | routing_number: constr(min_length=8,max_length=12) 8 | 9 | 10 | @dataclass 11 | class BankDetails: 12 | bank_details: AccountAndRoutingNumber 13 | 14 | 15 | @dataclass 16 | class Address: 17 | address: constr(min_length=1) 18 | 19 | AddressOrBankDetails = Union[Address, BankDetails] 20 | 21 | Position = Literal['Chef', 'Sous Chef', 'Host', 22 | 'Server', 'Delivery Driver'] 23 | @dataclass 24 | class Employee: 25 | name: str 26 | position: Position 27 | 28 | @dataclass 29 | class Dish: 30 | name: constr(min_length=1, max_length=16) 31 | price_in_cents: PositiveInt 32 | description: constr(min_length=1, max_length=80) 33 | picture: Optional[str] = None 34 | 35 | 36 | @dataclass 37 | class Dish: 38 | name: constr(min_length=1, max_length=16) 39 | 40 | from pydantic import conlist,constr 41 | from pydantic import validator 42 | @dataclass 43 | class Restaurant: 44 | name: constr(regex=r'^[a-zA-Z0-9 ]*$', 45 | min_length=1, max_length=16) 46 | owner: constr(min_length=1) 47 | address: constr(min_length=1) 48 | employees: conlist(Employee, min_items=2) 49 | dishes: conlist(Dish, min_items=3) 50 | number_of_seats: PositiveInt 51 | to_go: bool 52 | delivery: bool 53 | 54 | @validator('employees') 55 | def check_chef_and_server(cls, employees): 56 | if (any(e for e in employees if e.position == 'Chef') and 57 | any(e for e in employees if e.position == 'Server')): 58 | return employees 59 | raise ValueError('Must have at least one chef and one server') 60 | try: 61 | restaurant = Restaurant(**{ 62 | 'name': 'Dine n Dash', 63 | 'owner': 'Pat Viafore', 64 | 'address': '123 Fake St.', 65 | 'employees': [Employee('Pat', 'Chef'), Employee('Joe', 'Chef')], 66 | 'dishes': [Dish('abc'), Dish('def'), Dish('ghi')], 67 | 'number_of_seats': 5, 68 | 'to_go': False, 69 | 'delivery': True 70 | }) 71 | assert False, "Should not have been able to construct Restaurant" 72 | except ValidationError: 73 | pass 74 | -------------------------------------------------------------------------------- /code_examples/chapter14/wrong_type.yaml: -------------------------------------------------------------------------------- 1 | name: Viafore's 2 | owner: Pat Viafore 3 | address: 123 Fake St. Fakington, FA 01234 4 | employees: 5 | - name: Pat Viafore 6 | position: 3 7 | payment_details: 8 | bank_details: 9 | routing_number: "123456789" 10 | account_number: "123456789012" 11 | - name: Made-up McGee 12 | position: Server 13 | payment_details: 14 | bank_details: 15 | routing_number: "123456789" 16 | account_number: "123456789012" 17 | - name: Fabricated Frank 18 | position: Sous Chef 19 | payment_details: 20 | bank_details: 21 | routing_number: "123456789" 22 | account_number: "123456789012" 23 | - name: Illusory Ilsa 24 | position: Host 25 | payment_details: 26 | bank_details: 27 | routing_number: "123456789" 28 | account_number: "123456789012" 29 | dishes: 30 | - name: Pasta And Sausage 31 | price_in_cents: 1295 32 | description: Rigatoni and Sausage with a Tomato-Garlic-Basil Sauce 33 | - name: Pasta Bolognese 34 | price_in_cents: 1495 35 | description: Spaghetti with a rich Tomato and Beef Sauce 36 | - name: Caprese Salad 37 | price_in_cents: 795 38 | description: Tomato, Buffalo Mozzarella and Basil, drizzled with EVOO Olive Oil and Balsamic Vinegar from Moderna 39 | picture: caprese.png 40 | number_of_seats: 12 41 | to_go: true 42 | delivery: false 43 | -------------------------------------------------------------------------------- /code_examples/chapter15/restaurant_notification.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from dataclasses import dataclass 4 | from typing import Any, List, Dict, Set, Union 5 | 6 | notifications: list[Any] = [] 7 | 8 | Dish = str 9 | Ingredient = str 10 | 11 | @dataclass 12 | class NewSpecial: 13 | dish: Dish 14 | start_date: datetime.datetime 15 | end_date: datetime.datetime 16 | 17 | @dataclass 18 | class IngredientsOutOfStock: 19 | ingredients: set[Ingredient] 20 | 21 | @dataclass 22 | class IngredientsExpired: 23 | ingredients: set[Ingredient] 24 | 25 | @dataclass 26 | class NewMenuItem: 27 | dish: Dish 28 | 29 | Notification = Union[NewSpecial, IngredientsOutOfStock, IngredientsExpired, NewMenuItem] 30 | 31 | 32 | @dataclass 33 | class Text: 34 | phone_number: str 35 | 36 | @dataclass 37 | class Email: 38 | email_address: str 39 | 40 | @dataclass 41 | class SupplierAPI: 42 | pass 43 | 44 | NotificationMethod = Union[Text, Email, SupplierAPI] 45 | 46 | def notify(notification_method: NotificationMethod, notification: Notification): 47 | if isinstance(notification_method, Text): 48 | send_text(notification_method, notification) 49 | elif isinstance(notification_method, Email): 50 | send_email(notification_method, notification) 51 | elif isinstance(notification_method, SupplierAPI): 52 | send_to_supplier(notification) 53 | else: 54 | raise ValueError("Unsupported Notification Method") 55 | 56 | def send_text(text: Text, notification: Notification): 57 | if isinstance(notification, NewSpecial): 58 | # ... snip send text ... 59 | pass 60 | elif isinstance(notification, IngredientsOutOfStock): 61 | # ... snip send text ... 62 | pass 63 | elif isinstance(notification, IngredientsExpired): 64 | # ... snip send text ... 65 | pass 66 | elif isinstance(notification, NewMenuItem): 67 | # .. snip send text ... 68 | pass 69 | raise NotImplementedError("Notification method not supported") 70 | 71 | def send_email(email: Email, notification: Notification): 72 | # .. similar to send_text ... 73 | global notifications 74 | if isinstance(notification, IngredientsExpired): 75 | # ... snip send text ... 76 | notifications.append((email.email_address, notification.ingredients)) 77 | if isinstance(notification, NewMenuItem): 78 | # ... snip send text ... 79 | notifications.append((email.email_address, notification.dish)) 80 | 81 | 82 | def send_to_supplier(notification: Notification): 83 | # .. similar to send_text 84 | global notifications 85 | if isinstance(notification, IngredientsExpired): 86 | # ... snip send text ... 87 | notifications.append(("supplier", notification.ingredients)) 88 | 89 | users_to_notify: dict[type, list[NotificationMethod]] = { 90 | NewSpecial: [SupplierAPI(), Email("boss@company.org"), Email("marketing@company.org"), Text("555-2345")], 91 | IngredientsOutOfStock: [SupplierAPI(), Email("boss@company.org")], 92 | IngredientsExpired: [SupplierAPI(), Email("boss@company.org")], 93 | NewMenuItem: [Email("boss@company.org"), Email("marketing@company.org")] 94 | } 95 | 96 | def send_notification(notification: Notification): 97 | try: 98 | users = users_to_notify[type(notification)] 99 | except KeyError: 100 | raise ValueError("Unsupported Notification Method") 101 | for user in users: 102 | notify(user, notification) 103 | 104 | 105 | send_notification(NewMenuItem("Pasta")) 106 | assert notifications == [("boss@company.org", "Pasta"), ("marketing@company.org", "Pasta")] 107 | 108 | notifications = [] 109 | 110 | send_notification(IngredientsExpired({"Tomato", "Garlic"})) 111 | 112 | assert notifications == [("supplier", {"Tomato", "Garlic"}), ("boss@company.org", {"Tomato", "Garlic"})] 113 | -------------------------------------------------------------------------------- /code_examples/chapter15/run_tests.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash -eu 2 | python code_examples/chapter15/restaurant_notification.py 3 | 4 | mypy code_examples/chapter15/*.py 5 | 6 | echo "All Chapter 15 Tests Passed" 7 | -------------------------------------------------------------------------------- /code_examples/chapter17/call_repeat.py: -------------------------------------------------------------------------------- 1 | a = 1 2 | b = 2 3 | 4 | def inc_a(): 5 | global a 6 | a += 1 7 | 8 | from typing import Callable 9 | def do_twice(func: Callable, *args, **kwargs): 10 | func(*args, **kwargs) 11 | func(*args, **kwargs) 12 | 13 | do_twice(inc_a) 14 | assert a == 3 15 | 16 | def repeat_twice(func: Callable) -> Callable: 17 | ''' this is a function that calls the wrapped function a specified number of times ''' 18 | def _wrapper(*args, **kwargs): 19 | func(*args, **kwargs) 20 | func(*args, **kwargs) 21 | return _wrapper 22 | 23 | @repeat_twice 24 | def inc_b(): 25 | global b 26 | b+= 1 27 | 28 | inc_b() 29 | assert b == 4 30 | -------------------------------------------------------------------------------- /code_examples/chapter17/recommendation.py: -------------------------------------------------------------------------------- 1 | 2 | Meal = str 3 | Ingredient = str 4 | 5 | # dummy functions to get code to run 6 | def get_proximity(meal, _ ): 7 | return len(meal) 8 | 9 | def get_daily_specials(): 10 | return ["abc", "d", "efghi", "jk", "l", "mno", "p"] 11 | 12 | def recommend_meal(last_meal: Meal, 13 | specials: list[Meal], 14 | surplus: list[Ingredient]) -> list[Meal]: 15 | highest_proximity = 0 16 | for special in specials: 17 | if (proximity := get_proximity(special, surplus)) > highest_proximity: 18 | highest_proximity = proximity 19 | 20 | grouped_by_surplus_matching = [] 21 | for special in specials: 22 | if get_proximity(special, surplus) == highest_proximity: 23 | grouped_by_surplus_matching.append(special) 24 | 25 | filtered_meals = [] 26 | for meal in grouped_by_surplus_matching: 27 | if get_proximity(meal, last_meal) > .75: 28 | filtered_meals.append(meal) 29 | 30 | sorted_meals = sorted(filtered_meals, 31 | key=lambda meal: get_proximity(meal, last_meal), 32 | reverse=True) 33 | 34 | return sorted_meals[:3] 35 | 36 | assert recommend_meal("def", get_daily_specials(), ["fgh"]) == ["efghi"] 37 | -------------------------------------------------------------------------------- /code_examples/chapter17/recommendation_improved.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Callable 3 | 4 | import itertools 5 | 6 | Meal = str 7 | 8 | @dataclass 9 | class RecommendationPolicy: 10 | meals: list[str] 11 | initial_sorting_criteria: Callable 12 | grouping_criteria: Callable 13 | secondary_sorting_criteria: Callable 14 | selection_criteria: Callable 15 | desired_number_of_recommendations: int 16 | 17 | def recommend_meal(policy: RecommendationPolicy) -> list[Meal]: 18 | meals = policy.meals 19 | sorted_meals = sorted(meals, key=policy.initial_sorting_criteria, reverse=True) 20 | grouped_meals = itertools.groupby(sorted_meals, key=policy.grouping_criteria) 21 | _, top_grouped = next(grouped_meals) 22 | secondary_sorted = sorted(top_grouped, key=policy.secondary_sorting_criteria, reverse=True) 23 | candidates = itertools.takewhile(policy.selection_criteria, secondary_sorted) 24 | return list(candidates)[:policy.desired_number_of_recommendations] 25 | 26 | 27 | # dummy functions to get code to run 28 | def get_specials(): 29 | return ["abc", "d", "efghi", "jk", "l", "mno", "p"] 30 | 31 | def get_proximity_to_surplus_ingredients(meal): 32 | return len(meal) 33 | 34 | get_proximity_to_last_meal = get_proximity_to_surplus_ingredients 35 | 36 | def proximity_greater_than_75_percent(meal): 37 | return len(meal) > .75 38 | 39 | 40 | meal = recommend_meal(RecommendationPolicy( 41 | meals=get_specials(), 42 | initial_sorting_criteria=get_proximity_to_surplus_ingredients, 43 | grouping_criteria=get_proximity_to_surplus_ingredients, 44 | secondary_sorting_criteria=get_proximity_to_last_meal, 45 | selection_criteria=proximity_greater_than_75_percent, 46 | desired_number_of_recommendations=3) 47 | ) 48 | 49 | assert meal == ["efghi"] 50 | -------------------------------------------------------------------------------- /code_examples/chapter17/run_tests.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash -eu 2 | python code_examples/chapter17/call_repeat.py 3 | python code_examples/chapter17/recommendation.py 4 | python code_examples/chapter17/recommendation_improved.py 5 | 6 | mypy code_examples/chapter17/*.py 7 | 8 | echo "All Chapter 17 Tests Passed" 9 | -------------------------------------------------------------------------------- /code_examples/chapter18/run_tests.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash -eu 2 | python code_examples/chapter18/rxpy.py 3 | 4 | mypy code_examples/chapter18/*.py 5 | 6 | echo "All Chapter 18 Tests Passed" 7 | -------------------------------------------------------------------------------- /code_examples/chapter18/rxpy.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from enum import Enum 3 | 4 | import rx 5 | 6 | class Direction(Enum): 7 | NORTH = "NORTH" 8 | WEST = "WEST" 9 | SOUTH = "SOUTH" 10 | EAST = "EAST" 11 | 12 | @dataclass 13 | class LocationData: 14 | x: int 15 | y: int 16 | z: int 17 | 18 | @dataclass 19 | class BatteryLevel: 20 | percent: int 21 | 22 | @dataclass 23 | class WindData: 24 | speed: int 25 | direction: Direction 26 | 27 | @dataclass 28 | class CurrentWeight: 29 | grams: int 30 | 31 | def is_close_to_restaurant(*args): 32 | return False 33 | 34 | observable = rx.of( 35 | LocationData(x=3, y=12, z=40), 36 | BatteryLevel(percent=95), 37 | BatteryLevel(percent=94), 38 | WindData(speed=15, direction=Direction.NORTH), 39 | LocationData(x=3, y=12, z=35), 40 | LocationData(x=4, y=12, z=32), 41 | # ... snip 100s of events 42 | BatteryLevel(percent=72), 43 | CurrentWeight(grams=300), 44 | CurrentWeight(grams=100) 45 | ) 46 | 47 | val = None 48 | def save_value(x): 49 | global val 50 | val = x 51 | 52 | def save_average_weight(data): 53 | save_value(data) 54 | 55 | def save_max_altitude(data): 56 | save_value(data) 57 | 58 | import rx.operators 59 | 60 | get_average_weight = observable.pipe( 61 | rx.operators.filter(lambda data: isinstance(data, CurrentWeight)), 62 | rx.operators.map(lambda cw: cw.grams), 63 | rx.operators.average() 64 | ) 65 | 66 | get_average_weight.subscribe(save_average_weight) 67 | 68 | assert val == 200 69 | 70 | get_max_altitude = observable.pipe( 71 | rx.operators.skip_while(is_close_to_restaurant), 72 | rx.operators.filter(lambda data: isinstance(data, LocationData)), 73 | rx.operators.map(lambda loc: loc.z), 74 | rx.operators.max() 75 | ) 76 | 77 | get_max_altitude.subscribe(save_max_altitude) 78 | 79 | assert val == 40 80 | -------------------------------------------------------------------------------- /code_examples/chapter19/pluggable.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | from stevedore import extension 3 | 4 | 5 | Recipe = str 6 | Dish = str 7 | 8 | def get_inventory(): 9 | return {} 10 | 11 | def get_all_recipes() -> list[Recipe]: 12 | mgr = extension.ExtensionManager( 13 | namespace='ultimate_kitchen_assistant.recipe_maker', 14 | invoke_on_load=True, 15 | ) 16 | 17 | def get_recipes(extension): 18 | return extension.obj.get_recipes() 19 | 20 | return list(itertools.chain.from_iterable(mgr.map(get_recipes))) 21 | 22 | from stevedore import driver 23 | 24 | def make_dish(recipe: Recipe, module_name: str) -> Dish: 25 | mgr = driver.DriverManager( 26 | namespace='ultimate_kitchen_assistant.recipe_maker', 27 | name=module_name, 28 | invoke_on_load=True, 29 | ) 30 | 31 | return mgr.driver.prepare_dish(get_inventory(), recipe) 32 | 33 | assert get_all_recipes() == ["Linguine", "Spaghetti", "Taco"] 34 | 35 | assert make_dish("Linguine", "pasta_maker") == "Prepared Linguine" 36 | -------------------------------------------------------------------------------- /code_examples/chapter19/run_tests.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash -eu 2 | python code_examples/chapter19/pluggable.py 3 | 4 | echo "All Chapter 19 Tests Passed" 5 | -------------------------------------------------------------------------------- /code_examples/chapter2/double_items.py: -------------------------------------------------------------------------------- 1 | def double_value(value): 2 | return value + value 3 | 4 | assert double_value(5) == 10 5 | assert double_value("abc") == "abcabc" 6 | assert double_value([1,2,3]) == [1,2,3,1,2,3] 7 | -------------------------------------------------------------------------------- /code_examples/chapter2/memory_value.py: -------------------------------------------------------------------------------- 1 | from ctypes import string_at 2 | from sys import getsizeof 3 | from binascii import hexlify 4 | 5 | 6 | 7 | # account for little and big endian - will only work in CPython 8 | a = 0b01010000_01000001_01010100 9 | bytestring = hexlify(string_at(id(a), getsizeof(a))) 10 | assert b'544150' in bytestring or b'504154' in bytestring 11 | 12 | 13 | text = "PAT" 14 | bytestring = hexlify(string_at(id(text), getsizeof(text))) 15 | assert b'544150' in bytestring or b'504154' in bytestring 16 | -------------------------------------------------------------------------------- /code_examples/chapter2/print_items.py: -------------------------------------------------------------------------------- 1 | def print_items(items): 2 | for item in items: 3 | print(item) 4 | 5 | print_items([1,2,3]) 6 | print_items({4, 5, 6}) 7 | print_items({"A": 1, "B": 2, "C": 3}) 8 | -------------------------------------------------------------------------------- /code_examples/chapter2/run_tests.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash -eu 2 | python code_examples/chapter2/types_example.py 3 | python code_examples/chapter2/memory_value.py 4 | python code_examples/chapter2/double_items.py 5 | 6 | # Just prints out - just grab errros 7 | python code_examples/chapter2/print_items.py 8 | 9 | mypy code_examples/chapter2/*.py 10 | 11 | echo "All Chapter 2 Tests Passed" 12 | -------------------------------------------------------------------------------- /code_examples/chapter2/types_example.py: -------------------------------------------------------------------------------- 1 | assert type(3.14) == float 2 | assert type("This is another boring example") == str 3 | assert type(["Even", "more", "boring", "examples"]) == list 4 | -------------------------------------------------------------------------------- /code_examples/chapter20/hotdog.py: -------------------------------------------------------------------------------- 1 | from typing import NewType 2 | 3 | class HotDog: 4 | pass 5 | 6 | ReadyToServeHotDog = NewType("ReadyToServeHotDog", HotDog) 7 | 8 | def create_hot_dog() -> ReadyToServeHotDog: 9 | hot_dog = HotDog() 10 | return ReadyToServeHotDog(hot_dog) 11 | 12 | def create(): 13 | hot_dog=ReadyToServeHotDog(HotDog()) 14 | -------------------------------------------------------------------------------- /code_examples/chapter20/hotdog_checker.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | import astroid 3 | 4 | from pylint.checkers import BaseChecker 5 | from pylint.interfaces import IAstroidChecker 6 | from pylint.lint.pylinter import PyLinter 7 | 8 | class ServableHotDogChecker(BaseChecker): 9 | __implements__ = IAstroidChecker 10 | 11 | name = 'unverified-ready-to-serve-hotdog' 12 | priority = -1 13 | msgs = { 14 | 'W0001': ( 15 | 'ReadyToServeHotDog created outside of hotdog.prepare_for_serving.', 16 | 'unverified-ready-to-serve-hotdog', 17 | 'Only create a PreparedHotDog through hotdog.prepare_for_serving.' 18 | ), 19 | } 20 | 21 | def __init__(self, linter: Optional[PyLinter] = None): 22 | super(ServableHotDogChecker, self).__init__(linter) 23 | self._is_in_prepare_for_serving = False 24 | 25 | def visit_functiondef(self, node: astroid.scoped_nodes.FunctionDef): 26 | if (node.name == "prepare_for_serving" and 27 | node.parent.name =="hotdog" and 28 | isinstance(node.parent, astroid.scoped_nodes.Module)): 29 | self._is_in_prepare_for_serving = True 30 | 31 | def leave_functiondef(self, node: astroid.scoped_nodes.FunctionDef): 32 | if (node.name == "prepare_for_serving" and 33 | node.parent.name =="hotdog" and 34 | isinstance(node.parent, astroid.scoped_nodes.Module)): 35 | 36 | self._is_in_prepare_for_serving = False 37 | 38 | def visit_call(self, node: astroid.node_classes.Call): 39 | if node.func.name != 'ReadyToServeHotDog': 40 | return 41 | 42 | if self._is_in_prepare_for_serving: 43 | return 44 | 45 | self.add_message( 46 | 'unverified-ready-to-serve-hotdog', node=node, 47 | ) 48 | 49 | def register(linter: PyLinter): 50 | linter.register_checker(ServableHotDogChecker(linter)) 51 | -------------------------------------------------------------------------------- /code_examples/chapter20/lint_example.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | @dataclass 4 | class Author: 5 | cookbooks: list[str] 6 | 7 | def find_author(name: str): 8 | return Author([]) 9 | 10 | 11 | def add_authors_cookbooks(author_name: str, cookbooks: list[str] = []) -> bool: 12 | author = find_author(author_name) 13 | if author is None: 14 | assert False, "Author does not exist" 15 | else: 16 | for cookbook in author.cookbooks: 17 | cookbooks.append(cookbook) 18 | return True 19 | -------------------------------------------------------------------------------- /code_examples/chapter20/run_tests.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash -eux 2 | rm error.txt 3 | PYTHONPATH=$(pwd)/code_examples/chapter20 pylint --load-plugins hotdog_checker code_examples/chapter20/hotdog.py > error.txt || true 4 | 5 | grep "W0001: ReadyToServeHotDog created outside of hotdog.prepare_for_serving. (unverified-ready-to-serve-hotdog)" error.txt 6 | 7 | echo "All Chapter 20 Tests Passed" 8 | -------------------------------------------------------------------------------- /code_examples/chapter20/whitespace_checker.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | def get_amount_of_preceding_whitespace(line: str) -> int: 4 | # replace tabs with 4 spaces (and start tab/spaces flame-war) 5 | tab_normalized_text = line.replace("\t", " ") 6 | return len(tab_normalized_text) - len(tab_normalized_text.lstrip()) 7 | 8 | def get_average_whitespace(filename: str): 9 | with open(filename) as file_to_check: 10 | whitespace_count = [get_amount_of_preceding_whitespace(line) 11 | for line in file_to_check 12 | if line != ""] 13 | average = sum(whitespace_count) / len(whitespace_count) / 4 14 | print(f"Avg indentation level for {filename}: {average}") 15 | 16 | get_average_whitespace(sys.argv[1]) 17 | -------------------------------------------------------------------------------- /code_examples/chapter21/run_tests.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash -eux 2 | py.test code_examples/chapter21 3 | 4 | echo "All Chapter 21 Tests Passed" 5 | -------------------------------------------------------------------------------- /code_examples/chapter21/test_aaa_test.py: -------------------------------------------------------------------------------- 1 | def add_ingredient_to_database(*args, **kwargs): 2 | pass 3 | 4 | def set_ingredients(*arg, **kwargs): 5 | pass 6 | 7 | def get_calories(*args): 8 | return 1200 9 | 10 | def cleanup_database(): 11 | pass 12 | 13 | def test_calorie_calculation(): 14 | 15 | # arrange 16 | add_ingredient_to_database("Ground Beef", calories_per_pound=1500) 17 | add_ingredient_to_database("Bacon", calories_per_pound=2400) 18 | add_ingredient_to_database("Cheese", calories_per_pound=1800) 19 | # ... snip ingredients 20 | 21 | set_ingredients("Bacon Cheeseburger w/ Fries", 22 | ingredients=["Ground Beef", "Bacon"]) 23 | 24 | # act 25 | calories = get_calories("Bacon Cheeseburger w/ Fries") 26 | 27 | # assert 28 | assert calories == 1200 29 | 30 | #annihilate 31 | cleanup_database() 32 | -------------------------------------------------------------------------------- /code_examples/chapter21/test_basic_pytest.py: -------------------------------------------------------------------------------- 1 | def get_calories(*args): 2 | return 1200 3 | 4 | def test_get_calorie_count(): 5 | assert get_calories("Bacon Cheeseburger w/ Fries") == 1200 6 | -------------------------------------------------------------------------------- /code_examples/chapter21/test_context_manager.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | 3 | def add_base_ingredients_to_database(): 4 | pass 5 | 6 | def add_ingredient_to_database(*args, **kwargs): 7 | pass 8 | 9 | 10 | calories = 0 11 | 12 | def setup_bacon_cheeseburger(bacon): 13 | global calories 14 | calories = 1100 if "Turkey" in bacon else 1200 15 | 16 | def get_calories(*args): 17 | return calories 18 | 19 | def cleanup_database(): 20 | pass 21 | 22 | 23 | class Database: 24 | def add_ingredient(self, *args, **kwargs): 25 | pass 26 | 27 | def cleanup(self): 28 | pass 29 | 30 | @contextlib.contextmanager 31 | def construct_test_database(): 32 | yield Database() 33 | 34 | 35 | 36 | def test_calorie_calculation_bacon_cheeseburger(): 37 | with construct_test_database() as db: 38 | db.add_ingredient("Bacon", calories_per_pound=2400) 39 | setup_bacon_cheeseburger(bacon="Bacon") 40 | 41 | calories = get_calories("Bacon Cheeseburger w/ Fries") 42 | 43 | assert calories == 1200 44 | -------------------------------------------------------------------------------- /code_examples/chapter21/test_custom_hamcrest.py: -------------------------------------------------------------------------------- 1 | 2 | from collections import UserList 3 | class Dish(): 4 | 5 | def __init__(self, name): 6 | self.name = name 7 | 8 | def __str__(self): 9 | return self.name 10 | 11 | def make_vegan(self): 12 | pass 13 | 14 | def ingredients(self): 15 | return ["Turkey Burger", "Bun"] 16 | 17 | def create_dish(name): 18 | return Dish(name) 19 | 20 | 21 | from hamcrest.core.base_matcher import BaseMatcher 22 | from hamcrest.core.helpers.hasmethod import hasmethod 23 | 24 | def is_vegan(ingredient: str) -> bool: 25 | return ingredient not in ["Beef Burger"] 26 | 27 | class IsVegan(BaseMatcher): 28 | 29 | def _matches(self, dish): 30 | if not hasmethod(dish, "ingredients"): 31 | return False 32 | return all(is_vegan(ingredient) for ingredient in dish.ingredients()) 33 | 34 | def describe_to(self, description): 35 | description.append_text("Expected dish to be vegan") 36 | 37 | def describe_mismatch(self, dish, description): 38 | message = f"the following ingredients are not vegan: " 39 | message += ", ".join(ing for ing in dish.ingredients() if not is_vegan(ing)) 40 | description.append_text(message) 41 | 42 | 43 | def vegan(): 44 | return IsVegan() 45 | 46 | 47 | from hamcrest import assert_that, is_ 48 | def test_vegan_substitution(): 49 | dish = create_dish("Hamburger and Fries") 50 | dish.make_vegan() 51 | assert_that(dish, is_(vegan())) 52 | -------------------------------------------------------------------------------- /code_examples/chapter21/test_extracted.py: -------------------------------------------------------------------------------- 1 | def add_base_ingredients_to_database(): 2 | pass 3 | 4 | def add_ingredient_to_database(*args, **kwargs): 5 | pass 6 | 7 | 8 | calories = 0 9 | 10 | def setup_bacon_cheeseburger(bacon): 11 | global calories 12 | calories = 1100 if "Turkey" in bacon else 1200 13 | 14 | def get_calories(*args): 15 | return calories 16 | 17 | def cleanup_database(): 18 | pass 19 | 20 | 21 | def test_calorie_calculation_bacon_cheeseburger(): 22 | add_base_ingredients_to_database() 23 | add_ingredient_to_database("Bacon", calories_per_pound=2400) 24 | setup_bacon_cheeseburger(bacon="Bacon") 25 | 26 | calories = get_calories("Bacon Cheeseburger w/ Fries") 27 | 28 | assert calories == 1200 29 | 30 | cleanup_database() 31 | 32 | def test_calorie_calculation_bacon_cheeseburger_with_substitution(): 33 | add_base_ingredients_to_database() 34 | add_ingredient_to_database("Turkey Bacon", calories_per_pound=1700) 35 | setup_bacon_cheeseburger(bacon="Turkey Bacon") 36 | 37 | calories = get_calories("Bacon Cheeseburger w/ Fries") 38 | 39 | assert calories == 1100 40 | 41 | cleanup_database() 42 | -------------------------------------------------------------------------------- /code_examples/chapter21/test_fixture with_cleanup.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | 5 | class Database: 6 | def add_ingredient(self, *args, **kwargs): 7 | pass 8 | 9 | def cleanup(self): 10 | pass 11 | 12 | def setup_bacon_cheeseburger(**kwargs): 13 | pass 14 | 15 | def get_calories(*args): 16 | return 1200 17 | 18 | 19 | @pytest.fixture 20 | def db_creation(): 21 | # ... snip set up local sqlite database 22 | return Database() 23 | 24 | @pytest.fixture 25 | def test_database(db_creation): 26 | # ... snip adding all ingredients and meals 27 | try: 28 | yield db_creation 29 | finally: 30 | db_creation.cleanup() 31 | 32 | def test_calorie_calculation_bacon_cheeseburger(test_database): 33 | test_database.add_ingredient("Bacon", calories_per_pound=2400) 34 | setup_bacon_cheeseburger(bacon="Bacon") 35 | 36 | calories = get_calories("Bacon Cheeseburger w/ Fries") 37 | 38 | assert calories == 1200 39 | -------------------------------------------------------------------------------- /code_examples/chapter21/test_fixture.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | 5 | class Database: 6 | def add_ingredient(self, *args, **kwargs): 7 | pass 8 | 9 | def cleanup(self): 10 | pass 11 | 12 | @pytest.fixture 13 | def db_creation(): 14 | # ... snip set up local sqlite database 15 | return Database() 16 | 17 | def setup_bacon_cheeseburger(**kwargs): 18 | pass 19 | 20 | def get_calories(*args): 21 | return 1200 22 | 23 | @pytest.fixture 24 | def test_database(db_creation): 25 | # ... snip adding all ingredients and meals 26 | return db_creation 27 | 28 | def test_calorie_calculation_bacon_cheeseburger(test_database): 29 | test_database.add_ingredient("Bacon", calories_per_pound=2400) 30 | setup_bacon_cheeseburger(bacon="Bacon") 31 | 32 | calories = get_calories("Bacon Cheeseburger w/ Fries") 33 | 34 | assert calories == 1200 35 | 36 | test_database.cleanup() 37 | -------------------------------------------------------------------------------- /code_examples/chapter21/test_fixture_with_message.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | 5 | class Database: 6 | def add_ingredient(self, *args, **kwargs): 7 | pass 8 | 9 | def cleanup(self): 10 | pass 11 | 12 | def setup_bacon_cheeseburger(**kwargs): 13 | pass 14 | 15 | def get_calories(*args): 16 | return 1200 17 | 18 | 19 | @pytest.fixture 20 | def db_creation(): 21 | # ... snip set up local sqlite database 22 | return Database() 23 | 24 | @pytest.fixture 25 | def test_database(db_creation): 26 | # ... snip adding all ingredients and meals 27 | try: 28 | yield db_creation 29 | finally: 30 | db_creation.cleanup() 31 | 32 | def test_calorie_calculation_bacon_cheeseburger(test_database): 33 | test_database.add_ingredient("Bacon", calories_per_pound=2400) 34 | setup_bacon_cheeseburger(bacon="Bacon") 35 | 36 | calories = get_calories("Bacon Cheeseburger w/ Fries") 37 | 38 | assert calories == 1200, "Calories for Bacon Cheeseburger were wrong" 39 | -------------------------------------------------------------------------------- /code_examples/chapter21/test_hamcrest.py: -------------------------------------------------------------------------------- 1 | def create_menu(): 2 | return ["abc", "Def", "ghi"] 3 | 4 | def get_calories(dish): 5 | return 1200 6 | 7 | def find_owned_restaurants_in(city): 8 | return [] 9 | 10 | from hamcrest import assert_that, matches_regexp, is_, empty, equal_to 11 | def test_all_menu_items_are_alphanumeric(): 12 | menu = create_menu() 13 | for item in menu: 14 | assert_that(item, matches_regexp(r'[a-zA-Z0-9 ]')) 15 | 16 | def test_getting_calories(): 17 | dish = "Bacon Cheeseburger w/ Fries" 18 | calories = get_calories(dish) 19 | assert_that(calories, is_(equal_to(1200))) 20 | 21 | def test_no_restaurant_found_in_non_matching_areas(): 22 | city = "Huntsville, AL" 23 | restaurants = find_owned_restaurants_in(city) 24 | assert_that(restaurants, is_(empty())) 25 | -------------------------------------------------------------------------------- /code_examples/chapter21/test_parameterized.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | class Database: 5 | def add_ingredient(self, *args, **kwargs): 6 | pass 7 | 8 | def cleanup(self): 9 | pass 10 | 11 | def setup_dish_ingredients(args, **kwargs): 12 | pass 13 | 14 | @pytest.fixture 15 | def db_creation(): 16 | # ... snip set up local sqlite database 17 | return Database() 18 | 19 | 20 | @pytest.fixture 21 | def test_database(db_creation): 22 | # ... snip adding all ingredients and meals 23 | return db_creation 24 | 25 | 26 | def get_calories(dish: str): 27 | return { 28 | "Bacon Cheeseburger": 900, 29 | "Cobb Salad": 1000, 30 | "Buffalo Wings": 800, 31 | "Garlicky Brussels Sprouts": 200, 32 | "Mashed Potatoes": 400 33 | }[dish] 34 | 35 | 36 | 37 | @pytest.mark.parametrize( 38 | "extra_ingredients,dish_name,expected_calories", 39 | [ 40 | (["Bacon", 2400], "Bacon Cheeseburger", 900), 41 | ([], "Cobb Salad", 1000), 42 | ([], "Buffalo Wings", 800), 43 | ([], "Garlicky Brussels Sprouts", 200), 44 | ([], "Mashed Potatoes", 400) 45 | ] 46 | ) 47 | def test_calorie_calculation_bacon_cheeseburger(extra_ingredients, 48 | dish_name, 49 | expected_calories, 50 | test_database): 51 | for ingredient in extra_ingredients: 52 | test_database.add_ingredient(ingredient) 53 | 54 | # assume this function can set up any dish 55 | # alternatively, dish ingredients could be passed in as a test parameter 56 | setup_dish_ingredients(dish_name) 57 | 58 | calories = get_calories(dish_name) 59 | 60 | assert calories == expected_calories 61 | -------------------------------------------------------------------------------- /code_examples/chapter22/features/food.feature: -------------------------------------------------------------------------------- 1 | Feature: Vegan-friendly menu 2 | 3 | Scenario: Can substitute for vegan alternatives 4 | Given an order containing a Cheeseburger with Fries 5 | When I ask for vegan substitutions 6 | Then I receive the meal with no animal products 7 | -------------------------------------------------------------------------------- /code_examples/chapter22/features/steps/steps.py: -------------------------------------------------------------------------------- 1 | 2 | class CheeseburgerWithFries(): 3 | def substitute_vegan_ingredients(self): 4 | pass 5 | 6 | def ingredients(self): 7 | return [] 8 | 9 | def is_vegan(ingredient): 10 | return True 11 | 12 | 13 | class Meatloaf: 14 | pass 15 | 16 | from behave import given, when, then 17 | 18 | @given("an order containing {dish_name}") 19 | def setup_order(ctx, dish_name): 20 | if dish_name == "a Cheeseburger with Fries": 21 | ctx.dish = CheeseburgerWithFries() 22 | elif dish_name == "Meatloaf": 23 | ctx.dish = Meatloaf() 24 | ctx.dish = Meatloaf() 25 | 26 | @when("I ask for vegan substitutions") 27 | def substitute_vegan(ctx): 28 | if isinstance(ctx.dish, Meatloaf): 29 | return 30 | ctx.dish.substitute_vegan_ingredients() 31 | 32 | @then("I receive the meal with no animal products") 33 | def check_all_vegan(ctx): 34 | if isinstance(ctx.dish, Meatloaf): 35 | return 36 | assert all(is_vegan(ing) for ing in ctx.dish.ingredients()) 37 | 38 | 39 | @then(u'Then a non-vegan-substitutable error shows up') 40 | def step_impl(context): 41 | pass 42 | -------------------------------------------------------------------------------- /code_examples/chapter22/features/table_driven.feature: -------------------------------------------------------------------------------- 1 | Feature: Vegan-friendly menu 2 | 3 | Scenario Outline: Vegan Substitutions 4 | Given an order containing , 5 | When I ask for vegan substitutions 6 | Then 7 | 8 | Examples: Vegan Substitutable 9 | | dish_name | result | 10 | | a Cheeseburger with Fries | I receive the meal with no animal products | 11 | | Cobb Salad | I receive the meal with no animal products | 12 | | French Fries | I receive the meal with no animal products | 13 | | Lemonade | I receive the meal with no animal products | 14 | 15 | Examples: Not Vegan Substitutable 16 | | dish_name | result | 17 | | Meatloaf | a non-vegan-substitutable error shows up | 18 | | Meatballs | a non-vegan-substitutable error shows up | 19 | | Fried Shrimp | a non-vegan-substitutable error shows up | 20 | -------------------------------------------------------------------------------- /code_examples/chapter22/regex/food.feature: -------------------------------------------------------------------------------- 1 | Feature: Basic Given 2 | 3 | Scenario: Basic Given 4 | Given an order containing a Cheeseburger 5 | Given an order containing Meatloaf 6 | Given an order containing an egg 7 | -------------------------------------------------------------------------------- /code_examples/chapter22/regex/steps/step.py: -------------------------------------------------------------------------------- 1 | from behave import use_step_matcher 2 | 3 | use_step_matcher("re") 4 | 5 | def create_dish(dish_name): 6 | pass 7 | 8 | @given("an order containing [a |an ]?(?P.*)") 9 | def setup_order(context, dish_name): 10 | context.dish = create_dish(dish_name) 11 | -------------------------------------------------------------------------------- /code_examples/chapter23/test_basic_hypothesis.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from enum import Enum 3 | 4 | @dataclass 5 | class Meal: 6 | name: str 7 | calories: int 8 | 9 | class Recommendation(Enum): 10 | BY_CALORIES = 0 11 | 12 | def is_appetizer(dish): 13 | return dish.name in ["Spring Roll"] 14 | 15 | def is_salad(dish): 16 | return dish.name in ["Green Papaya Salad"] 17 | 18 | def is_main_dish(dish): 19 | return dish.name in ["Larb Chicken"] 20 | 21 | from hypothesis import given, example 22 | from hypothesis.strategies import integers 23 | 24 | 25 | def get_recommended_meal(Recommendation, calories: int) -> list[Meal]: 26 | return [Meal("Spring Roll", 120), 27 | Meal("Green Papaya Salad", 230), 28 | Meal("Larb Chicken", 500)] 29 | 30 | @given(integers(min_value=900)) 31 | def test_meal_recommendation_under_specific_calories(calories): 32 | meals = get_recommended_meal(Recommendation.BY_CALORIES, calories) 33 | assert len(meals) == 3 34 | assert is_appetizer(meals[0]) 35 | assert is_salad(meals[1]) 36 | assert is_main_dish(meals[2]) 37 | assert sum(meal.calories for meal in meals) < calories 38 | -------------------------------------------------------------------------------- /code_examples/chapter23/test_composite_strategies.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | @dataclass 4 | class Dish: 5 | name: str 6 | calories: int 7 | 8 | from hypothesis import given 9 | from hypothesis.strategies import composite, integers 10 | 11 | ThreeCourseMeal = tuple[Dish, Dish, Dish] 12 | 13 | @composite 14 | def three_course_meals(draw) -> ThreeCourseMeal: 15 | appetizer_calories = integers(min_value=100, max_value=900) 16 | main_dish_calories = integers(min_value=550, max_value=1800) 17 | dessert_calories = integers(min_value=500, max_value=1000) 18 | 19 | return (Dish("Appetizer", draw(appetizer_calories)), 20 | Dish("Main Dish", draw(main_dish_calories)), 21 | Dish("Dessert", draw(dessert_calories))) 22 | 23 | @given(three_course_meals()) 24 | def test_three_course_meal_substitutions(three_course_meal: ThreeCourseMeal): 25 | pass 26 | -------------------------------------------------------------------------------- /code_examples/chapter23/test_hypothesis_stateful.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | @dataclass(frozen=True) 4 | class Meal: 5 | price: int 6 | calories: int 7 | distance: int 8 | 9 | class MealRecommendationEngine: 10 | 11 | def __init__(self): 12 | self.filters = [] 13 | self.meals = [Meal(12, 300, 80), Meal(15, 1200, 5), Meal(14, 900, 55), Meal(13, 1800, 20)] 14 | 15 | def apply_price_filter(self, price): 16 | self.filters = [f for f in self.filters if f[0] != "price"] 17 | self.filters.append(("price", lambda m: m.price)) 18 | 19 | def apply_calorie_filter(self, calorie): 20 | self.filters = [f for f in self.filters if f[0] != "calorie"] 21 | self.filters.append(("calorie", lambda m: m.calories)) 22 | 23 | def apply_distance_filter(self, distance): 24 | self.filters = [f for f in self.filters if f[0] != "distance"] 25 | self.filters.append(("distance", lambda m: m.distance)) 26 | 27 | def get_meals(self): 28 | return reduce(lambda meals, f: sorted(meals, key=f[1]), 29 | self.filters, 30 | self.meals)[:3] 31 | 32 | 33 | from functools import reduce 34 | from hypothesis.strategies import integers 35 | from hypothesis.stateful import Bundle, RuleBasedStateMachine, invariant, rule 36 | 37 | class RecommendationChecker(RuleBasedStateMachine): 38 | def __init__(self): 39 | super().__init__() 40 | self.recommender = MealRecommendationEngine() 41 | self.filters = [] 42 | 43 | @rule(price_limit=integers(min_value=6, max_value=200)) 44 | def filter_by_price(self, price_limit): 45 | self.recommender.apply_price_filter(price_limit) 46 | self.filters = [f for f in self.filters if f[0] != "price"] 47 | self.filters.append(("price", lambda m: m.price)) 48 | 49 | @rule(calorie_limit=integers(min_value=500, max_value=2000)) 50 | def filter_by_calories(self, calorie_limit): 51 | self.recommender.apply_calorie_filter(calorie_limit) 52 | self.filters = [f for f in self.filters if f[0] != "calorie"] 53 | self.filters.append(("calorie", lambda m: m.calories)) 54 | 55 | @rule(distance_limit=integers(max_value=100)) 56 | def filter_by_distance(self, distance_limit): 57 | self.recommender.apply_distance_filter(distance_limit) 58 | self.filters = [f for f in self.filters if f[0] != "distance"] 59 | self.filters.append(("distance", lambda m: m.distance)) 60 | 61 | @invariant() 62 | def recommender_provides_three_unique_meals(self): 63 | assert len(self.recommender.get_meals()) == 3 64 | assert len(set(self.recommender.get_meals())) == 3 65 | 66 | @invariant() 67 | def meals_are_appropriately_ordered(self): 68 | meals = self.recommender.get_meals() 69 | ordered_meals = reduce(lambda meals, f: sorted(meals, key=f[1]), 70 | self.filters, 71 | meals) 72 | assert ordered_meals == meals 73 | 74 | 75 | TestRecommender = RecommendationChecker.TestCase 76 | -------------------------------------------------------------------------------- /code_examples/chapter24/calorie_tracker.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from enum import Enum 3 | 4 | @dataclass 5 | class Meal: 6 | name: str 7 | calories: int 8 | 9 | 10 | class WarningType(Enum): 11 | OVER_CALORIE_LIMIT = 0 12 | 13 | warnings = [] 14 | checkmarks = [] 15 | 16 | def display_warning(meal, warning_type): 17 | warnings.append(meal) 18 | 19 | def display_checkmark(meal): 20 | checkmarks.append(meal) 21 | 22 | def clear_warnings(): 23 | warnings.clear() 24 | checkmarks.clear() 25 | 26 | def check_meals_for_calorie_overage(meals: list[Meal], target: int): 27 | for meal in meals: 28 | target -= meal.calories 29 | if target < 0: 30 | display_warning(meal, WarningType.OVER_CALORIE_LIMIT) 31 | continue 32 | display_checkmark(meal) 33 | -------------------------------------------------------------------------------- /code_examples/chapter24/test_calorie_tracker.py: -------------------------------------------------------------------------------- 1 | from calorie_tracker import check_meals_for_calorie_overage, checkmarks, clear_warnings, Meal, warnings 2 | 3 | def assert_no_warnings_displayed_on_meal(meal_name): 4 | assert all(m.name != meal_name for m in warnings) 5 | 6 | def assert_checkmark_on_meal(meal_name): 7 | assert any(m.name == meal_name for m in checkmarks) 8 | 9 | def assert_meal_is_over_calories(meal_name): 10 | assert any(m.name == meal_name for m in warnings) 11 | 12 | 13 | def test_no_warnings_if_under_calories(): 14 | clear_warnings() 15 | meals = [Meal("Fish 'n' Chips", 1000)] 16 | check_meals_for_calorie_overage(meals, 1200) 17 | assert_no_warnings_displayed_on_meal("Fish 'n' Chips") 18 | assert_checkmark_on_meal("Fish 'n' Chips") 19 | 20 | def test_no_exception_thrown_if_no_meals(): 21 | clear_warnings() 22 | check_meals_for_calorie_overage([], 1200) 23 | # no explicit assert, just checking for no exceptions 24 | 25 | def test_meal_is_marked_as_over_calories(): 26 | clear_warnings() 27 | meals = [Meal("Fish 'n' Chips", 1000)] 28 | check_meals_for_calorie_overage(meals, 900) 29 | assert_meal_is_over_calories("Fish 'n' Chips") 30 | 31 | def test_meal_going_over_calories_does_not_conflict_with_previous_meals(): 32 | clear_warnings() 33 | meals = [Meal("Fish 'n' Chips", 1000), Meal("Banana Split", 400)] 34 | check_meals_for_calorie_overage(meals, 1200) 35 | assert_no_warnings_displayed_on_meal("Fish 'n' Chips") 36 | assert_checkmark_on_meal("Fish 'n' Chips") 37 | assert_meal_is_over_calories("Banana Split") 38 | -------------------------------------------------------------------------------- /code_examples/chapter3/find_workers.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import random 3 | from typing import List 4 | 5 | class WorkerDatabase: 6 | def get_all_workers(self) -> list[str]: 7 | return [] 8 | 9 | def get_emergency_workers(): 10 | return [] 11 | 12 | def is_available(name: str): 13 | return True 14 | 15 | worker_database = WorkerDatabase() 16 | OWNER = 'Pat' 17 | 18 | def schedule(worker, open_time): 19 | pass 20 | 21 | 22 | def schedule_restaurant_open(open_time: datetime.datetime, 23 | workers_needed: int): 24 | workers = find_workers_available_for_time(open_time) 25 | # use random.sample to pick X available workers 26 | # where X is the number of workers needed. 27 | for worker in random.sample(workers, workers_needed): 28 | schedule(worker, open_time) 29 | 30 | 31 | def find_workers_available_for_time(open_time: datetime.datetime): 32 | workers = worker_database.get_all_workers() 33 | available_workers = [worker for worker in workers 34 | if is_available(worker)] 35 | if available_workers: 36 | return available_workers 37 | 38 | # fall back to workers who listed they are available in 39 | # in an emergency 40 | emergency_workers = [worker for worker in get_emergency_workers() 41 | if is_available(worker)] 42 | 43 | if emergency_workers: 44 | return emergency_workers 45 | 46 | # Schedule the owner to open, they will find someone else 47 | return [OWNER] 48 | 49 | assert find_workers_available_for_time(datetime.datetime.now()) == ["Pat"] 50 | -------------------------------------------------------------------------------- /code_examples/chapter3/invalid/close_kitchen.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | def close_kitchen(): 3 | pass 4 | 5 | def closing_time(): 6 | return datetime.datetime.now() 7 | 8 | def log_time_closed(*args): 9 | pass 10 | 11 | 12 | class CustomDateTime: 13 | def __init__(self, text): 14 | pass 15 | 16 | # CustomDateTime offers all the same functionality with 17 | # datetime.datetime. We're using it here for it's better 18 | # logging facilities 19 | close_kitchen_if_past_close(CustomDateTime("now")) # no error 20 | 21 | 22 | def close_kitchen_if_past_close(point_in_time: datetime.datetime): 23 | if point_in_time >= closing_time(): 24 | close_kitchen() 25 | log_time_closed(point_in_time) 26 | -------------------------------------------------------------------------------- /code_examples/chapter3/invalid/invalid_example1.py: -------------------------------------------------------------------------------- 1 | def read_file_and_reverse_it(filename: str) -> str: 2 | with open(filename) as f: 3 | return f.read().encode("utf-8")[::-1] 4 | 5 | read_file_and_reverse_it('code_examples/chapter3/run_tests.sh') 6 | -------------------------------------------------------------------------------- /code_examples/chapter3/invalid/invalid_example2.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | # takes a list and adds the doubled values 3 | # to the end 4 | # [1,2,3] => [1,2,3,2,4,6] 5 | def add_doubled_values(my_list: list[int]): 6 | my_list.update([x*2 for x in my_list]) 7 | 8 | try: 9 | add_doubled_values([1,2,3]) 10 | assert False, "Should have thrown an exception" 11 | except AttributeError: 12 | pass 13 | -------------------------------------------------------------------------------- /code_examples/chapter3/invalid/invalid_example3.py: -------------------------------------------------------------------------------- 1 | 2 | ITALY_CITIES = ['Roma', 'Milano'] 3 | GERMAN_CITIES = ['Berlin', 'Frankfurt'] 4 | US_CITIES = ['Boston', 'Los Angeles'] 5 | # Our restaurant is named differently in different 6 | # in different parts of the world 7 | def get_restaurant_name(city: str) -> str: 8 | if city in ITALY_CITIES: 9 | return "Trattoria Viafore" 10 | if city in GERMAN_CITIES: 11 | return "Pat's Kantine" 12 | if city in US_CITIES: 13 | return "Pat's Place" 14 | return None 15 | 16 | 17 | if get_restaurant_name('Boston'): 18 | print("Location Found!") 19 | -------------------------------------------------------------------------------- /code_examples/chapter3/invalid/invalid_type.py: -------------------------------------------------------------------------------- 1 | a: int = 5 2 | a = "string" 3 | -------------------------------------------------------------------------------- /code_examples/chapter3/run_tests.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash -eu 2 | python code_examples/chapter3/find_workers.py 3 | python code_examples/chapter3/variable_annotation.py 4 | python code_examples/chapter3/invalid/invalid_example1.py 5 | python code_examples/chapter3/invalid/invalid_example2.py 6 | python code_examples/chapter3/invalid/invalid_example3.py 7 | python code_examples/chapter3/invalid/invalid_type.py 8 | 9 | mypy code_examples/chapter3/*.py 10 | 11 | for f in code_examples/chapter3/invalid/* 12 | do 13 | ! mypy "$f" 2>/dev/null 14 | done 15 | 16 | echo "All Chapter 3 Tests Passed" 17 | -------------------------------------------------------------------------------- /code_examples/chapter3/variable_annotation.py: -------------------------------------------------------------------------------- 1 | from find_workers import find_workers_available_for_time 2 | import datetime 3 | from typing import List 4 | 5 | def get_ratio(*args): 6 | return 3.0 7 | 8 | class Worker: 9 | pass 10 | 11 | open_time = datetime.datetime.now() 12 | workers: list[str] = find_workers_available_for_time(open_time) 13 | numbers: list[int] = [] 14 | ratio: float = get_ratio(5,3) 15 | 16 | number: int = 0 17 | text: str = "useless" 18 | values: list[float] = [1.2, 3.4, 6.0] 19 | worker: Worker = Worker() 20 | -------------------------------------------------------------------------------- /code_examples/chapter4/create_hot_dog.py: -------------------------------------------------------------------------------- 1 | def dispense_bun(): 2 | return Bun() 3 | 4 | class HotDog: 5 | 6 | def add_condiments(self, *args): 7 | pass 8 | 9 | class Bun: 10 | def add_frank(self, frank: str) -> HotDog: 11 | return HotDog() 12 | 13 | def dispense_ketchup(): 14 | return None 15 | 16 | def dispense_mustard(): 17 | return None 18 | 19 | def dispense_frank() -> str: 20 | return "frank" 21 | 22 | def dispense_hot_dog_to_customer(hot_dog: HotDog): 23 | pass 24 | 25 | def create_hot_dog(): 26 | bun = dispense_bun() 27 | frank = dispense_frank() 28 | hot_dog = bun.add_frank(frank) 29 | ketchup = dispense_ketchup() 30 | mustard = dispense_mustard() 31 | hot_dog.add_condiments(ketchup, mustard) 32 | dispense_hot_dog_to_customer(hot_dog) 33 | -------------------------------------------------------------------------------- /code_examples/chapter4/create_hot_dog_defensive.py: -------------------------------------------------------------------------------- 1 | def dispense_bun(): 2 | return Bun() 3 | 4 | class HotDog: 5 | 6 | def add_condiments(self, *args): 7 | pass 8 | 9 | class Bun: 10 | def add_frank(self, frank: str) -> HotDog: 11 | return HotDog() 12 | 13 | def dispense_ketchup(): 14 | return None 15 | 16 | def dispense_mustard(): 17 | return None 18 | 19 | def dispense_frank() -> str: 20 | return "frank" 21 | 22 | def dispense_hot_dog_to_customer(hot_dog: HotDog): 23 | pass 24 | 25 | def create_hot_dog(): 26 | bun = dispense_bun() 27 | if bun is None: 28 | print_error_code("Bun unavailable. Check for bun") 29 | return 30 | 31 | frank = dispense_frank() 32 | if frank is None: 33 | print_error_code("Frank was not properly dispensed") 34 | return 35 | 36 | hot_dog = bun.add_frank(frank) 37 | if hot_dog is None: 38 | print_error_code("Hot Dog unavailable. Check for Hot Dog") 39 | return 40 | 41 | ketchup = dispense_ketchup() 42 | mustard = dispense_mustard() 43 | if ketchup is None or mustard is None: 44 | print_error_code("Check for invalid catsup") 45 | return 46 | 47 | hot_dog.add_condiments(ketchup, mustard) 48 | dispense_hot_dog_to_customer(hot_dog) 49 | -------------------------------------------------------------------------------- /code_examples/chapter4/create_hot_dog_union.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Union 2 | 3 | class HotDog: 4 | pass 5 | 6 | class Pretzel: 7 | pass 8 | 9 | def dispense_hot_dog() -> HotDog: 10 | return HotDog() 11 | 12 | def dispense_pretzel() -> Pretzel: 13 | return Pretzel() 14 | 15 | from typing import Union 16 | def dispense_snack(user_input: str) -> Union[HotDog, Pretzel]: 17 | if user_input == "Hot Dog": 18 | return dispense_hot_dog() 19 | elif user_input == "Pretzel": 20 | return dispense_pretzel() 21 | raise RuntimeError("Should never reach this code, as an invalid input has been") 22 | -------------------------------------------------------------------------------- /code_examples/chapter4/invalid/dispense_bun.py: -------------------------------------------------------------------------------- 1 | 2 | class Bun: 3 | def __init__(self, args): 4 | pass 5 | 6 | def are_buns_available(): 7 | return False 8 | 9 | 10 | def dispense_bun() -> Bun: 11 | if not are_buns_available(): 12 | return None 13 | return Bun('Wheat') 14 | 15 | 16 | assert dispense_bun() is None 17 | -------------------------------------------------------------------------------- /code_examples/chapter4/invalid/final.py: -------------------------------------------------------------------------------- 1 | from typing import Final 2 | 3 | x: Final[int] = 5 4 | x = 3 5 | -------------------------------------------------------------------------------- /code_examples/chapter4/invalid/hotdog_invalid.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | class HotDog: 4 | def add_condiments(self, *args): 5 | return None 6 | class Bun: 7 | def add_frank(self, *args) -> HotDog: 8 | return HotDog() 9 | 10 | def dispense_frank() -> str: 11 | return "frank" 12 | 13 | def dispense_hot_dog(): 14 | return HotDog() 15 | 16 | 17 | def dispense_ketchup(): 18 | return "Ketchup" 19 | 20 | def dispense_mustard(): 21 | return "Mustard" 22 | 23 | def dispense_bun() -> Optional[Bun]: 24 | if False: 25 | return None 26 | return Bun() 27 | 28 | def dispense_hot_dog_to_customer(hot_dog): 29 | pass 30 | 31 | def create_hot_dog(): 32 | bun = dispense_bun() 33 | if bun is None: 34 | print_error_code("Bun could not be dispensed") 35 | return 36 | 37 | frank = dispense_frank() 38 | hot_dog = bun.add_frank(frank) 39 | ketchup = dispense_ketchup() 40 | mustard = dispense_mustard() 41 | hot_dog.add_condiments(ketchup, mustard) 42 | dispense_hot_dog_to_customer(hot_dog) 43 | 44 | 45 | create_hot_dog() 46 | -------------------------------------------------------------------------------- /code_examples/chapter4/invalid/literals.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Literal 3 | 4 | @dataclass 5 | class Error: 6 | error_code: Literal[1,2,3,4,5] 7 | disposed_of: bool 8 | 9 | @dataclass 10 | class Snack: 11 | name: Literal["Pretzel", "Hot Dog"] 12 | condiments: set[Literal["Mustard", "Ketchup"]] 13 | 14 | Error(0, False) 15 | Snack("Not Valid", set()) 16 | Snack("Pretzel", {"Mustard", "Relish"}) 17 | -------------------------------------------------------------------------------- /code_examples/chapter4/invalid/newtype.py: -------------------------------------------------------------------------------- 1 | from typing import NewType 2 | 3 | class Bun: 4 | def add_frank(frank: str): 5 | pass 6 | 7 | def dispense_bun() -> Bun: 8 | return Bun() 9 | 10 | 11 | def dispense_ketchup(): 12 | return None 13 | 14 | def dispense_mustard(): 15 | return None 16 | 17 | class HotDog: 18 | 19 | def add_condiments(self, *args): 20 | pass 21 | 22 | def dispense_hot_dog_to_customer(hot_dog: HotDog): 23 | pass 24 | 25 | def dispense_frank() -> str: 26 | return "Frank" 27 | 28 | def serve_to_customer(*args): 29 | pass 30 | 31 | ReadyToServeHotDog = NewType("ReadyToServeHotDog", HotDog) 32 | def create_hot_dog(): 33 | bun = dispense_bun() 34 | frank = dispense_frank() 35 | hot_dog = bun.add_frank(frank) 36 | ketchup = dispense_ketchup() 37 | mustard = dispense_mustard() 38 | hot_dog.add_condiments(ketchup, mustard) 39 | dispense_hot_dog_to_customer(hot_dog) 40 | 41 | def make_snack(): 42 | serve_to_customer(ReadyToServeHotDog(HotDog())) 43 | 44 | make_snack() 45 | -------------------------------------------------------------------------------- /code_examples/chapter4/invalid/union_hotdog.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Union 2 | 3 | class HotDog: 4 | pass 5 | 6 | class Pretzel: 7 | pass 8 | 9 | def dispense_hot_dog() -> HotDog: 10 | return HotDog() 11 | 12 | def dispense_pretzel() -> Pretzel: 13 | return Pretzel() 14 | 15 | from typing import Union 16 | def dispense_snack(user_input: str) -> Union[HotDog, Pretzel]: 17 | if user_input == "Hot Dog": 18 | return dispense_hot_dog() 19 | elif user_input == "Pretzel": 20 | return dispense_pretzel() 21 | raise RuntimeError("Should never reach this code, as an invalid input has been") 22 | -------------------------------------------------------------------------------- /code_examples/chapter4/product_type.py: -------------------------------------------------------------------------------- 1 | 2 | from dataclasses import dataclass 3 | from typing import Set 4 | # If you aren't familiar with dataclasses, you'll learn more in chapter 10 5 | # but for now, treat this as four fields grouped together and what types they are 6 | @dataclass 7 | class Snack: 8 | name: str 9 | condiments: set[str] 10 | error_code: int 11 | disposed_of: bool 12 | 13 | 14 | Snack("Hotdog", {"Mustard", "Ketchup"}, 5, False) 15 | -------------------------------------------------------------------------------- /code_examples/chapter4/run_tests.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash -eu 2 | python code_examples/chapter4/create_hot_dog.py 3 | python code_examples/chapter4/create_hot_dog_defensive.py 4 | python code_examples/chapter4/create_hot_dog_union.py 5 | python code_examples/chapter4/product_type.py 6 | python code_examples/chapter4/sum_type.py 7 | python code_examples/chapter4/invalid/dispense_bun.py 8 | python code_examples/chapter4/invalid/hotdog_invalid.py 9 | python code_examples/chapter4/invalid/union_hotdog.py 10 | python code_examples/chapter4/invalid/literals.py 11 | python code_examples/chapter4/invalid/final.py 12 | python code_examples/chapter4/invalid/newtype.py 13 | 14 | mypy code_examples/chapter4/*.py 15 | 16 | for f in code_examples/chapter4/invalid/* 17 | do 18 | ! mypy "$f" 2>/dev/null 19 | done 20 | 21 | echo "All Chapter 4 Tests Passed" 22 | -------------------------------------------------------------------------------- /code_examples/chapter4/sum_type.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Union, Set 3 | @dataclass 4 | class Error: 5 | error_code: int 6 | disposed_of: bool 7 | 8 | @dataclass 9 | class Snack: 10 | name: str 11 | condiments: set[str] 12 | 13 | snack: Union[Snack, Error] = Snack("Hotdog", {"Mustard", "Ketchup"}) 14 | 15 | snack = Error(5, True) 16 | -------------------------------------------------------------------------------- /code_examples/chapter5/abc.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import collections.abc 3 | from typing import Set 4 | 5 | 6 | def get_nutrition_information(text): 7 | return "arugula" 8 | 9 | def get_aliases(text): 10 | if text == 'rocket': 11 | return ['arugula'] 12 | 13 | class AliasedIngredients(collections.abc.Set): 14 | def __init__(self, ingredients: set[str]): 15 | self.ingredients = ingredients 16 | 17 | def __contains__(self, value: str): 18 | return value in self.ingredients or any(alias in self.ingredients for alias in get_aliases(value)) 19 | 20 | def __iter__(self): 21 | return iter(self.ingredients) 22 | 23 | def __len__(self): 24 | return len(self.ingredients) 25 | 26 | ingredients = AliasedIngredients({'arugula', 'eggplant', 'pepper'}) 27 | assert ingredients == {'arugula', 'eggplant', 'pepper'} 28 | 29 | assert len(ingredients) == 3 30 | 31 | assert 'arugula' in ingredients 32 | assert 'rocket' in ingredients 33 | -------------------------------------------------------------------------------- /code_examples/chapter5/count_authors.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from dataclasses import dataclass 3 | 4 | @dataclass 5 | class Cookbook: 6 | author: str 7 | 8 | AuthorToCountMapping = dict[str, int] 9 | def create_author_count_mapping(cookbooks: list[Cookbook]) -> AuthorToCountMapping: 10 | counter: dict[str, int] = defaultdict(lambda: 0) 11 | for book in cookbooks: 12 | counter[book.author] += 1 13 | return counter 14 | 15 | assert {'Pat Viafore': 2, 'J Kenji Alt-Lopez': 1} == create_author_count_mapping([Cookbook('Pat Viafore'), Cookbook('Pat Viafore'), Cookbook('J Kenji Alt-Lopez')]) 16 | -------------------------------------------------------------------------------- /code_examples/chapter5/generic.py: -------------------------------------------------------------------------------- 1 | 2 | from typing import TypeVar,Union,List 3 | T = TypeVar("T") 4 | 5 | class NutritionInfo: 6 | pass 7 | 8 | class Ingredient: 9 | pass 10 | 11 | class Restaurant: 12 | pass 13 | 14 | class APIErrorResponse: 15 | pass 16 | 17 | APIResponse = Union[T, APIErrorResponse] 18 | 19 | def get_nutrition_info(recipe: str) -> APIResponse[NutritionInfo]: 20 | return APIErrorResponse() 21 | 22 | def get_ingredients(recipe: str) -> APIResponse[list[Ingredient]]: 23 | return [] 24 | 25 | def get_restaurants_serving(recipe: str) -> APIResponse[list[Restaurant]]: 26 | return [Restaurant()] 27 | -------------------------------------------------------------------------------- /code_examples/chapter5/graph.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from typing import Generic, NewType, TypeVar, Union 3 | 4 | Restaurant = NewType('Restaurant', str) 5 | Recipe = NewType('Recipe', str) 6 | 7 | 8 | Node = TypeVar("Node") 9 | Edge = TypeVar("Edge") 10 | 11 | class Graph(Generic[Node, Edge]): 12 | def __init__(self): 13 | 14 | self.edges: dict[Node, list[Edge]] = defaultdict(list) 15 | 16 | def add_relation(self, node: Node, to: Edge): 17 | self.edges[node].append(to) 18 | 19 | def get_relations(self, node: Node) -> list[Edge]: 20 | return self.edges[node] 21 | 22 | restaurants: Graph[Restaurant, Restaurant] = Graph() 23 | recipes: Graph[Recipe, Recipe] = Graph() 24 | 25 | restaurant_dishes: Graph[Restaurant, Recipe] = Graph() 26 | 27 | recipes.add_relation(Recipe('Pasta Bolognese'), 28 | Recipe('Pasta with Sausage and Basil')) 29 | 30 | restaurant_dishes.add_relation(Restaurant('Viafores'), 31 | Recipe('Pasta Bolognese')) 32 | -------------------------------------------------------------------------------- /code_examples/chapter5/invalid/graph.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from typing import Generic, NewType, TypeVar, Union 3 | from typing import Dict, List 4 | 5 | Restaurant = NewType('Restaurant', str) 6 | Recipe = NewType('Recipe', str) 7 | 8 | 9 | T = TypeVar("T") 10 | W = TypeVar("W") 11 | 12 | class Graph(Generic[T, W]): 13 | def __init__(self): 14 | 15 | self.edges: Edges = defaultdict(list) 16 | 17 | def add_relation(self, node: T, to: W): 18 | self.edges[node].append(to) 19 | 20 | def get_relations(self, node: T) -> list[W]: 21 | return self.edges[node] 22 | 23 | restaurants: Graph[Restaurant, Restaurant] = Graph() 24 | 25 | restaurants.add_relation(Recipe('Cheeseburger'), Recipe('Hamburger')) 26 | 27 | -------------------------------------------------------------------------------- /code_examples/chapter5/overriding_dict.py: -------------------------------------------------------------------------------- 1 | def get_nutrition_information(text): 2 | return "arugula" 3 | 4 | def get_aliases(text): 5 | if text == 'rocket': 6 | return ['arugula'] 7 | 8 | class NutritionalInformation(dict): 9 | def __getitem__(self, key): 10 | try: 11 | return super().__getitem__(key) 12 | except KeyError: 13 | pass 14 | for alias in get_aliases(key): 15 | try: 16 | return super().__getitem__(alias) 17 | except KeyError: 18 | pass 19 | raise KeyError(f"Could not find {key} or any of its aliases") 20 | 21 | nutrition = NutritionalInformation() 22 | nutrition["arugula"] = get_nutrition_information("arugula") 23 | assert nutrition["arugula"] == nutrition["rocket"] # arugula == rocket 24 | assert nutrition.get("rocket", "No Key Found") == "No Key Found" # Uh Oh 25 | -------------------------------------------------------------------------------- /code_examples/chapter5/print_items.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import collections.abc 3 | 4 | def print_items(items: collections.abc.Iterable): 5 | for item in items: 6 | print(item) 7 | 8 | print_items('abc') 9 | print_items([1,2,3,4]) 10 | print_items({5, 3, 4}) 11 | -------------------------------------------------------------------------------- /code_examples/chapter5/reverse.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar,List 2 | T = TypeVar('T') 3 | def reverse(coll: list[T]) -> list[T]: 4 | return coll[::-1] 5 | 6 | assert reverse([1,2,3]) == [3,2,1] 7 | assert reverse(['a','b','c']) ==['c','b','a'] 8 | -------------------------------------------------------------------------------- /code_examples/chapter5/run_tests.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash -eu 2 | python code_examples/chapter5/graph.py 3 | python code_examples/chapter5/count_authors.py 4 | python code_examples/chapter5/typeddict.py 5 | python code_examples/chapter5/reverse.py 6 | python code_examples/chapter5/generic.py 7 | python code_examples/chapter5/overriding_dict.py 8 | python code_examples/chapter5/userdict.py 9 | python code_examples/chapter5/abc.py 10 | 11 | 12 | mypy code_examples/chapter5/*.py 13 | 14 | 15 | for f in code_examples/chapter5/invalid/* 16 | do 17 | if mypy "$f" ; then 18 | echo "Expected $f to fail, but passed" 19 | exit 1; 20 | fi 21 | done 22 | 23 | 24 | echo "All Chapter 5 Tests Passed" 25 | -------------------------------------------------------------------------------- /code_examples/chapter5/typeddict.py: -------------------------------------------------------------------------------- 1 | from typing import TypedDict 2 | 3 | 4 | recipe_name = 'Pasta Bolognese' 5 | def get_nutrition_from_spoonacular(recipe: str): 6 | return RecipeNutritionInformation({ 7 | 'recipes_used': 1, 8 | "calories": { 9 | 'value': 1, 10 | 'unit': 'g', 11 | 'confidenceRange95Percent': { 12 | 'min': 1, 13 | 'max': 2 14 | }, 15 | 'standardDeviation': 3.5 16 | }, 17 | "fat": { 18 | 'value': 1, 19 | 'unit': 'g', 20 | 'confidenceRange95Percent': { 21 | 'min': 1, 22 | 'max': 2 23 | }, 24 | 'standardDeviation': 3.5 25 | }, 26 | "protein": { 27 | 'value': 1, 28 | 'unit': 'g', 29 | 'confidenceRange95Percent': { 30 | 'min': 1, 31 | 'max': 2 32 | }, 33 | 'standardDeviation': 3.5 34 | }, 35 | "carbs": { 36 | 'value': 1, 37 | 'unit': 'g', 38 | 'confidenceRange95Percent': { 39 | 'min': 1, 40 | 'max': 2 41 | }, 42 | 'standardDeviation': 3.5 43 | } 44 | }) 45 | 46 | class Range(TypedDict): 47 | min: float 48 | max: float 49 | 50 | class NutritionInformation(TypedDict): 51 | value: int 52 | unit: str 53 | confidenceRange95Percent: Range 54 | standardDeviation: float 55 | 56 | class RecipeNutritionInformation(TypedDict): 57 | recipes_used: int 58 | calories: NutritionInformation 59 | fat: NutritionInformation 60 | protein: NutritionInformation 61 | carbs: NutritionInformation 62 | 63 | nutrition_information:RecipeNutritionInformation = get_nutrition_from_spoonacular(recipe_name) 64 | -------------------------------------------------------------------------------- /code_examples/chapter5/userdict.py: -------------------------------------------------------------------------------- 1 | from collections import UserDict 2 | 3 | def get_nutrition_information(text): 4 | return "arugula" 5 | 6 | def get_aliases(text): 7 | if text == 'rocket': 8 | return ['arugula'] 9 | class NutritionalInformation(UserDict): 10 | def __getitem__(self, key): 11 | try: 12 | return self.data[key] 13 | except KeyError: 14 | pass 15 | for alias in get_aliases(key): 16 | try: 17 | return self.data[alias] 18 | except KeyError: 19 | pass 20 | raise KeyError(f"Could not find {key} or any of its aliases") 21 | 22 | nutrition = NutritionalInformation() 23 | nutrition["arugula"] = get_nutrition_information("arugula") 24 | assert nutrition["arugula"] == nutrition["rocket"] # arugula == rocket 25 | assert nutrition.get("rocket", "No Key Found") == nutrition['arugula'] 26 | -------------------------------------------------------------------------------- /code_examples/chapter6/.pyre_configuration: -------------------------------------------------------------------------------- 1 | { 2 | "binary": "/home/pat/.pyenv/versions/3.9.0/bin/pyre.bin", 3 | "source_directories": [ 4 | "." 5 | ], 6 | "taint_models_path": ["stubs/taint"] 7 | } 8 | -------------------------------------------------------------------------------- /code_examples/chapter6/insecure.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | def create_recipe_on_disk(recipe): 4 | command = "touch ~/food_data/{}.json".format(recipe) 5 | return os.system(command) 6 | 7 | def create_recipe(): 8 | recipe = input("Enter in recipe") 9 | create_recipe_on_disk(recipe) 10 | -------------------------------------------------------------------------------- /code_examples/chapter6/stubs/taint/general.pysa: -------------------------------------------------------------------------------- 1 | # model for raw_input 2 | def input(__prompt = ...) -> TaintSource[UserControlled]: ... 3 | 4 | # model for os.system 5 | def os.system(command: TaintSink[RemoteCodeExecution]): ... 6 | 7 | -------------------------------------------------------------------------------- /code_examples/chapter6/stubs/taint/taint.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | { 4 | sources: [ 5 | { 6 | name: "UserControlled", 7 | comment: "use to annotate user input" 8 | } 9 | ], 10 | 11 | sinks: [ 12 | { 13 | name: "RemoteCodeExecution", 14 | comment: "use to annotate execution of code" 15 | } 16 | ], 17 | 18 | features: [], 19 | 20 | rules: [ 21 | { 22 | name: "Possible shell injection", 23 | code: 5001, 24 | sources: [ "UserControlled" ], 25 | sinks: [ "RemoteCodeExecution" ], 26 | message_format: "Data from [{$sources}] source(s) may reach [{$sinks}] sink(s)" 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /code_examples/chapter7/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pviafore/RobustPython/dafb95d801dff2c8ff7856ba46d3c052d54e0033/code_examples/chapter7/__init__.py -------------------------------------------------------------------------------- /code_examples/chapter7/automated_recipe_maker.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | # specifically using IntEnum for legacy reasons 4 | # in new code, Enum would be better 5 | class HeatLevel(IntEnum): 6 | LOW = 1 7 | MEDIUM_LOW = 3 8 | MEDIUM = 5 9 | MEDIUM_HIGH = 7 10 | HIGH = 10 11 | 12 | class Ingredient: 13 | 14 | def __init__(self, name, amount, units): 15 | self.name = name 16 | self.amount = amount 17 | self.units = units 18 | 19 | def adjust_proportion(self, factor): 20 | self.amount *= factor 21 | 22 | 23 | # These funcitons are "fake" instructions, they don't actually control a robot, but 24 | # will print out what they are doing. 25 | def put_receptacle_on_stovetop(receptacle, heat_level): 26 | print(f"Putting {receptacle.name} on {heat_level}") 27 | 28 | def add_ingredient(receptacle, ingredient): 29 | print(f"Adding {ingredient.amount} {ingredient.units} of {ingredient.name} to {receptacle.name}") 30 | 31 | def brown_on_all_sides(ingredient_name): 32 | print(f"Browning {ingredient_name} on all sides") 33 | 34 | def slice(ingredient, *ingredients, thickness_in_inches, to_ignore=[]): 35 | names = [ing.name for ing in ([ingredient] + list(ingredients)) if ing.name not in to_ignore] 36 | print(f"Slicing {','.join(names)} in {thickness_in_inches} inches") 37 | return Ingredient('Sliced '+','.join(names), 1, 'group') 38 | 39 | def dice(ingredient, *ingredients, to_ignore=[]): 40 | names = [ing.name for ing in ([ingredient] + list(ingredients)) if ing.name not in to_ignore] 41 | print(f"Dicing {','.join(names)}") 42 | return Ingredient('Diced '+','.join(names), 1, 'group') 43 | 44 | def is_not_cooked(_): 45 | return False 46 | 47 | def chiffonade(ingredient): 48 | print(f"Slicing {ingredient.name} into ribbons") 49 | return Ingredient('Chiffonade ' + ingredient.name, ingredient.amount, ingredient.units) 50 | 51 | def set_stir_mode(receptacle, frequency): 52 | print(f"Stirring {receptacle.name} {frequency}") 53 | 54 | 55 | class Dish: 56 | pass 57 | 58 | def divide(receptacle, servings): 59 | print(f"Dividing contents of {receptacle.name} into {servings} servings") 60 | return [Dish() for _ in range(servings)] 61 | 62 | def grate(ingredient): 63 | print(f"Grating {ingredient.name}") 64 | return ingredient 65 | 66 | def garnish(dishes, ingredient): 67 | print(f"Garnishing each dish with {ingredient.amount / len(dishes)} {ingredient.units} of {ingredient.name}") 68 | -------------------------------------------------------------------------------- /code_examples/chapter7/main.py: -------------------------------------------------------------------------------- 1 | from code_examples.chapter7.pasta_with_sausage import make_pasta_with_sausage 2 | 3 | make_pasta_with_sausage(3) 4 | -------------------------------------------------------------------------------- /code_examples/chapter7/pasta_with_sausage.py: -------------------------------------------------------------------------------- 1 | from fractions import Fraction 2 | import time 3 | 4 | import code_examples.chapter7.automated_recipe_maker as recipe_maker 5 | from code_examples.chapter7.automated_recipe_maker import Ingredient 6 | 7 | 8 | # I am purposely not using dataclasses here in order to 9 | # simulate a more "legacy" codebase. If new code, these should be dataclasses 10 | class Recipe: 11 | 12 | def __init__(self, servings, ingredients): 13 | self.servings = servings 14 | self.ingredients = ingredients 15 | 16 | def clear_ingredients(self): 17 | self.ingredients.clear() 18 | 19 | def get_ingredient(self, name): 20 | return next(i for i in self.ingredients if i.name == name) 21 | 22 | # Take a meal recipe and change the number of servings 23 | # recipe is a Recipe class 24 | def adjust_recipe(recipe, servings): 25 | new_ingredients = list(recipe.ingredients) 26 | recipe.clear_ingredients() 27 | for ingredient in new_ingredients: 28 | ingredient.adjust_proportion(Fraction(servings, recipe.servings)) 29 | return Recipe(servings, new_ingredients) 30 | 31 | 32 | # Pasta with Sasuage Automated Maker 33 | italian_sausage = Ingredient('Italian Sausage', 4, 'links') 34 | olive_oil = Ingredient('Olive Oil', 1, 'tablespoon') 35 | plum_tomato = Ingredient('Plum Tomato', 6, '') 36 | garlic = Ingredient('Garlic', 4, 'cloves') 37 | black_pepper = Ingredient('Black Pepper', 2, 'teaspoons') 38 | basil = Ingredient('Basil Leaves', 1, 'cup') 39 | pasta = Ingredient('Rigatoni', 1, 'pound') 40 | salt = Ingredient('Salt', 1, 'tablespoon') 41 | water = Ingredient('Water', 6, 'quarts') 42 | cheese = Ingredient('Pecorino Romano', 2, "ounces") 43 | pasta_with_sausage = Recipe(6, [italian_sausage, 44 | olive_oil, 45 | plum_tomato, 46 | garlic, 47 | black_pepper, 48 | pasta, 49 | salt, 50 | water, 51 | cheese, 52 | basil]) 53 | 54 | class Receptacle: 55 | ''' 56 | A class that stores various ingredients 57 | ''' 58 | 59 | def __init__(self, name): 60 | self.name = name 61 | self.ingredients = [] 62 | 63 | def add(self, ingredient): 64 | self.ingredients.append(ingredient) 65 | recipe_maker.add_ingredient(self, ingredient) 66 | 67 | def remove_ingredients(self, to_ignore=[]): 68 | names = [ing.name for ing in self.ingredients if ing.name not in to_ignore] 69 | self.ingredients.clear() 70 | return Ingredient('/'.join(names), 1, 'Mixture') 71 | 72 | def make_pasta_with_sausage(servings): # <2> 73 | sauté_pan = Receptacle('Sauté Pan') 74 | pasta_pot = Receptacle('Stock Pot') 75 | adjusted_recipe = adjust_recipe(pasta_with_sausage, servings) 76 | 77 | print("Prepping ingredients") # <3> 78 | adjusted_tomatoes = adjusted_recipe.get_ingredient('Plum Tomato') 79 | adjusted_garlic = adjusted_recipe.get_ingredient('Garlic') 80 | adjusted_cheese = adjusted_recipe.get_ingredient('Pecorino Romano') 81 | adjusted_basil = adjusted_recipe.get_ingredient('Basil Leaves') 82 | 83 | garlic_and_tomatoes = recipe_maker.dice(adjusted_tomatoes, 84 | adjusted_garlic) 85 | grated_cheese = recipe_maker.grate(adjusted_cheese) 86 | sliced_basil = recipe_maker.chiffonade(adjusted_basil) 87 | 88 | print("Cooking Pasta") # <4> 89 | pasta_pot.add(adjusted_recipe.get_ingredient('Water')) 90 | pasta_pot.add(adjusted_recipe.get_ingredient('Salt')) 91 | recipe_maker.put_receptacle_on_stovetop(pasta_pot, heat_level=10) 92 | pasta_pot.add(adjusted_recipe.get_ingredient('Rigatoni')) 93 | recipe_maker.set_stir_mode(pasta_pot, ('every minute')) 94 | 95 | print("Cooking Sausage") 96 | sauté_pan.add(adjusted_recipe.get_ingredient('Olive Oil')) 97 | heat_level = recipe_maker.HeatLevel.MEDIUM 98 | recipe_maker.put_receptacle_on_stovetop(sauté_pan, heat_level) 99 | sauté_pan.add(adjusted_recipe.get_ingredient('Italian Sausage')) 100 | recipe_maker.brown_on_all_sides('Italian Sausage') 101 | cooked_sausage = sauté_pan.remove_ingredients(to_ignore=['Olive Oil']) 102 | 103 | sliced_sausage = recipe_maker.slice(cooked_sausage, thickness_in_inches=.25) 104 | 105 | print("Making Sauce") 106 | sauté_pan.add(garlic_and_tomatoes) 107 | recipe_maker.set_stir_mode(sauté_pan, ('every minute')) 108 | while recipe_maker.is_not_cooked('Rigatoni'): 109 | time.sleep(30) 110 | cooked_pasta = pasta_pot.remove_ingredients(to_ignore=['Water', 'Salt']) 111 | 112 | sauté_pan.add(sliced_sausage) 113 | while recipe_maker.is_not_cooked('Italian Sausage'): 114 | time.sleep(30) 115 | 116 | print("Mixing ingredients together") 117 | sauté_pan.add(sliced_basil) 118 | sauté_pan.add(cooked_pasta) 119 | recipe_maker.set_stir_mode(sauté_pan, "once") 120 | 121 | print("Serving") # <5> 122 | dishes = recipe_maker.divide(sauté_pan, servings) 123 | 124 | recipe_maker.garnish(dishes, grated_cheese) 125 | return dishes 126 | -------------------------------------------------------------------------------- /code_examples/chapter7/run_tests.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash -eu 2 | PYTHONPATH=. python code_examples/chapter7/main.py 3 | 4 | 5 | mypy code_examples/chapter7/*.py 6 | monkeytype run code_examples/chapter7/main.py 7 | 8 | 9 | echo "All Chapter 7 Tests Passed" 10 | -------------------------------------------------------------------------------- /code_examples/chapter8/allergen.py: -------------------------------------------------------------------------------- 1 | from enum import auto, Enum 2 | from typing import Set 3 | class Allergen(Enum): 4 | FISH = auto() 5 | SHELLFISH = auto() 6 | TREE_NUTS = auto() 7 | PEANUTS = auto() 8 | GLUTEN = auto() 9 | SOY = auto() 10 | DAIRY = auto() 11 | 12 | 13 | allergens: set[Allergen] = {Allergen.FISH, Allergen.SOY} 14 | -------------------------------------------------------------------------------- /code_examples/chapter8/allergen_flag.py: -------------------------------------------------------------------------------- 1 | from enum import auto, Flag 2 | 3 | class Allergen(Flag): 4 | FISH = auto() 5 | SHELLFISH = auto() 6 | TREE_NUTS = auto() 7 | PEANUTS = auto() 8 | GLUTEN = auto() 9 | SOY = auto() 10 | DAIRY = auto() 11 | SEAFOOD = FISH | SHELLFISH 12 | ALL_NUTS = TREE_NUTS | PEANUTS 13 | 14 | allergens = Allergen.FISH | Allergen.SHELLFISH 15 | 16 | assert repr(allergens) == "" 17 | assert allergens & Allergen.FISH 18 | -------------------------------------------------------------------------------- /code_examples/chapter8/auto_enum.py: -------------------------------------------------------------------------------- 1 | from enum import auto, Enum 2 | class MotherSauce(Enum): 3 | BÉCHAMEL = auto() 4 | VELOUTÉ = auto() 5 | ESPAGNOLE = auto() 6 | TOMATO = auto() 7 | HOLLANDAISE = auto() 8 | 9 | assert repr(list(MotherSauce)) =="[, , , , ]" 10 | -------------------------------------------------------------------------------- /code_examples/chapter8/auto_enum_generate.py: -------------------------------------------------------------------------------- 1 | from enum import auto, Enum 2 | class MotherSauce(Enum): 3 | 4 | def _generate_next_value_(name: str, start, count, last_values): # type: ignore 5 | return name.capitalize() 6 | 7 | BÉCHAMEL = auto() 8 | VELOUTÉ = auto() 9 | ESPAGNOLE = auto() 10 | TOMATO = auto() 11 | HOLLANDAISE = auto() 12 | 13 | assert repr(list(MotherSauce)) =="[, , , , ]" 14 | -------------------------------------------------------------------------------- /code_examples/chapter8/liquid_measure.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | class ImperialLiquidMeasure(Enum): 3 | CUP = 8 4 | PINT = 16 5 | QUART = 32 6 | GALLON = 128 7 | 8 | assert ImperialLiquidMeasure.CUP != 8 9 | -------------------------------------------------------------------------------- /code_examples/chapter8/liquid_measure_intenum.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | class ImperialLiquidMeasure(IntEnum): 3 | CUP = 8 4 | PINT = 16 5 | QUART = 32 6 | GALLON = 128 7 | 8 | assert ImperialLiquidMeasure.CUP == 8 9 | assert ImperialLiquidMeasure.CUP.value == 8 10 | 11 | class Kitchenware(IntEnum): 12 | # Note to future programmers: these numbers are customer-defined 13 | # and apt to change 14 | PLATE = 7 15 | CUP = 8 16 | UTENSILS = 9 17 | 18 | def pour_into_larger_vessel(): 19 | pass 20 | def pour_into_smaller_vessel(): 21 | pass 22 | 23 | def pour_liquid(volume: ImperialLiquidMeasure): 24 | if volume == Kitchenware.CUP: 25 | pour_into_smaller_vessel() 26 | else: 27 | pour_into_larger_vessel() 28 | 29 | pour_liquid(ImperialLiquidMeasure.CUP) 30 | -------------------------------------------------------------------------------- /code_examples/chapter8/mother_sauces.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from enum import Enum 4 | class MotherSauce(Enum): 5 | BÉCHAMEL = "Béchamel" 6 | VELOUTÉ = "Velouté" 7 | ESPAGNOLE = "Espagnole" 8 | TOMATO = "Tomato" 9 | HOLLANDAISE = "Hollandaise" 10 | 11 | 12 | MotherSauce.BÉCHAMEL 13 | MotherSauce.HOLLANDAISE 14 | 15 | 16 | 17 | MotherSauce("Hollandaise") # OKAY 18 | 19 | try: 20 | MotherSauce("Alabama White BBQ Sauce") 21 | assert False, "Should not consider BBQ sauce a mother sauce" 22 | except: 23 | pass 24 | 25 | print(list(enumerate(MotherSauce, start=1))) 26 | assert list(enumerate(MotherSauce, start=1)) == [(1, MotherSauce.BÉCHAMEL), (2, MotherSauce.VELOUTÉ), (3, MotherSauce.ESPAGNOLE), 27 | (4, MotherSauce.TOMATO), (5,MotherSauce.HOLLANDAISE)] 28 | def create_daughter_sauce(mother_sauce: MotherSauce, 29 | extra_ingredients: list[str]): 30 | pass 31 | 32 | create_daughter_sauce(MotherSauce.TOMATO, []) 33 | -------------------------------------------------------------------------------- /code_examples/chapter8/mother_sauces_alias.py: -------------------------------------------------------------------------------- 1 | 2 | from enum import Enum 3 | class MotherSauce(Enum): 4 | BÉCHAMEL = "Béchamel" 5 | BECHAMEL = "Béchamel" 6 | VELOUTÉ = "Velouté" 7 | VELOUTE = "Velouté" 8 | ESPAGNOLE = "Espagnole" 9 | TOMATO = "Tomato" 10 | HOLLANDAISE = "Hollandaise" 11 | -------------------------------------------------------------------------------- /code_examples/chapter8/mother_sauces_bad.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | BÉCHAMEL = "Béchamel" 3 | VELOUTÉ = "Velouté" 4 | ESPAGNOLE = "Espagnole" 5 | TOMATO = "Tomato" 6 | HOLLANDAISE = "Hollandaise" 7 | MOTHER_SAUCES = (BÉCHAMEL, VELOUTÉ, ESPAGNOLE, TOMATO, HOLLANDAISE) 8 | 9 | assert MOTHER_SAUCES[2] == "Espagnole" 10 | 11 | def create_daughter_sauce(mother_sauce: str, 12 | extra_ingredients: list[str]): 13 | pass 14 | 15 | create_daughter_sauce(MOTHER_SAUCES[0], ["Onions"]) # not super helpful 16 | create_daughter_sauce(BÉCHAMEL, ["Onions"]) # Better 17 | create_daughter_sauce("Hollandaise", ["Horsedradish"]) 18 | create_daughter_sauce("Veloute", ["Mustard"]) 19 | # Definitely wrong 20 | create_daughter_sauce("Alabama White BBQ Sauce", []) 21 | -------------------------------------------------------------------------------- /code_examples/chapter8/mother_sauces_unique.py: -------------------------------------------------------------------------------- 1 | 2 | from enum import Enum, unique 3 | @unique 4 | class MotherSauce(Enum): 5 | BÉCHAMEL = "Béchamel" 6 | VELOUTÉ = "Velouté" 7 | ESPAGNOLE = "Espagnole" 8 | TOMATO = "Tomato" 9 | HOLLANDAISE = "Hollandaise" 10 | -------------------------------------------------------------------------------- /code_examples/chapter8/run_tests.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash -eu 2 | python code_examples/chapter8/mother_sauces_bad.py 3 | python code_examples/chapter8/mother_sauces.py 4 | python code_examples/chapter8/auto_enum.py 5 | python code_examples/chapter8/auto_enum_generate.py 6 | python code_examples/chapter8/allergen.py 7 | python code_examples/chapter8/allergen_flag.py 8 | python code_examples/chapter8/liquid_measure.py 9 | python code_examples/chapter8/liquid_measure_intenum.py 10 | python code_examples/chapter8/mother_sauces_alias.py 11 | python code_examples/chapter8/mother_sauces_unique.py 12 | 13 | mypy code_examples/chapter8/*.py 14 | 15 | echo "All Chapter 8 Tests Passed" 16 | -------------------------------------------------------------------------------- /code_examples/chapter9/fraction.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | @dataclass 3 | class MyFraction: 4 | numerator: int = 0 5 | denominator: int = 1 6 | -------------------------------------------------------------------------------- /code_examples/chapter9/frozen_recipe.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, FrozenInstanceError 2 | from typing import Set 3 | import datetime 4 | 5 | from enum import auto, Enum 6 | 7 | class ImperialMeasure(Enum): # <1> 8 | TEASPOON = auto() 9 | TABLESPOON = auto() 10 | CUP = auto() 11 | 12 | class Broth(Enum): # <2> 13 | VEGETABLE = auto() 14 | CHICKEN = auto() 15 | BEEF = auto() 16 | FISH = auto() 17 | 18 | @dataclass(frozen=True) # <3> 19 | class Ingredient: 20 | name: str 21 | amount: float = 1 22 | units: ImperialMeasure = ImperialMeasure.CUP 23 | 24 | @dataclass(frozen=True) 25 | class Recipe: 26 | aromatics: set[Ingredient] 27 | broth: Broth 28 | vegetables: set[Ingredient] 29 | meats: set[Ingredient] 30 | starches: set[Ingredient] 31 | garnishes: set[Ingredient] 32 | time_to_cook: datetime.timedelta 33 | 34 | pepper = Ingredient("Pepper", 1, ImperialMeasure.TABLESPOON) 35 | garlic = Ingredient("Garlic", 2, ImperialMeasure.TEASPOON) 36 | carrots = Ingredient("Carrots", .25, ImperialMeasure.CUP) 37 | celery = Ingredient("Celery", .25, ImperialMeasure.CUP) 38 | onions = Ingredient("Onions", .25, ImperialMeasure.CUP) 39 | parsley = Ingredient("Parsley", 2, ImperialMeasure.TABLESPOON) 40 | noodles = Ingredient("Noodles", 1.5, ImperialMeasure.CUP) 41 | chicken = Ingredient("Chicken", 1.5, ImperialMeasure.CUP) 42 | 43 | soup = Recipe( 44 | aromatics={pepper, garlic}, 45 | broth=Broth.CHICKEN, 46 | vegetables={celery, onions, carrots}, 47 | meats={chicken}, 48 | starches={noodles}, 49 | garnishes={parsley}, 50 | time_to_cook=datetime.timedelta(minutes=60)) 51 | 52 | try: 53 | # this is an error 54 | soup.broth = Broth.VEGETABLE # type: ignore 55 | assert False 56 | except FrozenInstanceError as e: 57 | pass 58 | 59 | # this is not an error 60 | soup = Recipe( 61 | aromatics=set(), 62 | broth=Broth.CHICKEN, 63 | vegetables=set(), 64 | meats=set(), 65 | starches=set(), 66 | garnishes=set(), 67 | time_to_cook=datetime.timedelta(seconds=3600)) 68 | 69 | soup.aromatics.add(Ingredient("Garlic")) 70 | -------------------------------------------------------------------------------- /code_examples/chapter9/namedtuple.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | NutritionInformation = namedtuple('NutritionInformation', ['calories', 'fat', 'carbohydrates']) 3 | nutrition = NutritionInformation(calories=100, fat=5, carbohydrates=10) 4 | assert nutrition.calories == 100 5 | -------------------------------------------------------------------------------- /code_examples/chapter9/nutritional_info.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | @dataclass(eq=True) 3 | class NutritionInformation: #type: ignore 4 | calories: int 5 | fat: int 6 | carbohydrates: int 7 | 8 | nutritionals = [NutritionInformation(calories=100, fat=1, carbohydrates=3), 9 | NutritionInformation(calories=50, fat=6, carbohydrates=4), 10 | NutritionInformation(calories=125, fat=12, carbohydrates=3)] 11 | 12 | try: 13 | sorted(nutritionals) # type: ignore 14 | assert False 15 | except TypeError as e: 16 | pass 17 | 18 | 19 | @dataclass(eq=True, order=True) 20 | class NutritionInformation: # type: ignore 21 | calories: int 22 | fat: int 23 | carbohydrates: int 24 | 25 | nutritionals = [NutritionInformation(calories=100, fat=1, carbohydrates=3), 26 | NutritionInformation(calories=50, fat=6, carbohydrates=4), 27 | NutritionInformation(calories=125, fat=12, carbohydrates=3)] 28 | 29 | assert sorted(nutritionals) == [NutritionInformation(calories=50, fat=6, carbohydrates=4), # type: ignore 30 | NutritionInformation(calories=100, fat=1, carbohydrates=3), 31 | NutritionInformation(calories=125, fat=12, carbohydrates=3)] 32 | 33 | 34 | @dataclass(eq=True) 35 | class NutritionInformation: # type: ignore 36 | calories: int 37 | fat: int 38 | carbohydrates: int 39 | 40 | def __lt__(self, rhs) -> bool: 41 | return ((self.fat, self.carbohydrates, self.calories) < 42 | (rhs.fat, rhs.carbohydrates, rhs.calories)) 43 | 44 | def __le__(self, rhs) -> bool: 45 | return self < rhs or self == rhs 46 | 47 | def __gt__(self, rhs) -> bool: 48 | return not self <= rhs 49 | 50 | def __ge__(self, rhs) -> bool: 51 | return not self < rhs 52 | 53 | nutritionals = [NutritionInformation(calories=100, fat=1, carbohydrates=3), 54 | NutritionInformation(calories=50, fat=6, carbohydrates=4), 55 | NutritionInformation(calories=125, fat=12, carbohydrates=3)] 56 | assert sorted(nutritionals) ==[NutritionInformation(calories=100, fat=1, carbohydrates=3), NutritionInformation(calories=50, fat=6, carbohydrates=4), NutritionInformation(calories=125, fat=12, carbohydrates=3)] # type: ignore 57 | -------------------------------------------------------------------------------- /code_examples/chapter9/recipe.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from dataclasses import dataclass 3 | from enum import auto, Enum 4 | from typing import Set 5 | 6 | class ImperialMeasure(Enum): # <1> 7 | TEASPOON = auto() 8 | TABLESPOON = auto() 9 | CUP = auto() 10 | 11 | class Broth(Enum): # <2> 12 | VEGETABLE = auto() 13 | CHICKEN = auto() 14 | BEEF = auto() 15 | FISH = auto() 16 | 17 | @dataclass(frozen=True) # <3> 18 | class Ingredient: 19 | name: str 20 | amount: float = 1 21 | units: ImperialMeasure = ImperialMeasure.CUP 22 | 23 | @dataclass(eq=True) 24 | class Recipe: # <4> 25 | aromatics: set[Ingredient] 26 | broth: Broth 27 | vegetables: set[Ingredient] 28 | meats: set[Ingredient] 29 | starches: set[Ingredient] 30 | garnishes: set[Ingredient] 31 | time_to_cook: datetime.timedelta 32 | 33 | def make_vegetarian(self): 34 | self.meats.clear() 35 | self.broth = Broth.VEGETABLE 36 | 37 | def get_ingredient_names(self): 38 | ingredients = (self.aromatics | 39 | self.vegetables | 40 | self.meats | 41 | self.starches | 42 | self.garnishes) 43 | 44 | return ({i.name for i in ingredients} | 45 | {self.broth.name.capitalize() + " Broth"}) 46 | 47 | pepper = Ingredient("Pepper", 1, ImperialMeasure.TABLESPOON) 48 | garlic = Ingredient("Garlic", 2, ImperialMeasure.TEASPOON) 49 | carrots = Ingredient("Carrots", .25, ImperialMeasure.CUP) 50 | celery = Ingredient("Celery", .25, ImperialMeasure.CUP) 51 | onions = Ingredient("Onions", .25, ImperialMeasure.CUP) 52 | parsley = Ingredient("Parsley", 2, ImperialMeasure.TABLESPOON) 53 | noodles = Ingredient("Noodles", 1.5, ImperialMeasure.CUP) 54 | chicken = Ingredient("Chicken", 1.5, ImperialMeasure.CUP) 55 | 56 | chicken_noodle_soup = Recipe( 57 | aromatics={pepper, garlic}, 58 | broth=Broth.CHICKEN, 59 | vegetables={celery, onions, carrots}, 60 | meats={chicken}, 61 | starches={noodles}, 62 | garnishes={parsley}, 63 | time_to_cook=datetime.timedelta(minutes=60)) 64 | 65 | assert chicken_noodle_soup.broth == Broth.CHICKEN 66 | chicken_noodle_soup.garnishes.add(pepper) 67 | assert chicken_noodle_soup.garnishes == {parsley, pepper} 68 | 69 | from copy import deepcopy 70 | noodle_soup = deepcopy(chicken_noodle_soup) 71 | noodle_soup.make_vegetarian() 72 | assert noodle_soup.get_ingredient_names() == {'Garlic', 'Pepper', 'Carrots', 'Celery', 'Onions', 'Noodles', 'Parsley', 'Vegetable Broth'} 73 | 74 | 75 | assert noodle_soup == noodle_soup 76 | assert chicken_noodle_soup != noodle_soup 77 | -------------------------------------------------------------------------------- /code_examples/chapter9/run_tests.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash -eu 2 | PYTHONPATH=. python code_examples/chapter9/fraction.py 3 | PYTHONPATH=. python code_examples/chapter9/recipe.py 4 | PYTHONPATH=. python code_examples/chapter9/nutritional_info.py 5 | PYTHONPATH=. python code_examples/chapter9/frozen_recipe.py 6 | PYTHONPATH=. python code_examples/chapter9/namedtuple.py 7 | 8 | mypy code_examples/chapter9/*.py 9 | 10 | echo "All Chapter 9 Tests Passed" 11 | -------------------------------------------------------------------------------- /code_examples/plugin/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='ultimate-kitchen-assistant', 5 | version='1.0', 6 | 7 | description='Ultimate Kitchen Assistant', 8 | 9 | author='Pat Viafore', 10 | author_email='pat@kudzera.com', 11 | packages=["ultimate_kitchen_assistant"], 12 | include_package_data=True, 13 | 14 | entry_points={ 15 | 'ultimate_kitchen_assistant.recipe_maker': [ 16 | 'pasta_maker = ultimate_kitchen_assistant.pasta_maker:PastaModule', 17 | 'tex_mex = ultimate_kitchen_assistant.tex_mex:TexMexModule' 18 | ], 19 | }, 20 | ) 21 | -------------------------------------------------------------------------------- /code_examples/plugin/ultimate_kitchen_assistant/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pviafore/RobustPython/dafb95d801dff2c8ff7856ba46d3c052d54e0033/code_examples/plugin/ultimate_kitchen_assistant/__init__.py -------------------------------------------------------------------------------- /code_examples/plugin/ultimate_kitchen_assistant/pasta_maker.py: -------------------------------------------------------------------------------- 1 | 2 | from ultimate_kitchen_assistant.plugin_spec import UltimateKitchenAssistantModule 3 | 4 | Ingredient = str 5 | Amount = str 6 | Recipe = str 7 | Dish = str 8 | 9 | class PastaModule(UltimateKitchenAssistantModule): 10 | def __init__(self): 11 | self.ingredients = ["Linguine", 12 | "Spaghetti" ] 13 | 14 | def get_recipes(self) -> list[Recipe]: 15 | return ["Linguine", "Spaghetti"] 16 | 17 | def prepare_dish(self, inventory: dict[Ingredient, Amount], recipe: Recipe) -> Dish: 18 | assert recipe == "Linguine" 19 | return "Prepared Linguine" 20 | -------------------------------------------------------------------------------- /code_examples/plugin/ultimate_kitchen_assistant/plugin_spec.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from typing import runtime_checkable, Protocol 3 | 4 | Dish = str 5 | Ingredient = str 6 | Recipe = str 7 | Amount = str 8 | 9 | @runtime_checkable 10 | class UltimateKitchenAssistantModule(Protocol): 11 | 12 | ingredients: list[Ingredient] 13 | 14 | @abstractmethod 15 | def get_recipes() -> list[Recipe]: 16 | raise NotImplementedError 17 | 18 | @abstractmethod 19 | def prepare_dish(inventory: dict[Ingredient, Amount], recipe: Recipe) -> Dish: 20 | raise NotImplementedError 21 | -------------------------------------------------------------------------------- /code_examples/plugin/ultimate_kitchen_assistant/tex_mex.py: -------------------------------------------------------------------------------- 1 | 2 | from ultimate_kitchen_assistant.plugin_spec import UltimateKitchenAssistantModule 3 | 4 | Ingredient = str 5 | Amount = str 6 | Recipe = str 7 | Dish = str 8 | 9 | class TexMexModule(UltimateKitchenAssistantModule): 10 | def __init__(self): 11 | self.ingredients = ["Tortilla", 12 | "Beef" ] 13 | 14 | def get_recipes(self) -> list[Recipe]: 15 | return ["Taco"] 16 | 17 | def prepare_dish(self, inventory: dict[Ingredient, Amount], recipe: Recipe) -> Dish: 18 | assert False, "No dishes supported in test application" 19 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | plugins = pydantic.mypy 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | behave 2 | hypothesis 3 | junit2html 4 | mypy 5 | monkeytype 6 | mutmut 7 | pydantic 8 | pyhamcrest 9 | pyre-check 10 | pytest 11 | pytype 12 | pyyaml 13 | rx 14 | stevedore 15 | 16 | code_examples/plugin 17 | -------------------------------------------------------------------------------- /run_tests.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash -u 2 | 3 | echo "CHAPTER 1 TESTS" 4 | ./code_examples/chapter1/run_tests.sh 5 | 6 | echo "CHAPTER 2 TESTS" 7 | ./code_examples/chapter2/run_tests.sh 8 | 9 | echo "CHAPTER 3 TESTS" 10 | ./code_examples/chapter3/run_tests.sh 11 | 12 | echo "CHAPTER 4 TESTS" 13 | ./code_examples/chapter4/run_tests.sh 14 | 15 | echo "CHAPTER 5 TESTS" 16 | ./code_examples/chapter5/run_tests.sh 17 | 18 | # chapter 7 is just pyre, so no custom code 19 | 20 | echo "CHAPTER 7 TESTS" 21 | ./code_examples/chapter7/run_tests.sh 22 | 23 | echo "CHAPTER 8 TESTS" 24 | ./code_examples/chapter8/run_tests.sh 25 | 26 | echo "CHAPTER 9 TESTS" 27 | ./code_examples/chapter9/run_tests.sh 28 | 29 | echo "CHAPTER 10 TESTS" 30 | ./code_examples/chapter10/run_tests.sh 31 | 32 | echo "CHAPTER 11 TESTS" 33 | ./code_examples/chapter11/run_tests.sh 34 | 35 | echo "CHAPTER 12 TESTS" 36 | ./code_examples/chapter12/run_tests.sh 37 | 38 | echo "CHAPTER 13 TESTS" 39 | ./code_examples/chapter13/run_tests.sh 40 | 41 | echo "CHAPTER 14 TESTS" 42 | ./code_examples/chapter14/run_tests.sh 43 | 44 | echo "CHAPTER 15 TESTS" 45 | ./code_examples/chapter15/run_tests.sh 46 | 47 | echo "CHAPTER 17 TESTS" 48 | ./code_examples/chapter17/run_tests.sh 49 | 50 | echo "CHAPTER 18 TESTS" 51 | ./code_examples/chapter18/run_tests.sh 52 | 53 | echo "CHAPTER 19 TESTS" 54 | ./code_examples/chapter19/run_tests.sh 55 | 56 | echo "CHAPTER 20 TESTS" 57 | ./code_examples/chapter20/run_tests.sh 58 | --------------------------------------------------------------------------------