├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── README.md ├── book ├── .gitignore ├── cover.png └── src │ ├── .gitignore │ ├── Makefile │ ├── README.rst │ ├── ch01 │ ├── Makefile │ ├── README.rst │ ├── before_black.py │ ├── src │ │ ├── __init__.py │ │ ├── annotations.py │ │ ├── other.py │ │ ├── test_annotations.py │ │ ├── type_hinting_example.py │ │ └── type_hinting_example_2.py │ └── tests │ ├── ch02 │ ├── Makefile │ ├── README.rst │ ├── src │ │ ├── assignment_expressions.py │ │ ├── callables.py │ │ ├── caveats.py │ │ ├── container.py │ │ ├── contextmanagers.py │ │ ├── data_classes.py │ │ ├── dynamic.py │ │ ├── indices.py │ │ ├── iterables.py │ │ ├── properties.py │ │ └── sequences.py │ └── tests │ │ ├── test_assignment_expressions.py │ │ ├── test_caveats.py │ │ ├── test_contextmanagers.py │ │ ├── test_data_classes.py │ │ ├── test_dynamic_attributes.py │ │ ├── test_iterables.py │ │ ├── test_properties.py │ │ └── test_sequences.py │ ├── ch03 │ ├── Makefile │ ├── README.rst │ ├── src │ │ ├── base.py │ │ ├── exceptions_1.py │ │ ├── exceptions_2.py │ │ ├── exceptions_3.py │ │ ├── inheritance_antipattern.py │ │ ├── inheritance_patterns.py │ │ ├── kis.py │ │ ├── multiple_inheritance.py │ │ ├── multiple_inheritance_2.py │ │ ├── orthogonal.py │ │ └── packing_1.py │ └── tests │ │ ├── test_exceptions_1.py │ │ ├── test_exceptions_2.py │ │ ├── test_exceptions_3.py │ │ ├── test_kis.py │ │ └── test_packing1.py │ ├── ch04 │ ├── Makefile │ ├── README.rst │ ├── src │ │ ├── __init__.py │ │ ├── dip_1.py │ │ ├── dip_2.py │ │ ├── isp.py │ │ ├── lsp_1.py │ │ ├── lsp_2.py │ │ ├── openclosed_1.py │ │ ├── openclosed_2.py │ │ └── openclosed_3.py │ ├── srp_1.py │ └── tests │ │ ├── test_dip.py │ │ ├── test_isp.py │ │ ├── test_lsp.py │ │ └── test_ocp.py │ ├── ch05 │ ├── Makefile │ ├── README.rst │ ├── src │ │ ├── composition_1.py │ │ ├── composition_2.py │ │ ├── coroutines.py │ │ ├── decorator_SoC_1.py │ │ ├── decorator_SoC_2.py │ │ ├── decorator_class_1.py │ │ ├── decorator_class_2.py │ │ ├── decorator_function_1.py │ │ ├── decorator_function_2.py │ │ ├── decorator_parametrized_1.py │ │ ├── decorator_parametrized_2.py │ │ ├── decorator_side_effects_1.py │ │ ├── decorator_side_effects_2.py │ │ ├── decorator_universal_1.py │ │ ├── decorator_universal_2.py │ │ ├── decorator_wraps_1.py │ │ ├── decorator_wraps_2.py │ │ ├── default_arguments.py │ │ ├── log.py │ │ └── pep0614.py │ └── tests │ │ ├── test_class_decorator.py │ │ ├── test_composition.py │ │ ├── test_coroutines.py │ │ ├── test_decorator_SoC.py │ │ ├── test_decorator_function.py │ │ ├── test_decorator_parametrized.py │ │ ├── test_decorator_universal.py │ │ ├── test_default_arguments.py │ │ └── test_wraps.py │ ├── ch06 │ ├── Makefile │ ├── README.rst │ ├── log.py │ ├── src │ │ ├── descriptors_1.py │ │ ├── descriptors_cpython_1.py │ │ ├── descriptors_cpython_2.py │ │ ├── descriptors_cpython_3.py │ │ ├── descriptors_implementation_1.py │ │ ├── descriptors_implementation_2.py │ │ ├── descriptors_methods_1.py │ │ ├── descriptors_methods_2.py │ │ ├── descriptors_methods_3.py │ │ ├── descriptors_methods_4.py │ │ ├── descriptors_pythonic_1.py │ │ ├── descriptors_pythonic_2.py │ │ ├── descriptors_types_1.py │ │ ├── descriptors_types_2.py │ │ └── descriptors_uses_1.py │ └── tests │ │ ├── test_descriptors_cpython.py │ │ ├── test_descriptors_methods.py │ │ ├── test_descriptors_pythonic.py │ │ └── test_descriptors_uses_1.py │ ├── ch07 │ ├── .gitignore │ ├── Makefile │ ├── README.rst │ ├── src │ │ ├── _generate_data.py │ │ ├── async_context_manager.py │ │ ├── async_iteration.py │ │ ├── generators_1.py │ │ ├── generators_2.py │ │ ├── generators_coroutines_1.py │ │ ├── generators_coroutines_2.py │ │ ├── generators_iteration_1.py │ │ ├── generators_iteration_2.py │ │ ├── generators_pythonic_1.py │ │ ├── generators_pythonic_2.py │ │ ├── generators_pythonic_3.py │ │ ├── generators_pythonic_4.py │ │ ├── generators_yieldfrom_1.py │ │ ├── generators_yieldfrom_2.py │ │ ├── generators_yieldfrom_3.py │ │ └── log.py │ └── tests │ │ ├── conftest.py │ │ ├── test_async_context_manager.py │ │ ├── test_async_iteration.py │ │ ├── test_generators.py │ │ ├── test_generators_coroutines.py │ │ ├── test_generators_iteration.py │ │ └── test_generators_pythonic.py │ ├── ch08 │ ├── .gitignore │ ├── Makefile │ ├── README.rst │ ├── mutation-testing.sh │ ├── run-coverage.sh │ ├── src │ │ ├── constants.py │ │ ├── coverage_1.py │ │ ├── coverage_caveats.py │ │ ├── doctest_module.py │ │ ├── doctest_module_test.py │ │ ├── mock_1.py │ │ ├── mock_2.py │ │ ├── mrstatus.py │ │ ├── mutation_testing_1.py │ │ ├── mutation_testing_2.py │ │ ├── refactoring_1.py │ │ ├── refactoring_2.py │ │ ├── ut_design_1.py │ │ ├── ut_design_2.py │ │ ├── ut_frameworks_1.py │ │ ├── ut_frameworks_2.py │ │ ├── ut_frameworks_3.py │ │ ├── ut_frameworks_4.py │ │ └── ut_frameworks_5.py │ └── tests │ │ ├── test_coverage_1.py │ │ ├── test_coverage_caveats.py │ │ ├── test_mock_1.py │ │ ├── test_mock_2.py │ │ ├── test_mutation_testing_1.py │ │ ├── test_mutation_testing_2.py │ │ ├── test_refactoring_1.py │ │ ├── test_refactoring_2.py │ │ ├── test_ut_design_2.py │ │ ├── test_ut_frameworks.py │ │ └── test_ut_frameworks_4.py │ ├── ch09 │ ├── Makefile │ ├── README.rst │ ├── src │ │ ├── _adapter_base.py │ │ ├── adapter_1.py │ │ ├── adapter_2.py │ │ ├── chain_of_responsibility_1.py │ │ ├── composite_1.py │ │ ├── decorator_1.py │ │ ├── decorator_2.py │ │ ├── log.py │ │ ├── monostate_1.py │ │ ├── monostate_2.py │ │ ├── monostate_3.py │ │ ├── monostate_4.py │ │ ├── state_1.py │ │ └── state_2.py │ └── tests │ │ ├── test_chain_of_responsibility_1.py │ │ ├── test_composite_1.py │ │ ├── test_decorator_1.py │ │ ├── test_decorator_2.py │ │ ├── test_monostate_1.py │ │ ├── test_monostate_2.py │ │ ├── test_monostate_3.py │ │ ├── test_monostate_4.py │ │ ├── test_state_1.py │ │ └── test_state_2.py │ ├── ch10 │ ├── README.rst │ └── service │ │ ├── .gitignore │ │ ├── Dockerfile │ │ ├── Makefile │ │ ├── README.rst │ │ ├── docker-compose.yml │ │ ├── libs │ │ ├── README.rst │ │ ├── storage │ │ │ ├── .gitignore │ │ │ ├── Makefile │ │ │ ├── README.rst │ │ │ ├── db │ │ │ │ ├── README.rst │ │ │ │ └── sql │ │ │ │ │ ├── 1_schema.sql │ │ │ │ │ └── 2_data.sql │ │ │ ├── requirements.txt │ │ │ ├── setup.py │ │ │ └── src │ │ │ │ └── storage │ │ │ │ ├── __init__.py │ │ │ │ ├── client.py │ │ │ │ ├── config.py │ │ │ │ ├── converters.py │ │ │ │ ├── status.py │ │ │ │ └── storage.py │ │ └── web │ │ │ ├── Makefile │ │ │ ├── README.rst │ │ │ ├── requirements.txt │ │ │ ├── setup.py │ │ │ └── src │ │ │ └── web │ │ │ ├── __init__.py │ │ │ └── view.py │ │ ├── setup.py │ │ └── statusweb │ │ ├── README.rst │ │ ├── __init__.py │ │ └── service.py │ └── requirements.txt └── talk └── src ├── Makefile ├── _0_meaning_bad.py ├── _0_meaning_good.py ├── _1_bad_duplicated.py ├── _1_decorators_bad.py ├── _1_decorators_good.py ├── _2_context_managers.py ├── _3_properties.py ├── _4_magics_bad.py ├── _4_magics_good.py ├── _5_idioms.py ├── base.py ├── requirements.txt └── test.py /.gitattributes: -------------------------------------------------------------------------------- 1 | book/cover.png filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/book/src" 5 | schedule: 6 | interval: daily 7 | time: "11:00" 8 | open-pull-requests-limit: 10 9 | target-branch: main 10 | ignore: 11 | - dependency-name: black 12 | versions: 13 | - 21.4b0 14 | - 21.4b1 15 | - dependency-name: hypothesis 16 | versions: 17 | - 6.1.0 18 | - 6.10.0 19 | - 6.3.4 20 | - 6.4.0 21 | - 6.4.2 22 | - 6.4.3 23 | - 6.8.3 24 | - 6.8.4 25 | - 6.8.5 26 | - 6.8.8 27 | - 6.9.1 28 | - 6.9.2 29 | - dependency-name: pytype 30 | versions: 31 | - 2021.3.10 32 | - 2021.4.1 33 | - 2021.4.9 34 | - dependency-name: yapf 35 | versions: 36 | - 0.31.0 37 | - dependency-name: pyflakes 38 | versions: 39 | - 2.3.0 40 | - dependency-name: pylint 41 | versions: 42 | - 2.7.4 43 | - dependency-name: pip-tools 44 | versions: 45 | - 6.0.0 46 | - dependency-name: requests 47 | versions: 48 | - 2.25.1 49 | - dependency-name: mypy 50 | versions: 51 | - "0.800" 52 | - dependency-name: coverage 53 | versions: 54 | - "5.4" 55 | - dependency-name: pytest 56 | versions: 57 | - 6.2.2 58 | - dependency-name: isort 59 | versions: 60 | - 5.7.0 61 | - dependency-name: pytest-cov 62 | versions: 63 | - 2.11.1 64 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | - pull_request 4 | 5 | permissions: 6 | contents: write 7 | 8 | jobs: 9 | build: 10 | defaults: 11 | run: 12 | working-directory: book/src 13 | runs-on: ubuntu-latest 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | python-version: [3.9] 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Setup Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v2 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | - name: Install dependencies 26 | run: | 27 | pip install -r requirements.txt 28 | - name: Run tests 29 | run: | 30 | make test 31 | auto-merge: 32 | needs: build 33 | runs-on: ubuntu-latest 34 | if: ${{ github.actor == 'dependabot[bot]' }} 35 | steps: 36 | - name: Dependabot metadata 37 | id: metadata 38 | uses: dependabot/fetch-metadata@v1.1.1 39 | with: 40 | github-token: "${{ secrets.GITHUB_TOKEN }}" 41 | - name: Enable auto-merge for Dependabot PRs 42 | if: ${{ steps.metadata.outputs.update-type == 'version-update:semver-patch' || steps.metadata.outputs.update-type == 'version-update:semver-minor' }} 43 | run: gh pr merge --auto --rebase "$PR_URL" 44 | env: 45 | PR_URL: ${{github.event.pull_request.html_url}} 46 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2021 Mariano Anaya 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 | # Clean code in Python 2 | 3 | ## Book 4 | 5 | The source code for the examples listed in the [book](https://www.amazon.es/gp/product/B08R961TRD/) is under 6 | ``book/src``. 7 | 8 | ![](book/cover.png) 9 | 10 | ### Testing the code 11 | To test the code from the book, you can either follow the instructions on this repository, or try out the [Docker image](https://hub.docker.com/r/rmariano/ccip). 12 | 13 | ## EuroPython 2016 Talk 14 | 15 | Sources of the talk, as presented at EuroPython 2016. 16 | 17 | The source code of the talk is under ``talk/src``. 18 | 19 | [Slides](https://speakerdeck.com/rmariano/clean-code-in-python) 20 | [EuroPython Page](https://ep2016.europython.eu/conference/talks/clean-code-in-python) 21 | [Video](https://www.youtube.com/watch?v=7ADbOHW1dTA) 22 | -------------------------------------------------------------------------------- /book/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.sw[op] 3 | .mypy_cache/ 4 | *.tar.gz 5 | .pytest_cache/ 6 | -------------------------------------------------------------------------------- /book/cover.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:1d2f3d3f5f7762d16ae8f29a7f821ed962974450ae738e4e02781afd81e8654a 3 | size 2476260 4 | -------------------------------------------------------------------------------- /book/src/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.sw[op] 3 | .mypy_cache/ 4 | *.tar.gz 5 | .pytest_cache/ 6 | .pytype 7 | env/ 8 | -------------------------------------------------------------------------------- /book/src/Makefile: -------------------------------------------------------------------------------- 1 | PYTHON=python3.9 2 | ENV_NAME=.env 3 | DOCKER_VERSION:=latest 4 | 5 | .PHONY: clean 6 | clean: 7 | find . -name "*.swp" -o -name "__pycache__" -o -name ".mypy_cache" | xargs rm -fr 8 | rm -fr $(ENV_NAME) 9 | 10 | .PHONY: setup 11 | setup: 12 | $(PYTHON) -m venv $(ENV_NAME) 13 | $(ENV_NAME)/bin/python -m pip install -r requirements.txt 14 | 15 | .PHONY: shell 16 | shell: 17 | docker run -it rmariano/ccip:$(DOCKER_VERSION) 18 | 19 | .PHONY: test 20 | test: 21 | for chapter_dir in $(shell ls -d ch0*); do \ 22 | echo "Testing $$chapter_dir"; \ 23 | $(MAKE) test -C $$chapter_dir PYTHONPATH=src PYTHON=$(PYTHON); \ 24 | done 25 | -------------------------------------------------------------------------------- /book/src/ch01/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: typehint 2 | typehint: 3 | mypy --ignore-missing-imports src/ 4 | 5 | .PHONY: test 6 | test: 7 | pytest tests/ 8 | 9 | .PHONY: lint 10 | lint: 11 | pylint src/ 12 | 13 | .PHONY: checklist 14 | checklist: lint typehint test 15 | 16 | .PHONY: black 17 | black: 18 | black -l 79 *.py 19 | 20 | .PHONY: clean 21 | clean: 22 | find . -type f -name "*.pyc" | xargs rm -fr 23 | find . -type d -name __pycache__ | xargs rm -fr 24 | -------------------------------------------------------------------------------- /book/src/ch01/README.rst: -------------------------------------------------------------------------------- 1 | Chapter 01 - Introduction, Tools, and Formatting 2 | ================================================ 3 | 4 | Run the tests:: 5 | 6 | make test 7 | -------------------------------------------------------------------------------- /book/src/ch01/before_black.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 1: Introduction, Tools, and Formatting 2 | 3 | > Black: 4 | A code that is compliant with PEP-8, but that still can be modified by back 5 | 6 | 7 | Run:: 8 | black -l 79 before_black.py 9 | 10 | To see the difference 11 | """ 12 | 13 | 14 | def my_function(name): 15 | """ 16 | >>> my_function('black') 17 | 'received Black' 18 | """ 19 | return 'received {0}'.format(name.title()) 20 | -------------------------------------------------------------------------------- /book/src/ch01/src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmariano/Clean-code-in-Python/ebd39c190f5c347c2084d2e52cb5f2006b68206c/book/src/ch01/src/__init__.py -------------------------------------------------------------------------------- /book/src/ch01/src/annotations.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 1: Introduction, Tools, and Formatting 2 | 3 | > Annotations 4 | """ 5 | from dataclasses import dataclass 6 | from typing import Tuple 7 | 8 | Client = Tuple[int, str] 9 | 10 | 11 | def process_clients(clients: list[Client]): # type: ignore 12 | ... 13 | 14 | 15 | @dataclass 16 | class Point: 17 | lat: float 18 | long: float 19 | 20 | 21 | def locate(latitude: float, longitude: float) -> Point: 22 | """Find an object in the map by its coordinates""" 23 | return Point(latitude, longitude) 24 | -------------------------------------------------------------------------------- /book/src/ch01/src/other.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 1: Introduction, Tools, and Formatting 2 | 3 | > Extra code for isolated examples 4 | """ 5 | 6 | 7 | def data_from_response(response: dict) -> dict: 8 | """If the response is OK, return its payload. 9 | - response: A dict like:: 10 | { 11 | "status": 200, # 12 | "timestamp": "....", # ISO format string of the current date time 13 | "payload": { ... } # dict with the returned data 14 | } 15 | - Returns a dictionary like:: 16 | {"data": { .. } } 17 | 18 | - Raises: 19 | - ValueError if the HTTP status is != 200 20 | """ 21 | if response["status"] != 200: 22 | raise ValueError 23 | return {"data": response["payload"]} 24 | -------------------------------------------------------------------------------- /book/src/ch01/src/test_annotations.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 01: Introduction, Tools, and Formatting 2 | 3 | Tests for annotations examples 4 | 5 | """ 6 | import pytest 7 | 8 | from src.annotations import Point, locate 9 | 10 | 11 | @pytest.mark.parametrize( 12 | "defined_object,expected", 13 | ( 14 | (locate, {"latitude": float, "longitude": float, "return": Point}), 15 | (Point, {"lat": float, "long": float}), 16 | ), 17 | ) 18 | def test_annotations(defined_object, expected): 19 | """test the class/functions against its expected annotations""" 20 | assert getattr(defined_object, "__annotations__") == expected 21 | -------------------------------------------------------------------------------- /book/src/ch01/src/type_hinting_example.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 1: Introduction, Tools, and Formatting 2 | 3 | > Tools for type hinting: examples 4 | """ 5 | from __future__ import annotations 6 | 7 | import logging 8 | from typing import List, Union, Tuple 9 | 10 | logging.basicConfig(level=logging.INFO) 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | def broadcast_notification( 15 | message: str, relevant_user_emails: Union[List[str], Tuple[str]] 16 | ): 17 | for email in relevant_user_emails: 18 | logger.info("Sending %r to %r", message, email) 19 | 20 | 21 | broadcast_notification("welcome", ["user1@domain.com", "user2@domain.com"]) 22 | broadcast_notification("welcome", "user1@domain.com") # type: ignore 23 | -------------------------------------------------------------------------------- /book/src/ch01/src/type_hinting_example_2.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 1: Introduction, Tools, and Formatting 2 | 3 | > Tools for type hinting: examples 4 | """ 5 | from __future__ import annotations 6 | 7 | import logging 8 | from typing import Iterable 9 | 10 | logging.basicConfig(level=logging.INFO) 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | def broadcast_notification(message: str, relevant_user_emails: Iterable[str]): 15 | for email in relevant_user_emails: 16 | logger.info("Sending %r to %r", message, email) 17 | 18 | 19 | broadcast_notification("welcome", ["user1@domain.com", "user2@domain.com"]) 20 | broadcast_notification("welcome", "user1@domain.com") 21 | -------------------------------------------------------------------------------- /book/src/ch01/tests: -------------------------------------------------------------------------------- 1 | src -------------------------------------------------------------------------------- /book/src/ch02/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | test: 3 | @$(PYTHON) -m doctest tests/*.py 4 | @$(PYTHON) -m unittest tests/*.py 5 | 6 | .PHONY: typehint 7 | typehint: 8 | mypy src tests 9 | 10 | .PHONY: lint 11 | lint: 12 | black --check --line-length=79 src tests 13 | 14 | .PHONY: format 15 | format: 16 | black --line-length=79 src tests 17 | 18 | .PHONY: clean 19 | clean: 20 | find . -type d -name __pycache__ | xargs rm -fr {} 21 | -------------------------------------------------------------------------------- /book/src/ch02/README.rst: -------------------------------------------------------------------------------- 1 | Chapter 02: Pythonic Code 2 | ========================= 3 | 4 | Test the code:: 5 | 6 | make test 7 | -------------------------------------------------------------------------------- /book/src/ch02/src/assignment_expressions.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Second edition 2 | Chapter 02: Assignment expressions 3 | """ 4 | 5 | import re 6 | from typing import Iterable, Set 7 | 8 | ARN_REGEX = re.compile(r"arn:aws:[a-z0-9\-]*:[a-z0-9\-]*:(?P\d+):.*") 9 | 10 | 11 | def collect_account_ids_from_arns(arns: Iterable[str]) -> Set[str]: 12 | """Given several ARNs in the form 13 | 14 | arn:partition:service:region:account-id:resource-id 15 | 16 | Collect the unique account IDs found on those strings, and return them. 17 | """ 18 | collected_account_ids = set() 19 | for arn in arns: 20 | matched = re.match(ARN_REGEX, arn) 21 | if matched is not None: 22 | account_id = matched.groupdict()["account_id"] 23 | collected_account_ids.add(account_id) 24 | return collected_account_ids 25 | 26 | 27 | def collect_account_ids_from_arns2(arns: Iterable[str]) -> Set[str]: 28 | matched_arns = filter(None, (re.match(ARN_REGEX, arn) for arn in arns)) 29 | return {m.groupdict()["account_id"] for m in matched_arns} 30 | 31 | 32 | def collect_account_ids_from_arns3(arns: Iterable[str]) -> Set[str]: 33 | return { 34 | matched.groupdict()["account_id"] 35 | for arn in arns 36 | if (matched := re.match(ARN_REGEX, arn)) is not None 37 | } 38 | -------------------------------------------------------------------------------- /book/src/ch02/src/callables.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 2: Pythonic Code 2 | 3 | > Callable objects 4 | 5 | """ 6 | 7 | from collections import defaultdict 8 | 9 | 10 | class CallCount: 11 | """ 12 | >>> cc = CallCount() 13 | >>> cc(1) 14 | 1 15 | >>> cc(2) 16 | 1 17 | >>> cc(1) 18 | 2 19 | >>> cc(1) 20 | 3 21 | >>> cc("something") 22 | 1 23 | 24 | >>> callable(cc) 25 | True 26 | """ 27 | 28 | def __init__(self): 29 | self._counts = defaultdict(int) 30 | 31 | def __call__(self, argument): 32 | self._counts[argument] += 1 33 | return self._counts[argument] 34 | -------------------------------------------------------------------------------- /book/src/ch02/src/caveats.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 2: Pythonic Code 2 | 3 | > Caveats in Python 4 | """ 5 | 6 | from collections import UserList 7 | 8 | 9 | def wrong_user_display(user_metadata: dict = {"name": "John", "age": 30}): 10 | name = user_metadata.pop("name") 11 | age = user_metadata.pop("age") 12 | 13 | return f"{name} ({age})" 14 | 15 | 16 | def user_display(user_metadata: dict = None): 17 | user_metadata = user_metadata or {"name": "John", "age": 30} 18 | 19 | name = user_metadata.pop("name") 20 | age = user_metadata.pop("age") 21 | 22 | return f"{name} ({age})" 23 | 24 | 25 | class BadList(list): 26 | def __getitem__(self, index): 27 | value = super().__getitem__(index) 28 | if index % 2 == 0: 29 | prefix = "even" 30 | else: 31 | prefix = "odd" 32 | return f"[{prefix}] {value}" 33 | 34 | 35 | class GoodList(UserList): 36 | def __getitem__(self, index): 37 | value = super().__getitem__(index) 38 | if index % 2 == 0: 39 | prefix = "even" 40 | else: 41 | prefix = "odd" 42 | return f"[{prefix}] {value}" 43 | -------------------------------------------------------------------------------- /book/src/ch02/src/container.py: -------------------------------------------------------------------------------- 1 | """Chapter 2 - Containers""" 2 | 3 | 4 | def mark_coordinate(grid, coord): 5 | if 0 <= coord.x < grid.width and 0 <= coord.y < grid.height: 6 | grid[coord] = 1 7 | 8 | if coord in grid: 9 | grid[coord] = 1 10 | 11 | 12 | class Boundaries: 13 | def __init__(self, width, height): 14 | self.width = width 15 | self.height = height 16 | 17 | def __contains__(self, coord): 18 | x, y = coord 19 | return 0 <= x < self.width and 0 <= y < self.height 20 | 21 | 22 | class Grid: 23 | def __init__(self, width, height): 24 | self.width = width 25 | self.height = height 26 | self.limits = Boundaries(width, height) 27 | 28 | def __contains__(self, coord): 29 | return coord in self.limits 30 | -------------------------------------------------------------------------------- /book/src/ch02/src/contextmanagers.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | 3 | 4 | run = print 5 | 6 | 7 | def stop_database(): 8 | run("systemctl stop postgresql.service") 9 | 10 | 11 | def start_database(): 12 | run("systemctl start postgresql.service") 13 | 14 | 15 | class DBHandler: 16 | def __enter__(self): 17 | stop_database() 18 | return self 19 | 20 | def __exit__(self, exc_type, ex_value, ex_traceback): 21 | start_database() 22 | 23 | 24 | def db_backup(): 25 | run("pg_dump database") 26 | 27 | 28 | @contextlib.contextmanager 29 | def db_handler(): 30 | try: 31 | stop_database() 32 | yield 33 | finally: 34 | start_database() 35 | 36 | 37 | class dbhandler_decorator(contextlib.ContextDecorator): 38 | def __enter__(self): 39 | stop_database() 40 | return self 41 | 42 | def __exit__(self, ext_type, ex_value, ex_traceback): 43 | start_database() 44 | 45 | 46 | @dbhandler_decorator() 47 | def offline_backup(): 48 | run("pg_dump database") 49 | 50 | 51 | def main(): 52 | with DBHandler(): 53 | db_backup() 54 | 55 | with db_handler(): 56 | db_backup() 57 | 58 | offline_backup() 59 | 60 | 61 | if __name__ == "__main__": 62 | main() 63 | -------------------------------------------------------------------------------- /book/src/ch02/src/data_classes.py: -------------------------------------------------------------------------------- 1 | """Clean code in Python - Second edition 2 | Chapter 2: Data classes 3 | """ 4 | 5 | from typing import List 6 | from dataclasses import dataclass, field 7 | 8 | 9 | R = 26 10 | 11 | 12 | @dataclass 13 | class RTrieNode: 14 | size = R 15 | value: int 16 | next_: List["RTrieNode"] = field( 17 | default_factory=lambda: [None] * R 18 | ) 19 | 20 | def __post_init__(self): 21 | if len(self.next_) != self.size: 22 | raise ValueError(f"Invalid length provided for next list") 23 | -------------------------------------------------------------------------------- /book/src/ch02/src/dynamic.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 2: Pythonic Code 2 | 3 | > Dynamic Attributes 4 | 5 | """ 6 | 7 | 8 | class DynamicAttributes: 9 | """ 10 | >>> dyn = DynamicAttributes("value") 11 | >>> dyn.attribute 12 | 'value' 13 | 14 | >>> dyn.fallback_test 15 | '[fallback resolved] test' 16 | 17 | >>> dyn.__dict__["fallback_new"] = "new value" 18 | >>> dyn.fallback_new 19 | 'new value' 20 | 21 | >>> getattr(dyn, "something", "default") 22 | 'default' 23 | """ 24 | 25 | def __init__(self, attribute): 26 | self.attribute = attribute 27 | 28 | def __getattr__(self, attr): 29 | if attr.startswith("fallback_"): 30 | name = attr.replace("fallback_", "") 31 | return f"[fallback resolved] {name}" 32 | raise AttributeError( 33 | f"{self.__class__.__name__} has no attribute {attr}" 34 | ) 35 | -------------------------------------------------------------------------------- /book/src/ch02/src/indices.py: -------------------------------------------------------------------------------- 1 | """Indexes and slices 2 | Getting elements by an index or range 3 | """ 4 | import doctest 5 | 6 | 7 | def index_last(): 8 | """ 9 | >>> my_numbers = (4, 5, 3, 9) 10 | >>> my_numbers[-1] 11 | 9 12 | >>> my_numbers[-3] 13 | 5 14 | """ 15 | 16 | 17 | def get_slices(): 18 | """ 19 | >>> my_numbers = (1, 1, 2, 3, 5, 8, 13, 21) 20 | >>> my_numbers[2:5] 21 | (2, 3, 5) 22 | >>> my_numbers[:3] 23 | (1, 1, 2) 24 | >>> my_numbers[3:] 25 | (3, 5, 8, 13, 21) 26 | >>> my_numbers[::] 27 | (1, 1, 2, 3, 5, 8, 13, 21) 28 | >>> my_numbers[1:7:2] 29 | (1, 3, 8) 30 | 31 | >>> interval = slice(1, 7, 2) 32 | >>> my_numbers[interval] 33 | (1, 3, 8) 34 | 35 | >>> interval = slice(None, 3) 36 | >>> my_numbers[interval] == my_numbers[:3] 37 | True 38 | """ 39 | 40 | 41 | def main(): 42 | index_last() 43 | get_slices() 44 | fail_count, _ = doctest.testmod(verbose=True) 45 | raise SystemExit(fail_count) 46 | 47 | 48 | if __name__ == "__main__": 49 | main() 50 | -------------------------------------------------------------------------------- /book/src/ch02/src/iterables.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 2: Pythonic Code 2 | 3 | > Iterable Objects 4 | """ 5 | from datetime import timedelta 6 | 7 | 8 | class DateRangeIterable: 9 | """An iterable that contains its own iterator object.""" 10 | 11 | def __init__(self, start_date, end_date): 12 | self.start_date = start_date 13 | self.end_date = end_date 14 | self._present_day = start_date 15 | 16 | def __iter__(self): 17 | return self 18 | 19 | def __next__(self): 20 | if self._present_day >= self.end_date: 21 | raise StopIteration() 22 | today = self._present_day 23 | self._present_day += timedelta(days=1) 24 | return today 25 | 26 | 27 | class DateRangeContainerIterable: 28 | """An range that builds its iteration through a generator.""" 29 | 30 | def __init__(self, start_date, end_date): 31 | self.start_date = start_date 32 | self.end_date = end_date 33 | 34 | def __iter__(self): 35 | current_day = self.start_date 36 | while current_day < self.end_date: 37 | yield current_day 38 | current_day += timedelta(days=1) 39 | 40 | 41 | class DateRangeSequence: 42 | """An range created by wrapping a sequence.""" 43 | 44 | def __init__(self, start_date, end_date): 45 | self.start_date = start_date 46 | self.end_date = end_date 47 | self._range = self._create_range() 48 | 49 | def _create_range(self): 50 | days = [] 51 | current_day = self.start_date 52 | while current_day < self.end_date: 53 | days.append(current_day) 54 | current_day += timedelta(days=1) 55 | return days 56 | 57 | def __getitem__(self, day_no): 58 | return self._range[day_no] 59 | 60 | def __len__(self): 61 | return len(self._range) 62 | -------------------------------------------------------------------------------- /book/src/ch02/src/properties.py: -------------------------------------------------------------------------------- 1 | """Clean code in Python - Second edition 2 | Chapter 2 - Properties 3 | """ 4 | 5 | 6 | class Coordinate: 7 | def __init__(self, lat: float, long: float) -> None: 8 | self._latitude = self._longitude = None 9 | self.latitude = lat 10 | self.longitude = long 11 | 12 | @property 13 | def latitude(self) -> float: 14 | return self._latitude 15 | 16 | @latitude.setter 17 | def latitude(self, lat_value: float) -> None: 18 | if -90 <= lat_value <= 90: 19 | self._latitude = lat_value 20 | else: 21 | raise ValueError(f"{lat_value} is an invalid value for latitude") 22 | 23 | @property 24 | def longitude(self) -> float: 25 | return self._longitude 26 | 27 | @longitude.setter 28 | def longitude(self, long_value: float) -> None: 29 | if -180 <= long_value <= 180: 30 | self._longitude = long_value 31 | else: 32 | raise ValueError(f"{long_value} is an invalid value for longitude") 33 | -------------------------------------------------------------------------------- /book/src/ch02/src/sequences.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 2: Pythonic Code 2 | 3 | > Sequences 4 | """ 5 | from collections.abc import Sequence 6 | 7 | 8 | class Items(Sequence): 9 | def __init__(self, *values): 10 | self._values = list(values) 11 | 12 | def __len__(self): 13 | return len(self._values) 14 | 15 | def __getitem__(self, item): 16 | return self._values.__getitem__(item) 17 | -------------------------------------------------------------------------------- /book/src/ch02/tests/test_assignment_expressions.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | from assignment_expressions import ( 5 | collect_account_ids_from_arns, 6 | collect_account_ids_from_arns2, 7 | collect_account_ids_from_arns3, 8 | ) 9 | 10 | 11 | class TestCollectIds(unittest.TestCase): 12 | data = ( 13 | "arn:aws:iam::123456789012:user/Development/product_1234/*", 14 | "arn:aws:iam::123456789012:user", 15 | "arn:aws:s3:::my_corporate_bucket/*", 16 | "arn:aws:iam::999999999999:user/Development/product_1234/*", 17 | ) 18 | expected = {"123456789012", "999999999999"} 19 | test_cases = ( 20 | collect_account_ids_from_arns, 21 | collect_account_ids_from_arns2, 22 | collect_account_ids_from_arns3, 23 | ) 24 | 25 | def test(self): 26 | for case in self.test_cases: 27 | with self.subTest(testing=case): 28 | self.assertSetEqual(self.expected, obtained := case(self.data), obtained) 29 | 30 | 31 | if __name__ == "__main__": 32 | unittest.main() 33 | -------------------------------------------------------------------------------- /book/src/ch02/tests/test_caveats.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from caveats import BadList, GoodList 3 | 4 | 5 | class TestCaveats(unittest.TestCase): 6 | def test_bad_list(self): 7 | bl = BadList((0, 1, 2, 3, 4, 5)) 8 | self.assertEqual(bl[0], "[even] 0") 9 | self.assertEqual(bl[3], "[odd] 3") 10 | self.assertRaises(TypeError, str.join, bl) 11 | 12 | def test_good_list(self): 13 | gl = GoodList((0, 1, 2)) 14 | self.assertEqual(gl[0], "[even] 0") 15 | self.assertEqual(gl[1], "[odd] 1") 16 | 17 | expected = "[even] 0; [odd] 1; [even] 2" 18 | self.assertEqual("; ".join(gl), expected) 19 | 20 | 21 | if __name__ == "__main__": 22 | unittest.main() 23 | -------------------------------------------------------------------------------- /book/src/ch02/tests/test_contextmanagers.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from contextlib import ContextDecorator 3 | from unittest.mock import patch, DEFAULT 4 | 5 | from contextmanagers import dbhandler_decorator, offline_backup, db_handler 6 | 7 | 8 | class TestDBHandler(unittest.TestCase): 9 | def _patch_deps(self): 10 | return patch.multiple("contextmanagers", stop_database=DEFAULT, start_database=DEFAULT) 11 | 12 | def test_cm_calls_functions(self): 13 | with self._patch_deps() as deps: 14 | with dbhandler_decorator() as db_handler: 15 | handler_class = db_handler.__class__ 16 | self.assertTrue(issubclass(handler_class, ContextDecorator), handler_class) 17 | 18 | deps["stop_database"].assert_called_once_with() 19 | deps["start_database"].assert_called_once_with() 20 | 21 | def test_cm_autocalled(self): 22 | with self._patch_deps() as deps: 23 | offline_backup() 24 | 25 | deps["stop_database"].assert_called_once_with() 26 | deps["start_database"].assert_called_once_with() 27 | 28 | def test_context_decorator_called_on_exception(self): 29 | """In case of an exception, the __exit__ is called anyways.""" 30 | with self._patch_deps() as deps, patch("contextmanagers.run", side_effect=RuntimeError), self.assertRaises( 31 | RuntimeError 32 | ): 33 | offline_backup() 34 | 35 | deps["start_database"].assert_called_once_with() 36 | 37 | def test_db_handler_on_exception(self): 38 | with self._patch_deps() as deps, self.assertRaises(RuntimeError): 39 | with db_handler(): 40 | raise RuntimeError("something went wrong!") 41 | 42 | deps["start_database"].assert_called_once_with() 43 | 44 | 45 | if __name__ == "__main__": 46 | unittest.main() 47 | -------------------------------------------------------------------------------- /book/src/ch02/tests/test_data_classes.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from data_classes import RTrieNode 4 | 5 | 6 | class TestRTrieNode(unittest.TestCase): 7 | def test_next_nodes_differ(self): 8 | node1 = RTrieNode(1) 9 | node2 = RTrieNode(1) 10 | 11 | self.assertIsNot(node1.next_, node2.next_) 12 | 13 | node1.next_[:2] = [2, 3] 14 | self.assertTrue(all(n is None for n in node2.next_)) 15 | self.assertEqual(node1.next_, [2, 3, *(None for _ in range(RTrieNode.size - 2))]) 16 | 17 | def test_invalid_next(self): 18 | with self.assertRaises(ValueError): 19 | RTrieNode(1, [1, 2, 3]) 20 | 21 | def test_valid_next_provided(self): 22 | next_array = list(range(RTrieNode.size)) 23 | node = RTrieNode(0, next_array) 24 | self.assertListEqual(node.next_, next_array) 25 | 26 | 27 | if __name__ == "__main__": 28 | unittest.main() 29 | -------------------------------------------------------------------------------- /book/src/ch02/tests/test_dynamic_attributes.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from dynamic import DynamicAttributes 3 | 4 | 5 | class TestDynamic(unittest.TestCase): 6 | def test_dynamic_attributes(self): 7 | dyn = DynamicAttributes("value") 8 | 9 | self.assertEqual(dyn.attribute, "value") 10 | self.assertEqual(dyn.fallback_test, "[fallback resolved] test") 11 | self.assertEqual(getattr(dyn, "something", "default"), "default") 12 | with self.assertRaisesRegex(AttributeError, ".* has no attribute \S+"): 13 | dyn.something_not_found 14 | 15 | 16 | if __name__ == "__main__": 17 | unittest.main() 18 | -------------------------------------------------------------------------------- /book/src/ch02/tests/test_iterables.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from datetime import datetime, timedelta 3 | 4 | from iterables import ( 5 | DateRangeContainerIterable, 6 | DateRangeIterable, 7 | DateRangeSequence, 8 | ) 9 | 10 | 11 | class TestIterables(unittest.TestCase): 12 | def setUp(self): 13 | self.start_date = datetime(2016, 7, 17) 14 | self.end_date = datetime(2016, 7, 24) 15 | self.expected = [datetime(2016, 7, i) for i in range(17, 24)] 16 | 17 | def _base_test_date_range(self, range_cls): 18 | date_range = range_cls(self.start_date, self.end_date) 19 | self.assertListEqual(list(date_range), self.expected) 20 | self.assertEqual(date_range.start_date, self.start_date) 21 | self.assertEqual(date_range.end_date, self.end_date) 22 | 23 | def test_date_range(self): 24 | for range_cls in ( 25 | DateRangeIterable, 26 | DateRangeContainerIterable, 27 | DateRangeSequence, 28 | ): 29 | with self.subTest(type_=range_cls.__name__): 30 | self._base_test_date_range(range_cls) 31 | 32 | def test_date_range_sequence(self): 33 | date_range = DateRangeSequence(self.start_date, self.end_date) 34 | 35 | self.assertEqual(date_range[0], self.start_date) 36 | self.assertEqual(date_range[-1], self.end_date - timedelta(days=1)) 37 | self.assertEqual(len(date_range), len(self.expected)) 38 | 39 | 40 | if __name__ == "__main__": 41 | unittest.main() 42 | -------------------------------------------------------------------------------- /book/src/ch02/tests/test_properties.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from properties import Coordinate 3 | 4 | 5 | class TestProperties(unittest.TestCase): 6 | def test_invalid_coordinates(self): 7 | for lat, long in ( 8 | (-91, 0), 9 | (91, 0), 10 | (90.0001, 0), 11 | (-90.0001, 0), 12 | (0, -181), 13 | (0, -180.0001), 14 | (0, -200), 15 | (0, 180.0001), 16 | (0, 200), 17 | (0, -180.0001), 18 | (0, -200), 19 | (-90.001, -180.001), 20 | (90.001, 180.001), 21 | ): 22 | with self.subTest(case="invalid", lat=lat, long=long), self.assertRaises(ValueError): 23 | Coordinate(lat, long) 24 | 25 | def test_valid_coordinates(self): 26 | for lat, long in ( 27 | (0, 0), 28 | (90, 0), 29 | (-90, 0), 30 | (90, 180), 31 | (90, -180), 32 | (-90, 180), 33 | (-90, -180), 34 | (0, 180), 35 | (0, -180), 36 | (41, 2), 37 | (41.5, 2.1), 38 | ): 39 | with self.subTest(case="valid", lat=lat, long=long): 40 | coord = Coordinate(lat, long) 41 | 42 | self.assertEqual(coord.latitude, lat) 43 | self.assertEqual(coord.longitude, long) 44 | 45 | 46 | if __name__ == "__main__": 47 | unittest.main() 48 | -------------------------------------------------------------------------------- /book/src/ch02/tests/test_sequences.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from sequences import Items 3 | 4 | 5 | class TestSequences(unittest.TestCase): 6 | def test_items(self): 7 | items = Items(1, 2, 3, 4, 5) 8 | 9 | self.assertEqual(items[-1], 5) 10 | self.assertEqual(items[0], 1) 11 | self.assertEqual(len(items), 5) 12 | 13 | 14 | if __name__ == "__main__": 15 | unittest.main() 16 | -------------------------------------------------------------------------------- /book/src/ch03/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | test: 3 | @$(PYTHON) -m doctest src/*.py 4 | @$(PYTHON) -m unittest tests/*.py 5 | 6 | .PHONY: typehint 7 | typehint: 8 | mypy src tests 9 | 10 | .PHONY: lint 11 | lint: 12 | black --check --line-length=79 src tests 13 | 14 | .PHONY: format 15 | format: 16 | black --line-length=79 src tests 17 | 18 | .PHONY: clean 19 | clean: 20 | find . -name "*.swp" -o -name "__pycache__" | xargs rm -fr 21 | -------------------------------------------------------------------------------- /book/src/ch03/README.rst: -------------------------------------------------------------------------------- 1 | Chapter 03 - General traits of good code 2 | ======================================== 3 | This directory contains the examples for chapter 3 of the book. 4 | 5 | 6 | Testing 7 | ------- 8 | To run the tests for this chapter, issue the following command:: 9 | 10 | make test 11 | -------------------------------------------------------------------------------- /book/src/ch03/src/base.py: -------------------------------------------------------------------------------- 1 | """Clean code in Python - Chapter 03 2 | Common definitions 3 | """ 4 | 5 | 6 | class Connector: 7 | """Abstract the connection to a database.""" 8 | 9 | def connect(self): 10 | """Connect to a data source.""" 11 | return self 12 | 13 | @staticmethod 14 | def send(data): 15 | return data 16 | 17 | 18 | class Event: 19 | def __init__(self, payload): 20 | self._payload = payload 21 | 22 | def decode(self): 23 | return f"decoded {self._payload}" 24 | -------------------------------------------------------------------------------- /book/src/ch03/src/exceptions_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 3: General Traits of Good Code 2 | 3 | > Error Handling - Exceptions 4 | """ 5 | import logging 6 | import time 7 | 8 | from base import Connector, Event 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | class DataTransport: 14 | """An example of an object handling exceptions of different levels.""" 15 | 16 | _RETRY_BACKOFF: int = 5 17 | _RETRY_TIMES: int = 3 18 | 19 | def __init__(self, connector: Connector) -> None: 20 | self._connector = connector 21 | self.connection = None 22 | 23 | def deliver_event(self, event: Event): 24 | try: 25 | self.connect() 26 | data = event.decode() 27 | self.send(data) 28 | except ConnectionError as e: 29 | logger.info("connection error detected: %s", e) 30 | raise 31 | except ValueError as e: 32 | logger.error("%r contains incorrect data: %s", event, e) 33 | raise 34 | 35 | def connect(self): 36 | for _ in range(self._RETRY_TIMES): 37 | try: 38 | self.connection = self._connector.connect() 39 | except ConnectionError as e: 40 | logger.info( 41 | "%s: attempting new connection in %is", 42 | e, 43 | self._RETRY_BACKOFF, 44 | ) 45 | time.sleep(self._RETRY_BACKOFF) 46 | else: 47 | return self.connection 48 | raise ConnectionError( 49 | f"Couldn't connect after {self._RETRY_TIMES} times" 50 | ) 51 | 52 | def send(self, data: bytes): 53 | return self.connection.send(data) 54 | -------------------------------------------------------------------------------- /book/src/ch03/src/exceptions_3.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 3: General Traits of Good Code 2 | 3 | > Exceptions 4 | """ 5 | 6 | 7 | class InternalDataError(Exception): 8 | """An exception with the data of our domain problem.""" 9 | 10 | 11 | def process(data_dictionary, record_id): 12 | try: 13 | return data_dictionary[record_id] 14 | except KeyError as e: 15 | raise InternalDataError("Record not present") from e 16 | -------------------------------------------------------------------------------- /book/src/ch03/src/inheritance_antipattern.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 3: Traits of good code 2 | 3 | > Inheritance & Composition - Inheritance anti-pattern 4 | """ 5 | import collections 6 | from datetime import datetime 7 | from unittest import TestCase, main 8 | 9 | 10 | class TransactionalPolicy(collections.UserDict): 11 | """Example of an incorrect use of inheritance.""" 12 | 13 | def change_in_policy(self, customer_id, **new_policy_data): 14 | self[customer_id].update(**new_policy_data) 15 | 16 | 17 | class TestPolicy(TestCase): 18 | def test_get_policy(self): 19 | policy = TransactionalPolicy( 20 | { 21 | "client001": { 22 | "fee": 1000.0, 23 | "expiration_date": datetime(2020, 1, 3), 24 | } 25 | } 26 | ) 27 | self.assertDictEqual( 28 | policy["client001"], 29 | {"fee": 1000.0, "expiration_date": datetime(2020, 1, 3)}, 30 | ) 31 | 32 | policy.change_in_policy( 33 | "client001", expiration_date=datetime(2020, 1, 4) 34 | ) 35 | self.assertDictEqual( 36 | policy["client001"], 37 | {"fee": 1000.0, "expiration_date": datetime(2020, 1, 4)}, 38 | ) 39 | 40 | 41 | if __name__ == "__main__": 42 | main() 43 | -------------------------------------------------------------------------------- /book/src/ch03/src/inheritance_patterns.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 3: Traits of good code 2 | 3 | > Inheritance & Composition: use of composition 4 | """ 5 | 6 | from datetime import datetime 7 | from unittest import TestCase, main 8 | 9 | 10 | class TransactionalPolicy: 11 | """Example refactored to use composition.""" 12 | 13 | def __init__(self, policy_data, **extra_data): 14 | self._data = {**policy_data, **extra_data} 15 | 16 | def change_in_policy(self, customer_id, **new_policy_data): 17 | self._data[customer_id].update(**new_policy_data) 18 | 19 | def __getitem__(self, customer_id): 20 | return self._data[customer_id] 21 | 22 | def __len__(self): 23 | return len(self._data) 24 | 25 | 26 | class TestPolicy(TestCase): 27 | def test_get_policy(self): 28 | policy = TransactionalPolicy( 29 | { 30 | "client001": { 31 | "fee": 1000.0, 32 | "expiration_date": datetime(2020, 1, 3), 33 | } 34 | } 35 | ) 36 | self.assertDictEqual( 37 | policy["client001"], 38 | {"fee": 1000.0, "expiration_date": datetime(2020, 1, 3)}, 39 | ) 40 | 41 | policy.change_in_policy( 42 | "client001", expiration_date=datetime(2020, 1, 4) 43 | ) 44 | self.assertDictEqual( 45 | policy["client001"], 46 | {"fee": 1000.0, "expiration_date": datetime(2020, 1, 4)}, 47 | ) 48 | 49 | 50 | if __name__ == "__main__": 51 | main() 52 | -------------------------------------------------------------------------------- /book/src/ch03/src/kis.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 3: General traits of good code 2 | 3 | > Keep It Simple 4 | """ 5 | 6 | 7 | class ComplicatedNamespace: 8 | """An convoluted example of initializing an object with some properties. 9 | 10 | >>> cn = ComplicatedNamespace.init_with_data( 11 | ... id_=42, user="root", location="127.0.0.1", extra="excluded" 12 | ... ) 13 | >>> cn.id_, cn.user, cn.location 14 | (42, 'root', '127.0.0.1') 15 | 16 | >>> hasattr(cn, "extra") 17 | False 18 | 19 | """ 20 | 21 | ACCEPTED_VALUES = ("id_", "user", "location") 22 | 23 | @classmethod 24 | def init_with_data(cls, **data): 25 | instance = cls() 26 | for key, value in data.items(): 27 | if key in cls.ACCEPTED_VALUES: 28 | setattr(instance, key, value) 29 | return instance 30 | 31 | 32 | class Namespace: 33 | """Create an object from keyword arguments. 34 | 35 | >>> cn = Namespace( 36 | ... id_=42, user="root", location="127.0.0.1", extra="excluded" 37 | ... ) 38 | >>> cn.id_, cn.user, cn.location 39 | (42, 'root', '127.0.0.1') 40 | 41 | >>> hasattr(cn, "extra") 42 | False 43 | """ 44 | 45 | ACCEPTED_VALUES = ("id_", "user", "location") 46 | 47 | def __init__(self, **data): 48 | for attr_name, attr_value in data.items(): 49 | if attr_name in self.ACCEPTED_VALUES: 50 | setattr(self, attr_name, attr_value) 51 | -------------------------------------------------------------------------------- /book/src/ch03/src/multiple_inheritance.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 3: General traits of good code 2 | 3 | > Multiple inheritance: MRO 4 | 5 | """ 6 | 7 | 8 | class BaseModule: 9 | module_name = "top" 10 | 11 | def __init__(self, module_name): 12 | self.name = module_name 13 | 14 | def __str__(self): 15 | return f"{self.module_name}:{self.name}" 16 | 17 | 18 | class BaseModule1(BaseModule): 19 | module_name = "module-1" 20 | 21 | 22 | class BaseModule2(BaseModule): 23 | module_name = "module-2" 24 | 25 | 26 | class BaseModule3(BaseModule): 27 | module_name = "module-3" 28 | 29 | 30 | class ConcreteModuleA12(BaseModule1, BaseModule2): 31 | """Extend 1 & 2 32 | 33 | >>> str(ConcreteModuleA12('name')) 34 | 'module-1:name' 35 | """ 36 | 37 | 38 | class ConcreteModuleB23(BaseModule2, BaseModule3): 39 | """Extend 2 & 3 40 | 41 | >>> str(ConcreteModuleB23("test")) 42 | 'module-2:test' 43 | """ 44 | -------------------------------------------------------------------------------- /book/src/ch03/src/multiple_inheritance_2.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 3: General traits of good code 2 | 3 | > Multiple inheritance: Mixins 4 | 5 | """ 6 | 7 | 8 | class BaseTokenizer: 9 | """ 10 | >>> tk = BaseTokenizer("28a2320b-fd3f-4627-9792-a2b38e3c46b0") 11 | >>> list(tk) 12 | ['28a2320b', 'fd3f', '4627', '9792', 'a2b38e3c46b0'] 13 | """ 14 | 15 | def __init__(self, str_token): 16 | self.str_token = str_token 17 | 18 | def __iter__(self): 19 | yield from self.str_token.split("-") 20 | 21 | 22 | class UpperIterableMixin: 23 | def __iter__(self): 24 | return map(str.upper, super().__iter__()) 25 | 26 | 27 | class Tokenizer(UpperIterableMixin, BaseTokenizer): 28 | """ 29 | >>> tk = Tokenizer("28a2320b-fd3f-4627-9792-a2b38e3c46b0") 30 | >>> list(tk) 31 | ['28A2320B', 'FD3F', '4627', '9792', 'A2B38E3C46B0'] 32 | """ 33 | -------------------------------------------------------------------------------- /book/src/ch03/src/orthogonal.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 3: General traits of good code 2 | 3 | > Orthogonality 4 | 5 | """ 6 | 7 | 8 | def calculate_price(base_price: float, tax: float, discount: float) -> float: 9 | """ 10 | >>> calculate_price(10, 0.2, 0.5) 11 | 6.0 12 | 13 | >>> calculate_price(10, 0.2, 0) 14 | 12.0 15 | """ 16 | return (base_price * (1 + tax)) * (1 - discount) 17 | 18 | 19 | def show_price(price: float) -> str: 20 | """ 21 | >>> show_price(1000) 22 | '$ 1,000.00' 23 | 24 | >>> show_price(1_250.75) 25 | '$ 1,250.75' 26 | """ 27 | return "$ {0:,.2f}".format(price) 28 | 29 | 30 | def str_final_price( 31 | base_price: float, tax: float, discount: float, fmt_function=str 32 | ) -> str: 33 | """ 34 | 35 | >>> str_final_price(10, 0.2, 0.5) 36 | '6.0' 37 | 38 | >>> str_final_price(1000, 0.2, 0) 39 | '1200.0' 40 | 41 | >>> str_final_price(1000, 0.2, 0.1, fmt_function=show_price) 42 | '$ 1,080.00' 43 | 44 | """ 45 | return fmt_function(calculate_price(base_price, tax, discount)) 46 | -------------------------------------------------------------------------------- /book/src/ch03/src/packing_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 3: General Traits of Good Code 2 | 3 | > Packing / unpacking 4 | """ 5 | from dataclasses import dataclass 6 | 7 | 8 | USERS = [(i, f"first_name_{i}", f"last_name_{i}") for i in range(1_000)] 9 | 10 | 11 | @dataclass 12 | class User: 13 | user_id: int 14 | first_name: str 15 | last_name: str 16 | 17 | 18 | def bad_users_from_rows(dbrows) -> list: 19 | """A bad case (non-pythonic) of creating ``User``s from DB rows.""" 20 | return [User(row[0], row[1], row[2]) for row in dbrows] 21 | 22 | 23 | def users_from_rows(dbrows) -> list: 24 | """Create ``User``s from DB rows.""" 25 | return [ 26 | User(user_id, first_name, last_name) 27 | for (user_id, first_name, last_name) in dbrows 28 | ] 29 | 30 | 31 | def users_from_rows2(dbrows) -> list: 32 | """Create ``User``s from DB rows.""" 33 | return [User(*row) for row in dbrows] 34 | -------------------------------------------------------------------------------- /book/src/ch03/tests/test_exceptions_3.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 3: General Traits of Good Code 2 | """ 3 | import unittest 4 | 5 | from exceptions_3 import InternalDataError, process 6 | 7 | 8 | class TestExceptions(unittest.TestCase): 9 | def test_original_exception(self): 10 | try: 11 | process({}, "anything") 12 | except InternalDataError as e: 13 | self.assertIsInstance(e.__cause__, KeyError) 14 | 15 | 16 | if __name__ == "__main__": 17 | unittest.main() 18 | -------------------------------------------------------------------------------- /book/src/ch03/tests/test_kis.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 3: General traits of good code 2 | 3 | > Keep It Simple 4 | """ 5 | import unittest 6 | 7 | 8 | from kis import Namespace 9 | 10 | 11 | class TestKis(unittest.TestCase): 12 | def test_namespace(self): 13 | cn = Namespace( 14 | id_=42, user="root", location="127.0.0.1", extra="excluded" 15 | ) 16 | self.assertEqual( 17 | (cn.id_, cn.user, cn.location), (42, "root", "127.0.0.1") 18 | ) 19 | self.assertFalse(hasattr(cn, "extra")) 20 | 21 | 22 | if __name__ == "__main__": 23 | unittest.main() 24 | -------------------------------------------------------------------------------- /book/src/ch03/tests/test_packing1.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from packing_1 import ( 4 | User, 5 | bad_users_from_rows, 6 | users_from_rows, 7 | users_from_rows2, 8 | USERS, 9 | ) 10 | 11 | 12 | class TestPacking(unittest.TestCase): 13 | def test_users_list(self): 14 | for function in bad_users_from_rows, users_from_rows, users_from_rows2: 15 | with self.subTest(function=function): 16 | users = function(USERS) 17 | self.assertTrue(all(isinstance(u, User) for u in users)) 18 | 19 | 20 | if __name__ == "__main__": 21 | unittest.main() 22 | -------------------------------------------------------------------------------- /book/src/ch04/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | test: 3 | @$(PYTHON) -m doctest src/*.py 4 | @$(PYTHON) -m unittest tests/*.py 5 | 6 | .PHONY: typehint 7 | typehint: 8 | mypy src tests 9 | 10 | .PHONY: lint 11 | lint: 12 | black --check --line-length=79 src tests 13 | 14 | .PHONY: format 15 | format: 16 | black --line-length=79 src tests 17 | 18 | .PHONY: clean 19 | clean: 20 | find . -name "*.swp" -o -name "__pycache__" | xargs rm -fr 21 | -------------------------------------------------------------------------------- /book/src/ch04/README.rst: -------------------------------------------------------------------------------- 1 | Chapter 04: The SOLID Principles 2 | ================================ 3 | Code examples for chapter 4. 4 | 5 | All the commands here assume the presence of a virtual environment as described in the main section. 6 | 7 | Run tests:: 8 | 9 | make test 10 | -------------------------------------------------------------------------------- /book/src/ch04/src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmariano/Clean-code-in-Python/ebd39c190f5c347c2084d2e52cb5f2006b68206c/book/src/ch04/src/__init__.py -------------------------------------------------------------------------------- /book/src/ch04/src/dip_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Second edition 2 | Chapter 04 - The SOLID Principles 3 | 4 | DIP: Dependency Inversion Principle 5 | 6 | Demo of the dependency injection section. 7 | """ 8 | from __future__ import annotations 9 | 10 | import json 11 | 12 | from abc import ABCMeta, abstractmethod 13 | 14 | 15 | class Event: 16 | def __init__(self, content: dict) -> None: 17 | self._content = content 18 | 19 | def serialise(self): 20 | return json.dumps(self._content) 21 | 22 | 23 | class DataTargetClient(metaclass=ABCMeta): 24 | @abstractmethod 25 | def send(self, content: bytes): 26 | """Send raw content to a particular target.""" 27 | 28 | 29 | class Syslog(DataTargetClient): 30 | def send(self, content: bytes): 31 | return f"[{self.__class__.__name__}] sent {len(content)} bytes" 32 | 33 | 34 | class EventStreamer: 35 | def __init__(self, target: DataTargetClient): 36 | self.target = target 37 | 38 | def stream(self, events: list[Event]) -> None: 39 | for event in events: 40 | self.target.send(event.serialise()) 41 | -------------------------------------------------------------------------------- /book/src/ch04/src/dip_2.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Second edition 2 | Chapter 04 - The SOLID Principles 3 | 4 | DIP: Dependency Inversion Principle 5 | 6 | Demo of the dependency injection section using a library 7 | """ 8 | from __future__ import annotations 9 | 10 | import json 11 | 12 | from abc import ABCMeta, abstractmethod 13 | 14 | import pinject 15 | 16 | 17 | class Event: 18 | def __init__(self, content: dict) -> None: 19 | self._content = content 20 | 21 | def serialise(self): 22 | return json.dumps(self._content) 23 | 24 | 25 | class DataTargetClient(metaclass=ABCMeta): 26 | @abstractmethod 27 | def send(self, content: bytes): 28 | """Send raw content to a particular target.""" 29 | 30 | 31 | class Syslog(DataTargetClient): 32 | def send(self, content: bytes): 33 | return f"[{self.__class__.__name__}] sent {len(content)} bytes" 34 | 35 | 36 | class EventStreamer: 37 | def __init__(self, target: DataTargetClient): 38 | self.target = target 39 | 40 | def stream(self, events: list[Event]) -> None: 41 | for event in events: 42 | self.target.send(event.serialise()) 43 | 44 | 45 | class _EventStreamerBindingSpec(pinject.BindingSpec): 46 | def provide_target(self): 47 | return Syslog() 48 | 49 | 50 | object_graph = pinject.new_object_graph( 51 | binding_specs=[_EventStreamerBindingSpec()] 52 | ) 53 | -------------------------------------------------------------------------------- /book/src/ch04/src/isp.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Second edition 2 | Chapter 04 - The SOLID Principles 3 | 4 | ISP: Interface Segregation Principle 5 | """ 6 | 7 | from abc import ABCMeta, abstractmethod 8 | 9 | 10 | class XMLEventParser(metaclass=ABCMeta): 11 | @abstractmethod 12 | def from_xml(xml_data: str): 13 | """Parse an event from a source in XML representation.""" 14 | 15 | 16 | class JSONEventParser(metaclass=ABCMeta): 17 | @abstractmethod 18 | def from_json(json_data: str): 19 | """Parse an event from a source in JSON format.""" 20 | 21 | 22 | class EventParser(XMLEventParser, JSONEventParser): 23 | """An event parser that can create an event from source data either 24 | in XML or JSON format. 25 | """ 26 | 27 | def from_xml(xml_data): 28 | pass 29 | 30 | def from_json(json_data: str): 31 | pass 32 | -------------------------------------------------------------------------------- /book/src/ch04/src/lsp_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 4, The SOLID Principles 2 | 3 | > Liskov's Substitution Principle (LSP) 4 | 5 | Detecting violations of LSP through tools (mypy, pylint, etc.) 6 | """ 7 | 8 | 9 | class Event: 10 | ... 11 | 12 | def meets_condition(self, event_data: dict) -> bool: 13 | return False 14 | 15 | 16 | class LoginEvent(Event): 17 | def meets_condition(self, event_data: list) -> bool: 18 | return bool(event_data) 19 | 20 | 21 | class LogoutEvent(Event): 22 | def meets_condition(self, event_data: dict, override: bool) -> bool: 23 | if override: 24 | return True 25 | ... 26 | -------------------------------------------------------------------------------- /book/src/ch04/src/openclosed_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 4 2 | 3 | The open/closed principle 4 | 5 | Counter-example of the open/closed principle. 6 | 7 | An example that does not comply with this principle and should be refactored. 8 | """ 9 | from dataclasses import dataclass 10 | 11 | 12 | @dataclass 13 | class Event: 14 | raw_data: dict 15 | 16 | 17 | class UnknownEvent(Event): 18 | """A type of event that cannot be identified from its data.""" 19 | 20 | 21 | class LoginEvent(Event): 22 | """A event representing a user that has just entered the system.""" 23 | 24 | 25 | class LogoutEvent(Event): 26 | """An event representing a user that has just left the system.""" 27 | 28 | 29 | class SystemMonitor: 30 | """Identify events that occurred in the system 31 | 32 | >>> l1 = SystemMonitor({"before": {"session": 0}, "after": {"session": 1}}) 33 | >>> l1.identify_event().__class__.__name__ 34 | 'LoginEvent' 35 | 36 | >>> l2 = SystemMonitor({"before": {"session": 1}, "after": {"session": 0}}) 37 | >>> l2.identify_event().__class__.__name__ 38 | 'LogoutEvent' 39 | 40 | >>> l3 = SystemMonitor({"before": {"session": 1}, "after": {"session": 1}}) 41 | >>> l3.identify_event().__class__.__name__ 42 | 'UnknownEvent' 43 | 44 | """ 45 | 46 | def __init__(self, event_data): 47 | self.event_data = event_data 48 | 49 | def identify_event(self): 50 | if ( 51 | self.event_data["before"]["session"] == 0 52 | and self.event_data["after"]["session"] == 1 53 | ): 54 | return LoginEvent(self.event_data) 55 | elif ( 56 | self.event_data["before"]["session"] == 1 57 | and self.event_data["after"]["session"] == 0 58 | ): 59 | return LogoutEvent(self.event_data) 60 | 61 | return UnknownEvent(self.event_data) 62 | -------------------------------------------------------------------------------- /book/src/ch04/srp_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 4, The SOLID Principles 2 | 3 | > SRP: Single Responsibility Principle 4 | """ 5 | 6 | 7 | class SystemMonitor: 8 | def load_activity(self): 9 | """Get the events from a source, to be processed.""" 10 | 11 | def identify_events(self): 12 | """Parse the source raw data into events (domain objects).""" 13 | 14 | def stream_events(self): 15 | """Send the parsed events to an external agent.""" 16 | -------------------------------------------------------------------------------- /book/src/ch04/tests/test_dip.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from dip_1 import EventStreamer, DataTargetClient, Event 4 | from dip_2 import object_graph, EventStreamer as EventStreamer2, Syslog 5 | 6 | 7 | class DoubleClient(DataTargetClient): 8 | def __init__(self): 9 | self._sent_count = 0 10 | 11 | def send(self, content: bytes): 12 | self._sent_count += 1 13 | return "" 14 | 15 | 16 | class TestEventStreamer(unittest.TestCase): 17 | def test_stream(self): 18 | double_client = DoubleClient() 19 | event_streamer = EventStreamer(double_client) 20 | events_data = [ 21 | Event({"transaction": "tx001"}), 22 | Event({"transaction": "tx002"}), 23 | ] 24 | event_streamer.stream(events_data) 25 | self.assertEqual(double_client._sent_count, len(events_data)) 26 | 27 | def test_dependency_injected(self): 28 | event_streamer = object_graph.provide(EventStreamer2) 29 | 30 | self.assertIsInstance(event_streamer, EventStreamer2) 31 | self.assertIsInstance(event_streamer.target, Syslog) 32 | 33 | 34 | if __name__ == "__main__": 35 | unittest.main() 36 | -------------------------------------------------------------------------------- /book/src/ch04/tests/test_isp.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from isp import EventParser 4 | 5 | 6 | class TestEventParser(unittest.TestCase): 7 | def test_parse_from_xml(self): 8 | self.assertIsInstance(EventParser(), EventParser) 9 | 10 | 11 | if __name__ == "__main__": 12 | unittest.main() 13 | -------------------------------------------------------------------------------- /book/src/ch04/tests/test_lsp.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 4, The SOLID Principles 2 | 3 | Liskov's Substitution Principle (LSP) 4 | Tests 5 | """ 6 | import unittest 7 | 8 | from lsp_2 import Event 9 | 10 | 11 | class TestLSP(unittest.TestCase): 12 | def test_vaidate_preconditions(self): 13 | invalid_events = ( 14 | "not a dict", 15 | {"reason": "doesn't contain key 'before'", "after": {"foo": "1"}}, 16 | {"reason": "doesn't contain key 'after'", "before": {"foo": "1"}}, 17 | {"before": "'before' is not a dict'", "after": {"foo": "1"}}, 18 | {"after": "not a dict", "before": {"foo": "1"}}, 19 | ) 20 | for event in invalid_events: 21 | with self.subTest(event=event), self.assertRaises(ValueError): 22 | Event.validate_precondition(event) 23 | 24 | def test_valid_precondition(self): 25 | self.assertIsNone( 26 | Event.validate_precondition( 27 | {"before": {"foo": 42}, "after": {"bar": 42}} 28 | ) 29 | ) 30 | 31 | 32 | if __name__ == "__main__": 33 | unittest.main() 34 | -------------------------------------------------------------------------------- /book/src/ch04/tests/test_ocp.py: -------------------------------------------------------------------------------- 1 | """Clean code in Python - Second edition 2 | Chapter 04, the SOLID principles 3 | 4 | Tests for the Open/Closed Principle (OCP) examples 1 through 3 5 | """ 6 | import unittest 7 | 8 | from openclosed_1 import SystemMonitor as SystemMonitor1 9 | from openclosed_2 import SystemMonitor as SystemMonitor2 10 | from openclosed_3 import SystemMonitor as SystemMonitor3 11 | 12 | 13 | class TestBaseMixin: 14 | def test_identify_event(self): 15 | for event_data, expected_event_name in self.test_cases: 16 | monitor = self.class_under_test(event_data) 17 | identified_event = monitor.identify_event() 18 | with self.subTest(data=event_data, expected=expected_event_name): 19 | self.assertEqual( 20 | identified_event.__class__.__name__, expected_event_name 21 | ) 22 | 23 | 24 | class BaseTestOCP(unittest.TestCase): 25 | test_cases = ( 26 | ({"before": {"session": 0}, "after": {"session": 1}}, "LoginEvent"), 27 | ({"before": {"session": 1}, "after": {"session": 0}}, "LogoutEvent"), 28 | ({"before": {"session": 1}, "after": {"session": 1}}, "UnknownEvent"), 29 | ) 30 | 31 | 32 | class TestOCP1(TestBaseMixin, BaseTestOCP): 33 | class_under_test = SystemMonitor1 34 | 35 | 36 | class TestOCP2(TestBaseMixin, BaseTestOCP): 37 | class_under_test = SystemMonitor2 38 | 39 | 40 | class TestOCP3(TestBaseMixin, BaseTestOCP): 41 | class_under_test = SystemMonitor3 42 | test_cases = ( 43 | *BaseTestOCP.test_cases, 44 | ({"after": {"transaction": "Tx001"}}, "TransactionEvent"), 45 | ({"after": {"not-a-transaction": "Tx001"}}, "UnknownEvent"), 46 | ) 47 | 48 | 49 | if __name__ == "__main__": 50 | unittest.main() 51 | -------------------------------------------------------------------------------- /book/src/ch05/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | test: 3 | @$(PYTHON) -m doctest tests/*.py 4 | @$(PYTHON) -m unittest tests/*.py 5 | 6 | .PHONY: typehint 7 | typehint: 8 | mypy src tests 9 | 10 | .PHONY: checklist 11 | checklist: lint typehint test 12 | 13 | .PHONY: lint 14 | lint: 15 | black --check --line-length=79 \ 16 | --exclude src/pep0614.py \ 17 | src tests 18 | 19 | .PHONY: format 20 | format: 21 | black --line-length=79 \ 22 | --exclude src/pep0614.py \ 23 | src tests 24 | 25 | .PHONY: clean 26 | clean: 27 | find . -name "*.swp" -o -name "__pycache__" | xargs rm -fr 28 | -------------------------------------------------------------------------------- /book/src/ch05/README.rst: -------------------------------------------------------------------------------- 1 | Chapter 05 - Decorators 2 | ======================= 3 | The source files are in the `src/` directory and the tests files under `tests/`. 4 | 5 | Run the tests with:: 6 | 7 | make test 8 | 9 | Creating Decorators 10 | ^^^^^^^^^^^^^^^^^^^ 11 | 12 | 1. Function decorators 13 | 14 | 1.1 ``decorator_function_1.py``. 15 | 16 | 1.2 ``decorator_function_2.py`` 17 | 18 | 2. Class decorators 19 | 20 | 2.1 ``decorator_class_1.py`` 21 | 22 | 2.2 ``decorator_class_2.py`` 23 | 24 | 2.3 ``decorator_class_3.py`` 25 | 26 | 3. Other decorators (generators, coroutines, etc.). 27 | 28 | 4. Passing Arguments to Decorators 29 | 30 | 4.1 As a decorator function: ``decorator_parametrized_1.py`` 31 | 32 | 4.2 As a decorator object: ``decorator_parametrized_2.py`` 33 | 34 | 35 | Issues to avoid when creating decorators 36 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 37 | 38 | 1. Keep the properties of the original attributes (docstring, name, etc.), 39 | by using ``functools.wraps``. 40 | 41 | 1.1 ``decorator_wraps_1.py`` 42 | 43 | 2. Don't have side effects on the main body of the decorator. This will run 44 | at parsing time, and will most likely fail. 45 | 46 | 2.1 ``decorator_side_effects_1.py`` 47 | 48 | 2.2 ``decorator_side_effects_2.py`` 49 | 50 | 3. Make sure the decorated function is equivalent to the wrapped one, in 51 | terms of inspection, signature checking, etc. 52 | 53 | 3.1 Create decorators that work for functions, methods, static methods, class methods, etc. 54 | 55 | 3.2 Use the ``wrapt`` package to create effective decorators. 56 | 57 | 58 | Other Topics 59 | ^^^^^^^^^^^^ 60 | 61 | * The DRY Principle with Decorators (reusing code). 62 | * Separation of Concerns with Decorators. 63 | 64 | Listings: ``decorator_SoC_{1,2}.py`` 65 | -------------------------------------------------------------------------------- /book/src/ch05/src/composition_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 5: Decorators 2 | 3 | Composition over inheritance, example 1 4 | """ 5 | from dataclasses import dataclass 6 | 7 | 8 | class BaseResolverMixin: 9 | def __getattr__(self, attr: str): 10 | if attr.startswith("resolve_"): 11 | *_, actual_attr = attr.partition("resolve_") 12 | else: 13 | actual_attr = attr 14 | try: 15 | return self.__dict__[actual_attr] 16 | except KeyError as e: 17 | raise AttributeError from e 18 | 19 | 20 | @dataclass 21 | class Customer(BaseResolverMixin): 22 | customer_id: str 23 | name: str 24 | address: str 25 | -------------------------------------------------------------------------------- /book/src/ch05/src/composition_2.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 5: Decorators 2 | 3 | Composition over inheritance, example 1 4 | """ 5 | from dataclasses import dataclass 6 | 7 | 8 | def _resolver_method(self, attr): 9 | """The resolution method of attributes that will replace __getattr__.""" 10 | if attr.startswith("resolve_"): 11 | *_, actual_attr = attr.partition("resolve_") 12 | else: 13 | actual_attr = attr 14 | try: 15 | return self.__dict__[actual_attr] 16 | except KeyError as e: 17 | raise AttributeError from e 18 | 19 | 20 | def with_resolver(cls): 21 | """Set the custom resolver method to a class.""" 22 | cls.__getattr__ = _resolver_method 23 | return cls 24 | 25 | 26 | @dataclass 27 | @with_resolver 28 | class Customer: 29 | customer_id: str 30 | name: str 31 | address: str 32 | -------------------------------------------------------------------------------- /book/src/ch05/src/coroutines.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 5: Decorators 2 | 3 | > Decorators for coroutines & functions 4 | """ 5 | import inspect 6 | import asyncio 7 | from functools import wraps 8 | import time 9 | 10 | X, Y = 1, 2 11 | 12 | 13 | def decorator(callable): 14 | """Call with fixed values""" 15 | 16 | @wraps(callable) 17 | def wrapped(): 18 | return callable(X, Y) 19 | 20 | return wrapped 21 | 22 | 23 | @decorator 24 | def func(x, y): 25 | return x + y 26 | 27 | 28 | @decorator 29 | async def coro(x, y): 30 | return x + y 31 | 32 | 33 | def timing(callable): 34 | @wraps(callable) 35 | def wrapped(*args, **kwargs): 36 | start = time.time() 37 | result = callable(*args, **kwargs) 38 | latency = time.time() - start 39 | return {"latency": latency, "result": result} 40 | 41 | @wraps(callable) 42 | async def wrapped_coro(*args, **kwargs): 43 | start = time.time() 44 | result = await callable(*args, **kwargs) 45 | latency = time.time() - start 46 | return {"latency": latency, "result": result} 47 | 48 | if inspect.iscoroutinefunction(callable): 49 | return wrapped_coro 50 | 51 | return wrapped 52 | 53 | 54 | @timing 55 | def func2(): 56 | time.sleep(0.1) 57 | return 42 58 | 59 | 60 | @timing 61 | async def coro2(): 62 | await asyncio.sleep(0.1) 63 | return 42 64 | -------------------------------------------------------------------------------- /book/src/ch05/src/decorator_SoC_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 5: Decorators 2 | 3 | > Separation of Concerns (SoC). 4 | Break a coupled decorator into smaller ones. 5 | """ 6 | import functools 7 | import time 8 | 9 | from log import logger 10 | 11 | 12 | def traced_function(function): 13 | @functools.wraps(function) 14 | def wrapped(*args, **kwargs): 15 | logger.info("started execution of %s", function.__qualname__) 16 | start_time = time.time() 17 | result = function(*args, **kwargs) 18 | logger.info( 19 | "function %s took %.2fs", 20 | function.__qualname__, 21 | time.time() - start_time, 22 | ) 23 | return result 24 | 25 | return wrapped 26 | 27 | 28 | @traced_function 29 | def operation1(): 30 | time.sleep(2) 31 | logger.info("running operation 1") 32 | return 2 33 | -------------------------------------------------------------------------------- /book/src/ch05/src/decorator_SoC_2.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 5 2 | 3 | Separation of Concerns (SoC). 4 | Break a coupled decorator into smaller ones. 5 | """ 6 | import time 7 | from functools import wraps 8 | 9 | from log import logger 10 | 11 | 12 | def log_execution(function): 13 | @wraps(function) 14 | def wrapped(*args, **kwargs): 15 | logger.info("started execution of %s", function.__qualname__) 16 | return function(*kwargs, **kwargs) 17 | 18 | return wrapped 19 | 20 | 21 | def measure_time(function): 22 | @wraps(function) 23 | def wrapped(*args, **kwargs): 24 | start_time = time.time() 25 | result = function(*args, **kwargs) 26 | 27 | logger.info( 28 | "function %s took %.2f", 29 | function.__qualname__, 30 | time.time() - start_time, 31 | ) 32 | return result 33 | 34 | return wrapped 35 | 36 | 37 | @measure_time 38 | @log_execution 39 | def operation(): 40 | time.sleep(3) 41 | logger.info("running operation...") 42 | return 33 43 | -------------------------------------------------------------------------------- /book/src/ch05/src/decorator_class_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 5: Decorators 2 | 3 | > Class decorators. 4 | """ 5 | from datetime import datetime 6 | from dataclasses import dataclass 7 | 8 | 9 | class LoginEventSerializer: 10 | def __init__(self, event): 11 | self.event = event 12 | 13 | def serialize(self) -> dict: 14 | return { 15 | "username": self.event.username, 16 | "password": "**redacted**", 17 | "ip": self.event.ip, 18 | "timestamp": self.event.timestamp.strftime("%Y-%m-%d %H:%M"), 19 | } 20 | 21 | 22 | @dataclass 23 | class LoginEvent: 24 | SERIALIZER = LoginEventSerializer 25 | 26 | username: str 27 | password: str 28 | ip: str 29 | timestamp: datetime 30 | 31 | def serialize(self) -> dict: 32 | return self.SERIALIZER(self).serialize() 33 | -------------------------------------------------------------------------------- /book/src/ch05/src/decorator_function_2.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 5: Decorators 2 | 3 | Example for the first section: the decorator doesn't allow parameters. 4 | > Function decorators 5 | - Creating a decorator to be applied over a function. 6 | - Implement the decorator as an object. 7 | """ 8 | from functools import wraps 9 | 10 | from decorator_function_1 import ControlledException 11 | from log import logger 12 | 13 | 14 | class Retry: 15 | def __init__(self, operation): 16 | wraps(operation)(self) 17 | self.operation = operation 18 | 19 | def __call__(self, *args, **kwargs): 20 | last_raised = None 21 | RETRIES_LIMIT = 3 22 | for _ in range(RETRIES_LIMIT): 23 | try: 24 | return self.operation(*args, **kwargs) 25 | except ControlledException as e: 26 | logger.info("retrying %s", self.operation.__qualname__) 27 | last_raised = e 28 | raise last_raised 29 | 30 | 31 | @Retry 32 | def run_operation(task): 33 | """Run the operation in the task""" 34 | return task.run() 35 | -------------------------------------------------------------------------------- /book/src/ch05/src/decorator_parametrized_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 5: Decorators 2 | 3 | Parametrized decorators using functions 4 | """ 5 | 6 | from functools import wraps 7 | from typing import Sequence, Optional 8 | 9 | from decorator_function_1 import ControlledException 10 | from log import logger 11 | 12 | 13 | _DEFAULT_RETRIES_LIMIT = 3 14 | 15 | 16 | def with_retry( 17 | retries_limit: int = _DEFAULT_RETRIES_LIMIT, 18 | allowed_exceptions: Optional[Sequence[Exception]] = None, 19 | ): 20 | allowed_exceptions = allowed_exceptions or (ControlledException,) # type: ignore 21 | 22 | def retry(operation): 23 | @wraps(operation) 24 | def wrapped(*args, **kwargs): 25 | last_raised = None 26 | for _ in range(retries_limit): 27 | try: 28 | return operation(*args, **kwargs) 29 | except allowed_exceptions as e: 30 | logger.warning( 31 | "retrying %s due to %s", operation.__qualname__, e 32 | ) 33 | last_raised = e 34 | raise last_raised 35 | 36 | return wrapped 37 | 38 | return retry 39 | -------------------------------------------------------------------------------- /book/src/ch05/src/decorator_parametrized_2.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 5: Decorators 2 | 3 | Parametrized decorators using callable objects. 4 | """ 5 | from functools import wraps 6 | from typing import Optional, Sequence 7 | 8 | from decorator_function_1 import ControlledException 9 | from log import logger 10 | 11 | _DEFAULT_RETRIES_LIMIT = 3 12 | 13 | 14 | class WithRetry: 15 | def __init__( 16 | self, 17 | retries_limit: int = _DEFAULT_RETRIES_LIMIT, 18 | allowed_exceptions: Optional[Sequence[Exception]] = None, 19 | ) -> None: 20 | self.retries_limit = retries_limit 21 | self.allowed_exceptions = allowed_exceptions or (ControlledException,) 22 | 23 | def __call__(self, operation): 24 | @wraps(operation) 25 | def wrapped(*args, **kwargs): 26 | last_raised = None 27 | for _ in range(self.retries_limit): 28 | try: 29 | return operation(*args, **kwargs) 30 | except self.allowed_exceptions as e: 31 | logger.warning( 32 | "retrying %s due to %s", operation.__qualname__, e 33 | ) 34 | last_raised = e 35 | raise last_raised 36 | 37 | return wrapped 38 | -------------------------------------------------------------------------------- /book/src/ch05/src/decorator_side_effects_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 5: Decorators 2 | 3 | > Undesired side effects on decorators 4 | """ 5 | 6 | import time 7 | from functools import wraps 8 | 9 | from log import logger 10 | 11 | 12 | def traced_function_wrong(function): 13 | """An example of a badly defined decorator.""" 14 | logger.info("started execution of %s", function) 15 | start_time = time.time() 16 | 17 | @wraps(function) 18 | def wrapped(*args, **kwargs): 19 | result = function(*args, **kwargs) 20 | logger.info( 21 | "function %s took %.2fs", function, time.time() - start_time 22 | ) 23 | return result 24 | 25 | return wrapped 26 | 27 | 28 | @traced_function_wrong 29 | def process_with_delay(callback, delay=0): 30 | logger.info("sleep(%d)", delay) 31 | return callback 32 | 33 | 34 | def traced_function(function): 35 | @wraps(function) 36 | def wrapped(*args, **kwargs): 37 | logger.info("started execution of %s", function) 38 | start_time = time.time() 39 | result = function(*args, **kwargs) 40 | logger.info( 41 | "function %s took %.2fs", function, time.time() - start_time 42 | ) 43 | return result 44 | 45 | return wrapped 46 | 47 | 48 | @traced_function 49 | def call_with_delay(callback, delay=0): 50 | logger.info("sleep(%d)", delay) 51 | return callback 52 | -------------------------------------------------------------------------------- /book/src/ch05/src/decorator_side_effects_2.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 5: Decorators 2 | 3 | > Example of desired side effects on decorators 4 | 5 | """ 6 | 7 | 8 | EVENTS_REGISTRY = {} 9 | 10 | 11 | def register_event(event_cls): 12 | """Place the class for the event into the registry to make it accessible in 13 | the module. 14 | """ 15 | EVENTS_REGISTRY[event_cls.__name__] = event_cls 16 | return event_cls 17 | 18 | 19 | class Event: 20 | """A base event object""" 21 | 22 | 23 | class UserEvent: 24 | TYPE = "user" 25 | 26 | 27 | @register_event 28 | class UserLoginEvent(UserEvent): 29 | """Represents the event of a user when it has just accessed the system.""" 30 | 31 | 32 | @register_event 33 | class UserLogoutEvent(UserEvent): 34 | """Event triggered right after a user abandoned the system.""" 35 | 36 | 37 | def test(): 38 | """ 39 | >>> sorted(EVENTS_REGISTRY.keys()) == sorted(('UserLoginEvent', 'UserLogoutEvent')) 40 | True 41 | """ 42 | -------------------------------------------------------------------------------- /book/src/ch05/src/decorator_universal_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 5: Decorators 2 | 3 | Universal Decorators: create decorators that can be applied to several 4 | different objects (e.g. functions, methods), and won't fail. 5 | 6 | """ 7 | from functools import wraps 8 | 9 | from log import logger 10 | 11 | 12 | class DBDriver: 13 | def __init__(self, dbstring: str) -> None: 14 | self.dbstring = dbstring 15 | 16 | def execute(self, query: str) -> str: 17 | return f"query {query} at {self.dbstring}" 18 | 19 | 20 | def inject_db_driver(function): 21 | """This decorator converts the parameter by creating a ``DBDriver`` 22 | instance from the database dsn string. 23 | """ 24 | 25 | @wraps(function) 26 | def wrapped(dbstring): 27 | return function(DBDriver(dbstring)) 28 | 29 | return wrapped 30 | 31 | 32 | @inject_db_driver 33 | def run_query(driver): 34 | return driver.execute("test_function") 35 | 36 | 37 | class DataHandler: 38 | """The decorator will not work for methods as it is defined.""" 39 | 40 | @inject_db_driver 41 | def run_query(self, driver): 42 | return driver.execute(self.__class__.__name__) 43 | -------------------------------------------------------------------------------- /book/src/ch05/src/decorator_universal_2.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 5: Decorators 2 | 3 | > Universal Decorators: create decorators that can be applied to several 4 | different objects (e.g. functions, methods), and won't fail. 5 | 6 | - Fix the failing decorator 7 | """ 8 | 9 | from functools import wraps 10 | from types import MethodType 11 | 12 | 13 | class DBDriver: 14 | def __init__(self, dbstring: str) -> None: 15 | self.dbstring = dbstring 16 | 17 | def execute(self, query: str) -> str: 18 | return f"query {query} at {self.dbstring}" 19 | 20 | 21 | class inject_db_driver: 22 | """Convert a string to a DBDriver instance and pass this to the wrapped 23 | function. 24 | """ 25 | 26 | def __init__(self, function) -> None: 27 | self.function = function 28 | wraps(self.function)(self) 29 | 30 | def __call__(self, dbstring): 31 | return self.function(DBDriver(dbstring)) 32 | 33 | def __get__(self, instance, owner): 34 | if instance is None: 35 | return self 36 | return self.__class__(MethodType(self.function, instance)) 37 | 38 | 39 | @inject_db_driver 40 | def run_query(driver): 41 | return driver.execute("test_function_2") 42 | 43 | 44 | class DataHandler: 45 | @inject_db_driver 46 | def run_query(self, driver): 47 | return driver.execute("test_method_2") 48 | -------------------------------------------------------------------------------- /book/src/ch05/src/decorator_wraps_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 5: Decorators 2 | 3 | > functools.wraps 4 | 5 | """ 6 | 7 | from log import logger 8 | 9 | 10 | def trace_decorator(function): 11 | def wrapped(*args, **kwargs): 12 | logger.info("running %s", function.__qualname__) 13 | return function(*args, **kwargs) 14 | 15 | return wrapped 16 | 17 | 18 | @trace_decorator 19 | def process_account(account_id: str): 20 | """Process an account by Id.""" 21 | logger.info("processing account %s", account_id) 22 | ... 23 | -------------------------------------------------------------------------------- /book/src/ch05/src/decorator_wraps_2.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 5: Decorators 2 | 3 | > functools.wraps 4 | """ 5 | 6 | from functools import wraps 7 | 8 | from log import logger 9 | 10 | 11 | def trace_decorator(function): 12 | """Log when a function is being called.""" 13 | 14 | @wraps(function) 15 | def wrapped(*args, **kwargs): 16 | logger.info("running %s", function.__qualname__) 17 | return function(*args, **kwargs) 18 | 19 | return wrapped 20 | 21 | 22 | @trace_decorator 23 | def process_account(account_id: str): 24 | """Process an account by Id.""" 25 | logger.info("processing account %s", account_id) 26 | ... 27 | 28 | 29 | def decorator(original_function): 30 | @wraps(original_function) 31 | def decorated_function(*args, **kwargs): 32 | # modifications done by the decorator ... 33 | return original_function(*args, **kwargs) 34 | 35 | return decorated_function 36 | -------------------------------------------------------------------------------- /book/src/ch05/src/default_arguments.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 5: Decorators 2 | 3 | Simplified calls for decorators with default arguments. 4 | """ 5 | from functools import wraps, partial 6 | 7 | DEFAULT_X = 1 8 | DEFAULT_Y = 2 9 | 10 | 11 | def decorator(function=None, *, x=DEFAULT_X, y=DEFAULT_Y): 12 | if function is None: 13 | return partial( 14 | decorator, x=x, y=y 15 | ) # also lambda f: decorator(f, x=x, y=y) 16 | 17 | @wraps(function) 18 | def wrapped(): 19 | return function(x, y) 20 | 21 | return wrapped 22 | -------------------------------------------------------------------------------- /book/src/ch05/src/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") 4 | logger = logging.getLogger(__name__) 5 | -------------------------------------------------------------------------------- /book/src/ch05/src/pep0614.py: -------------------------------------------------------------------------------- 1 | """Clean code in Python - chapter 05 2 | 3 | > PEP0614: Relaxing Grammar restrictions on decorators 4 | https://www.python.org/dev/peps/pep-0614/ 5 | """ 6 | 7 | 8 | def _log(f, *args, **kwargs): 9 | print(f"calling {f.__qualname__!r} with {args=} and {kwargs=}") 10 | return f(*args, **kwargs) 11 | 12 | 13 | @(lambda f: lambda *args, **kwargs: _log(f, *args, **kwargs)) 14 | def func(x): 15 | return x + 1 16 | -------------------------------------------------------------------------------- /book/src/ch05/tests/test_class_decorator.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 5: Decorators 2 | Unit tests for class decorators 3 | """ 4 | import unittest 5 | from datetime import datetime 6 | from decorator_class_1 import LoginEvent as LoginEvent1 7 | from decorator_class_2 import LoginEvent as LoginEvent2 8 | 9 | 10 | class TestLoginEventSerialized(unittest.TestCase): 11 | classes_under_test = (LoginEvent1, LoginEvent2) 12 | 13 | def test_serializetion(self): 14 | for class_ in self.classes_under_test: 15 | with self.subTest(case=class_): 16 | event = class_( 17 | "username", 18 | "password", 19 | "127.0.0.1", 20 | datetime(2016, 7, 20, 15, 45), 21 | ) 22 | expected = { 23 | "username": "username", 24 | "password": "**redacted**", 25 | "ip": "127.0.0.1", 26 | "timestamp": "2016-07-20 15:45", 27 | } 28 | self.assertEqual(event.serialize(), expected) 29 | 30 | 31 | if __name__ == "__main__": 32 | unittest.main() 33 | -------------------------------------------------------------------------------- /book/src/ch05/tests/test_composition.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 5: Decorators 2 | 3 | 4 | Composition over inheritance, tests for examples 1 & 2 5 | """ 6 | import unittest 7 | 8 | from composition_1 import Customer as Customer1 9 | from composition_2 import Customer as Customer2 10 | 11 | 12 | class BaseTestMixin: 13 | def test_resolver_finds_attributes(self): 14 | with self.subTest(test_class=self._CLASS_TO_TEST): 15 | customer = self._CLASS_TO_TEST(1, "foo", "address") 16 | 17 | self.assertEqual(customer.resolve_customer_id, 1) 18 | self.assertEqual(customer.resolve_name, "foo") 19 | self.assertEqual(customer.resolve_address, "address") 20 | self.assertEqual(customer.customer_id, 1) 21 | 22 | def test_resolver_attribute_error(self): 23 | with self.subTest(test_class=self._CLASS_TO_TEST): 24 | customer = self._CLASS_TO_TEST(1, "foo", "address") 25 | 26 | self.assertEqual(customer.name, "foo") 27 | with self.assertRaises(AttributeError): 28 | customer.resolve_foo 29 | 30 | 31 | class TestInheritance(BaseTestMixin, unittest.TestCase): 32 | _CLASS_TO_TEST = Customer1 33 | 34 | 35 | class TestDecorator(BaseTestMixin, unittest.TestCase): 36 | _CLASS_TO_TEST = Customer2 37 | 38 | 39 | if __name__ == "__main__": 40 | unittest.main() 41 | -------------------------------------------------------------------------------- /book/src/ch05/tests/test_coroutines.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from coroutines import X, Y, func, coro, func2, coro2 4 | 5 | 6 | class TestCoroutinesDecorators(unittest.IsolatedAsyncioTestCase): 7 | def test_function(self): 8 | self.assertEqual(func(), X + Y) 9 | 10 | async def test_coroutine(self): 11 | self.assertEqual(await coro(), X + Y) 12 | 13 | def test_timing_function(self): 14 | result = func2() 15 | self.assertTrue(result["latency"] >= 0.1) 16 | self.assertEqual(result["result"], 42) 17 | 18 | async def test_timing_coroutine(self): 19 | result = await coro2() 20 | self.assertTrue(result["latency"] >= 0.1) 21 | self.assertEqual(result["result"], 42) 22 | 23 | 24 | if __name__ == "__main__": 25 | unittest.main() 26 | -------------------------------------------------------------------------------- /book/src/ch05/tests/test_decorator_SoC.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 5: Decorators 2 | 3 | Test for separation of concerns with decorators. 4 | 5 | """ 6 | import unittest 7 | from unittest import mock 8 | 9 | from decorator_SoC_1 import operation1 10 | from decorator_SoC_2 import operation 11 | 12 | 13 | def mocked_time(): 14 | _delta_time = 0 15 | 16 | def time(): 17 | nonlocal _delta_time 18 | _delta_time += 1 19 | return _delta_time 20 | 21 | return time 22 | 23 | 24 | @mock.patch("time.sleep") 25 | @mock.patch("time.time", side_effect=mocked_time()) 26 | @mock.patch("decorator_SoC_1.logger") 27 | class TestSoC1(unittest.TestCase): 28 | def test_operation(self, logger, time, sleep): 29 | operation1() 30 | expected_calls = [ 31 | mock.call("started execution of %s", "operation1"), 32 | mock.call("running operation 1"), 33 | mock.call("function %s took %.2fs", "operation1", 1), 34 | ] 35 | logger.info.assert_has_calls(expected_calls) 36 | 37 | 38 | @mock.patch("time.sleep") 39 | @mock.patch("time.time", side_effect=mocked_time()) 40 | @mock.patch("decorator_SoC_2.logger") 41 | class TestSoC2(unittest.TestCase): 42 | def test_operation(self, logger, time, sleep): 43 | operation() 44 | expected_calls = [ 45 | mock.call("started execution of %s", "operation"), 46 | mock.call("running operation..."), 47 | mock.call("function %s took %.2f", "operation", 1), 48 | ] 49 | logger.info.assert_has_calls(expected_calls) 50 | 51 | 52 | if __name__ == "__main__": 53 | unittest.main() 54 | -------------------------------------------------------------------------------- /book/src/ch05/tests/test_decorator_universal.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 5: Decorators 2 | 3 | Tests for decorator_universal_1 4 | 5 | """ 6 | 7 | from unittest import TestCase, main, mock 8 | 9 | from decorator_universal_1 import DataHandler, run_query 10 | from decorator_universal_2 import DataHandler as DataHandler_2 11 | from decorator_universal_2 import run_query as run_query_2 12 | 13 | 14 | class TestDecorator(TestCase): 15 | def setUp(self): 16 | self.logger = mock.patch("log.logger.info").start() 17 | 18 | def tearDown(self): 19 | self.logger.stop() 20 | 21 | def test_decorator_function_ok(self): 22 | self.assertEqual( 23 | run_query("function_ok"), "query test_function at function_ok" 24 | ) 25 | 26 | def test_decorator_method_fails(self): 27 | data_handler = DataHandler() 28 | self.assertRaisesRegex( 29 | TypeError, 30 | "\S+ takes \d+ positional argument but \d+ were given", 31 | data_handler.run_query, 32 | "method_fails", 33 | ) 34 | 35 | def test_decorator_function_2(self): 36 | self.assertEqual( 37 | run_query_2("second_works"), 38 | "query test_function_2 at second_works", 39 | ) 40 | 41 | def test_decorator_method_2(self): 42 | data_handler = DataHandler_2() 43 | self.assertEqual( 44 | data_handler.run_query("method_2"), 45 | "query test_method_2 at method_2", 46 | ) 47 | 48 | 49 | if __name__ == "__main__": 50 | main() 51 | -------------------------------------------------------------------------------- /book/src/ch05/tests/test_default_arguments.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 5: Decorators 2 | 3 | tests for default_arguments.py 4 | """ 5 | 6 | import unittest 7 | 8 | from default_arguments import decorator, DEFAULT_X, DEFAULT_Y 9 | 10 | 11 | class TestDefaultArgumentsDecorator(unittest.TestCase): 12 | def test_default_callable(self): 13 | @decorator() 14 | def my_function(x, y): 15 | return {"x": x, "y": y} 16 | 17 | obtained = my_function() 18 | self.assertDictEqual(obtained, {"x": DEFAULT_X, "y": DEFAULT_Y}) 19 | 20 | def test_default_no_callable(self): 21 | @decorator 22 | def my_function(x, y): 23 | return {"x": x, "y": y} 24 | 25 | obtained = my_function() 26 | self.assertDictEqual(obtained, {"x": DEFAULT_X, "y": DEFAULT_Y}) 27 | 28 | def test_one_argument(self): 29 | @decorator(x=2) 30 | def f1(x, y): 31 | return x + y 32 | 33 | @decorator(y=3) 34 | def f2(x, y): 35 | return x + y 36 | 37 | self.assertEqual(f1(), 2 + DEFAULT_Y) 38 | self.assertEqual(f2(), DEFAULT_X + 3) 39 | 40 | def test_all_arguments(self): 41 | @decorator(x=3, y=4) 42 | def my_function(x, y): 43 | return x + y 44 | 45 | self.assertEqual(my_function(), 3 + 4) 46 | 47 | 48 | if __name__ == "__main__": 49 | unittest.main() 50 | -------------------------------------------------------------------------------- /book/src/ch05/tests/test_wraps.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 5: Decorators 2 | 3 | Tests for examples with ``functools.wraps`` 4 | """ 5 | 6 | import unittest 7 | 8 | from decorator_wraps_1 import process_account as process_account_1 9 | from decorator_wraps_2 import process_account as process_account_2 10 | 11 | 12 | class TestWraps1(unittest.TestCase): 13 | def test_name_incorrect(self): 14 | self.assertEqual( 15 | process_account_1.__qualname__, "trace_decorator..wrapped" 16 | ) 17 | 18 | def test_no_docstring(self): 19 | self.assertIsNone(process_account_1.__doc__) 20 | 21 | def test_no_annotations(self): 22 | self.assertDictEqual(process_account_1.__annotations__, {}) 23 | 24 | 25 | class TestWraps2(unittest.TestCase): 26 | def test_name_solved(self): 27 | self.assertEqual(process_account_2.__qualname__, "process_account") 28 | 29 | def test_docsting_preserved(self): 30 | self.assertTrue(process_account_2.__doc__.startswith("Process")) 31 | 32 | def test_annotations(self): 33 | self.assertDictEqual( 34 | process_account_2.__annotations__, {"account_id": str} 35 | ) 36 | 37 | 38 | if __name__ == "__main__": 39 | unittest.main() 40 | -------------------------------------------------------------------------------- /book/src/ch06/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean 2 | clean: 3 | find . -name "*.swp" -o -name "__pycache__" | xargs rm -fr 4 | 5 | .PHONY: test 6 | test: 7 | @$(PYTHON) -m doctest src/*.py 8 | @$(PYTHON) -m unittest tests/*.py 9 | 10 | .PHONY: typehint 11 | typehint: 12 | mypy src tests 13 | 14 | .PHONY: lint 15 | lint: 16 | black --check --line-length=79 src tests 17 | 18 | .PHONY: format 19 | format: 20 | black --line-length=79 src tests 21 | 22 | .PHONY: checklist 23 | checklist: lint typehint test 24 | -------------------------------------------------------------------------------- /book/src/ch06/README.rst: -------------------------------------------------------------------------------- 1 | Getting More out of our Objects With Descriptors 2 | ================================================ 3 | 4 | Run tests:: 5 | 6 | make test 7 | 8 | A First Look at Descriptors 9 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 10 | 11 | * ``descriptor_1.py``: First high-level example illustrating the descriptor 12 | protocol. 13 | 14 | 15 | Methods 16 | ------- 17 | 18 | Files for these examples are ``descriptors_methods_{1,2,3,4}.py``, respectively 19 | for: 20 | 21 | * ``__get__`` 22 | * ``__set__`` 23 | * ``__delete__`` 24 | * ``__set_name__`` 25 | 26 | Descriptors in Action 27 | --------------------- 28 | 29 | * An application of descriptors: ``descriptors_pythonic_{1,2}.py`` 30 | * Different forms of implementing descriptors (``__dict__`` vs. ``weakref``): 31 | ``descriptors_implementation_{1,2}.py`` 32 | 33 | 34 | Uses of Descriptors in CPython 35 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 36 | 37 | * Internals of descriptors: ``descriptors_cpython_{1..3}.py``. 38 | * Functions and methods: ``descriptors_methods_{1..4}.py``. 39 | * ``__slots__`` 40 | * ``@property``, ``@classmethod``, and ``@staticmethod``. 41 | 42 | 43 | Uses of descriptors 44 | ^^^^^^^^^^^^^^^^^^^ 45 | 46 | * Reuse code 47 | * Avoid class decorators: ``descriptors_uses_{1,2}.py`` 48 | -------------------------------------------------------------------------------- /book/src/ch06/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logging.basicConfig(level=logging.INFO, format="%(message)s") 4 | logger = logging.getLogger(__name__) 5 | -------------------------------------------------------------------------------- /book/src/ch06/src/descriptors_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter6: Getting more out of our objects with 2 | descriptors 3 | 4 | > Illustrate the basic workings of the descriptor protocol. 5 | """ 6 | import logging 7 | 8 | logging.basicConfig(level=logging.INFO) 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class DescriptorClass: 13 | def __get__(self, instance, owner): 14 | if instance is None: 15 | return self 16 | logger.info( 17 | "Call: %s.__get__(%r, %r)", 18 | self.__class__.__name__, 19 | instance, 20 | owner, 21 | ) 22 | return instance 23 | 24 | 25 | class ClientClass: 26 | descriptor = DescriptorClass() 27 | -------------------------------------------------------------------------------- /book/src/ch06/src/descriptors_cpython_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 6: Descriptors 2 | 3 | > How Python uses descriptors internally. 4 | 5 | """ 6 | from types import MethodType 7 | 8 | 9 | class Method: 10 | def __init__(self, name): 11 | self.name = name 12 | 13 | def __call__(self, instance, arg1, arg2): 14 | print(f"{self.name}: {instance} called with {arg1} and {arg2}") 15 | 16 | 17 | class MyClass1: 18 | method = Method("Internal call") 19 | 20 | 21 | class NewMethod: 22 | def __init__(self, name): 23 | self.name = name 24 | 25 | def __call__(self, instance, arg1, arg2): 26 | print(f"{self.name}: {instance} called with {arg1} and {arg2}") 27 | 28 | def __get__(self, instance, owner): 29 | if instance is None: 30 | return self 31 | return MethodType(self, instance) 32 | 33 | 34 | class MyClass2: 35 | method = NewMethod("Internal call") 36 | -------------------------------------------------------------------------------- /book/src/ch06/src/descriptors_cpython_2.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 6: Descriptors 2 | 3 | > How Python uses descriptors internally: __slots__ 4 | """ 5 | from dataclasses import dataclass 6 | 7 | 8 | @dataclass 9 | class Coordinate2D: 10 | """Example of a class that uses __slots__.""" 11 | 12 | __slots__ = ("lat", "long") 13 | 14 | lat: float 15 | long: float 16 | 17 | def __repr__(self): 18 | return f"{self.__class__.__name__}({self.lat}, {self.long})" 19 | -------------------------------------------------------------------------------- /book/src/ch06/src/descriptors_cpython_3.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 6: Descriptors 2 | 3 | > How Python uses descriptors internally: @clasmethod 4 | 5 | """ 6 | from types import MethodType 7 | 8 | 9 | class ClassMethod: 10 | def __init__(self, method): 11 | self.method = method 12 | 13 | def __call__(self, *args, **kwargs): 14 | return self.method(*args, **kwargs) 15 | 16 | def __get__(self, instance, owner): 17 | return MethodType(self.method, owner) 18 | 19 | 20 | class MyClass: 21 | """ 22 | >>> MyClass().class_method("first", "second") 23 | 'MyClass called with arguments: first, and second' 24 | 25 | >>> MyClass.class_method("one", "two") 26 | 'MyClass called with arguments: one, and two' 27 | 28 | >>> MyClass().method() # doctest: +ELLIPSIS 29 | 'MyClass called with arguments: self, and from method' 30 | """ 31 | 32 | @ClassMethod 33 | def class_method(cls, arg1, arg2) -> str: 34 | return f"{cls.__name__} called with arguments: {arg1}, and {arg2}" # type: ignore 35 | 36 | def method(self): 37 | return self.class_method("self", "from method") 38 | 39 | 40 | class classproperty: 41 | def __init__(self, fget): 42 | self.fget = fget 43 | 44 | def __get__(self, instance, owner): 45 | return self.fget(owner) 46 | 47 | 48 | def read_prefix_from_config(): 49 | return "" 50 | 51 | 52 | class TableEvent: 53 | """ 54 | >>> TableEvent.topic 55 | 'public.user' 56 | 57 | >>> TableEvent().topic 58 | 'public.user' 59 | """ 60 | 61 | schema = "public" 62 | table = "user" 63 | 64 | @classproperty 65 | def topic(cls): 66 | prefix = read_prefix_from_config() 67 | return f"{prefix}{cls.schema}.{cls.table}" 68 | -------------------------------------------------------------------------------- /book/src/ch06/src/descriptors_implementation_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter6: Getting more out of our objects with 2 | descriptors 3 | 4 | > Different forms of implementing descriptors (``__dict__`` vs. ``weakref``) 5 | 6 | - The global state problem 7 | """ 8 | 9 | 10 | class SharedDataDescriptor: 11 | def __init__(self, initial_value): 12 | self.value = initial_value 13 | 14 | def __get__(self, instance, owner): 15 | if instance is None: 16 | return self 17 | return self.value 18 | 19 | def __set__(self, instance, value): 20 | self.value = value 21 | 22 | 23 | class ClientClass: 24 | """ 25 | >>> client1 = ClientClass() 26 | >>> client1.descriptor 27 | 'first value' 28 | 29 | >>> client2 = ClientClass() 30 | >>> client2.descriptor 31 | 'first value' 32 | 33 | >>> client2.descriptor = "value for client 2" 34 | >>> client2.descriptor 35 | 'value for client 2' 36 | 37 | >>> client1.descriptor 38 | 'value for client 2' 39 | """ 40 | 41 | descriptor = SharedDataDescriptor("first value") 42 | -------------------------------------------------------------------------------- /book/src/ch06/src/descriptors_implementation_2.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 6: Descriptors 2 | 3 | > Different forms of implementing descriptors (``__dict__`` vs. ``weakref``) 4 | 5 | - Implementation with weakref 6 | """ 7 | 8 | from weakref import WeakKeyDictionary 9 | 10 | 11 | class DescriptorClass: 12 | def __init__(self, initial_value): 13 | self.value = initial_value 14 | self.mapping = WeakKeyDictionary() 15 | 16 | def __get__(self, instance, owner): 17 | if instance is None: 18 | return self 19 | return self.mapping.get(instance, self.value) 20 | 21 | def __set__(self, instance, value): 22 | self.mapping[instance] = value 23 | 24 | 25 | class ClientClass: 26 | """ 27 | >>> client1 = ClientClass() 28 | >>> client2 = ClientClass() 29 | 30 | >>> client1.descriptor = "new value" 31 | 32 | client1 must have the new value, whilst client2 has to still be with the 33 | default one: 34 | 35 | >>> client1.descriptor 36 | 'new value' 37 | >>> client2.descriptor 38 | 'default value' 39 | 40 | Changing the value for client2 doesn't affect client1 41 | 42 | >>> client2.descriptor = "value for client2" 43 | >>> client2.descriptor 44 | 'value for client2' 45 | >>> client2.descriptor != client1.descriptor 46 | True 47 | """ 48 | 49 | descriptor = DescriptorClass("default value") 50 | -------------------------------------------------------------------------------- /book/src/ch06/src/descriptors_methods_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 6: Descriptors 2 | 3 | > Methods of the descriptor interface: __get__ 4 | """ 5 | 6 | 7 | class DescriptorClass: 8 | def __get__(self, instance, owner): 9 | if instance is None: 10 | return f"{self.__class__.__name__}.{owner.__name__}" 11 | return f"value for {instance}" 12 | 13 | 14 | class ClientClass: 15 | """ 16 | >>> ClientClass.descriptor 17 | 'DescriptorClass.ClientClass' 18 | 19 | >>> ClientClass().descriptor # doctest: +ELLIPSIS 20 | 'value for ' 21 | """ 22 | 23 | descriptor = DescriptorClass() 24 | -------------------------------------------------------------------------------- /book/src/ch06/src/descriptors_methods_2.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 6: Descriptors 2 | 3 | > Methods of the descriptor interface: __set__ 4 | 5 | """ 6 | from typing import Callable, Any 7 | 8 | 9 | class Validation: 10 | """A configurable validation callable.""" 11 | 12 | def __init__( 13 | self, validation_function: Callable[[Any], bool], error_msg: str 14 | ) -> None: 15 | self.validation_function = validation_function 16 | self.error_msg = error_msg 17 | 18 | def __call__(self, value): 19 | if not self.validation_function(value): 20 | raise ValueError(f"{value!r} {self.error_msg}") 21 | 22 | 23 | class Field: 24 | """A class attribute with validation functions configured over it.""" 25 | 26 | def __init__(self, *validations): 27 | self._name = None 28 | self.validations = validations 29 | 30 | def __set_name__(self, owner, name): 31 | self._name = name 32 | 33 | def __get__(self, instance, owner): 34 | if instance is None: 35 | return self 36 | return instance.__dict__[self._name] 37 | 38 | def validate(self, value): 39 | for validation in self.validations: 40 | validation(value) 41 | 42 | def __set__(self, instance, value): 43 | self.validate(value) 44 | instance.__dict__[self._name] = value 45 | 46 | 47 | class ClientClass: 48 | descriptor = Field( 49 | Validation(lambda x: isinstance(x, (int, float)), "is not a number"), 50 | Validation(lambda x: x >= 0, "is not >= 0"), 51 | ) 52 | -------------------------------------------------------------------------------- /book/src/ch06/src/descriptors_methods_3.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 6: Descriptors 2 | 3 | > Methods of the descriptor interface: __delete__ 4 | 5 | """ 6 | 7 | 8 | class ProtectedAttribute: 9 | def __init__(self, requires_role=None) -> None: 10 | self.permission_required = requires_role 11 | self._name = None 12 | 13 | def __set_name__(self, owner, name): 14 | self._name = name 15 | 16 | def __set__(self, user, value): 17 | if value is None: 18 | raise ValueError(f"{self._name} can't be set to None") 19 | user.__dict__[self._name] = value 20 | 21 | def __delete__(self, user): 22 | if self.permission_required in user.permissions: 23 | user.__dict__[self._name] = None 24 | else: 25 | raise ValueError( 26 | f"User {user!s} doesn't have {self.permission_required} " 27 | "permission" 28 | ) 29 | 30 | 31 | class User: 32 | """Only users with "admin" privileges can remove their email address.""" 33 | 34 | email = ProtectedAttribute(requires_role="admin") 35 | 36 | def __init__( 37 | self, username: str, email: str, permission_list: list = None 38 | ) -> None: 39 | self.username = username 40 | self.email = email 41 | self.permissions = permission_list or [] 42 | 43 | def __str__(self): 44 | return self.username 45 | -------------------------------------------------------------------------------- /book/src/ch06/src/descriptors_pythonic_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 6: Descriptors 2 | 3 | > First example: the naive implementation using properties 4 | """ 5 | 6 | 7 | class Traveler: 8 | """A person visiting several cities. 9 | 10 | We wish to track the path of the traveller, as he or she is visiting each 11 | new city. 12 | """ 13 | 14 | def __init__(self, name, current_city): 15 | self.name = name 16 | self._current_city = current_city 17 | self._cities_visited = [current_city] 18 | 19 | @property 20 | def current_city(self): 21 | return self._current_city 22 | 23 | @current_city.setter 24 | def current_city(self, new_city): 25 | if new_city != self._current_city: 26 | self._cities_visited.append(new_city) 27 | self._current_city = new_city 28 | 29 | @property 30 | def cities_visited(self): 31 | return self._cities_visited 32 | -------------------------------------------------------------------------------- /book/src/ch06/src/descriptors_types_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 6: Descriptors 2 | 3 | > Types of Descriptors: 4 | 5 | 1. Non-Data Descriptors (non-overriding) 6 | 2. Data Descriptors (overriding) 7 | 8 | Code examples to illustrate [1]. 9 | """ 10 | 11 | 12 | class NonDataDescriptor: 13 | """A descriptor that doesn't implement __set__.""" 14 | 15 | def __get__(self, instance, owner): 16 | if instance is None: 17 | return self 18 | return 42 19 | 20 | 21 | class ClientClass: 22 | """Test NonDataDescriptor. 23 | 24 | >>> client = ClientClass() 25 | >>> client.descriptor 26 | 42 27 | 28 | >>> client.descriptor = 43 29 | >>> client.descriptor 30 | 43 31 | 32 | >>> del client.descriptor 33 | >>> client.descriptor 34 | 42 35 | 36 | >>> vars(client) 37 | {} 38 | 39 | >>> client.descriptor 40 | 42 41 | 42 | >>> client.descriptor = 99 43 | >>> vars(client) 44 | {'descriptor': 99} 45 | 46 | >>> del client.descriptor 47 | >>> vars(client) 48 | {} 49 | >>> client.descriptor 50 | 42 51 | 52 | """ 53 | 54 | descriptor = NonDataDescriptor() 55 | -------------------------------------------------------------------------------- /book/src/ch06/src/descriptors_types_2.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 6: Descriptors 2 | 3 | > Types of Descriptors: 4 | 5 | 1. Non-Data Descriptors (non-overriding) 6 | 2. Data Descriptors (overriding) 7 | 8 | Code examples to illustrate [2]. 9 | """ 10 | from log import logger 11 | 12 | 13 | class DataDescriptor: 14 | """A descriptor that implements __get__ & __set__.""" 15 | 16 | def __get__(self, instance, owner): 17 | if instance is None: 18 | return self 19 | return 42 20 | 21 | def __set__(self, instance, value): 22 | logger.debug("setting %s.descriptor to %s", instance, value) 23 | instance.__dict__["descriptor"] = value 24 | 25 | 26 | class ClientClass: 27 | """Test DataDescriptor 28 | 29 | Let's see what the value of the descriptor returns:: 30 | 31 | >>> client = ClientClass() 32 | >>> client.descriptor 33 | 42 34 | 35 | >>> client.descriptor = 99 36 | >>> client.descriptor 37 | 42 38 | 39 | >>> vars(client) 40 | {'descriptor': 99} 41 | 42 | >>> client.__dict__["descriptor"] 43 | 99 44 | 45 | >>> del client.descriptor 46 | Traceback (most recent call last): 47 | File "", line 1, in 48 | AttributeError: __delete__ 49 | 50 | """ 51 | 52 | descriptor = DataDescriptor() 53 | -------------------------------------------------------------------------------- /book/src/ch06/src/descriptors_uses_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 6: Descriptors 2 | 3 | > Using descriptors instead of class decorators 4 | 5 | """ 6 | from dataclasses import dataclass 7 | from datetime import datetime 8 | from functools import partial 9 | from typing import Any, Callable 10 | 11 | 12 | class BaseFieldTransformation: 13 | """Base class to define descriptors that convert values.""" 14 | 15 | def __init__(self, transformation: Callable[[Any, str], str]) -> None: 16 | self._name = None 17 | self.transformation = transformation 18 | 19 | def __get__(self, instance, owner): 20 | if instance is None: 21 | return self 22 | raw_value = instance.__dict__[self._name] 23 | return self.transformation(raw_value) 24 | 25 | def __set_name__(self, owner, name): 26 | self._name = name 27 | 28 | def __set__(self, instance, value): 29 | instance.__dict__[self._name] = value 30 | 31 | 32 | ShowOriginal = partial(BaseFieldTransformation, transformation=lambda x: x) 33 | HideField = partial( 34 | BaseFieldTransformation, transformation=lambda x: "**redacted**" 35 | ) 36 | FormatTime = partial( 37 | BaseFieldTransformation, 38 | transformation=lambda ft: ft.strftime("%Y-%m-%d %H:%M"), 39 | ) 40 | 41 | 42 | @dataclass 43 | class LoginEvent: 44 | 45 | username: str = ShowOriginal() # type: ignore 46 | password: str = HideField() # type: ignore 47 | ip: str = ShowOriginal() # type: ignore 48 | timestamp: datetime = FormatTime() # type: ignore 49 | 50 | def serialize(self) -> dict: 51 | return { 52 | "username": self.username, 53 | "password": self.password, 54 | "ip": self.ip, 55 | "timestamp": self.timestamp, 56 | } 57 | -------------------------------------------------------------------------------- /book/src/ch06/tests/test_descriptors_pythonic.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 6: Descriptors 2 | 3 | > A Pythonic Implementation 4 | 5 | Tests for src/descriptors_pythonic_{1,2}.py 6 | """ 7 | import unittest 8 | 9 | from descriptors_pythonic_1 import Traveler as TravelerNaiveImplementation 10 | from descriptors_pythonic_2 import Traveler as TravelerWithDescriptor 11 | 12 | 13 | class TestDescriptorTraceability(unittest.TestCase): 14 | def _test_case(self, traveller_cls): 15 | alice = traveller_cls("Alice", "Barcelona") 16 | alice.current_city = "Paris" 17 | alice.current_city = "Brussels" 18 | alice.current_city = "Amsterdam" 19 | 20 | self.assertListEqual( 21 | alice.cities_visited, 22 | ["Barcelona", "Paris", "Brussels", "Amsterdam"], 23 | ) 24 | self.assertEqual(alice.current_city, "Amsterdam") 25 | 26 | alice.current_city = "Amsterdam" 27 | self.assertListEqual( 28 | alice.cities_visited, 29 | ["Barcelona", "Paris", "Brussels", "Amsterdam"], 30 | ) 31 | 32 | bob = traveller_cls("Bob", "Rotterdam") 33 | bob.current_city = "Amsterdam" 34 | 35 | self.assertEqual(bob.current_city, "Amsterdam") 36 | self.assertListEqual(bob.cities_visited, ["Rotterdam", "Amsterdam"]) 37 | 38 | def test_trace_attribute(self): 39 | for test_cls in ( 40 | TravelerNaiveImplementation, 41 | TravelerWithDescriptor, 42 | ): 43 | with self.subTest(case=test_cls): 44 | self._test_case(test_cls) 45 | 46 | 47 | if __name__ == "__main__": 48 | unittest.main() 49 | -------------------------------------------------------------------------------- /book/src/ch06/tests/test_descriptors_uses_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 6: Descriptors 2 | 3 | > Tests for descriptors_uses_1.py 4 | 5 | """ 6 | from datetime import datetime 7 | from unittest import TestCase, main 8 | 9 | from descriptors_uses_1 import LoginEvent 10 | 11 | DATE_TIME = datetime(2016, 7, 20, 15, 45) 12 | 13 | 14 | class TestLoginEvent(TestCase): 15 | def test_serialization(self): 16 | event = LoginEvent( 17 | username="username", 18 | password="password", 19 | ip="127.0.0.1", 20 | timestamp=DATE_TIME, 21 | ) 22 | expected = { 23 | "username": "username", 24 | "password": "**redacted**", 25 | "ip": "127.0.0.1", 26 | "timestamp": "2016-07-20 15:45", 27 | } 28 | self.assertEqual(event.serialize(), expected) 29 | 30 | def test_retrieve_transformed_value(self): 31 | event = LoginEvent( 32 | username="username", 33 | password="password", 34 | ip="127.0.0.1", 35 | timestamp=DATE_TIME, 36 | ) 37 | self.assertEqual(event.password, "**redacted**") 38 | self.assertEqual(event.timestamp, "2016-07-20 15:45") 39 | self.assertEqual(event.username, "username") 40 | self.assertEqual(event.ip, "127.0.0.1") 41 | 42 | def test_object_keeps_original_values(self): 43 | event = LoginEvent( 44 | username="username", 45 | password="password", 46 | ip="127.0.0.1", 47 | timestamp=DATE_TIME, 48 | ) 49 | self.assertEqual(event.__dict__["password"], "password") 50 | 51 | 52 | if __name__ == "__main__": 53 | main() 54 | -------------------------------------------------------------------------------- /book/src/ch07/.gitignore: -------------------------------------------------------------------------------- 1 | *.trace.txt 2 | -------------------------------------------------------------------------------- /book/src/ch07/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean 2 | clean: 3 | find . -name "*.swp" -o -name "__pycache__" | xargs rm -fr 4 | 5 | .PHONY: test 6 | test: lint typehint 7 | @$(PYTHON) -m doctest src/*.py 8 | pytest tests $(ARGS) 9 | 10 | .PHONY: typehint 11 | typehint: 12 | @mypy src/*.py 13 | 14 | .PHONY: lint 15 | lint: 16 | black --check --line-length=79 src tests 17 | 18 | .PHONY: format 19 | format: 20 | black --line-length=79 src tests 21 | -------------------------------------------------------------------------------- /book/src/ch07/README.rst: -------------------------------------------------------------------------------- 1 | Clean Code in Python - Chapter 7: Generators, Iterators, and Asynchronous Programming 2 | ================================================== 3 | 4 | Run the tests:: 5 | 6 | make test 7 | 8 | 9 | Working With Generators 10 | ----------------------- 11 | #. A First Glimpse at Generators: ``generators_1.py`` 12 | #. The States of a Generator: ``generators_2.py`` 13 | 14 | 15 | Idiomatic Iteration 16 | ------------------- 17 | #. Idioms for Iteration in Python: ``generators_pythonic_1.py`` 18 | #. The Iterator Design Pattern, the Python way 19 | 20 | #. A first approach of iteration with the iteration pattern: ``generators_pythonic_2.py`` 21 | #. The idiomatic way: using an Iterator: ``generators_pythonic_3.py`` 22 | 23 | 24 | Coroutines 25 | ---------- 26 | #. The Methods of the Generator Interface: ``generators_coroutines_1.py`` 27 | #. Working with Coroutines: ``generators_coroutines_2.py`` 28 | #. Delegating Coroutines: ``generators_yieldfrom_{1..3}.py`` 29 | #. Asynchronous context managers: ``async_context_manager.py`` 30 | #. Asynchronous iteration: ``async_iteration.py`` 31 | -------------------------------------------------------------------------------- /book/src/ch07/src/_generate_data.py: -------------------------------------------------------------------------------- 1 | """Helper to generate test data.""" 2 | import os 3 | from tempfile import gettempdir 4 | 5 | PURCHASES_FILE = os.path.join(gettempdir(), "purchases.csv") 6 | 7 | 8 | def create_purchases_file(filename, entries=1_000_000): 9 | if os.path.exists(PURCHASES_FILE): 10 | return 11 | 12 | with open(filename, "w+") as f: 13 | for i in range(entries): 14 | line = f"2018-01-01,{i}\n" 15 | f.write(line) 16 | 17 | 18 | if __name__ == "__main__": 19 | create_purchases_file(PURCHASES_FILE) 20 | -------------------------------------------------------------------------------- /book/src/ch07/src/async_context_manager.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Second edition - Chapter 7 2 | 3 | > Generators / Asynchronous context managers 4 | """ 5 | import contextlib 6 | import asyncio 7 | 8 | 9 | async def stop_database(): 10 | await asyncio.sleep(0.1) 11 | print("systemctl stop postgresql.service") 12 | 13 | 14 | async def start_database(): 15 | await asyncio.sleep(0.2) 16 | print("systemctp start postgresql.service") 17 | 18 | 19 | @contextlib.asynccontextmanager 20 | async def db_management(): 21 | try: 22 | await stop_database() 23 | yield 24 | finally: 25 | await start_database() 26 | 27 | 28 | async def create_metrics_logger(): 29 | await asyncio.sleep(0.01) 30 | return "metrics-logger" 31 | 32 | 33 | @contextlib.asynccontextmanager 34 | async def metrics_logger(): 35 | yield await create_metrics_logger() 36 | 37 | 38 | async def run_db_backup(): 39 | async with db_management(), metrics_logger(): 40 | print("Performing DB backup...") 41 | -------------------------------------------------------------------------------- /book/src/ch07/src/async_iteration.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 7: Generators, Iterators, and Asynchronous Programming 2 | 3 | > Asynchronous programming / asynchronous iteration 4 | """ 5 | import asyncio 6 | 7 | 8 | async def coroutine(index: int) -> int: 9 | await asyncio.sleep(0.1) 10 | return index * 1_000 11 | 12 | 13 | class RecordStreamer: 14 | def __init__(self, max_rows=100) -> None: 15 | self._current_row = 0 16 | self._max_rows = max_rows 17 | 18 | def __aiter__(self): 19 | return self 20 | 21 | async def __anext__(self): 22 | if self._current_row < self._max_rows: 23 | row = (self._current_row, await coroutine(self._current_row)) 24 | self._current_row += 1 25 | return row 26 | raise StopAsyncIteration 27 | 28 | 29 | NOT_SET = object() 30 | 31 | 32 | async def anext(async_generator_expression, default=NOT_SET): 33 | try: 34 | return await async_generator_expression.__anext__() 35 | except StopAsyncIteration: 36 | if default is NOT_SET: 37 | raise 38 | return default 39 | 40 | 41 | async def record_streamer(max_rows): 42 | current_row = 0 43 | while current_row < max_rows: 44 | row = (current_row, await coroutine(current_row)) 45 | current_row += 1 46 | yield row 47 | -------------------------------------------------------------------------------- /book/src/ch07/src/generators_2.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 7: Generators, Iterators, and Asynchronous Programming 2 | 3 | > Introduction to generators 4 | """ 5 | 6 | 7 | def sequence(start=0): 8 | """ 9 | >>> import inspect 10 | >>> seq = sequence() 11 | >>> inspect.getgeneratorstate(seq) 12 | 'GEN_CREATED' 13 | 14 | >>> seq = sequence() 15 | >>> next(seq) 16 | 0 17 | >>> inspect.getgeneratorstate(seq) 18 | 'GEN_SUSPENDED' 19 | 20 | >>> seq = sequence() 21 | >>> next(seq) 22 | 0 23 | >>> seq.close() 24 | >>> inspect.getgeneratorstate(seq) 25 | 'GEN_CLOSED' 26 | >>> next(seq) # doctest: +ELLIPSIS 27 | Traceback (most recent call last): 28 | ... 29 | StopIteration 30 | 31 | """ 32 | while True: 33 | yield start 34 | start += 1 35 | -------------------------------------------------------------------------------- /book/src/ch07/src/generators_coroutines_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 7: Generators, Iterators, and Asynchronous Programming 2 | 3 | > Methods of the Generators Interface. 4 | 5 | """ 6 | import time 7 | 8 | from log import logger 9 | 10 | 11 | class DBHandler: 12 | """Simulate reading from the database by pages.""" 13 | 14 | def __init__(self, db): 15 | self.db = db 16 | self.is_closed = False 17 | 18 | def read_n_records(self, limit): 19 | return [(i, f"row {i}") for i in range(limit)] 20 | 21 | def close(self): 22 | logger.debug("closing connection to database %r", self.db) 23 | self.is_closed = True 24 | 25 | 26 | def stream_db_records(db_handler): 27 | """Example of .close() 28 | 29 | >>> streamer = stream_db_records(DBHandler("testdb")) # doctest: +ELLIPSIS 30 | >>> len(next(streamer)) 31 | 10 32 | 33 | >>> len(next(streamer)) 34 | 10 35 | """ 36 | try: 37 | while True: 38 | yield db_handler.read_n_records(10) 39 | time.sleep(0.1) 40 | except GeneratorExit: 41 | db_handler.close() 42 | 43 | 44 | class CustomException(Exception): 45 | """An exception of the domain model.""" 46 | 47 | 48 | def stream_data(db_handler): 49 | """Test the ``.throw()`` method. 50 | 51 | >>> streamer = stream_data(DBHandler("testdb")) 52 | >>> len(next(streamer)) 53 | 10 54 | """ 55 | while True: 56 | try: 57 | yield db_handler.read_n_records(10) 58 | except CustomException as e: 59 | logger.info("controlled error %r, continuing", e) 60 | except Exception as e: 61 | logger.info("unhandled error %r, stopping", e) 62 | db_handler.close() 63 | break 64 | -------------------------------------------------------------------------------- /book/src/ch07/src/generators_coroutines_2.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 7: Generators, Iterators, and Asynchronous Programming 2 | 3 | > Using Coroutines. 4 | 5 | """ 6 | 7 | 8 | def _stream_db_records(db_handler): 9 | retrieved_data = None 10 | previous_page_size = 10 11 | try: 12 | while True: 13 | page_size = yield retrieved_data 14 | if page_size is None: 15 | page_size = previous_page_size 16 | 17 | previous_page_size = page_size 18 | 19 | retrieved_data = db_handler.read_n_records(page_size) 20 | except GeneratorExit: 21 | db_handler.close() 22 | 23 | 24 | def stream_db_records(db_handler): 25 | retrieved_data = None 26 | page_size = 10 27 | try: 28 | while True: 29 | page_size = (yield retrieved_data) or page_size 30 | retrieved_data = db_handler.read_n_records(page_size) 31 | except GeneratorExit: 32 | db_handler.close() 33 | 34 | 35 | def prepare_coroutine(coroutine): 36 | def wrapped(*args, **kwargs): 37 | advanced_coroutine = coroutine(*args, **kwargs) 38 | next(advanced_coroutine) 39 | return advanced_coroutine 40 | 41 | return wrapped 42 | 43 | 44 | @prepare_coroutine 45 | def auto_stream_db_records(db_handler): 46 | """This coroutine is automatically advanced so it doesn't need the first 47 | next() call. 48 | """ 49 | retrieved_data = None 50 | page_size = 10 51 | try: 52 | while True: 53 | page_size = (yield retrieved_data) or page_size 54 | retrieved_data = db_handler.read_n_records(page_size) 55 | except GeneratorExit: 56 | db_handler.close() 57 | -------------------------------------------------------------------------------- /book/src/ch07/src/generators_iteration_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 7: Generators, Iterators, and Asynchronous Programming 2 | 3 | > The Interface for Iteration 4 | 5 | * Distinguish between iterable objects and iterators 6 | * Create iterators 7 | """ 8 | 9 | 10 | class SequenceIterator: 11 | """ 12 | >>> si = SequenceIterator(1, 2) 13 | >>> next(si) 14 | 1 15 | >>> next(si) 16 | 3 17 | >>> next(si) 18 | 5 19 | """ 20 | 21 | def __init__(self, start=0, step=1): 22 | self.current = start 23 | self.step = step 24 | 25 | def __next__(self): 26 | value = self.current 27 | self.current += self.step 28 | return value 29 | -------------------------------------------------------------------------------- /book/src/ch07/src/generators_iteration_2.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 7: Generators, Iterators, and Asynchronous Programming 2 | 3 | > The Interface for Iteration: sequences 4 | 5 | """ 6 | 7 | import logging 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class SequenceWrapper: 13 | def __init__(self, original_sequence): 14 | self.seq = original_sequence 15 | 16 | def __getitem__(self, item): 17 | value = self.seq[item] 18 | logger.debug("%s getting %s", self.__class__.__name__, item) 19 | return value 20 | 21 | def __len__(self): 22 | return len(self.seq) 23 | 24 | 25 | class MappedRange: 26 | """Apply a transformation to a range of numbers.""" 27 | 28 | def __init__(self, transformation, start, end): 29 | self._transformation = transformation 30 | self._wrapped = range(start, end) 31 | 32 | def __getitem__(self, index): 33 | value = self._wrapped.__getitem__(index) 34 | result = self._transformation(value) 35 | logger.info("Index %d: %s", index, result) 36 | return result 37 | 38 | def __len__(self): 39 | return len(self._wrapped) 40 | -------------------------------------------------------------------------------- /book/src/ch07/src/generators_pythonic_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 7: Generators, Iterators, and Asynchronous Programming 2 | 3 | > Idiomatic Iteration 4 | 5 | """ 6 | 7 | 8 | class NumberSequence: 9 | """ 10 | >>> seq = NumberSequence() 11 | >>> seq.next() 12 | 0 13 | >>> seq.next() 14 | 1 15 | 16 | >>> seq2 = NumberSequence(10) 17 | >>> seq2.next() 18 | 10 19 | >>> seq2.next() 20 | 11 21 | 22 | """ 23 | 24 | def __init__(self, start=0): 25 | self.current = start 26 | 27 | def next(self): 28 | current = self.current 29 | self.current += 1 30 | return current 31 | 32 | 33 | class SequenceOfNumbers: 34 | """ 35 | >>> list(zip(SequenceOfNumbers(), "abcdef")) 36 | [(0, 'a'), (1, 'b'), (2, 'c'), (3, 'd'), (4, 'e'), (5, 'f')] 37 | 38 | >>> seq = SequenceOfNumbers(100) 39 | >>> next(seq) 40 | 100 41 | >>> next(seq) 42 | 101 43 | 44 | """ 45 | 46 | def __init__(self, start=0): 47 | self.current = start 48 | 49 | def __next__(self): 50 | current = self.current 51 | self.current += 1 52 | return current 53 | 54 | def __iter__(self): 55 | return self 56 | 57 | 58 | def sequence(start=0): 59 | """ 60 | >>> seq = sequence(10) 61 | >>> next(seq) 62 | 10 63 | >>> next(seq) 64 | 11 65 | 66 | >>> list(zip(sequence(), "abcdef")) 67 | [(0, 'a'), (1, 'b'), (2, 'c'), (3, 'd'), (4, 'e'), (5, 'f')] 68 | """ 69 | while True: 70 | yield start 71 | start += 1 72 | -------------------------------------------------------------------------------- /book/src/ch07/src/generators_pythonic_2.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 7: Generators, Iterators, and Asynchronous Programming 2 | 3 | > Idiomatic Iteration with itertools 4 | 5 | """ 6 | import logging 7 | from itertools import filterfalse, tee 8 | 9 | logging.basicConfig(level=logging.INFO) 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | class IteratorWrapper: 14 | def __init__(self, iterable): 15 | self.iterable = iter(iterable) 16 | 17 | def __next__(self): 18 | value = next(self.iterable) 19 | logger.debug( 20 | "%s Producing next value: %r", self.__class__.__name__, value 21 | ) 22 | return value 23 | 24 | def __iter__(self): 25 | return self 26 | 27 | 28 | def partition(condition, iterable): 29 | """Return 2 new iterables 30 | 31 | true_cond, false_cond = partition(condition, iterable) 32 | 33 | * in true_cond, condition is true over all elements of iterable 34 | * in false_cond, condition is false over all elements of iterable 35 | """ 36 | for_true, for_false = tee(iterable) 37 | return filter(condition, for_true), filterfalse(condition, for_false) 38 | 39 | 40 | iterable = IteratorWrapper( 41 | {"name": f"element_{i}", "index": i} for i in range(20) 42 | ) 43 | 44 | 45 | def is_even(record): 46 | return record["index"] % 2 == 0 47 | 48 | 49 | def show(records): 50 | return ", ".join(e["name"] for e in records) 51 | 52 | 53 | if __name__ == "__main__": 54 | even, odd = partition(is_even, iterable) 55 | 56 | logger.info( 57 | "\n\tEven records: %s\n\t Odd records: %s", show(even), show(odd) 58 | ) 59 | -------------------------------------------------------------------------------- /book/src/ch07/src/generators_pythonic_3.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 7: Generators, Iterators, and Asynchronous Programming 2 | 3 | > Idiomatic Iteration 4 | 5 | """ 6 | import logging 7 | from itertools import tee 8 | from statistics import median 9 | 10 | logging.basicConfig(level=logging.INFO) 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | def produce_values(how_many): 15 | for i in range(1, how_many + 1): 16 | logger.debug("producing purchase %i", i) 17 | yield i 18 | 19 | 20 | def process_purchases(purchases): 21 | min_, max_, avg = tee(purchases, 3) 22 | return min(min_), max(max_), median(avg) 23 | 24 | 25 | def main(): 26 | data = produce_values(7) 27 | obtained = process_purchases(data) 28 | logger.info("Obtained: %s", obtained) 29 | assert obtained == (1, 7, 4) 30 | 31 | 32 | if __name__ == "__main__": 33 | main() 34 | -------------------------------------------------------------------------------- /book/src/ch07/src/generators_pythonic_4.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 7: Generators, Iterators, and Asynchronous Programming 2 | 3 | > Idiomatic Iteration: simplifying loops 4 | 5 | """ 6 | import unittest 7 | 8 | from log import logger 9 | 10 | 11 | def search_nested_bad(array, desired_value): 12 | """Example of an iteration in a nested loop.""" 13 | coords = None 14 | for i, row in enumerate(array): 15 | for j, cell in enumerate(row): 16 | if cell == desired_value: 17 | coords = (i, j) 18 | break 19 | 20 | if coords is not None: 21 | break 22 | 23 | if coords is None: 24 | raise ValueError(f"{desired_value} not found") 25 | 26 | logger.info("value %r found at [%i, %i]", desired_value, *coords) 27 | return coords 28 | 29 | 30 | def _iterate_array2d(array2d): 31 | for i, row in enumerate(array2d): 32 | for j, cell in enumerate(row): 33 | yield (i, j), cell 34 | 35 | 36 | def search_nested(array, desired_value): 37 | """Searching in multiple dimensions with a single loop.""" 38 | try: 39 | coord = next( 40 | coord 41 | for (coord, cell) in _iterate_array2d(array) 42 | if cell == desired_value 43 | ) 44 | except StopIteration as e: 45 | raise ValueError(f"{desired_value} not found") from e 46 | 47 | logger.info("value %r found at [%i, %i]", desired_value, *coord) 48 | return coord 49 | 50 | 51 | if __name__ == "__main__": 52 | unittest.main() 53 | -------------------------------------------------------------------------------- /book/src/ch07/src/generators_yieldfrom_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 7: Generators, Iterators, and Asynchronous Programming 2 | 3 | > The ``yield from`` syntax: chain generators. 4 | 5 | """ 6 | 7 | 8 | def chain(*iterables): 9 | """ 10 | >>> list(chain("hello", ["world"], ("tuple", " of ", "values."))) 11 | ['h', 'e', 'l', 'l', 'o', 'world', 'tuple', ' of ', 'values.'] 12 | """ 13 | for it in iterables: 14 | yield from it 15 | 16 | 17 | def _chain(*iterables): 18 | for it in iterables: 19 | for value in it: 20 | yield value 21 | 22 | 23 | def all_powers(n, power): 24 | yield from (n**i for i in range(power + 1)) 25 | -------------------------------------------------------------------------------- /book/src/ch07/src/generators_yieldfrom_2.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 7: Generators, Iterators, and Asynchronous Programming 2 | 3 | > yield from: Capture the return value. 4 | """ 5 | 6 | from log import logger 7 | 8 | 9 | def sequence(name, start, end): 10 | logger.info("%s started at %i", name, start) 11 | yield from range(start, end) 12 | logger.info("%s finished at %i", name, end) 13 | return end 14 | 15 | 16 | def main(): 17 | step1 = yield from sequence("first", 0, 5) 18 | step2 = yield from sequence("second", step1, 10) 19 | return step1 + step2 20 | -------------------------------------------------------------------------------- /book/src/ch07/src/generators_yieldfrom_3.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 7: Generators, Iterators, and Asynchronous Programming 2 | 3 | > yield from: send values & throw exceptions 4 | 5 | """ 6 | from log import logger 7 | 8 | 9 | class CustomException(Exception): 10 | """A type of exception that is under control.""" 11 | 12 | 13 | def sequence(name, start, end): 14 | value = start 15 | logger.info("%s started at %i", name, value) 16 | while value < end: 17 | try: 18 | received = yield value 19 | logger.info("%s received %r", name, received) 20 | value += 1 21 | except CustomException as e: 22 | logger.info("%s is handling %s", name, e) 23 | received = yield "OK" 24 | return end 25 | 26 | 27 | def main(): 28 | step1 = yield from sequence("first", 0, 5) 29 | step2 = yield from sequence("second", step1, 10) 30 | return step1 + step2 31 | -------------------------------------------------------------------------------- /book/src/ch07/src/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logging.basicConfig(level=logging.INFO, format="%(message)s") 4 | logger = logging.getLogger(__name__) 5 | -------------------------------------------------------------------------------- /book/src/ch07/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from unittest.mock import patch 4 | 5 | 6 | @pytest.fixture(autouse=True) 7 | @pytest.mark.asyncio 8 | async def no_sleep(): 9 | with patch("asyncio.sleep"), patch("time.sleep"): 10 | yield 11 | -------------------------------------------------------------------------------- /book/src/ch07/tests/test_async_context_manager.py: -------------------------------------------------------------------------------- 1 | """Tests for async_context_manager""" 2 | 3 | import unittest 4 | from unittest.mock import DEFAULT, patch 5 | 6 | from async_context_manager import db_management, run_db_backup 7 | 8 | 9 | class TestAsyncContextManager(unittest.IsolatedAsyncioTestCase): 10 | def _patch_deps(self): 11 | return patch.multiple( 12 | "async_context_manager", 13 | stop_database=DEFAULT, 14 | start_database=DEFAULT, 15 | create_metrics_logger=DEFAULT, 16 | ) 17 | 18 | async def test_db_handler_on_exception(self): 19 | with self._patch_deps() as deps, self.assertRaises(RuntimeError): 20 | async with db_management(): 21 | raise RuntimeError("something went wrong!") 22 | 23 | deps["start_database"].assert_called_once_with() 24 | 25 | async def test_cm_autocalled(self): 26 | with self._patch_deps() as deps: 27 | await run_db_backup() 28 | 29 | deps["stop_database"].assert_called_once_with() 30 | deps["start_database"].assert_called_once_with() 31 | deps["create_metrics_logger"].assert_called_once_with() 32 | 33 | 34 | if __name__ == "__main__": 35 | unittest.main() 36 | -------------------------------------------------------------------------------- /book/src/ch07/tests/test_generators.py: -------------------------------------------------------------------------------- 1 | """Clean code in Python - Chapter 07: Using generators 2 | 3 | > Tests for: generators_1.py 4 | """ 5 | from unittest import TestCase, main 6 | 7 | from generators_1 import PurchasesStats 8 | 9 | 10 | class TestPurchaseStats(TestCase): 11 | def test_calculations(self): 12 | stats = PurchasesStats(range(1, 11 + 1)).process() 13 | 14 | self.assertEqual(stats.min_price, 1) 15 | self.assertEqual(stats.max_price, 11) 16 | self.assertEqual(stats.avg_price, 6) 17 | 18 | def test_empty(self): 19 | self.assertRaises(ValueError, PurchasesStats, []) 20 | 21 | 22 | if __name__ == "__main__": 23 | main() 24 | -------------------------------------------------------------------------------- /book/src/ch07/tests/test_generators_iteration.py: -------------------------------------------------------------------------------- 1 | """Tests for generators_iteration_*.py""" 2 | from unittest import TestCase, main 3 | 4 | from generators_iteration_2 import MappedRange, SequenceWrapper 5 | 6 | 7 | class TestSequenceWrapper(TestCase): 8 | def test_sequence(self): 9 | sequence = SequenceWrapper(list(range(10))) 10 | for i in sequence: 11 | self.assertEqual(i, sequence[i]) 12 | 13 | 14 | class TestMappedRange(TestCase): 15 | def test_limits(self): 16 | self.assertEqual(len(MappedRange(None, 1, 10)), 9) 17 | 18 | seq = MappedRange(abs, -5, 5) 19 | self.assertEqual(seq[-5], 0) 20 | self.assertEqual(seq[-1], 4) 21 | self.assertEqual(seq[4], abs(-1)) 22 | self.assertRaises(IndexError, seq.__getitem__, -16) 23 | self.assertRaises(IndexError, seq.__getitem__, 10) 24 | 25 | def test_getitem(self): 26 | seq = MappedRange(lambda x: x**2, 1, 10) 27 | self.assertEqual(seq[5], 36) 28 | 29 | def test_iterate(self): 30 | test_data = ( # start, end, expected 31 | (0, 5, [1, 2, 3, 4, 5]), 32 | (100, 106, [101, 102, 103, 104, 105, 106]), 33 | ) 34 | for start, end, expected in test_data: 35 | with self.subTest(start=start, end=end, expected=expected): 36 | seq = MappedRange(lambda x: x + 1, start, end) 37 | self.assertEqual(list(seq), expected) 38 | 39 | 40 | if __name__ == "__main__": 41 | main() 42 | -------------------------------------------------------------------------------- /book/src/ch07/tests/test_generators_pythonic.py: -------------------------------------------------------------------------------- 1 | from itertools import chain 2 | from random import choice, randint 3 | from unittest import TestCase, main 4 | 5 | from generators_pythonic_3 import process_purchases, produce_values 6 | from generators_pythonic_4 import search_nested, search_nested_bad 7 | 8 | 9 | class TestPurchaseStats(TestCase): 10 | """Tests for generators_pythonic_3.py""" 11 | 12 | def test_calculations(self): 13 | min_price, max_price, avg_price = process_purchases(produce_values(11)) 14 | 15 | self.assertEqual(min_price, 1) 16 | self.assertEqual(max_price, 11) 17 | self.assertEqual(avg_price, 6) 18 | 19 | def test_empty(self): 20 | self.assertRaises(ValueError, process_purchases, []) 21 | 22 | 23 | class TestSimplifiedIteration(TestCase): 24 | """Tests for generators_pythonic_4.py""" 25 | 26 | def test_found(self): 27 | test_matrix = [[randint(1, 100) for _ in range(10)] for _ in range(10)] 28 | to_search_for = choice(list(chain.from_iterable(test_matrix))) 29 | for finding_function in (search_nested_bad, search_nested): 30 | row, column = finding_function(test_matrix, to_search_for) 31 | self.assertEqual(test_matrix[row][column], to_search_for) 32 | 33 | def test_not_found(self): 34 | matrix = [[i for i in range(10)] for _ in range(2)] 35 | for ffunc in search_nested_bad, search_nested: 36 | self.assertRaises(ValueError, ffunc, matrix, -1) 37 | 38 | def test_search_nested(self): 39 | matrix = [[1, 1], [2, 2]] 40 | with self.assertRaisesRegex(ValueError, "99 not found") as exc: 41 | search_nested(matrix, 99) 42 | assert exc.exception.__cause__.__class__ is StopIteration 43 | 44 | 45 | if __name__ == "__main__": 46 | main() 47 | -------------------------------------------------------------------------------- /book/src/ch08/.gitignore: -------------------------------------------------------------------------------- 1 | .coverage 2 | .hypothesis/ 3 | .pytest_cache/ 4 | -------------------------------------------------------------------------------- /book/src/ch08/Makefile: -------------------------------------------------------------------------------- 1 | CASE:=1 2 | .PHONY: test 3 | test: 4 | @$(PYTHON) -m doctest src/*.py 5 | @$(VIRTUAL_ENV)/bin/pytest tests/ 6 | 7 | .PHONY: clean 8 | clean: 9 | find . -type d -name __pycache__ | xargs rm -fr {} 10 | rm -fr .coverage .pytest_cache/ .hypothesis/ 11 | 12 | .PHONY: coverage 13 | coverage: 14 | bash run-coverage.sh 15 | 16 | .PHONY: mutation 17 | mutation: 18 | @bash mutation-testing.sh $(CASE) 19 | 20 | .PHONY: typehint 21 | typehint: 22 | @mypy src/*.py 23 | 24 | .PHONY: lint 25 | lint: 26 | black --check --line-length=79 src tests 27 | 28 | .PHONY: format 29 | format: 30 | black --line-length=79 src tests 31 | 32 | .PHONY: checklist 33 | checklist: lint typehint test 34 | -------------------------------------------------------------------------------- /book/src/ch08/README.rst: -------------------------------------------------------------------------------- 1 | Clean code in Python - Chapter 8: Unit testing and refactoring 2 | ============================================================== 3 | 4 | Run the tests:: 5 | 6 | make test 7 | 8 | 9 | Running extra test cases 10 | ^^^^^^^^^^^^^^^^^^^^^^^^ 11 | For example for to try the mutation testing, or coverage, you can use the 12 | following command:: 13 | 14 | make coverage 15 | make mutation 16 | 17 | There are two test cases for each one (1 & 2), which can be specified in the 18 | command. For example:: 19 | 20 | make coverage CASE=1 21 | make mutation CASE=1 22 | 23 | As usual, if you don't have the ``make`` command available, you can always run 24 | the code manually with ``python3 .py``. 25 | -------------------------------------------------------------------------------- /book/src/ch08/mutation-testing.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export PYTHONPATH=src 3 | CASE=${CASE:-"1"} 4 | 5 | echo "Running mutation testing ${CASE}" 6 | 7 | mut.py \ 8 | --target src/mutation_testing_${CASE}.py \ 9 | --unit-test tests/test_mutation_testing_${CASE}.py \ 10 | --operator AOD `# delete arithmetic operator` \ 11 | --operator AOR `# replace arithmetic operator` \ 12 | --operator COD `# delete conditional operator` \ 13 | --operator COI `# insert conditional operator` \ 14 | --operator CRP `# replace constant` \ 15 | --operator ROR `# replace relational operator` \ 16 | --show-mutants 17 | -------------------------------------------------------------------------------- /book/src/ch08/run-coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PYTHONPATH=src pytest \ 4 | --cov-report term-missing \ 5 | --cov=coverage_1 \ 6 | tests/test_coverage_1.py 7 | -------------------------------------------------------------------------------- /book/src/ch08/src/constants.py: -------------------------------------------------------------------------------- 1 | """Definitions""" 2 | 3 | STATUS_ENDPOINT = "http://localhost:8080/mrstatus" 4 | -------------------------------------------------------------------------------- /book/src/ch08/src/coverage_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 8: Unit Testing & Refactoring 2 | 3 | > Coverage 4 | """ 5 | 6 | from enum import Enum 7 | 8 | 9 | class MergeRequestStatus(Enum): 10 | APPROVED = "approved" 11 | REJECTED = "rejected" 12 | PENDING = "pending" 13 | OPEN = "open" 14 | CLOSED = "closed" 15 | 16 | 17 | class MergeRequestException(Exception): 18 | """Something went wrong with the merge request""" 19 | 20 | 21 | class AcceptanceThreshold: 22 | def __init__(self, merge_request_context: dict) -> None: 23 | self._context = merge_request_context 24 | 25 | def status(self): 26 | if self._context["downvotes"]: 27 | return MergeRequestStatus.REJECTED 28 | elif len(self._context["upvotes"]) >= 2: 29 | return MergeRequestStatus.APPROVED 30 | return MergeRequestStatus.PENDING 31 | 32 | 33 | class MergeRequest: 34 | def __init__(self): 35 | self._context = {"upvotes": set(), "downvotes": set()} 36 | self._status = MergeRequestStatus.OPEN 37 | 38 | def close(self): 39 | self._status = MergeRequestStatus.CLOSED 40 | 41 | @property 42 | def status(self): 43 | if self._status == MergeRequestStatus.CLOSED: 44 | return self._status 45 | 46 | return AcceptanceThreshold(self._context).status() 47 | 48 | def _cannot_vote_if_closed(self): 49 | if self._status == MergeRequestStatus.CLOSED: 50 | raise MergeRequestException("can't vote on a closed merge request") 51 | 52 | def upvote(self, by_user): 53 | self._cannot_vote_if_closed() 54 | 55 | self._context["downvotes"].discard(by_user) 56 | self._context["upvotes"].add(by_user) 57 | 58 | def downvote(self, by_user): 59 | self._cannot_vote_if_closed() 60 | 61 | self._context["upvotes"].discard(by_user) 62 | self._context["downvotes"].add(by_user) 63 | -------------------------------------------------------------------------------- /book/src/ch08/src/coverage_caveats.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 8: Unit Testing & Refactoring 2 | > Coverage: caveats 3 | """ 4 | 5 | 6 | def my_function(number: int): 7 | return "even" if number % 2 == 0 else "odd" 8 | -------------------------------------------------------------------------------- /book/src/ch08/src/doctest_module.py: -------------------------------------------------------------------------------- 1 | def convert_num(num_str: str): 2 | """ 3 | >>> convert_num("12345") 4 | 12345 5 | 6 | >>> convert_num("-12345") 7 | -12345 8 | 9 | >>> convert_num("12345-") 10 | -12345 11 | 12 | >>> convert_num("-12345-") 13 | 12345 14 | """ 15 | num, sign = num_str[:-1], num_str[-1] 16 | if sign == "-": 17 | return -int(num) 18 | return int(num_str) 19 | -------------------------------------------------------------------------------- /book/src/ch08/src/doctest_module_test.py: -------------------------------------------------------------------------------- 1 | import doctest 2 | import unittest 3 | import doctest_module 4 | 5 | 6 | def load_tests(loader, tests, ignore): 7 | tests.addTests(doctest.DocTestSuite(doctest_module)) 8 | return tests 9 | 10 | 11 | if __name__ == "__main__": 12 | unittest.main() 13 | -------------------------------------------------------------------------------- /book/src/ch08/src/mock_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 8: Unit Testing 2 | 3 | > Mock Objects 4 | """ 5 | 6 | from typing import Dict, List 7 | 8 | 9 | class GitBranch: 10 | def __init__(self, commits: List[Dict]): 11 | self._commits = {c["id"]: c for c in commits} 12 | 13 | def __getitem__(self, commit_id): 14 | return self._commits[commit_id] 15 | 16 | def __len__(self): 17 | return len(self._commits) 18 | 19 | 20 | def author_by_id(commit_id, branch): 21 | return branch[commit_id]["author"] 22 | -------------------------------------------------------------------------------- /book/src/ch08/src/mock_2.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 8: Unit Testing 2 | 3 | > Mock Objects 4 | 5 | """ 6 | from datetime import datetime 7 | 8 | import requests 9 | from constants import STATUS_ENDPOINT 10 | 11 | 12 | class BuildStatus: 13 | """The CI status of a pull request.""" 14 | 15 | @staticmethod 16 | def build_date() -> str: 17 | return datetime.utcnow().isoformat() 18 | 19 | @classmethod 20 | def notify(cls, merge_request_id, status): 21 | build_status = { 22 | "id": merge_request_id, 23 | "status": status, 24 | "built_at": cls.build_date(), 25 | } 26 | response = requests.post(STATUS_ENDPOINT, json=build_status) 27 | response.raise_for_status() 28 | return response 29 | -------------------------------------------------------------------------------- /book/src/ch08/src/mrstatus.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class MergeRequestStatus(Enum): 5 | APPROVED = "approved" 6 | REJECTED = "rejected" 7 | PENDING = "pending" 8 | 9 | 10 | class MergeRequestExtendedStatus(Enum): 11 | APPROVED = "approved" 12 | REJECTED = "rejected" 13 | PENDING = "pending" 14 | OPEN = "open" 15 | CLOSED = "closed" 16 | 17 | 18 | class MergeRequestException(Exception): 19 | """Something went wrong with the merge request.""" 20 | -------------------------------------------------------------------------------- /book/src/ch08/src/mutation_testing_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 8: Unit Testing & Refactoring 2 | 3 | > Mutation Testing 4 | """ 5 | from mrstatus import MergeRequestStatus as Status 6 | 7 | 8 | def evaluate_merge_request(upvote_count, downvotes_count): 9 | if downvotes_count > 0: 10 | return Status.REJECTED 11 | if upvote_count >= 2: 12 | return Status.APPROVED 13 | return Status.PENDING 14 | -------------------------------------------------------------------------------- /book/src/ch08/src/mutation_testing_2.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 8: Unit Testing & Refactoring 2 | 3 | > Mutation Testing 2 4 | """ 5 | 6 | from mrstatus import MergeRequestStatus 7 | 8 | 9 | def evaluate_merge_request(upvote_counts, downvotes_count): 10 | if downvotes_count > 0: 11 | return MergeRequestStatus.REJECTED 12 | if upvote_counts >= 2: 13 | return MergeRequestStatus.APPROVED 14 | return MergeRequestStatus.PENDING 15 | -------------------------------------------------------------------------------- /book/src/ch08/src/refactoring_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 8: Unit Testing 2 | 3 | > Refactoring Code 4 | """ 5 | 6 | from datetime import datetime 7 | 8 | from constants import STATUS_ENDPOINT 9 | 10 | 11 | class BuildStatus: 12 | 13 | endpoint = STATUS_ENDPOINT 14 | 15 | def __init__(self, transport): 16 | self.transport = transport 17 | 18 | @staticmethod 19 | def build_date() -> str: 20 | return datetime.utcnow().isoformat() 21 | 22 | def compose_payload(self, merge_request_id, status) -> dict: 23 | return { 24 | "id": merge_request_id, 25 | "status": status, 26 | "built_at": self.build_date(), 27 | } 28 | 29 | def deliver(self, payload): 30 | response = self.transport.post(self.endpoint, json=payload) 31 | response.raise_for_status() 32 | return response 33 | 34 | def notify(self, merge_request_id, status): 35 | return self.deliver(self.compose_payload(merge_request_id, status)) 36 | -------------------------------------------------------------------------------- /book/src/ch08/src/refactoring_2.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 8: Unit Testing and refactoring 2 | 3 | > Refactoring Code 4 | 5 | """ 6 | from mrstatus import MergeRequestExtendedStatus, MergeRequestException 7 | 8 | 9 | class AcceptanceThreshold: 10 | def __init__(self, merge_request_context: dict) -> None: 11 | self._context = merge_request_context 12 | 13 | def status(self): 14 | if self._context["downvotes"]: 15 | return MergeRequestExtendedStatus.REJECTED 16 | elif len(self._context["upvotes"]) >= 2: 17 | return MergeRequestExtendedStatus.APPROVED 18 | return MergeRequestExtendedStatus.PENDING 19 | 20 | 21 | class MergeRequest: 22 | def __init__(self): 23 | self._context = {"upvotes": set(), "downvotes": set()} 24 | self._status = MergeRequestExtendedStatus.OPEN 25 | 26 | def close(self): 27 | self._status = MergeRequestExtendedStatus.CLOSED 28 | 29 | @property 30 | def status(self): 31 | if self._status == MergeRequestExtendedStatus.CLOSED: 32 | return self._status 33 | 34 | return AcceptanceThreshold(self._context).status() 35 | 36 | def _cannot_vote_if_closed(self): 37 | if self._status == MergeRequestExtendedStatus.CLOSED: 38 | raise MergeRequestException("can't vote on a closed merge request") 39 | 40 | def upvote(self, by_user): 41 | self._cannot_vote_if_closed() 42 | 43 | self._context["downvotes"].discard(by_user) 44 | self._context["upvotes"].add(by_user) 45 | 46 | def downvote(self, by_user): 47 | self._cannot_vote_if_closed() 48 | 49 | self._context["upvotes"].discard(by_user) 50 | self._context["downvotes"].add(by_user) 51 | -------------------------------------------------------------------------------- /book/src/ch08/src/ut_design_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 8: Unit Testing 2 | 3 | > Unit Testing and Software Design 4 | """ 5 | import logging 6 | import random 7 | 8 | logging.basicConfig(level=logging.INFO) 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class MetricsClient: 13 | """3rd-party metrics client""" 14 | 15 | def send(self, metric_name, metric_value): 16 | if not isinstance(metric_name, str): 17 | raise TypeError("expected type str for metric_name") 18 | 19 | if not isinstance(metric_value, str): 20 | raise TypeError("expected type str for metric_value") 21 | 22 | logger.info("sending %s = %s", metric_name, metric_value) 23 | 24 | 25 | class Process: 26 | """A job that runs in iterations, and depends on an external object.""" 27 | 28 | def __init__(self): 29 | self.client = MetricsClient() # A 3rd-party metrics client 30 | 31 | def process_iterations(self, n_iterations): 32 | for i in range(n_iterations): 33 | result = self.run_process() 34 | self.client.send(f"iteration.{i}", str(result)) 35 | 36 | def run_process(self): 37 | return random.randint(1, 100) 38 | 39 | 40 | if __name__ == "__main__": 41 | Process().process_iterations(10) 42 | -------------------------------------------------------------------------------- /book/src/ch08/src/ut_design_2.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 8: Unit Testing 2 | 3 | > Unit Testing and Software Design 4 | """ 5 | import logging 6 | import random 7 | 8 | logging.basicConfig(level=logging.INFO) 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class MetricsClient: 13 | """3rd-party metrics client""" 14 | 15 | def send(self, metric_name, metric_value): 16 | if not isinstance(metric_name, str): 17 | raise TypeError("expected type str for metric_name") 18 | 19 | if not isinstance(metric_value, str): 20 | raise TypeError("expected type str for metric_value") 21 | 22 | logger.info("sending %s = %s", metric_name, metric_value) 23 | 24 | 25 | class WrappedClient: 26 | """An object under our control that wraps the 3rd party one.""" 27 | 28 | def __init__(self): 29 | self.client = MetricsClient() 30 | 31 | def send(self, metric_name, metric_value): 32 | return self.client.send(str(metric_name), str(metric_value)) 33 | 34 | 35 | class Process: 36 | """Same process, now using a wrapper object.""" 37 | 38 | def __init__(self): 39 | self.client = WrappedClient() 40 | 41 | def process_iterations(self, n_iterations): 42 | for i in range(n_iterations): 43 | result = self.run_process() 44 | self.client.send("iteration.{}".format(i), result) 45 | 46 | def run_process(self): 47 | return random.randint(1, 100) 48 | 49 | 50 | if __name__ == "__main__": 51 | Process().process_iterations(10) 52 | -------------------------------------------------------------------------------- /book/src/ch08/src/ut_frameworks_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 8: Unit Testing & Refactoring 2 | 3 | > Frameworks and Libraries for Unit Testing 4 | """ 5 | 6 | from mrstatus import MergeRequestStatus 7 | 8 | 9 | class MergeRequest: 10 | """An entity abstracting a merge request.""" 11 | 12 | def __init__(self): 13 | self._context = {"upvotes": set(), "downvotes": set()} 14 | 15 | @property 16 | def status(self): 17 | if self._context["downvotes"]: 18 | return MergeRequestStatus.REJECTED 19 | elif len(self._context["upvotes"]) >= 2: 20 | return MergeRequestStatus.APPROVED 21 | return MergeRequestStatus.PENDING 22 | 23 | def upvote(self, by_user): 24 | self._context["downvotes"].discard(by_user) 25 | self._context["upvotes"].add(by_user) 26 | 27 | def downvote(self, by_user): 28 | self._context["upvotes"].discard(by_user) 29 | self._context["downvotes"].add(by_user) 30 | -------------------------------------------------------------------------------- /book/src/ch08/src/ut_frameworks_2.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 8: Unit Testing & Refactoring 2 | 3 | > Frameworks and Libraries for Unit Testing 4 | 5 | """ 6 | 7 | from mrstatus import MergeRequestException 8 | from mrstatus import MergeRequestExtendedStatus as MergeRequestStatus 9 | 10 | 11 | class MergeRequest: 12 | def __init__(self): 13 | self._context = {"upvotes": set(), "downvotes": set()} 14 | self._status = MergeRequestStatus.OPEN 15 | 16 | def close(self): 17 | self._status = MergeRequestStatus.CLOSED 18 | 19 | @property 20 | def status(self): 21 | if self._status == MergeRequestStatus.CLOSED: 22 | return self._status 23 | 24 | if self._context["downvotes"]: 25 | return MergeRequestStatus.REJECTED 26 | elif len(self._context["upvotes"]) >= 2: 27 | return MergeRequestStatus.APPROVED 28 | return MergeRequestStatus.PENDING 29 | 30 | def _cannot_vote_if_closed(self): 31 | if self._status == MergeRequestStatus.CLOSED: 32 | raise MergeRequestException("can't vote on a closed merge request") 33 | 34 | def upvote(self, by_user): 35 | self._cannot_vote_if_closed() 36 | 37 | self._context["downvotes"].discard(by_user) 38 | self._context["upvotes"].add(by_user) 39 | 40 | def downvote(self, by_user): 41 | self._cannot_vote_if_closed() 42 | 43 | self._context["upvotes"].discard(by_user) 44 | self._context["downvotes"].add(by_user) 45 | -------------------------------------------------------------------------------- /book/src/ch08/src/ut_frameworks_3.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 8: Unit Testing & Refactoring 2 | 3 | > Frameworks and Libraries for Unit Testing 4 | 5 | """ 6 | 7 | from mrstatus import MergeRequestException 8 | from mrstatus import MergeRequestExtendedStatus as MergeRequestStatus 9 | 10 | 11 | class AcceptanceThreshold: 12 | def __init__(self, merge_request_context: dict) -> None: 13 | self._context = merge_request_context 14 | 15 | def status(self): 16 | if self._context["downvotes"]: 17 | return MergeRequestStatus.REJECTED 18 | elif len(self._context["upvotes"]) >= 2: 19 | return MergeRequestStatus.APPROVED 20 | return MergeRequestStatus.PENDING 21 | 22 | 23 | class MergeRequest: 24 | def __init__(self): 25 | self._context = {"upvotes": set(), "downvotes": set()} 26 | self._status = MergeRequestStatus.OPEN 27 | 28 | def close(self): 29 | self._status = MergeRequestStatus.CLOSED 30 | 31 | @property 32 | def status(self): 33 | if self._status == MergeRequestStatus.CLOSED: 34 | return self._status 35 | 36 | return AcceptanceThreshold(self._context).status() 37 | 38 | def _cannot_vote_if_closed(self): 39 | if self._status == MergeRequestStatus.CLOSED: 40 | raise MergeRequestException("can't vote on a closed merge request") 41 | 42 | def upvote(self, by_user): 43 | self._cannot_vote_if_closed() 44 | 45 | self._context["downvotes"].discard(by_user) 46 | self._context["upvotes"].add(by_user) 47 | 48 | def downvote(self, by_user): 49 | self._cannot_vote_if_closed() 50 | 51 | self._context["upvotes"].discard(by_user) 52 | self._context["downvotes"].add(by_user) 53 | -------------------------------------------------------------------------------- /book/src/ch08/src/ut_frameworks_4.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 8: Unit Testing & Refactoring 2 | 3 | > Frameworks and Libraries for Unit Testing 4 | 5 | """ 6 | 7 | from mrstatus import MergeRequestException 8 | from mrstatus import MergeRequestExtendedStatus as MergeRequestStatus 9 | from ut_frameworks_3 import AcceptanceThreshold 10 | 11 | 12 | class MergeRequest: 13 | def __init__(self): 14 | self._context = {"upvotes": set(), "downvotes": set()} 15 | self._status = MergeRequestStatus.OPEN 16 | 17 | def close(self): 18 | self._status = MergeRequestStatus.CLOSED 19 | 20 | @property 21 | def status(self): 22 | if self._status == MergeRequestStatus.CLOSED: 23 | return self._status 24 | 25 | return AcceptanceThreshold(self._context).status() 26 | 27 | def _cannot_vote_if_closed(self): 28 | if self._status == MergeRequestStatus.CLOSED: 29 | raise MergeRequestException("can't vote on a closed merge request") 30 | 31 | def upvote(self, by_user): 32 | self._cannot_vote_if_closed() 33 | 34 | self._context["downvotes"].discard(by_user) 35 | self._context["upvotes"].add(by_user) 36 | 37 | def downvote(self, by_user): 38 | self._cannot_vote_if_closed() 39 | 40 | self._context["upvotes"].discard(by_user) 41 | self._context["downvotes"].add(by_user) 42 | -------------------------------------------------------------------------------- /book/src/ch08/tests/test_coverage_caveats.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from coverage_caveats import my_function 3 | 4 | 5 | @pytest.mark.parametrize("number,expected", [(2, "even")]) 6 | def test_my_function(number, expected): 7 | assert my_function(number) == expected 8 | -------------------------------------------------------------------------------- /book/src/ch08/tests/test_mock_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 8: Unit Testing 2 | 3 | > Mock Objects 4 | 5 | - File under test: mock_1.py 6 | 7 | """ 8 | 9 | from unittest.mock import MagicMock 10 | 11 | from mock_1 import GitBranch, author_by_id 12 | 13 | 14 | def test_find_commit(): 15 | branch = GitBranch([{"id": "123", "author": "dev1"}]) 16 | assert author_by_id("123", branch) == "dev1" 17 | 18 | 19 | def test_find_any(): 20 | mbranch = MagicMock() 21 | mbranch.__getitem__.return_value = {"author": "test"} 22 | assert author_by_id("123", mbranch) == "test" 23 | -------------------------------------------------------------------------------- /book/src/ch08/tests/test_mock_2.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 8: Unit Testing 2 | 3 | > Mock Objects 4 | 5 | - File under test: mock_1.py 6 | 7 | """ 8 | 9 | from unittest import mock 10 | 11 | from constants import STATUS_ENDPOINT 12 | from mock_2 import BuildStatus 13 | 14 | 15 | @mock.patch("mock_2.requests") 16 | def test_build_notification_sent(mock_requests): 17 | build_date = "2018-01-01T00:00:01" 18 | with mock.patch("mock_2.BuildStatus.build_date", return_value=build_date): 19 | BuildStatus.notify(123, "OK") 20 | 21 | expected_payload = {"id": 123, "status": "OK", "built_at": build_date} 22 | mock_requests.post.assert_called_with( 23 | STATUS_ENDPOINT, json=expected_payload 24 | ) 25 | -------------------------------------------------------------------------------- /book/src/ch08/tests/test_mutation_testing_1.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from mrstatus import MergeRequestStatus as Status 4 | from mutation_testing_1 import evaluate_merge_request 5 | 6 | 7 | class TestMergeRequestEvaluation(unittest.TestCase): 8 | def test_approved(self): 9 | result = evaluate_merge_request(3, 0) 10 | self.assertEqual(result, Status.APPROVED) 11 | 12 | 13 | if __name__ == "__main__": 14 | unittest.main() 15 | -------------------------------------------------------------------------------- /book/src/ch08/tests/test_mutation_testing_2.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from collections import namedtuple 3 | from itertools import starmap 4 | 5 | from mrstatus import MergeRequestStatus as Status 6 | from mutation_testing_1 import evaluate_merge_request 7 | 8 | _TestCase = namedtuple( 9 | "_TestCase", "number_approved,number_rejected,expected_status" 10 | ) 11 | 12 | TEST_DATA = tuple( 13 | starmap( 14 | _TestCase, 15 | ( 16 | (2, 1, Status.REJECTED), 17 | (0, 1, Status.REJECTED), 18 | (2, 0, Status.APPROVED), 19 | (3, 0, Status.APPROVED), 20 | (1, 0, Status.PENDING), 21 | (0, 0, Status.PENDING), 22 | ), 23 | ) 24 | ) 25 | 26 | status_str = { 27 | Status.REJECTED: "rejected", 28 | Status.APPROVED: "approved", 29 | Status.PENDING: "pending", 30 | } 31 | 32 | 33 | class TestMergeRequestEvaluation(unittest.TestCase): 34 | def test_status_resolution(self): 35 | for number_approved, number_rejected, expected_status in TEST_DATA: 36 | obtained = evaluate_merge_request(number_approved, number_rejected) 37 | 38 | self.assertEqual(obtained, expected_status) 39 | 40 | def test_string_values(self): 41 | for number_approved, number_rejected, expected_status in TEST_DATA: 42 | obtained = evaluate_merge_request(number_approved, number_rejected) 43 | 44 | self.assertEqual(obtained.value, status_str[obtained]) 45 | 46 | 47 | if __name__ == "__main__": 48 | unittest.main() 49 | -------------------------------------------------------------------------------- /book/src/ch08/tests/test_refactoring_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 8: Unit Testing 2 | 3 | > Refactoring Code 4 | 5 | - File under test: refactoring_1.py 6 | 7 | """ 8 | from unittest.mock import Mock 9 | 10 | import pytest 11 | 12 | from refactoring_1 import BuildStatus 13 | 14 | 15 | @pytest.fixture 16 | def build_status(): 17 | bstatus = BuildStatus(Mock()) 18 | bstatus.build_date = Mock(return_value="2018-01-01T00:00:01") 19 | return bstatus 20 | 21 | 22 | def test_build_notification_sent(build_status): 23 | 24 | build_status.notify(1234, "OK") 25 | 26 | expected_payload = { 27 | "id": 1234, 28 | "status": "OK", 29 | "built_at": build_status.build_date(), 30 | } 31 | 32 | build_status.transport.post.assert_called_with( 33 | build_status.endpoint, json=expected_payload 34 | ) 35 | -------------------------------------------------------------------------------- /book/src/ch08/tests/test_ut_design_2.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase, main 2 | from unittest.mock import Mock 3 | 4 | from ut_design_2 import WrappedClient 5 | 6 | 7 | class TestWrappedClient(TestCase): 8 | def test_send_converts_types(self): 9 | wrapped_client = WrappedClient() 10 | wrapped_client.client = Mock() 11 | wrapped_client.send("value", 1) 12 | 13 | wrapped_client.client.send.assert_called_with("value", "1") 14 | 15 | 16 | if __name__ == "__main__": 17 | main() 18 | -------------------------------------------------------------------------------- /book/src/ch09/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | test: 3 | @$(PYTHON) -m doctest src/*.py 4 | @$(PYTHON) -m unittest tests/*.py 5 | 6 | .PHONY: clean 7 | clean: 8 | find . -type f -name "*.pyc" -delete 9 | find . -type d -name __pycache__ | xargs rm -fr {} 10 | 11 | .PHONY: typehint 12 | typehint: 13 | @mypy src/*.py 14 | 15 | .PHONY: lint 16 | lint: 17 | black --check --line-length=79 src tests 18 | 19 | .PHONY: format 20 | format: 21 | black --line-length=79 src tests 22 | 23 | .PHONY: checklist 24 | checklist: lint typehint test 25 | -------------------------------------------------------------------------------- /book/src/ch09/README.rst: -------------------------------------------------------------------------------- 1 | Clean code in Python - Chapter 09: Common design patterns 2 | ========================================================= 3 | 4 | Run tests with:: 5 | 6 | make test -------------------------------------------------------------------------------- /book/src/ch09/src/_adapter_base.py: -------------------------------------------------------------------------------- 1 | from log import logger 2 | 3 | 4 | class UsernameLookup: 5 | def search(self, user_namespace): 6 | logger.info("looking for %s", user_namespace) 7 | -------------------------------------------------------------------------------- /book/src/ch09/src/adapter_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 9: Common Design Patterns 2 | 3 | > Adapter (Inheritance) 4 | """ 5 | 6 | from _adapter_base import UsernameLookup 7 | 8 | 9 | class UserSource(UsernameLookup): 10 | def fetch(self, user_id, username): 11 | user_namespace = self._adapt_arguments(user_id, username) 12 | return self.search(user_namespace) 13 | 14 | @staticmethod 15 | def _adapt_arguments(user_id, username): 16 | return f"{user_id}:{username}" 17 | -------------------------------------------------------------------------------- /book/src/ch09/src/adapter_2.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 9: Common Design Patterns 2 | 3 | > Adapter (Composition) 4 | """ 5 | 6 | from _adapter_base import UsernameLookup 7 | 8 | 9 | class UserSource: 10 | def __init__(self, username_lookup: UsernameLookup) -> None: 11 | self.username_lookup = username_lookup 12 | 13 | def fetch(self, user_id, username): 14 | user_namespace = self._adapt_arguments(user_id, username) 15 | return self.username_lookup.search(user_namespace) 16 | 17 | @staticmethod 18 | def _adapt_arguments(user_id, username): 19 | return f"{user_id}:{username}" 20 | -------------------------------------------------------------------------------- /book/src/ch09/src/chain_of_responsibility_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 9: Common Design Patterns 2 | 3 | > Chain of Responsibility 4 | """ 5 | import re 6 | from typing import Optional, Pattern 7 | 8 | 9 | class Event: 10 | pattern: Optional[Pattern[str]] = None 11 | 12 | def __init__(self, next_event=None): 13 | self.successor = next_event 14 | 15 | def process(self, logline: str): 16 | if self.can_process(logline): 17 | return self._process(logline) 18 | 19 | if self.successor is not None: 20 | return self.successor.process(logline) 21 | 22 | def _process(self, logline: str) -> dict: 23 | parsed_data = self._parse_data(logline) 24 | return { 25 | "type": self.__class__.__name__, 26 | "id": parsed_data["id"], 27 | "value": parsed_data["value"], 28 | } 29 | 30 | @classmethod 31 | def can_process(cls, logline: str) -> bool: 32 | return ( 33 | cls.pattern is not None and cls.pattern.match(logline) is not None 34 | ) 35 | 36 | @classmethod 37 | def _parse_data(cls, logline: str) -> dict: 38 | if not cls.pattern: 39 | return {} 40 | if (parsed := cls.pattern.match(logline)) is not None: 41 | return parsed.groupdict() 42 | return {} 43 | 44 | 45 | class LoginEvent(Event): 46 | pattern = re.compile(r"(?P\d+):\s+login\s+(?P\S+)") 47 | 48 | 49 | class LogoutEvent(Event): 50 | pattern = re.compile(r"(?P\d+):\s+logout\s+(?P\S+)") 51 | 52 | 53 | class SessionEvent(Event): 54 | pattern = re.compile(r"(?P\d+):\s+log(in|out)\s+(?P\S+)") 55 | -------------------------------------------------------------------------------- /book/src/ch09/src/composite_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 9: Common Design Patterns 2 | 3 | > Composite 4 | """ 5 | 6 | from typing import Iterable, Union 7 | 8 | 9 | class Product: 10 | def __init__(self, name: str, price: float) -> None: 11 | self._name = name 12 | self._price = price 13 | 14 | @property 15 | def price(self): 16 | return self._price 17 | 18 | 19 | class ProductBundle: 20 | def __init__( 21 | self, 22 | name: str, 23 | perc_discount: float, 24 | *products: Iterable[Union[Product, "ProductBundle"]] 25 | ) -> None: 26 | self._name = name 27 | self._perc_discount = perc_discount 28 | self._products = products 29 | 30 | @property 31 | def price(self) -> float: 32 | total = sum(p.price for p in self._products) # type: ignore 33 | return total * (1 - self._perc_discount) 34 | -------------------------------------------------------------------------------- /book/src/ch09/src/decorator_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 9: Common Design Patterns 2 | 3 | > Decorator 4 | """ 5 | 6 | 7 | class DictQuery: 8 | def __init__(self, **kwargs): 9 | self._raw_query = kwargs 10 | 11 | def render(self) -> dict: 12 | return self._raw_query 13 | 14 | 15 | class QueryEnhancer: 16 | def __init__(self, query: DictQuery): 17 | self.decorated = query 18 | 19 | def render(self): 20 | return self.decorated.render() 21 | 22 | 23 | class RemoveEmpty(QueryEnhancer): 24 | def render(self): 25 | original = super().render() 26 | return {k: v for k, v in original.items() if v} 27 | 28 | 29 | class CaseInsensitive(QueryEnhancer): 30 | def render(self): 31 | original = super().render() 32 | return {k: v.lower() for k, v in original.items()} 33 | -------------------------------------------------------------------------------- /book/src/ch09/src/decorator_2.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 9: Common Design Patterns 2 | 3 | > Decorator: A function-based version 4 | """ 5 | from typing import Callable, Dict, Iterable 6 | 7 | 8 | class DictQuery: 9 | def __init__(self, **kwargs): 10 | self._raw_query = kwargs 11 | 12 | def render(self) -> dict: 13 | return self._raw_query 14 | 15 | 16 | class QueryEnhancer: 17 | def __init__( 18 | self, 19 | query: DictQuery, 20 | *decorators: Iterable[Callable[[Dict[str, str]], Dict[str, str]]] 21 | ) -> None: 22 | self._decorated = query 23 | self._decorators = decorators 24 | 25 | def render(self): 26 | current_result = self._decorated.render() 27 | for deco in self._decorators: 28 | current_result = deco(current_result) 29 | return current_result 30 | 31 | 32 | def remove_empty(original: dict) -> dict: 33 | return {k: v for k, v in original.items() if v} 34 | 35 | 36 | def case_insensitive(original: dict) -> dict: 37 | return {k: v.lower() for k, v in original.items()} 38 | -------------------------------------------------------------------------------- /book/src/ch09/src/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logging.basicConfig() 4 | logger = logging.getLogger(__name__) 5 | -------------------------------------------------------------------------------- /book/src/ch09/src/monostate_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 9: Common Design Patterns 2 | 3 | > Monostate Pattern 4 | """ 5 | from log import logger 6 | 7 | 8 | class GitFetcher: 9 | _current_tag = None 10 | 11 | def __init__(self, tag): 12 | self.current_tag = tag 13 | 14 | @property 15 | def current_tag(self): 16 | if self._current_tag is None: 17 | raise AttributeError("tag was never set") 18 | return self._current_tag 19 | 20 | @current_tag.setter 21 | def current_tag(self, new_tag): 22 | self.__class__._current_tag = new_tag 23 | 24 | def pull(self): 25 | logger.info("pulling from %s", self.current_tag) 26 | return self.current_tag 27 | -------------------------------------------------------------------------------- /book/src/ch09/src/monostate_2.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 9: Common Design Patterns 2 | 3 | > Monostate Pattern 4 | """ 5 | 6 | from log import logger 7 | 8 | 9 | class SharedAttribute: 10 | def __init__(self, initial_value=None): 11 | self.value = initial_value 12 | self._name = None 13 | 14 | def __get__(self, instance, owner): 15 | if instance is None: 16 | return self 17 | if self.value is None: 18 | raise AttributeError(f"{self._name} was never set") 19 | return self.value 20 | 21 | def __set__(self, instance, new_value): 22 | self.value = new_value 23 | 24 | def __set_name__(self, owner, name): 25 | self._name = name 26 | 27 | 28 | class GitFetcher: 29 | 30 | current_tag = SharedAttribute() 31 | current_branch = SharedAttribute() 32 | 33 | def __init__(self, tag, branch=None): 34 | self.current_tag = tag 35 | self.current_branch = branch 36 | 37 | def pull(self): 38 | logger.info("pulling from %s", self.current_tag) 39 | return self.current_tag 40 | -------------------------------------------------------------------------------- /book/src/ch09/src/monostate_3.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 9: Common Design Patterns 2 | 3 | > Monostate Pattern: BORG 4 | """ 5 | from log import logger 6 | 7 | 8 | class BaseFetcher: 9 | def __init__(self, source): 10 | self.source = source 11 | 12 | 13 | class TagFetcher(BaseFetcher): 14 | _attributes: dict = {} 15 | 16 | def __init__(self, source): 17 | self.__dict__ = self.__class__._attributes 18 | super().__init__(source) 19 | 20 | def pull(self): 21 | logger.info("pulling from tag %s", self.source) 22 | return f"Tag = {self.source}" 23 | 24 | 25 | class BranchFetcher(BaseFetcher): 26 | _attributes: dict = {} 27 | 28 | def __init__(self, source): 29 | self.__dict__ = self.__class__._attributes 30 | super().__init__(source) 31 | 32 | def pull(self): 33 | logger.info("pulling from branch %s", self.source) 34 | return f"Branch = {self.source}" 35 | -------------------------------------------------------------------------------- /book/src/ch09/src/monostate_4.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 9: Common Design Patterns 2 | 3 | > Monostate Pattern: Borg 4 | """ 5 | from log import logger 6 | 7 | 8 | class SharedAllMixin: 9 | def __init__(self, *args, **kwargs): 10 | try: 11 | self.__class__._attributes 12 | except AttributeError: 13 | self.__class__._attributes = {} 14 | 15 | self.__dict__ = self.__class__._attributes 16 | super().__init__(*args, **kwargs) 17 | 18 | 19 | class BaseFetcher: 20 | def __init__(self, source): 21 | self.source = source 22 | 23 | 24 | class TagFetcher(SharedAllMixin, BaseFetcher): 25 | def pull(self): 26 | logger.info("pulling from tag %s", self.source) 27 | return f"Tag = {self.source}" 28 | 29 | 30 | class BranchFetcher(SharedAllMixin, BaseFetcher): 31 | def pull(self): 32 | logger.info("pulling from branch %s", self.source) 33 | return f"Branch = {self.source}" 34 | -------------------------------------------------------------------------------- /book/src/ch09/tests/test_composite_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 9: Common Design Patterns 2 | 3 | > Tests Composite 4 | """ 5 | 6 | import unittest 7 | 8 | from composite_1 import Product, ProductBundle 9 | 10 | 11 | class TestProducts(unittest.TestCase): 12 | def test_product_bundle(self): 13 | 14 | tablet = Product("tablet", 200) 15 | bundle = ProductBundle( 16 | "electronics", 17 | 0.1, 18 | tablet, 19 | Product("smartphone", 100), 20 | Product("laptop", 700), 21 | ) 22 | self.assertEqual(tablet.price, 200) 23 | self.assertEqual(bundle.price, 900) 24 | 25 | def test_nested_bundle(self): 26 | electronics = ProductBundle( 27 | "electronics", 28 | 0, 29 | ProductBundle( 30 | "smartphones", 31 | 0.15, 32 | Product("smartphone1", 200), 33 | Product("smartphone2", 700), 34 | ), 35 | ProductBundle( 36 | "laptops", 37 | 0.05, 38 | Product("laptop1", 700), 39 | Product("laptop2", 950), 40 | ), 41 | ) 42 | tablets = ProductBundle( 43 | "tablets", 0.05, Product("tablet1", 200), Product("tablet2", 300) 44 | ) 45 | total = ProductBundle("total", 0, electronics, tablets) 46 | expected_total_price = (0.85 * 900) + (0.95 * 1650) + (0.95 * 500) 47 | 48 | self.assertEqual(total.price, expected_total_price) 49 | 50 | 51 | if __name__ == "__main__": 52 | unittest.main() 53 | -------------------------------------------------------------------------------- /book/src/ch09/tests/test_decorator_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 9: Common Design Patterns 2 | 3 | > Test Decorator 4 | """ 5 | import unittest 6 | 7 | from decorator_1 import CaseInsensitive, DictQuery, RemoveEmpty 8 | 9 | 10 | class TestDecoration(unittest.TestCase): 11 | def setUp(self): 12 | self.query = DictQuery( 13 | foo="bar", empty="", none=None, upper="UPPERCASE", title="Title" 14 | ) 15 | 16 | def test_no_decorate(self): 17 | expected = { 18 | "foo": "bar", 19 | "empty": "", 20 | "none": None, 21 | "upper": "UPPERCASE", 22 | "title": "Title", 23 | } 24 | self.assertDictEqual(self.query.render(), expected) 25 | 26 | def test_decorate(self): 27 | expected = {"foo": "bar", "upper": "uppercase", "title": "title"} 28 | result = CaseInsensitive(RemoveEmpty(self.query)).render() 29 | self.assertDictEqual(result, expected) 30 | 31 | 32 | if __name__ == "__main__": 33 | unittest.main() 34 | -------------------------------------------------------------------------------- /book/src/ch09/tests/test_decorator_2.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 9: Common Design Patterns 2 | 3 | > Test Decorator 2 4 | """ 5 | import unittest 6 | 7 | from decorator_2 import ( 8 | DictQuery, 9 | QueryEnhancer, 10 | case_insensitive, 11 | remove_empty, 12 | ) 13 | 14 | 15 | class TestDecoration(unittest.TestCase): 16 | def setUp(self): 17 | self.query = DictQuery( 18 | foo="bar", empty="", none=None, upper="UPPERCASE", title="Title" 19 | ) 20 | 21 | def test_no_decorate(self): 22 | expected = { 23 | "foo": "bar", 24 | "empty": "", 25 | "none": None, 26 | "upper": "UPPERCASE", 27 | "title": "Title", 28 | } 29 | self.assertDictEqual(self.query.render(), expected) 30 | 31 | def test_decorate(self): 32 | expected = {"foo": "bar", "upper": "uppercase", "title": "title"} 33 | result = QueryEnhancer( 34 | self.query, remove_empty, case_insensitive 35 | ).render() 36 | self.assertDictEqual(result, expected) 37 | 38 | 39 | if __name__ == "__main__": 40 | unittest.main() 41 | -------------------------------------------------------------------------------- /book/src/ch09/tests/test_monostate_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 9: Common Design Patterns 2 | 3 | > Test Monostate Pattern 4 | """ 5 | import unittest 6 | 7 | from monostate_1 import GitFetcher 8 | 9 | 10 | class TestFetcher(unittest.TestCase): 11 | def test_fetch_single(self): 12 | fetcher = GitFetcher(0.1) 13 | self.assertEqual(fetcher.pull(), 0.1) 14 | 15 | def test_fetch_multiple(self): 16 | f1 = GitFetcher(0.1) 17 | f2 = GitFetcher(0.2) 18 | 19 | self.assertEqual(f1.pull(), 0.2) 20 | # There is a new version in f1's request 21 | f1.current_tag = 0.3 22 | 23 | self.assertEqual(f2.pull(), 0.3) 24 | self.assertEqual(f1.pull(), 0.3) 25 | 26 | def test_multiple_consecutive_versions(self): 27 | fetchers = {GitFetcher(i) for i in range(5)} 28 | 29 | self.assertTrue(all(f.current_tag == 4 for f in fetchers)) 30 | 31 | def test_never_set(self): 32 | fetcher = GitFetcher(None) 33 | self.assertRaisesRegex( 34 | AttributeError, "\S+ was never set", fetcher.pull 35 | ) 36 | 37 | 38 | if __name__ == "__main__": 39 | unittest.main() 40 | -------------------------------------------------------------------------------- /book/src/ch09/tests/test_monostate_2.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 9: Common Design Patterns 2 | 3 | > Test Monostate Pattern 4 | """ 5 | import unittest 6 | 7 | from monostate_2 import GitFetcher 8 | 9 | 10 | class TestCurrentTag(unittest.TestCase): 11 | def test_fetch_single(self): 12 | fetcher = GitFetcher(0.1) 13 | self.assertEqual(fetcher.pull(), 0.1) 14 | 15 | def test_fetch_multiple(self): 16 | f1 = GitFetcher(0.1) 17 | f2 = GitFetcher(0.2) 18 | 19 | self.assertEqual(f1.pull(), 0.2) 20 | # There is a new version in f1's request 21 | f1.current_tag = 0.3 22 | 23 | self.assertEqual(f2.pull(), 0.3) 24 | self.assertEqual(f1.pull(), 0.3) 25 | 26 | def test_multiple_consecutive_versions(self): 27 | fetchers = {GitFetcher(i) for i in range(5)} 28 | 29 | self.assertTrue(all(f.current_tag == 4 for f in fetchers)) 30 | 31 | def test_never_set(self): 32 | fetcher = GitFetcher(None) 33 | self.assertRaisesRegex( 34 | AttributeError, "\S+ was never set", fetcher.pull 35 | ) 36 | 37 | 38 | class TestCurrentBranch(unittest.TestCase): 39 | def test_current_branch(self): 40 | f1 = GitFetcher(0.1, "mainline") 41 | GitFetcher(0.2, "develop") 42 | 43 | self.assertEqual(f1.current_tag, 0.2) 44 | self.assertEqual(f1.current_branch, "develop") 45 | 46 | 47 | if __name__ == "__main__": 48 | unittest.main() 49 | -------------------------------------------------------------------------------- /book/src/ch09/tests/test_state_1.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 9: Common Design Patterns 2 | 3 | > Test State 4 | """ 5 | 6 | 7 | import unittest 8 | 9 | from state_1 import Closed, InvalidTransitionError, Merged, MergeRequest, Open 10 | 11 | 12 | class TestMergeRequestTransitions(unittest.TestCase): 13 | def setUp(self): 14 | self.mr = MergeRequest("develop", "mainline") 15 | 16 | def test_reopen(self): 17 | self.mr.approvals = 3 18 | self.mr.open() 19 | 20 | self.assertEqual(self.mr.approvals, 0) 21 | 22 | def test_open_to_closed(self): 23 | self.mr.approvals = 2 24 | self.assertIsInstance(self.mr.state, Open) 25 | self.mr.close() 26 | self.assertEqual(self.mr.approvals, 0) 27 | self.assertIsInstance(self.mr.state, Closed) 28 | 29 | def test_closed_to_open(self): 30 | self.mr.close() 31 | self.assertIsInstance(self.mr.state, Closed) 32 | self.mr.open() 33 | self.assertIsInstance(self.mr.state, Open) 34 | 35 | def test_double_close(self): 36 | self.mr.close() 37 | self.mr.close() 38 | 39 | def test_open_to_merge(self): 40 | self.mr.merge() 41 | self.assertIsInstance(self.mr.state, Merged) 42 | 43 | def test_merge_is_final(self): 44 | self.mr.merge() 45 | regex = "already merged request" 46 | self.assertRaisesRegex(InvalidTransitionError, regex, self.mr.open) 47 | self.assertRaisesRegex(InvalidTransitionError, regex, self.mr.close) 48 | 49 | def test_cannot_merge_closed(self): 50 | self.mr.close() 51 | self.assertRaises(InvalidTransitionError, self.mr.merge) 52 | 53 | 54 | if __name__ == "__main__": 55 | unittest.main() 56 | -------------------------------------------------------------------------------- /book/src/ch09/tests/test_state_2.py: -------------------------------------------------------------------------------- 1 | """Clean Code in Python - Chapter 9: Common Design Patterns 2 | 3 | > Test State 2 4 | """ 5 | 6 | 7 | import unittest 8 | 9 | from state_2 import Closed, InvalidTransitionError, Merged, MergeRequest, Open 10 | 11 | 12 | class TestMergeRequestTransitions(unittest.TestCase): 13 | def setUp(self): 14 | self.mr = MergeRequest("develop", "mainline") 15 | 16 | def test_reopen(self): 17 | self.mr.approvals = 3 18 | self.mr.open() 19 | 20 | self.assertEqual(self.mr.approvals, 0) 21 | 22 | def test_open_to_closed(self): 23 | self.mr.approvals = 2 24 | self.assertEqual(self.mr.status, Open.__name__) 25 | self.mr.close() 26 | self.assertEqual(self.mr.approvals, 0) 27 | self.assertEqual(self.mr.status, Closed.__name__) 28 | 29 | def test_closed_to_open(self): 30 | self.mr.close() 31 | self.assertEqual(self.mr.status, Closed.__name__) 32 | self.mr.open() 33 | self.assertEqual(self.mr.status, Open.__name__) 34 | 35 | def test_double_close(self): 36 | self.mr.close() 37 | self.mr.close() 38 | 39 | def test_open_to_merge(self): 40 | self.mr.merge() 41 | self.assertEqual(self.mr.status, Merged.__name__) 42 | 43 | def test_merge_is_final(self): 44 | self.mr.merge() 45 | regex = "already merged request" 46 | self.assertRaisesRegex(InvalidTransitionError, regex, self.mr.open) 47 | self.assertRaisesRegex(InvalidTransitionError, regex, self.mr.close) 48 | 49 | def test_cannot_merge_closed(self): 50 | self.mr.close() 51 | self.assertRaises(InvalidTransitionError, self.mr.merge) 52 | 53 | 54 | if __name__ == "__main__": 55 | unittest.main() 56 | -------------------------------------------------------------------------------- /book/src/ch10/README.rst: -------------------------------------------------------------------------------- 1 | Clean code in Python - Chapter 10: Clean architecture 2 | ===================================================== 3 | 4 | Requirements for running the examples: 5 | 6 | * Docker 7 | * Python 3.9+ 8 | 9 | There is an example service under the ``service`` directory. Follow its ``Makefile`` and ``README`` instructions to 10 | setup. 11 | 12 | Check the ``README`` files under ``service/``. 13 | 14 | To test with a local database, you will need to create its Docker image. Instructions for this are at 15 | ``service/libs/storage/db/``. This will create the database image with some testing data on it. 16 | -------------------------------------------------------------------------------- /book/src/ch10/service/.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | -------------------------------------------------------------------------------- /book/src/ch10/service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9-slim-buster 2 | 3 | RUN apt-get update && \ 4 | apt-get install -y --no-install-recommends \ 5 | python-dev \ 6 | gcc \ 7 | musl-dev \ 8 | make \ 9 | && rm -rf /var/lib/apt/lists/* 10 | 11 | WORKDIR /app 12 | ADD . /app 13 | 14 | RUN pip install /app/libs/web /app/libs/storage 15 | RUN pip install /app 16 | 17 | EXPOSE 8080 18 | CMD ["/usr/local/bin/status-service"] 19 | -------------------------------------------------------------------------------- /book/src/ch10/service/Makefile: -------------------------------------------------------------------------------- 1 | LISTEN_HOST:="127.0.0.1" 2 | LISTEN_PORT:=8080 3 | DBHOST:="127.0.0.1" 4 | DBPORT:=5432 5 | 6 | .PHONY: clean 7 | clean: 8 | find . -type d -name __pycache__ | xargs rm -fr {} 9 | find . -type d -name "*.egg-info" | xargs rm -fr {} 10 | 11 | .PHONY: build 12 | build: 13 | docker-compose build 14 | 15 | .PHONY: run 16 | run: 17 | docker-compose run --service-ports -d web 18 | 19 | .PHONY: typehint 20 | typehint: 21 | mypy --ignore-missing-imports libs/web 22 | mypy --ignore-missing-imports libs/storage 23 | mypy --ignore-missing-imports statusweb 24 | 25 | .PHONY: lint 26 | lint: 27 | black --check --line-length=79 libs statusweb 28 | 29 | .PHONY: format 30 | format: 31 | black --line-length=79 libs statusweb 32 | 33 | .PHONY: build-deps 34 | build-deps: 35 | $(MAKE) -C libs/web build 36 | $(MAKE) -C libs/storage build 37 | -------------------------------------------------------------------------------- /book/src/ch10/service/README.rst: -------------------------------------------------------------------------------- 1 | Chapter 10 - Example of a Service 2 | ================================= 3 | 4 | Structure 5 | --------- 6 | At a glance: 7 | 8 | - ``libs``: With the Python packages (dependencies) needed by the service. 9 | - ``statusweb``: The service itself. Imports its dependencies from ``libs```. 10 | 11 | The service itself is placed in the ``statusweb`` directory. 12 | The ``./libs`` directory contains Python packages that dependencies. Typically, (unless you follow a mono-repo 13 | structure, which is also a possibility), they would belog in a separate repository, but this directory should make it 14 | clear that they're other packages, tailor-made for internal use. 15 | 16 | 17 | Service 18 | ------- 19 | Case: A delivery platform. Check the status of each delivery order by an HTTP GET. 20 | 21 | Service: Delivery Status 22 | Persistency: A RDBMS 23 | Response Format: JSON 24 | 25 | 26 | Running the Service 27 | ------------------- 28 | In order to test the service locally, you'd need to set the following environment variable: ``DBPASSWORD``, and then 29 | build the images:: 30 | 31 | make build 32 | 33 | Note: this Makefile target runs the ``docker`` command line interface. Depending on your configuration you might need to 34 | run it with ``sudo`` if you get permission errors. 35 | 36 | Run the service with:: 37 | 38 | make run 39 | 40 | Note: If you still need to run the previous command with ``sudo``, pass the ``-E`` flag, so that the environment 41 | variables are passed along. 42 | 43 | 44 | Assuming data has been loaded, this can be tested with any HTTP client:: 45 | 46 | $ curl http://localhost:5000/status/1 47 | {"id":1,"status":"dispatched","msg":"Order was dispatched on 2018-08-01T22:25:12+00:00"} 48 | 49 | $ curl http://localhost:5000/status/99 50 | Error: 99 was not found 51 | -------------------------------------------------------------------------------- /book/src/ch10/service/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | services: 3 | web: 4 | build: . 5 | ports: 6 | - "5000:5000" 7 | volumes: 8 | - .:/code 9 | links: 10 | - db 11 | environment: 12 | - LISTEN_PORT=${LISTEN_PORT:-5000} 13 | - DBUSER=${DBUSER:-delivery-app} 14 | - DBNAME=${DBNAME:-delivery-db} 15 | - DBPASSWORD=${DBPASSWORD} 16 | db: 17 | image: postgres:13 18 | volumes: 19 | - ./libs/storage/db/sql:/docker-entrypoint-initdb.d/ 20 | environment: 21 | - POSTGRES_USER=${DBUSER:-delivery-app} 22 | - POSTGRES_DB=${DBNAME:-delivery-db} 23 | - POSTGRES_PASSWORD=${DBPASSWORD} 24 | -------------------------------------------------------------------------------- /book/src/ch10/service/libs/README.rst: -------------------------------------------------------------------------------- 1 | service/libs 2 | ============ 3 | 4 | Directory with the dependencies (libraries) for the main application. 5 | 6 | - ``storage``: Python package that abstracts the database. 7 | - ``web``: Python package that abstracts the web framework. 8 | -------------------------------------------------------------------------------- /book/src/ch10/service/libs/storage/.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | -------------------------------------------------------------------------------- /book/src/ch10/service/libs/storage/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build 2 | build: 3 | python setup.py sdist 4 | -------------------------------------------------------------------------------- /book/src/ch10/service/libs/storage/README.rst: -------------------------------------------------------------------------------- 1 | Layer of Abstraction to the DB 2 | ============================== 3 | 4 | This package is to be used as a library, imported from ``service``. 5 | 6 | Instructions to create a test database are at ``db/README.rst``. 7 | -------------------------------------------------------------------------------- /book/src/ch10/service/libs/storage/db/README.rst: -------------------------------------------------------------------------------- 1 | Database 2 | ^^^^^^^^ 3 | This directory contains the initialization files for the database under the ``./sql`` directory, and the code for the 4 | library that abstracts the persistence layer in the ``storage`` module. 5 | -------------------------------------------------------------------------------- /book/src/ch10/service/libs/storage/db/sql/1_schema.sql: -------------------------------------------------------------------------------- 1 | /** DB schema for test **/ 2 | 3 | CREATE TABLE delivery_order_status ( 4 | delivery_id SERIAL NOT NULL PRIMARY KEY, 5 | status CHAR(1) -- 'd', 't', 'f' 6 | ); 7 | 8 | 9 | CREATE TABLE dispatched ( 10 | delivery_id INTEGER NOT NULL PRIMARY KEY REFERENCES delivery_order_status(delivery_id), 11 | dispatched_at TIMESTAMP WITH TIME ZONE 12 | ); 13 | 14 | 15 | CREATE TABLE in_transit ( 16 | delivery_id INTEGER NOT NULL PRIMARY KEY REFERENCES delivery_order_status(delivery_id), 17 | location VARCHAR(200) 18 | ); 19 | 20 | 21 | CREATE TABLE finished ( 22 | delivery_id INTEGER NOT NULL PRIMARY KEY REFERENCES delivery_order_status(delivery_id), 23 | delivered_at TIMESTAMP WITH TIME ZONE 24 | ); 25 | -------------------------------------------------------------------------------- /book/src/ch10/service/libs/storage/db/sql/2_data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO delivery_order_status (delivery_id, status) VALUES 2 | (1, 'd'), 3 | (2, 'd'), 4 | (3, 't'), 5 | (4, 't'), 6 | (5, 't'), 7 | (6, 't'), 8 | (7, 't'), 9 | (8, 'f'), 10 | (9, 'f'), 11 | (10, 'f'), 12 | (11, 'f') 13 | ; 14 | 15 | INSERT INTO dispatched (delivery_id, dispatched_at) VALUES 16 | (1, '2018-08-01 22:25:12'), 17 | (2, '2018-08-02 14:45:18') 18 | ; 19 | 20 | INSERT INTO in_transit(delivery_id, location) VALUES 21 | (3, 'at (41.3870° N, 2.1700° E)'), 22 | (4, 'at (41.3870° N, 2.1700° E)'), 23 | (5, 'at (41.3870° N, 2.1700° E)'), 24 | (6, 'at (41.3870° N, 2.1700° E)'), 25 | (7, 'at (41.3870° N, 2.1700° E)') 26 | ; 27 | 28 | INSERT INTO finished (delivery_id, delivered_at) VALUES 29 | (8, '2018-08-01 22:25:12'), 30 | (9, '2018-08-01 22:25:12'), 31 | (10, '2018-08-01 22:25:12'), 32 | (11, '2018-08-01 22:25:12') 33 | ; 34 | -------------------------------------------------------------------------------- /book/src/ch10/service/libs/storage/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile setup.py 6 | # 7 | asyncpg==0.21.0 # via storage (setup.py) 8 | -------------------------------------------------------------------------------- /book/src/ch10/service/libs/storage/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | with open("README.rst", "r") as longdesc: 4 | long_description = longdesc.read() 5 | 6 | 7 | install_requires = ["asyncpg>=0.21,<1"] 8 | 9 | setup( 10 | name="storage", 11 | description="Abstraction of the Database for the delivery status service", 12 | long_description=long_description, 13 | author="Dev team", 14 | version="0.1.0", 15 | packages=find_packages(where="src/"), 16 | package_dir={"": "src"}, 17 | install_requires=install_requires, 18 | ) 19 | -------------------------------------------------------------------------------- /book/src/ch10/service/libs/storage/src/storage/__init__.py: -------------------------------------------------------------------------------- 1 | """Expose the functionality of the package.""" 2 | from .client import DBClient 3 | from .storage import DeliveryStatusQuery 4 | from .converters import OrderNotFoundError 5 | 6 | 7 | __all__ = ["DBClient", "DeliveryStatusQuery", "OrderNotFoundError"] 8 | -------------------------------------------------------------------------------- /book/src/ch10/service/libs/storage/src/storage/client.py: -------------------------------------------------------------------------------- 1 | """Abstraction to the database. 2 | 3 | Provide a client to connect to the database and expose a custom API, at the 4 | convenience of the application. 5 | 6 | """ 7 | import asyncpg 8 | 9 | from .config import DB_CONFIG 10 | 11 | 12 | async def DBClient(): 13 | return await asyncpg.connect(**DB_CONFIG) 14 | -------------------------------------------------------------------------------- /book/src/ch10/service/libs/storage/src/storage/config.py: -------------------------------------------------------------------------------- 1 | """Configuration file for the DB Client 2 | Modify this file according to your environment. 3 | """ 4 | import os 5 | 6 | 7 | def _extract_from_env(variable, *, default=None): 8 | try: 9 | return os.environ[variable] 10 | 11 | except KeyError as e: 12 | if default is not None: 13 | return default 14 | 15 | raise RuntimeError(f"Environment variable {variable} not set") from e 16 | 17 | 18 | DB_CONFIG = { 19 | "user": _extract_from_env("DBUSER"), 20 | "password": _extract_from_env("DBPASSWORD"), 21 | "database": _extract_from_env("DBNAME"), 22 | "host": "db", 23 | "port": 5432, 24 | } 25 | -------------------------------------------------------------------------------- /book/src/ch10/service/libs/storage/src/storage/converters.py: -------------------------------------------------------------------------------- 1 | """Convert the row resulting from a query to the Entities object.""" 2 | from .status import ( 3 | DeliveryOrder, 4 | DispatchedOrder, 5 | OrderDelivered, 6 | OrderInTransit, 7 | ) 8 | 9 | 10 | def build_dispatched(row): 11 | return DispatchedOrder(row.dispatched_at) 12 | 13 | 14 | def build_in_transit(row): 15 | return OrderInTransit(row.location) 16 | 17 | 18 | def build_delivered(row): 19 | return OrderDelivered(row.delivered_at) 20 | 21 | 22 | _BUILD_MAPPING = { 23 | "d": build_dispatched, 24 | "t": build_in_transit, 25 | "f": build_delivered, 26 | } 27 | 28 | 29 | class WrappedRow: 30 | def __init__(self, row): 31 | self._row = row 32 | 33 | def __getattr__(self, attrname): 34 | return self._row[attrname] 35 | 36 | 37 | class OrderNotFoundError(Exception): 38 | """The requested order does not appear listed.""" 39 | 40 | 41 | def build_from_row(delivery_id, row): 42 | if row is None: 43 | raise OrderNotFoundError(f"{delivery_id} was not found") 44 | 45 | row = WrappedRow(row) 46 | status_builder = _BUILD_MAPPING[row.status] 47 | status = status_builder(row) 48 | return DeliveryOrder(delivery_id, status) 49 | -------------------------------------------------------------------------------- /book/src/ch10/service/libs/storage/src/storage/storage.py: -------------------------------------------------------------------------------- 1 | """Adapter from the low level (BD) to high level objects.""" 2 | 3 | from .client import DBClient 4 | from .converters import build_from_row 5 | from .status import DeliveryOrder 6 | 7 | 8 | class DeliveryStatusQuery: 9 | def __init__(self, delivery_id: int, dbclient: DBClient) -> None: 10 | self._delivery_id = delivery_id 11 | self._client = dbclient 12 | 13 | async def get(self) -> DeliveryOrder: 14 | """Get the current status for this delivery.""" 15 | results = await self._run_query() 16 | return build_from_row(self._delivery_id, results) 17 | 18 | async def _run_query(self): 19 | return await self._client.fetchrow( 20 | """ 21 | SELECT status, d.dispatched_at, t.location, f.delivered_at 22 | FROM delivery_order_status as dos 23 | LEFT JOIN dispatched as d ON (dos.delivery_id = d.delivery_id) 24 | LEFT JOIN in_transit as t ON (dos.delivery_id = t.delivery_id) 25 | LEFT JOIN finished as f ON (dos.delivery_id = f.delivery_id) 26 | WHERE dos.delivery_id = $1 27 | """, 28 | self._delivery_id, 29 | ) 30 | -------------------------------------------------------------------------------- /book/src/ch10/service/libs/web/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build 2 | build: 3 | python setup.py sdist 4 | -------------------------------------------------------------------------------- /book/src/ch10/service/libs/web/README.rst: -------------------------------------------------------------------------------- 1 | libs/web 2 | ======== 3 | 4 | Python package that installs abstractions over the web application. This 5 | package is being imported from the service, and it doesn't require extra setup 6 | (no Docker image, etc.). -------------------------------------------------------------------------------- /book/src/ch10/service/libs/web/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile setup.py 6 | # 7 | aiofiles==0.6.0 # via sanic 8 | certifi==2024.7.4 # via httpx 9 | h11==0.9.0 # via httpcore 10 | httpcore==0.11.1 # via httpx 11 | httptools==0.1.1 # via sanic 12 | httpx==0.23.0 # via sanic 13 | idna==3.7 # via rfc3986 14 | multidict==5.0.0 # via sanic 15 | rfc3986[idna2008]==1.4.0 # via httpx 16 | sanic==20.12.7 # via web (setup.py) 17 | sniffio==1.2.0 # via httpcore, httpx 18 | ujson==5.4.0 # via sanic 19 | uvloop==0.14.0 # via sanic 20 | websockets==9.1 # via sanic 21 | -------------------------------------------------------------------------------- /book/src/ch10/service/libs/web/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | with open("README.rst", "r") as longdesc: 4 | long_description = longdesc.read() 5 | 6 | 7 | install_requires = ["sanic>=20,<21"] 8 | 9 | setup( 10 | name="web", 11 | description="Library with helpers for the web-related functionality", 12 | long_description=long_description, 13 | author="Dev team", 14 | version="0.1.0", 15 | packages=find_packages(where="src/"), 16 | package_dir={"": "src"}, 17 | install_requires=install_requires, 18 | ) 19 | -------------------------------------------------------------------------------- /book/src/ch10/service/libs/web/src/web/__init__.py: -------------------------------------------------------------------------------- 1 | from sanic import Sanic 2 | from sanic.exceptions import NotFound 3 | 4 | from .view import View 5 | 6 | app = Sanic("delivery_status") 7 | 8 | 9 | def register_route(view_object, route): 10 | app.add_route(view_object.as_view(), route) 11 | 12 | 13 | __all__ = ["View", "app", "register_route"] 14 | -------------------------------------------------------------------------------- /book/src/ch10/service/libs/web/src/web/view.py: -------------------------------------------------------------------------------- 1 | """View helper Objects""" 2 | from sanic.response import json 3 | from sanic.views import HTTPMethodView 4 | 5 | 6 | class View(HTTPMethodView): 7 | """Extend with the logic of the application""" 8 | 9 | async def get(self, request, *args, **kwargs): 10 | response = await self._get(request, *args, **kwargs) 11 | return json(response) 12 | -------------------------------------------------------------------------------- /book/src/ch10/service/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | 4 | with open("README.rst", "r") as longdesc: 5 | long_description = longdesc.read() 6 | 7 | 8 | install_requires = ["web==0.1.0", "storage==0.1.0"] 9 | 10 | setup( 11 | name="delistatus", 12 | description="Check the status of a delivery order", 13 | long_description=long_description, 14 | author="Dev team", 15 | version="0.1.0", 16 | packages=find_packages(), 17 | install_requires=install_requires, 18 | entry_points={ 19 | "console_scripts": ["status-service = statusweb.service:main"] 20 | }, 21 | ) 22 | -------------------------------------------------------------------------------- /book/src/ch10/service/statusweb/README.rst: -------------------------------------------------------------------------------- 1 | Micro-service: ``statusweb`` 2 | ============================ 3 | 4 | Dependencies 5 | ------------ 6 | 7 | Python packages: 8 | 9 | - ``libs.storage`` 10 | - ``libs.web`` 11 | 12 | System Dependencies: 13 | 14 | - A ``PostgreSQL`` database up and running. 15 | 16 | Structure 17 | --------- 18 | 19 | - ``service.py`` runs the micro-servie (exposing an HTTP endpoint). 20 | -------------------------------------------------------------------------------- /book/src/ch10/service/statusweb/__init__.py: -------------------------------------------------------------------------------- 1 | """Entry point to the ``statusweb`` package.""" 2 | -------------------------------------------------------------------------------- /book/src/ch10/service/statusweb/service.py: -------------------------------------------------------------------------------- 1 | """Entry point of the delivery service.""" 2 | import os 3 | 4 | from storage import DBClient, DeliveryStatusQuery, OrderNotFoundError 5 | from web import NotFound, View, app, register_route 6 | 7 | LISTEN_PORT = os.getenv("LISTEN_PORT", 5000) 8 | 9 | 10 | class DeliveryView(View): 11 | async def _get(self, request, delivery_id: int): 12 | dsq = DeliveryStatusQuery(int(delivery_id), await DBClient()) 13 | try: 14 | result = await dsq.get() 15 | except OrderNotFoundError as e: 16 | raise NotFound(str(e)) from e 17 | 18 | return result.message() 19 | 20 | 21 | register_route(DeliveryView, "/status/") 22 | 23 | 24 | def main(): 25 | app.run(host="0.0.0.0", port=LISTEN_PORT) 26 | 27 | 28 | if __name__ == "__main__": 29 | main() 30 | -------------------------------------------------------------------------------- /book/src/requirements.txt: -------------------------------------------------------------------------------- 1 | black==24.10.0 2 | coverage==7.8.2 3 | flake8==7.1.2 4 | hypothesis==6.135.0 5 | isort==5.13.2 6 | MutPy==0.6.1 7 | mypy==1.16.0 8 | mypy-extensions==1.1.0 9 | pinject==0.14.1 10 | pip-tools==7.4.1 11 | pyflakes==3.2.0 12 | pylint==3.3.7 13 | pytest==8.4.0 14 | pytest-cov==5.0.0 15 | pytype==2024.9.13 16 | requests==2.32.3 17 | yapf==0.43.0 18 | -------------------------------------------------------------------------------- /talk/src/Makefile: -------------------------------------------------------------------------------- 1 | COMPILED_HTML := compiled_html 2 | 3 | test: 4 | $(VIRTUAL_ENV)/bin/py.test --cov-report term-missing --cov=. test.py 5 | 6 | setup: 7 | $(VIRTUAL_ENV)/bin/pip install -r requirements.txt 8 | 9 | # use: highlight SOURCE= 10 | highlight: 11 | mkdir -p $(COMPILED_HTML) 12 | $(VIRTUAL_ENV)/bin/pygmentize \ 13 | -f html \ 14 | -l python3 \ 15 | -O full,style=colorful \ 16 | $(SOURCE) > $(COMPILED_HTML)/$(SOURCE).html 17 | 18 | .PHONY: test setup highlight 19 | -------------------------------------------------------------------------------- /talk/src/_0_meaning_bad.py: -------------------------------------------------------------------------------- 1 | """ 2 | Examples of code logic with no aparent meaning assigned, 3 | making it hard to figure out the intention of the code. 4 | """ 5 | 6 | 7 | def elapse(year): 8 | days = 365 9 | if year % 4 == 0 or (year % 100 == 0 and year % 400 == 0): 10 | days += 1 11 | for day in range(1, days + 1): 12 | print("Day {} of {}".format(day, year)) 13 | -------------------------------------------------------------------------------- /talk/src/_0_meaning_good.py: -------------------------------------------------------------------------------- 1 | """ 2 | This time, the function is rewritten, naming its parts, 3 | so it is clear what is doing at each stage 4 | """ 5 | 6 | 7 | def is_leap(year): 8 | return year % 4 == 0 or (year % 100 == 0 and year % 400 == 0) 9 | 10 | 11 | def number_of_days(year): 12 | return 366 if is_leap(year) else 365 13 | 14 | 15 | def elapse(year): 16 | days = number_of_days(year) 17 | for day in range(1, days + 1): 18 | print("Day {} of {}".format(day, year)) 19 | -------------------------------------------------------------------------------- /talk/src/_1_decorators_bad.py: -------------------------------------------------------------------------------- 1 | """ 2 | Examples of the application of Python decorators in order to 3 | reduce code duplication. 4 | It presents first, the naïve approach, with duplicated code, 5 | and then, the improved solution using decorators. 6 | """ 7 | from base import logger 8 | 9 | 10 | def decorator(original_function): 11 | def inner(*args, **kwargs): 12 | # modify original function, or add extra logic 13 | return original_function(*args, **kwargs) 14 | return inner 15 | 16 | 17 | # 1. Repeated 18 | def update_db_indexes(cursor): 19 | commands = ( 20 | """REINDEX DATABASE transactional""", 21 | ) 22 | try: 23 | for command in commands: 24 | cursor.execute(command) 25 | except Exception as e: 26 | logger.exception("Error in update_db_indexes: %s", e) 27 | return -1 28 | else: 29 | logger.info("update_db_indexes run successfully") 30 | return 0 31 | 32 | 33 | def move_data_archives(cursor): 34 | commands = ( 35 | """INSERT INTO archive_orders SELECT * from orders 36 | WHERE order_date < '2016-01-01' """, 37 | """DELETE from orders WHERE order_date < '2016-01-01' """,) 38 | try: 39 | for command in commands: 40 | cursor.execute(command) 41 | except Exception as e: 42 | logger.exception("Error in move_data_archives: %s", e) 43 | return -1 44 | else: 45 | logger.info("move_data_archives run successfully") 46 | return 0 47 | -------------------------------------------------------------------------------- /talk/src/_1_decorators_good.py: -------------------------------------------------------------------------------- 1 | from base import logger 2 | 3 | 4 | # 2. with decorators 5 | 6 | def db_status_handler(db_script_function): 7 | def inner(cursor): 8 | commands = db_script_function(cursor) 9 | function_name = db_script_function.__qualname__ 10 | try: 11 | for command in commands: 12 | cursor.execute(command) 13 | except Exception as e: 14 | logger.exception("Error in %s: %s", function_name, e) 15 | return -1 16 | else: 17 | logger.info("%s run successfully", function_name) 18 | return 0 19 | return inner 20 | 21 | 22 | @db_status_handler 23 | def update_db_indexes(cursor): 24 | return ( 25 | """REINDEX DATABASE transactional""", 26 | ) 27 | 28 | 29 | @db_status_handler 30 | def move_data_archives(cursor): 31 | return ( 32 | """INSERT INTO archive_orders SELECT * from orders 33 | WHERE order_date < '2016-01-01' """, 34 | """DELETE from orders WHERE order_date < '2016-01-01' """, 35 | ) 36 | -------------------------------------------------------------------------------- /talk/src/_2_context_managers.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | from base import start_database_service, stop_database_service, run_offline_db_backup 3 | 4 | ################################################### 5 | 6 | 7 | class DBHandler: 8 | def __enter__(self): 9 | stop_database_service() 10 | return self 11 | 12 | def __exit__(self, *exc): 13 | start_database_service() 14 | 15 | 16 | def test_first_backup(): 17 | with DBHandler(): 18 | run_offline_db_backup() 19 | 20 | 21 | class db_status_handler(contextlib.ContextDecorator): 22 | def __enter__(self): 23 | stop_database_service() 24 | return self 25 | 26 | def __exit__(self, *exc): 27 | start_database_service() 28 | 29 | 30 | @db_status_handler() 31 | def offline_db_backup(): 32 | print("Running backup on database...") 33 | print("Backup finished") 34 | 35 | 36 | """ 37 | 1. 38 | Stopping database service 39 | systemctl stop postgres 40 | Running backup on database... 41 | Backup finished 42 | Starting database service 43 | systemctl start postgres 44 | 45 | 2. 46 | Stopping database service 47 | systemctl stop postgres 48 | Running backup on database... 49 | Backup finished 50 | Starting database service 51 | systemctl start postgres 52 | """ 53 | -------------------------------------------------------------------------------- /talk/src/_3_properties.py: -------------------------------------------------------------------------------- 1 | import redis 2 | 3 | 4 | class PlayerStatus: 5 | 6 | def __init__(self): 7 | self.redis_connection = redis.StrictRedis() 8 | self.key = "score_1" 9 | 10 | def accumulate_points(self, new_points): 11 | current_score = int(self.redis_connection.get(self.key) or 0) 12 | score = current_score + new_points 13 | self.redis_connection.set(self.key, score) 14 | 15 | @property 16 | def points(self): 17 | return int(self.redis_connection.get(self.key) or 0) 18 | 19 | @points.setter 20 | def points(self, new_points): 21 | self.redis_connection.set(self.key, new_points) 22 | 23 | """ 24 | player_status = PlayerStatus() 25 | player_status.accumulate_points(20) 26 | player_status.points += 20 27 | player_status.points = 20 28 | print(player_status.points) 29 | """ 30 | -------------------------------------------------------------------------------- /talk/src/_4_magics_bad.py: -------------------------------------------------------------------------------- 1 | """ 2 | magic method example: not called 3 | rewrite of the same example function with the search logic 4 | entwined with the calling code. 5 | """ 6 | 7 | 8 | def request_product_for_customer(customer, product, current_stock): 9 | product_available_in_stock = False 10 | for category in current_stock.categories: 11 | for prod in category.products: 12 | if prod.count > 0 and prod.id == product.id: 13 | product_available_in_stock = True 14 | if product_available_in_stock: 15 | requested_product = current_stock.request(product) 16 | customer.assign_product(requested_product) 17 | else: 18 | return "Product not available" 19 | -------------------------------------------------------------------------------- /talk/src/_4_magics_good.py: -------------------------------------------------------------------------------- 1 | """ 2 | magic methods: __contains__ 3 | 4 | Reimplementation of the request function, but this time calling the magic method 5 | implemented in the class, to make the code more readable. 6 | """ 7 | 8 | def request_product_for_customer(customer, product, current_stock): 9 | if product in current_stock: 10 | requested_product = current_stock.request(product) 11 | customer.assign_product(requested_product) 12 | else: 13 | return "Product not available" 14 | -------------------------------------------------------------------------------- /talk/src/_5_idioms.py: -------------------------------------------------------------------------------- 1 | """ 2 | This section collects other common idioms in Python for performing 3 | general tasks in a more efficient way. 4 | 5 | The code is for explanatory purposes only. 6 | """ 7 | 8 | DATA_FILE = '/tmp/file.txt' 9 | 10 | 11 | def count_words_1(words): 12 | count = {} 13 | for word in words: 14 | if word in count: 15 | count[word] += 1 16 | else: 17 | count[word] = 1 18 | return count 19 | 20 | 21 | def count_words_2(words): 22 | count = {} 23 | for word in words: 24 | count[word] = count.get(word, 0) + 1 25 | return count 26 | 27 | 28 | def count_words_3(words): 29 | from collections import Counter 30 | count = Counter(words) 31 | return count 32 | 33 | 34 | def ignore_exceptions_1(): 35 | data = None 36 | try: 37 | with open(DATA_FILE) as f: 38 | data = f.read() 39 | except (FileNotFoundError, PermissionError): 40 | pass 41 | return data 42 | 43 | 44 | def ignore_exceptions_2(): 45 | data = None 46 | import contextlib 47 | with contextlib.suppress(FileNotFoundError, PermissionError): 48 | with open(DATA_FILE) as f: 49 | data = f.read() 50 | return data 51 | 52 | 53 | def find_first_even_1(*numbers): 54 | for number in numbers: 55 | if number % 2 == 0: 56 | return number 57 | 58 | 59 | def find_first_even_2(*numbers): 60 | try: 61 | return [number for number in numbers if number % 2 == 0][0] 62 | except IndexError: 63 | pass 64 | 65 | 66 | def find_first_even_3(*numbers): 67 | return next((number for number in numbers if number % 2 == 0), None) 68 | -------------------------------------------------------------------------------- /talk/src/requirements.txt: -------------------------------------------------------------------------------- 1 | ipython==8.10.0 2 | pytest==2.9.1 3 | coverage==4.0.3 4 | pytest-cov==2.2.1 5 | pytest-mock==1.1 6 | redis==4.4.4 7 | pygments==2.15.0 8 | --------------------------------------------------------------------------------