├── .codespellignore ├── .github └── workflows │ ├── lint_pr.yml │ └── lint_python.yml ├── .gitignore ├── .travis.yml ├── Makefile ├── README.md ├── config_backup ├── .coveragerc ├── setup.cfg └── tox.ini ├── lint.sh ├── patterns ├── __init__.py ├── behavioral │ ├── __init__.py │ ├── catalog.py │ ├── chain_of_responsibility.py │ ├── chaining_method.py │ ├── command.py │ ├── iterator.py │ ├── iterator_alt.py │ ├── mediator.py │ ├── memento.py │ ├── observer.py │ ├── publish_subscribe.py │ ├── registry.py │ ├── specification.py │ ├── state.py │ ├── strategy.py │ ├── template.py │ ├── visitor.py │ └── viz │ │ ├── catalog.py.png │ │ ├── chain.py.png │ │ ├── chaining_method.py.png │ │ ├── command.py.png │ │ ├── iterator.py.png │ │ ├── mediator.py.png │ │ ├── memento.py.png │ │ ├── observer.py.png │ │ ├── publish_subscribe.py.png │ │ ├── registry.py.png │ │ ├── specification.py.png │ │ ├── state.py.png │ │ ├── strategy.py.png │ │ ├── template.py.png │ │ └── visitor.py.png ├── creational │ ├── __init__.py │ ├── abstract_factory.py │ ├── borg.py │ ├── builder.py │ ├── factory.py │ ├── lazy_evaluation.py │ ├── pool.py │ ├── prototype.py │ └── viz │ │ ├── abstract_factory.py.png │ │ ├── borg.py.png │ │ ├── builder.py.png │ │ ├── factory_method.py.png │ │ ├── lazy_evaluation.py.png │ │ ├── pool.py.png │ │ └── prototype.py.png ├── dependency_injection.py ├── fundamental │ ├── __init__.py │ ├── delegation_pattern.py │ └── viz │ │ └── delegation_pattern.py.png ├── other │ ├── __init__.py │ ├── blackboard.py │ ├── graph_search.py │ └── hsm │ │ ├── __init__.py │ │ ├── classes_hsm.png │ │ ├── classes_test_hsm.png │ │ └── hsm.py └── structural │ ├── 3-tier.py │ ├── __init__.py │ ├── adapter.py │ ├── bridge.py │ ├── composite.py │ ├── decorator.py │ ├── facade.py │ ├── flyweight.py │ ├── flyweight_with_metaclass.py │ ├── front_controller.py │ ├── mvc.py │ ├── proxy.py │ └── viz │ ├── 3-tier.py.png │ ├── adapter.py.png │ ├── bridge.py.png │ ├── composite.py.png │ ├── decorator.py.png │ ├── facade.py.png │ ├── flyweight.py.png │ ├── front_controller.py.png │ ├── mvc.py.png │ └── proxy.py.png ├── pyproject.toml ├── requirements-dev.txt ├── setup.py └── tests ├── __init__.py ├── behavioral ├── test_observer.py ├── test_publish_subscribe.py ├── test_state.py └── test_strategy.py ├── creational ├── test_abstract_factory.py ├── test_borg.py ├── test_builder.py ├── test_lazy.py ├── test_pool.py └── test_prototype.py ├── structural ├── test_adapter.py ├── test_bridge.py ├── test_decorator.py └── test_proxy.py └── test_hsm.py /.codespellignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | .idea 4 | *.egg-info/ 5 | .tox/ 6 | env/ 7 | venv/ 8 | .env 9 | .venv 10 | .vscode/ 11 | .python-version 12 | .coverage 13 | build/ 14 | dist/ -------------------------------------------------------------------------------- /.github/workflows/lint_pr.yml: -------------------------------------------------------------------------------- 1 | name: lint_pull_request 2 | on: [pull_request, push] 3 | jobs: 4 | check_changes: 5 | runs-on: ubuntu-24.04 6 | outputs: 7 | has_python_changes: ${{ steps.changed-files.outputs.has_python_changes }} 8 | files: ${{ steps.changed-files.outputs.files }} 9 | steps: 10 | - uses: actions/checkout@v3 11 | with: 12 | fetch-depth: 0 # To get all history for git diff commands 13 | 14 | - name: Get changed Python files 15 | id: changed-files 16 | run: | 17 | if [ "${{ github.event_name }}" == "pull_request" ]; then 18 | # For PRs, compare against base branch 19 | CHANGED_FILES=$(git diff --name-only --diff-filter=ACMRT origin/${{ github.base_ref }} HEAD | grep '\.py$' | grep -v "^setup\.py$" || echo "") 20 | # Check if setup.py specifically changed 21 | SETUP_PY_CHANGED=$(git diff --name-only --diff-filter=ACMRT origin/${{ github.base_ref }} HEAD | grep "^setup\.py$" || echo "") 22 | if [ ! -z "$SETUP_PY_CHANGED" ]; then 23 | CHANGED_FILES="$CHANGED_FILES $SETUP_PY_CHANGED" 24 | fi 25 | else 26 | # For pushes, use the before/after SHAs 27 | CHANGED_FILES=$(git diff --name-only --diff-filter=ACMRT ${{ github.event.before }} ${{ github.event.after }} | grep '\.py$' | grep -v "^setup\.py$" || echo "") 28 | # Check if setup.py specifically changed 29 | SETUP_PY_CHANGED=$(git diff --name-only --diff-filter=ACMRT ${{ github.event.before }} ${{ github.event.after }} | grep "^setup\.py$" || echo "") 30 | if [ ! -z "$SETUP_PY_CHANGED" ]; then 31 | CHANGED_FILES="$CHANGED_FILES $SETUP_PY_CHANGED" 32 | fi 33 | fi 34 | 35 | # Check if any Python files were changed and set the output accordingly 36 | if [ -z "$CHANGED_FILES" ]; then 37 | echo "No Python files changed" 38 | echo "has_python_changes=false" >> $GITHUB_OUTPUT 39 | echo "files=" >> $GITHUB_OUTPUT 40 | else 41 | echo "Changed Python files: $CHANGED_FILES" 42 | echo "has_python_changes=true" >> $GITHUB_OUTPUT 43 | echo "files=$CHANGED_FILES" >> $GITHUB_OUTPUT 44 | fi 45 | 46 | - name: PR information 47 | if: ${{ github.event_name == 'pull_request' }} 48 | run: | 49 | if [[ "${{ steps.changed-files.outputs.has_python_changes }}" == "true" ]]; then 50 | echo "This PR contains Python changes that will be linted." 51 | else 52 | echo "This PR contains no Python changes, but still requires manual approval." 53 | fi 54 | 55 | lint: 56 | needs: check_changes 57 | if: ${{ needs.check_changes.outputs.has_python_changes == 'true' }} 58 | runs-on: ubuntu-24.04 59 | strategy: 60 | fail-fast: false 61 | matrix: 62 | tool: [flake8, format, mypy, pytest, pyupgrade, tox] 63 | steps: 64 | # Additional check to ensure we have Python files before proceeding 65 | - name: Verify Python changes 66 | run: | 67 | if [[ "${{ needs.check_changes.outputs.has_python_changes }}" != "true" ]]; then 68 | echo "No Python files were changed. Skipping linting." 69 | exit 0 70 | fi 71 | 72 | - uses: actions/checkout@v3 73 | with: 74 | fetch-depth: 0 75 | 76 | - uses: actions/setup-python@v4 77 | with: 78 | python-version: 3.12 79 | 80 | - uses: actions/cache@v3 81 | with: 82 | path: ~/.cache/pip 83 | key: ${{ runner.os }}-pip-${{ hashFiles('requirements-dev.txt') }} 84 | restore-keys: | 85 | ${{ runner.os }}-pip- 86 | 87 | - name: Install dependencies 88 | run: | 89 | python -m pip install --upgrade pip 90 | pip install -r requirements-dev.txt 91 | 92 | # Flake8 linting 93 | - name: Lint with flake8 94 | if: ${{ matrix.tool == 'flake8' }} 95 | id: flake8 96 | run: | 97 | echo "Linting files: ${{ needs.check_changes.outputs.files }}" 98 | flake8 ${{ needs.check_changes.outputs.files }} --count --show-source --statistics 99 | 100 | # Format checking with isort and black 101 | - name: Format check 102 | if: ${{ matrix.tool == 'format' }} 103 | id: format 104 | run: | 105 | echo "Checking format with isort for: ${{ needs.check_changes.outputs.files }}" 106 | isort --profile black --check ${{ needs.check_changes.outputs.files }} 107 | echo "Checking format with black for: ${{ needs.check_changes.outputs.files }}" 108 | black --check ${{ needs.check_changes.outputs.files }} 109 | 110 | # Type checking with mypy 111 | - name: Type check with mypy 112 | if: ${{ matrix.tool == 'mypy' }} 113 | id: mypy 114 | run: | 115 | echo "Type checking: ${{ needs.check_changes.outputs.files }}" 116 | mypy --ignore-missing-imports ${{ needs.check_changes.outputs.files }} 117 | 118 | # Run tests with pytest 119 | - name: Run tests with pytest 120 | if: ${{ matrix.tool == 'pytest' }} 121 | id: pytest 122 | run: | 123 | echo "Running pytest discovery..." 124 | python -m pytest --collect-only -v 125 | 126 | # First run any test files that correspond to changed files 127 | echo "Running tests for changed files..." 128 | changed_files="${{ needs.check_changes.outputs.files }}" 129 | 130 | # Extract module paths from changed files 131 | modules=() 132 | for file in $changed_files; do 133 | # Convert file path to module path (remove .py and replace / with .) 134 | if [[ $file == patterns/* ]]; then 135 | module_path=${file%.py} 136 | module_path=${module_path//\//.} 137 | modules+=("$module_path") 138 | fi 139 | done 140 | 141 | # Run tests for each module 142 | for module in "${modules[@]}"; do 143 | echo "Testing module: $module" 144 | python -m pytest -xvs tests/ -k "$module" || true 145 | done 146 | 147 | # Then run doctests on the changed files 148 | echo "Running doctests for changed files..." 149 | for file in $changed_files; do 150 | if [[ $file == *.py ]]; then 151 | echo "Running doctest for $file" 152 | python -m pytest --doctest-modules -v $file || true 153 | fi 154 | done 155 | 156 | # Check Python version compatibility 157 | - name: Check Python version compatibility 158 | if: ${{ matrix.tool == 'pyupgrade' }} 159 | id: pyupgrade 160 | run: pyupgrade --py312-plus ${{ needs.check_changes.outputs.files }} 161 | 162 | # Run tox 163 | - name: Run tox 164 | if: ${{ matrix.tool == 'tox' }} 165 | id: tox 166 | run: | 167 | echo "Running tox integration for changed files..." 168 | changed_files="${{ needs.check_changes.outputs.files }}" 169 | 170 | # Create a temporary tox configuration that extends the original one 171 | echo "[tox]" > tox_pr.ini 172 | echo "envlist = py312" >> tox_pr.ini 173 | echo "skip_missing_interpreters = true" >> tox_pr.ini 174 | 175 | echo "[testenv]" >> tox_pr.ini 176 | echo "setenv =" >> tox_pr.ini 177 | echo " COVERAGE_FILE = .coverage.{envname}" >> tox_pr.ini 178 | echo "deps =" >> tox_pr.ini 179 | echo " -r requirements-dev.txt" >> tox_pr.ini 180 | echo "allowlist_externals =" >> tox_pr.ini 181 | echo " pytest" >> tox_pr.ini 182 | echo " coverage" >> tox_pr.ini 183 | echo " python" >> tox_pr.ini 184 | echo "commands =" >> tox_pr.ini 185 | 186 | # Check if we have any implementation files that changed 187 | pattern_files=0 188 | test_files=0 189 | 190 | for file in $changed_files; do 191 | if [[ $file == patterns/* ]]; then 192 | pattern_files=1 193 | elif [[ $file == tests/* ]]; then 194 | test_files=1 195 | fi 196 | done 197 | 198 | # Only run targeted tests, no baseline 199 | echo " # Run specific tests for changed files" >> tox_pr.ini 200 | 201 | has_tests=false 202 | 203 | # Add coverage-focused test commands 204 | for file in $changed_files; do 205 | if [[ $file == *.py ]]; then 206 | # Run coverage tests for implementation files 207 | if [[ $file == patterns/* ]]; then 208 | module_name=$(basename $file .py) 209 | 210 | # Get the pattern type (behavioral, structural, etc.) 211 | if [[ $file == patterns/behavioral/* ]]; then 212 | pattern_dir="behavioral" 213 | elif [[ $file == patterns/creational/* ]]; then 214 | pattern_dir="creational" 215 | elif [[ $file == patterns/structural/* ]]; then 216 | pattern_dir="structural" 217 | elif [[ $file == patterns/fundamental/* ]]; then 218 | pattern_dir="fundamental" 219 | elif [[ $file == patterns/other/* ]]; then 220 | pattern_dir="other" 221 | else 222 | pattern_dir="" 223 | fi 224 | 225 | echo " # Testing $file" >> tox_pr.ini 226 | 227 | # Check if specific test exists 228 | if [ -n "$pattern_dir" ]; then 229 | test_path="tests/${pattern_dir}/test_${module_name}.py" 230 | echo " if [ -f \"${test_path}\" ]; then echo \"Test file ${test_path} exists: true\" && coverage run -m pytest -xvs --cov=patterns --cov-append ${test_path}; else echo \"Test file ${test_path} exists: false\"; fi" >> tox_pr.ini 231 | 232 | # Also try to find any test that might include this module 233 | echo " coverage run -m pytest -xvs --cov=patterns --cov-append tests/${pattern_dir}/ -k \"${module_name}\" --no-header" >> tox_pr.ini 234 | fi 235 | 236 | # Run doctests for the file 237 | echo " coverage run -m pytest --doctest-modules -v --cov=patterns --cov-append $file" >> tox_pr.ini 238 | 239 | has_tests=true 240 | fi 241 | 242 | # Run test files directly if modified 243 | if [[ $file == tests/* ]]; then 244 | echo " coverage run -m pytest -xvs --cov=patterns --cov-append $file" >> tox_pr.ini 245 | has_tests=true 246 | fi 247 | fi 248 | done 249 | 250 | # If we didn't find any specific tests to run, mention it 251 | if [ "$has_tests" = false ]; then 252 | echo " python -c \"print('No specific tests found for changed files. Consider adding tests.')\"" >> tox_pr.ini 253 | # Add a minimal test to avoid failure, but ensure it generates coverage data 254 | echo " coverage run -m pytest -xvs --cov=patterns --cov-append -k \"not integration\" --no-header" >> tox_pr.ini 255 | fi 256 | 257 | # Add coverage report command 258 | echo " coverage combine" >> tox_pr.ini 259 | echo " coverage report -m" >> tox_pr.ini 260 | 261 | # Run tox with the custom configuration 262 | echo "Running tox with custom PR configuration..." 263 | echo "======================== TOX CONFIG ========================" 264 | cat tox_pr.ini 265 | echo "===========================================================" 266 | tox -c tox_pr.ini 267 | 268 | summary: 269 | needs: [check_changes, lint] 270 | # Run summary in all cases, regardless of whether lint job ran 271 | if: ${{ always() }} 272 | runs-on: ubuntu-24.04 273 | steps: 274 | - uses: actions/checkout@v3 275 | 276 | - name: Summarize results 277 | run: | 278 | echo "## Pull Request Lint Results" >> $GITHUB_STEP_SUMMARY 279 | if [[ "${{ needs.check_changes.outputs.has_python_changes }}" == "true" ]]; then 280 | echo "Linting has completed for all Python files changed in this PR." >> $GITHUB_STEP_SUMMARY 281 | echo "See individual job logs for detailed results." >> $GITHUB_STEP_SUMMARY 282 | else 283 | echo "No Python files were changed in this PR. Linting was skipped." >> $GITHUB_STEP_SUMMARY 284 | fi 285 | echo "" >> $GITHUB_STEP_SUMMARY 286 | echo "⚠️ **Note:** This PR still requires manual approval regardless of linting results." >> $GITHUB_STEP_SUMMARY 287 | -------------------------------------------------------------------------------- /.github/workflows/lint_python.yml: -------------------------------------------------------------------------------- 1 | name: lint_python 2 | on: [pull_request, push] 3 | jobs: 4 | lint_python: 5 | runs-on: ubuntu-24.04 6 | steps: 7 | - uses: actions/checkout@v3 8 | - uses: actions/setup-python@v4 9 | with: 10 | python-version: 3.12 11 | - name: Install dependencies 12 | run: | 13 | python -m pip install --upgrade pip 14 | pip install -r requirements-dev.txt 15 | - name: Lint with flake8 16 | run: flake8 ./patterns --count --show-source --statistics 17 | continue-on-error: true 18 | - name: Format check with isort and black 19 | run: | 20 | isort --profile black --check ./patterns 21 | black --check ./patterns 22 | continue-on-error: true 23 | - name: Type check with mypy 24 | run: mypy --ignore-missing-imports ./patterns || true 25 | continue-on-error: true 26 | - name: Run tests with pytest 27 | run: | 28 | pytest ./patterns 29 | pytest --doctest-modules ./patterns || true 30 | continue-on-error: true 31 | - name: Check Python version compatibility 32 | run: shopt -s globstar && pyupgrade --py312-plus ./patterns/**/*.py 33 | continue-on-error: true 34 | - name: Run tox 35 | run: tox 36 | continue-on-error: true 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | .idea 4 | *.egg-info/ 5 | .tox/ 6 | env/ 7 | venv/ 8 | .env 9 | .venv 10 | .vscode/ 11 | .python-version 12 | .coverage 13 | build/ 14 | dist/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: linux 2 | dist: noble 3 | language: python 4 | 5 | jobs: 6 | include: 7 | - python: "3.12" 8 | env: TOXENV=py312 9 | 10 | cache: 11 | - pip 12 | 13 | install: 14 | - pip install codecov tox 15 | 16 | script: 17 | - tox 18 | 19 | after_success: 20 | - codecov 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # REDNAFI 2 | # This only works with embedded venv not virtualenv 3 | # Install venv: python3.8 -m venv venv 4 | # Activate venv: source venv/bin/activate 5 | 6 | # Usage (line =black line length, path = action path, ignore= exclude folders) 7 | # ------ 8 | # make pylinter [make pylinter line=88 path=.] 9 | # make pyupgrade 10 | 11 | path := . 12 | line := 88 13 | ignore := *env 14 | 15 | all: 16 | @echo 17 | 18 | .PHONY: checkvenv 19 | checkvenv: 20 | # raises error if environment is not active 21 | ifeq ("$(VIRTUAL_ENV)","") 22 | @echo "Venv is not activated!" 23 | @echo "Activate venv first." 24 | @echo 25 | exit 1 26 | endif 27 | 28 | .PHONY: pyupgrade 29 | pyupgrade: checkvenv 30 | # checks if pip-tools is installed 31 | ifeq ("$(wildcard venv/bin/pip-compile)","") 32 | @echo "Installing Pip-tools..." 33 | @pip install pip-tools 34 | endif 35 | 36 | ifeq ("$(wildcard venv/bin/pip-sync)","") 37 | @echo "Installing Pip-tools..." 38 | @pip install pip-tools 39 | endif 40 | 41 | # pip-tools 42 | # @pip-compile --upgrade requirements-dev.txt 43 | @pip-sync requirements-dev.txt 44 | 45 | 46 | .PHONY: pylinter 47 | pylinter: checkvenv 48 | # checks if black is installed 49 | ifeq ("$(wildcard venv/bin/black)","") 50 | @echo "Installing Black..." 51 | @pip install black 52 | endif 53 | 54 | # checks if isort is installed 55 | ifeq ("$(wildcard venv/bin/isort)","") 56 | @echo "Installing Isort..." 57 | @pip install isort 58 | endif 59 | 60 | # checks if flake8 is installed 61 | ifeq ("$(wildcard venv/bin/flake8)","") 62 | @echo -e "Installing flake8..." 63 | @pip install flake8 64 | @echo 65 | endif 66 | 67 | # black 68 | @echo "Applying Black" 69 | @echo "----------------\n" 70 | @black --line-length $(line) --exclude $(ignore) $(path) 71 | @echo 72 | 73 | # isort 74 | @echo "Applying Isort" 75 | @echo "----------------\n" 76 | @isort --atomic --profile black $(path) 77 | @echo 78 | 79 | # flake8 80 | @echo "Applying Flake8" 81 | @echo "----------------\n" 82 | @flake8 --max-line-length "$(line)" \ 83 | --max-complexity "18" \ 84 | --select "B,C,E,F,W,T4,B9" \ 85 | --ignore "E203,E266,E501,W503,F403,F401,E402" \ 86 | --exclude ".git,__pycache__,old, build, \ 87 | dist, venv, .tox" $(path) 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | python-patterns 2 | =============== 3 | 4 | A collection of design patterns and idioms in Python. 5 | 6 | Remember that each pattern has its own trade-offs. And you need to pay attention more to why you're choosing a certain pattern than to how to implement it. 7 | 8 | Current Patterns 9 | ---------------- 10 | 11 | __Creational Patterns__: 12 | 13 | | Pattern | Description | 14 | |:-------:| ----------- | 15 | | [abstract_factory](patterns/creational/abstract_factory.py) | use a generic function with specific factories | 16 | | [borg](patterns/creational/borg.py) | a singleton with shared-state among instances | 17 | | [builder](patterns/creational/builder.py) | instead of using multiple constructors, builder object receives parameters and returns constructed objects | 18 | | [factory](patterns/creational/factory.py) | delegate a specialized function/method to create instances | 19 | | [lazy_evaluation](patterns/creational/lazy_evaluation.py) | lazily-evaluated property pattern in Python | 20 | | [pool](patterns/creational/pool.py) | preinstantiate and maintain a group of instances of the same type | 21 | | [prototype](patterns/creational/prototype.py) | use a factory and clones of a prototype for new instances (if instantiation is expensive) | 22 | 23 | __Structural Patterns__: 24 | 25 | | Pattern | Description | 26 | |:-------:| ----------- | 27 | | [3-tier](patterns/structural/3-tier.py) | data<->business logic<->presentation separation (strict relationships) | 28 | | [adapter](patterns/structural/adapter.py) | adapt one interface to another using a white-list | 29 | | [bridge](patterns/structural/bridge.py) | a client-provider middleman to soften interface changes | 30 | | [composite](patterns/structural/composite.py) | lets clients treat individual objects and compositions uniformly | 31 | | [decorator](patterns/structural/decorator.py) | wrap functionality with other functionality in order to affect outputs | 32 | | [facade](patterns/structural/facade.py) | use one class as an API to a number of others | 33 | | [flyweight](patterns/structural/flyweight.py) | transparently reuse existing instances of objects with similar/identical state | 34 | | [front_controller](patterns/structural/front_controller.py) | single handler requests coming to the application | 35 | | [mvc](patterns/structural/mvc.py) | model<->view<->controller (non-strict relationships) | 36 | | [proxy](patterns/structural/proxy.py) | an object funnels operations to something else | 37 | 38 | __Behavioral Patterns__: 39 | 40 | | Pattern | Description | 41 | |:-------:| ----------- | 42 | | [chain_of_responsibility](patterns/behavioral/chain_of_responsibility.py) | apply a chain of successive handlers to try and process the data | 43 | | [catalog](patterns/behavioral/catalog.py) | general methods will call different specialized methods based on construction parameter | 44 | | [chaining_method](patterns/behavioral/chaining_method.py) | continue callback next object method | 45 | | [command](patterns/behavioral/command.py) | bundle a command and arguments to call later | 46 | | [iterator](patterns/behavioral/iterator.py) | traverse a container and access the container's elements | 47 | | [iterator](patterns/behavioral/iterator_alt.py) (alt. impl.)| traverse a container and access the container's elements | 48 | | [mediator](patterns/behavioral/mediator.py) | an object that knows how to connect other objects and act as a proxy | 49 | | [memento](patterns/behavioral/memento.py) | generate an opaque token that can be used to go back to a previous state | 50 | | [observer](patterns/behavioral/observer.py) | provide a callback for notification of events/changes to data | 51 | | [publish_subscribe](patterns/behavioral/publish_subscribe.py) | a source syndicates events/data to 0+ registered listeners | 52 | | [registry](patterns/behavioral/registry.py) | keep track of all subclasses of a given class | 53 | | [specification](patterns/behavioral/specification.py) | business rules can be recombined by chaining the business rules together using boolean logic | 54 | | [state](patterns/behavioral/state.py) | logic is organized into a discrete number of potential states and the next state that can be transitioned to | 55 | | [strategy](patterns/behavioral/strategy.py) | selectable operations over the same data | 56 | | [template](patterns/behavioral/template.py) | an object imposes a structure but takes pluggable components | 57 | | [visitor](patterns/behavioral/visitor.py) | invoke a callback for all items of a collection | 58 | 59 | __Design for Testability Patterns__: 60 | 61 | | Pattern | Description | 62 | |:-------:| ----------- | 63 | | [dependency_injection](patterns/dependency_injection.py) | 3 variants of dependency injection | 64 | 65 | __Fundamental Patterns__: 66 | 67 | | Pattern | Description | 68 | |:-------:| ----------- | 69 | | [delegation_pattern](patterns/fundamental/delegation_pattern.py) | an object handles a request by delegating to a second object (the delegate) | 70 | 71 | __Others__: 72 | 73 | | Pattern | Description | 74 | |:-------:| ----------- | 75 | | [blackboard](patterns/other/blackboard.py) | architectural model, assemble different sub-system knowledge to build a solution, AI approach - non gang of four pattern | 76 | | [graph_search](patterns/other/graph_search.py) | graphing algorithms - non gang of four pattern | 77 | | [hsm](patterns/other/hsm/hsm.py) | hierarchical state machine - non gang of four pattern | 78 | 79 | 80 | Videos 81 | ------ 82 | [Design Patterns in Python by Peter Ullrich](https://www.youtube.com/watch?v=bsyjSW46TDg) 83 | 84 | [Sebastian Buczyński - Why you don't need design patterns in Python?](https://www.youtube.com/watch?v=G5OeYHCJuv0) 85 | 86 | [You Don't Need That!](https://www.youtube.com/watch?v=imW-trt0i9I) 87 | 88 | [Pluggable Libs Through Design Patterns](https://www.youtube.com/watch?v=PfgEU3W0kyU) 89 | 90 | 91 | Contributing 92 | ------------ 93 | When an implementation is added or modified, please review the following guidelines: 94 | 95 | ##### Docstrings 96 | Add module level description in form of a docstring with links to corresponding references or other useful information. 97 | 98 | Add "Examples in Python ecosystem" section if you know some. It shows how patterns could be applied to real-world problems. 99 | 100 | [facade.py](patterns/structural/facade.py) has a good example of detailed description, 101 | but sometimes the shorter one as in [template.py](patterns/behavioral/template.py) would suffice. 102 | 103 | ##### Python 2 compatibility 104 | To see Python 2 compatible versions of some patterns please check-out the [legacy](https://github.com/faif/python-patterns/tree/legacy) tag. 105 | 106 | ##### Update README 107 | When everything else is done - update corresponding part of README. 108 | 109 | ##### Travis CI 110 | Please run the following before submitting a patch 111 | - `black .` This lints your code. 112 | 113 | Then either: 114 | - `tox` or `tox -e ci37` This runs unit tests. see tox.ini for further details. 115 | - If you have a bash compatible shell use `./lint.sh` This script will lint and test your code. This script mirrors the CI pipeline actions. 116 | 117 | You can also run `flake8` or `pytest` commands manually. Examples can be found in `tox.ini`. 118 | 119 | ## Contributing via issue triage [![Open Source Helpers](https://www.codetriage.com/faif/python-patterns/badges/users.svg)](https://www.codetriage.com/faif/python-patterns) 120 | 121 | You can triage issues and pull requests which may include reproducing bug reports or asking for vital information, such as version numbers or reproduction instructions. If you would like to start triaging issues, one easy way to get started is to [subscribe to python-patterns on CodeTriage](https://www.codetriage.com/faif/python-patterns). 122 | -------------------------------------------------------------------------------- /config_backup/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | 4 | [report] 5 | ; Regexes for lines to exclude from consideration 6 | exclude_also = 7 | ; Don't complain about missing debug-only code: 8 | def __repr__ 9 | if self\.debug 10 | 11 | ; Don't complain if tests don't hit defensive assertion code: 12 | raise AssertionError 13 | raise NotImplementedError 14 | 15 | ; Don't complain if non-runnable code isn't run: 16 | if 0: 17 | if __name__ == .__main__.: 18 | 19 | ; Don't complain about abstract methods, they aren't run: 20 | @(abc\.)?abstractmethod 21 | 22 | ignore_errors = True 23 | 24 | [html] 25 | directory = coverage_html_report -------------------------------------------------------------------------------- /config_backup/setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | ignore = E266 E731 W503 4 | exclude = venv* 5 | 6 | [tool:pytest] 7 | filterwarnings = 8 | ; ignore TestRunner class from facade example 9 | ignore:.*test class 'TestRunner'.*:Warning 10 | 11 | [mypy] 12 | python_version = 3.12 13 | ignore_missing_imports = True 14 | -------------------------------------------------------------------------------- /config_backup/tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py312,cov-report 3 | skip_missing_interpreters = true 4 | usedevelop = true 5 | 6 | [testenv] 7 | setenv = 8 | COVERAGE_FILE = .coverage.{envname} 9 | deps = 10 | -r requirements-dev.txt 11 | allowlist_externals = 12 | pytest 13 | flake8 14 | mypy 15 | commands = 16 | flake8 --exclude="venv/,.tox/" patterns/ 17 | ; `randomly-seed` option from `pytest-randomly` helps with deterministic outputs for examples like `other/blackboard.py` 18 | pytest --randomly-seed=1234 --doctest-modules patterns/ 19 | pytest -s -vv --cov=patterns/ --log-level=INFO tests/ 20 | 21 | 22 | [testenv:cov-report] 23 | setenv = 24 | COVERAGE_FILE = .coverage 25 | deps = coverage 26 | commands = 27 | coverage combine 28 | coverage report 29 | -------------------------------------------------------------------------------- /lint.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | pip install --upgrade pip 4 | pip install black codespell flake8 isort mypy pytest pyupgrade tox 5 | pip install -e . 6 | 7 | source_dir="./patterns" 8 | 9 | codespell --quiet-level=2 ./patterns # --ignore-words-list="" --skip="" 10 | flake8 "${source_dir}" --count --show-source --statistics 11 | isort --profile black "${source_dir}" 12 | tox 13 | mypy --ignore-missing-imports "${source_dir}" || true 14 | pytest "${source_dir}" 15 | pytest --doctest-modules "${source_dir}" || true 16 | shopt -s globstar && pyupgrade --py312-plus ${source_dir}/*.py 17 | -------------------------------------------------------------------------------- /patterns/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/__init__.py -------------------------------------------------------------------------------- /patterns/behavioral/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/behavioral/__init__.py -------------------------------------------------------------------------------- /patterns/behavioral/catalog.py: -------------------------------------------------------------------------------- 1 | """ 2 | A class that uses different static function depending of a parameter passed in 3 | init. Note the use of a single dictionary instead of multiple conditions 4 | """ 5 | 6 | __author__ = "Ibrahim Diop " 7 | 8 | 9 | class Catalog: 10 | """catalog of multiple static methods that are executed depending on an init 11 | 12 | parameter 13 | """ 14 | 15 | def __init__(self, param: str) -> None: 16 | 17 | # dictionary that will be used to determine which static method is 18 | # to be executed but that will be also used to store possible param 19 | # value 20 | self._static_method_choices = { 21 | "param_value_1": self._static_method_1, 22 | "param_value_2": self._static_method_2, 23 | } 24 | 25 | # simple test to validate param value 26 | if param in self._static_method_choices.keys(): 27 | self.param = param 28 | else: 29 | raise ValueError(f"Invalid Value for Param: {param}") 30 | 31 | @staticmethod 32 | def _static_method_1() -> None: 33 | print("executed method 1!") 34 | 35 | @staticmethod 36 | def _static_method_2() -> None: 37 | print("executed method 2!") 38 | 39 | def main_method(self) -> None: 40 | """will execute either _static_method_1 or _static_method_2 41 | 42 | depending on self.param value 43 | """ 44 | self._static_method_choices[self.param]() 45 | 46 | 47 | # Alternative implementation for different levels of methods 48 | class CatalogInstance: 49 | """catalog of multiple methods that are executed depending on an init 50 | 51 | parameter 52 | """ 53 | 54 | def __init__(self, param: str) -> None: 55 | self.x1 = "x1" 56 | self.x2 = "x2" 57 | # simple test to validate param value 58 | if param in self._instance_method_choices: 59 | self.param = param 60 | else: 61 | raise ValueError(f"Invalid Value for Param: {param}") 62 | 63 | def _instance_method_1(self) -> None: 64 | print(f"Value {self.x1}") 65 | 66 | def _instance_method_2(self) -> None: 67 | print(f"Value {self.x2}") 68 | 69 | _instance_method_choices = { 70 | "param_value_1": _instance_method_1, 71 | "param_value_2": _instance_method_2, 72 | } 73 | 74 | def main_method(self) -> None: 75 | """will execute either _instance_method_1 or _instance_method_2 76 | 77 | depending on self.param value 78 | """ 79 | self._instance_method_choices[self.param].__get__(self)() # type: ignore 80 | # type ignore reason: https://github.com/python/mypy/issues/10206 81 | 82 | 83 | class CatalogClass: 84 | """catalog of multiple class methods that are executed depending on an init 85 | 86 | parameter 87 | """ 88 | 89 | x1 = "x1" 90 | x2 = "x2" 91 | 92 | def __init__(self, param: str) -> None: 93 | # simple test to validate param value 94 | if param in self._class_method_choices: 95 | self.param = param 96 | else: 97 | raise ValueError(f"Invalid Value for Param: {param}") 98 | 99 | @classmethod 100 | def _class_method_1(cls) -> None: 101 | print(f"Value {cls.x1}") 102 | 103 | @classmethod 104 | def _class_method_2(cls) -> None: 105 | print(f"Value {cls.x2}") 106 | 107 | _class_method_choices = { 108 | "param_value_1": _class_method_1, 109 | "param_value_2": _class_method_2, 110 | } 111 | 112 | def main_method(self): 113 | """will execute either _class_method_1 or _class_method_2 114 | 115 | depending on self.param value 116 | """ 117 | self._class_method_choices[self.param].__get__(None, self.__class__)() # type: ignore 118 | # type ignore reason: https://github.com/python/mypy/issues/10206 119 | 120 | 121 | class CatalogStatic: 122 | """catalog of multiple static methods that are executed depending on an init 123 | 124 | parameter 125 | """ 126 | 127 | def __init__(self, param: str) -> None: 128 | # simple test to validate param value 129 | if param in self._static_method_choices: 130 | self.param = param 131 | else: 132 | raise ValueError(f"Invalid Value for Param: {param}") 133 | 134 | @staticmethod 135 | def _static_method_1() -> None: 136 | print("executed method 1!") 137 | 138 | @staticmethod 139 | def _static_method_2() -> None: 140 | print("executed method 2!") 141 | 142 | _static_method_choices = { 143 | "param_value_1": _static_method_1, 144 | "param_value_2": _static_method_2, 145 | } 146 | 147 | def main_method(self) -> None: 148 | """will execute either _static_method_1 or _static_method_2 149 | 150 | depending on self.param value 151 | """ 152 | 153 | self._static_method_choices[self.param].__get__(None, self.__class__)() # type: ignore 154 | # type ignore reason: https://github.com/python/mypy/issues/10206 155 | 156 | 157 | def main(): 158 | """ 159 | >>> test = Catalog('param_value_2') 160 | >>> test.main_method() 161 | executed method 2! 162 | 163 | >>> test = CatalogInstance('param_value_1') 164 | >>> test.main_method() 165 | Value x1 166 | 167 | >>> test = CatalogClass('param_value_2') 168 | >>> test.main_method() 169 | Value x2 170 | 171 | >>> test = CatalogStatic('param_value_1') 172 | >>> test.main_method() 173 | executed method 1! 174 | """ 175 | 176 | 177 | if __name__ == "__main__": 178 | import doctest 179 | 180 | doctest.testmod() 181 | -------------------------------------------------------------------------------- /patterns/behavioral/chain_of_responsibility.py: -------------------------------------------------------------------------------- 1 | """ 2 | *What is this pattern about? 3 | 4 | The Chain of responsibility is an object oriented version of the 5 | `if ... elif ... elif ... else ...` idiom, with the 6 | benefit that the condition–action blocks can be dynamically rearranged 7 | and reconfigured at runtime. 8 | 9 | This pattern aims to decouple the senders of a request from its 10 | receivers by allowing request to move through chained 11 | receivers until it is handled. 12 | 13 | Request receiver in simple form keeps a reference to a single successor. 14 | As a variation some receivers may be capable of sending requests out 15 | in several directions, forming a `tree of responsibility`. 16 | 17 | *TL;DR 18 | Allow a request to pass down a chain of receivers until it is handled. 19 | """ 20 | 21 | from abc import ABC, abstractmethod 22 | from typing import Optional, Tuple 23 | 24 | 25 | class Handler(ABC): 26 | def __init__(self, successor: Optional["Handler"] = None): 27 | self.successor = successor 28 | 29 | def handle(self, request: int) -> None: 30 | """ 31 | Handle request and stop. 32 | If can't - call next handler in chain. 33 | 34 | As an alternative you might even in case of success 35 | call the next handler. 36 | """ 37 | res = self.check_range(request) 38 | if not res and self.successor: 39 | self.successor.handle(request) 40 | 41 | @abstractmethod 42 | def check_range(self, request: int) -> Optional[bool]: 43 | """Compare passed value to predefined interval""" 44 | 45 | 46 | class ConcreteHandler0(Handler): 47 | """Each handler can be different. 48 | Be simple and static... 49 | """ 50 | 51 | @staticmethod 52 | def check_range(request: int) -> Optional[bool]: 53 | if 0 <= request < 10: 54 | print(f"request {request} handled in handler 0") 55 | return True 56 | return None 57 | 58 | 59 | class ConcreteHandler1(Handler): 60 | """... With it's own internal state""" 61 | 62 | start, end = 10, 20 63 | 64 | def check_range(self, request: int) -> Optional[bool]: 65 | if self.start <= request < self.end: 66 | print(f"request {request} handled in handler 1") 67 | return True 68 | return None 69 | 70 | 71 | class ConcreteHandler2(Handler): 72 | """... With helper methods.""" 73 | 74 | def check_range(self, request: int) -> Optional[bool]: 75 | start, end = self.get_interval_from_db() 76 | if start <= request < end: 77 | print(f"request {request} handled in handler 2") 78 | return True 79 | return None 80 | 81 | @staticmethod 82 | def get_interval_from_db() -> Tuple[int, int]: 83 | return (20, 30) 84 | 85 | 86 | class FallbackHandler(Handler): 87 | @staticmethod 88 | def check_range(request: int) -> Optional[bool]: 89 | print(f"end of chain, no handler for {request}") 90 | return False 91 | 92 | 93 | def main(): 94 | """ 95 | >>> h0 = ConcreteHandler0() 96 | >>> h1 = ConcreteHandler1() 97 | >>> h2 = ConcreteHandler2(FallbackHandler()) 98 | >>> h0.successor = h1 99 | >>> h1.successor = h2 100 | 101 | >>> requests = [2, 5, 14, 22, 18, 3, 35, 27, 20] 102 | >>> for request in requests: 103 | ... h0.handle(request) 104 | request 2 handled in handler 0 105 | request 5 handled in handler 0 106 | request 14 handled in handler 1 107 | request 22 handled in handler 2 108 | request 18 handled in handler 1 109 | request 3 handled in handler 0 110 | end of chain, no handler for 35 111 | request 27 handled in handler 2 112 | request 20 handled in handler 2 113 | """ 114 | 115 | 116 | if __name__ == "__main__": 117 | import doctest 118 | 119 | doctest.testmod(optionflags=doctest.ELLIPSIS) 120 | -------------------------------------------------------------------------------- /patterns/behavioral/chaining_method.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | class Person: 5 | def __init__(self, name: str) -> None: 6 | self.name = name 7 | 8 | def do_action(self, action: Action) -> Action: 9 | print(self.name, action.name, end=" ") 10 | return action 11 | 12 | 13 | class Action: 14 | def __init__(self, name: str) -> None: 15 | self.name = name 16 | 17 | def amount(self, val: str) -> Action: 18 | print(val, end=" ") 19 | return self 20 | 21 | def stop(self) -> None: 22 | print("then stop") 23 | 24 | 25 | def main(): 26 | """ 27 | >>> move = Action('move') 28 | >>> person = Person('Jack') 29 | >>> person.do_action(move).amount('5m').stop() 30 | Jack move 5m then stop 31 | """ 32 | 33 | 34 | if __name__ == "__main__": 35 | import doctest 36 | 37 | doctest.testmod() 38 | -------------------------------------------------------------------------------- /patterns/behavioral/command.py: -------------------------------------------------------------------------------- 1 | """ 2 | Command pattern decouples the object invoking a job from the one who knows 3 | how to do it. As mentioned in the GoF book, a good example is in menu items. 4 | You have a menu that has lots of items. Each item is responsible for doing a 5 | special thing and you want your menu item just call the execute method when 6 | it is pressed. To achieve this you implement a command object with the execute 7 | method for each menu item and pass to it. 8 | 9 | *About the example 10 | We have a menu containing two items. Each item accepts a file name, one hides the file 11 | and the other deletes it. Both items have an undo option. 12 | Each item is a MenuItem class that accepts the corresponding command as input and executes 13 | it's execute method when it is pressed. 14 | 15 | *TL;DR 16 | Object oriented implementation of callback functions. 17 | 18 | *Examples in Python ecosystem: 19 | Django HttpRequest (without execute method): 20 | https://docs.djangoproject.com/en/2.1/ref/request-response/#httprequest-objects 21 | """ 22 | 23 | from typing import List, Union 24 | 25 | 26 | class HideFileCommand: 27 | """ 28 | A command to hide a file given its name 29 | """ 30 | 31 | def __init__(self) -> None: 32 | # an array of files hidden, to undo them as needed 33 | self._hidden_files: List[str] = [] 34 | 35 | def execute(self, filename: str) -> None: 36 | print(f"hiding {filename}") 37 | self._hidden_files.append(filename) 38 | 39 | def undo(self) -> None: 40 | filename = self._hidden_files.pop() 41 | print(f"un-hiding {filename}") 42 | 43 | 44 | class DeleteFileCommand: 45 | """ 46 | A command to delete a file given its name 47 | """ 48 | 49 | def __init__(self) -> None: 50 | # an array of deleted files, to undo them as needed 51 | self._deleted_files: List[str] = [] 52 | 53 | def execute(self, filename: str) -> None: 54 | print(f"deleting {filename}") 55 | self._deleted_files.append(filename) 56 | 57 | def undo(self) -> None: 58 | filename = self._deleted_files.pop() 59 | print(f"restoring {filename}") 60 | 61 | 62 | class MenuItem: 63 | """ 64 | The invoker class. Here it is items in a menu. 65 | """ 66 | 67 | def __init__(self, command: Union[HideFileCommand, DeleteFileCommand]) -> None: 68 | self._command = command 69 | 70 | def on_do_press(self, filename: str) -> None: 71 | self._command.execute(filename) 72 | 73 | def on_undo_press(self) -> None: 74 | self._command.undo() 75 | 76 | 77 | def main(): 78 | """ 79 | >>> item1 = MenuItem(DeleteFileCommand()) 80 | 81 | >>> item2 = MenuItem(HideFileCommand()) 82 | 83 | # create a file named `test-file` to work with 84 | >>> test_file_name = 'test-file' 85 | 86 | # deleting `test-file` 87 | >>> item1.on_do_press(test_file_name) 88 | deleting test-file 89 | 90 | # restoring `test-file` 91 | >>> item1.on_undo_press() 92 | restoring test-file 93 | 94 | # hiding `test-file` 95 | >>> item2.on_do_press(test_file_name) 96 | hiding test-file 97 | 98 | # un-hiding `test-file` 99 | >>> item2.on_undo_press() 100 | un-hiding test-file 101 | """ 102 | 103 | 104 | if __name__ == "__main__": 105 | import doctest 106 | 107 | doctest.testmod() 108 | -------------------------------------------------------------------------------- /patterns/behavioral/iterator.py: -------------------------------------------------------------------------------- 1 | """ 2 | http://ginstrom.com/scribbles/2007/10/08/design-patterns-python-style/ 3 | Implementation of the iterator pattern with a generator 4 | 5 | *TL;DR 6 | Traverses a container and accesses the container's elements. 7 | """ 8 | 9 | 10 | def count_to(count: int): 11 | """Counts by word numbers, up to a maximum of five""" 12 | numbers = ["one", "two", "three", "four", "five"] 13 | yield from numbers[:count] 14 | 15 | 16 | # Test the generator 17 | def count_to_two() -> None: 18 | return count_to(2) 19 | 20 | 21 | def count_to_five() -> None: 22 | return count_to(5) 23 | 24 | 25 | def main(): 26 | """ 27 | # Counting to two... 28 | >>> for number in count_to_two(): 29 | ... print(number) 30 | one 31 | two 32 | 33 | # Counting to five... 34 | >>> for number in count_to_five(): 35 | ... print(number) 36 | one 37 | two 38 | three 39 | four 40 | five 41 | """ 42 | 43 | 44 | if __name__ == "__main__": 45 | import doctest 46 | 47 | doctest.testmod() 48 | -------------------------------------------------------------------------------- /patterns/behavioral/iterator_alt.py: -------------------------------------------------------------------------------- 1 | """ 2 | Implementation of the iterator pattern using the iterator protocol from Python 3 | 4 | *TL;DR 5 | Traverses a container and accesses the container's elements. 6 | """ 7 | 8 | from __future__ import annotations 9 | 10 | 11 | class NumberWords: 12 | """Counts by word numbers, up to a maximum of five""" 13 | 14 | _WORD_MAP = ( 15 | "one", 16 | "two", 17 | "three", 18 | "four", 19 | "five", 20 | ) 21 | 22 | def __init__(self, start: int, stop: int) -> None: 23 | self.start = start 24 | self.stop = stop 25 | 26 | def __iter__(self) -> NumberWords: # this makes the class an Iterable 27 | return self 28 | 29 | def __next__(self) -> str: # this makes the class an Iterator 30 | if self.start > self.stop or self.start > len(self._WORD_MAP): 31 | raise StopIteration 32 | current = self.start 33 | self.start += 1 34 | return self._WORD_MAP[current - 1] 35 | 36 | 37 | # Test the iterator 38 | 39 | 40 | def main(): 41 | """ 42 | # Counting to two... 43 | >>> for number in NumberWords(start=1, stop=2): 44 | ... print(number) 45 | one 46 | two 47 | 48 | # Counting to five... 49 | >>> for number in NumberWords(start=1, stop=5): 50 | ... print(number) 51 | one 52 | two 53 | three 54 | four 55 | five 56 | """ 57 | 58 | 59 | if __name__ == "__main__": 60 | import doctest 61 | 62 | doctest.testmod() 63 | -------------------------------------------------------------------------------- /patterns/behavioral/mediator.py: -------------------------------------------------------------------------------- 1 | """ 2 | https://www.djangospin.com/design-patterns-python/mediator/ 3 | 4 | Objects in a system communicate through a Mediator instead of directly with each other. 5 | This reduces the dependencies between communicating objects, thereby reducing coupling. 6 | 7 | *TL;DR 8 | Encapsulates how a set of objects interact. 9 | """ 10 | 11 | from __future__ import annotations 12 | 13 | 14 | class ChatRoom: 15 | """Mediator class""" 16 | 17 | def display_message(self, user: User, message: str) -> None: 18 | print(f"[{user} says]: {message}") 19 | 20 | 21 | class User: 22 | """A class whose instances want to interact with each other""" 23 | 24 | def __init__(self, name: str) -> None: 25 | self.name = name 26 | self.chat_room = ChatRoom() 27 | 28 | def say(self, message: str) -> None: 29 | self.chat_room.display_message(self, message) 30 | 31 | def __str__(self) -> str: 32 | return self.name 33 | 34 | 35 | def main(): 36 | """ 37 | >>> molly = User('Molly') 38 | >>> mark = User('Mark') 39 | >>> ethan = User('Ethan') 40 | 41 | >>> molly.say("Hi Team! Meeting at 3 PM today.") 42 | [Molly says]: Hi Team! Meeting at 3 PM today. 43 | >>> mark.say("Roger that!") 44 | [Mark says]: Roger that! 45 | >>> ethan.say("Alright.") 46 | [Ethan says]: Alright. 47 | """ 48 | 49 | 50 | if __name__ == "__main__": 51 | import doctest 52 | 53 | doctest.testmod() 54 | -------------------------------------------------------------------------------- /patterns/behavioral/memento.py: -------------------------------------------------------------------------------- 1 | """ 2 | http://code.activestate.com/recipes/413838-memento-closure/ 3 | 4 | *TL;DR 5 | Provides the ability to restore an object to its previous state. 6 | """ 7 | 8 | from copy import copy, deepcopy 9 | from typing import Callable, List 10 | 11 | 12 | def memento(obj, deep=False): 13 | state = deepcopy(obj.__dict__) if deep else copy(obj.__dict__) 14 | 15 | def restore(): 16 | obj.__dict__.clear() 17 | obj.__dict__.update(state) 18 | 19 | return restore 20 | 21 | 22 | class Transaction: 23 | """A transaction guard. 24 | 25 | This is, in fact, just syntactic sugar around a memento closure. 26 | """ 27 | 28 | deep = False 29 | states: List[Callable[[], None]] = [] 30 | 31 | def __init__(self, deep, *targets): 32 | self.deep = deep 33 | self.targets = targets 34 | self.commit() 35 | 36 | def commit(self): 37 | self.states = [memento(target, self.deep) for target in self.targets] 38 | 39 | def rollback(self): 40 | for a_state in self.states: 41 | a_state() 42 | 43 | 44 | def Transactional(method): 45 | """Adds transactional semantics to methods. Methods decorated with 46 | @Transactional will roll back to entry-state upon exceptions. 47 | 48 | :param method: The function to be decorated. 49 | """ 50 | def transaction(obj, *args, **kwargs): 51 | state = memento(obj) 52 | try: 53 | return method(obj, *args, **kwargs) 54 | except Exception as e: 55 | state() 56 | raise e 57 | return transaction 58 | 59 | class NumObj: 60 | def __init__(self, value): 61 | self.value = value 62 | 63 | def __repr__(self): 64 | return f"<{self.__class__.__name__}: {self.value!r}>" 65 | 66 | def increment(self): 67 | self.value += 1 68 | 69 | @Transactional 70 | def do_stuff(self): 71 | self.value = "1111" # <- invalid value 72 | self.increment() # <- will fail and rollback 73 | 74 | 75 | def main(): 76 | """ 77 | >>> num_obj = NumObj(-1) 78 | >>> print(num_obj) 79 | 80 | 81 | >>> a_transaction = Transaction(True, num_obj) 82 | 83 | >>> try: 84 | ... for i in range(3): 85 | ... num_obj.increment() 86 | ... print(num_obj) 87 | ... a_transaction.commit() 88 | ... print('-- committed') 89 | ... for i in range(3): 90 | ... num_obj.increment() 91 | ... print(num_obj) 92 | ... num_obj.value += 'x' # will fail 93 | ... print(num_obj) 94 | ... except Exception: 95 | ... a_transaction.rollback() 96 | ... print('-- rolled back') 97 | 98 | 99 | 100 | -- committed 101 | 102 | 103 | 104 | -- rolled back 105 | 106 | >>> print(num_obj) 107 | 108 | 109 | >>> print('-- now doing stuff ...') 110 | -- now doing stuff ... 111 | 112 | >>> try: 113 | ... num_obj.do_stuff() 114 | ... except Exception: 115 | ... print('-> doing stuff failed!') 116 | ... import sys 117 | ... import traceback 118 | ... traceback.print_exc(file=sys.stdout) 119 | -> doing stuff failed! 120 | Traceback (most recent call last): 121 | ... 122 | TypeError: ...str...int... 123 | 124 | >>> print(num_obj) 125 | 126 | """ 127 | 128 | 129 | if __name__ == "__main__": 130 | import doctest 131 | 132 | doctest.testmod(optionflags=doctest.ELLIPSIS) 133 | -------------------------------------------------------------------------------- /patterns/behavioral/observer.py: -------------------------------------------------------------------------------- 1 | """ 2 | http://code.activestate.com/recipes/131499-observer-pattern/ 3 | 4 | *TL;DR 5 | Maintains a list of dependents and notifies them of any state changes. 6 | 7 | *Examples in Python ecosystem: 8 | Django Signals: https://docs.djangoproject.com/en/3.1/topics/signals/ 9 | Flask Signals: https://flask.palletsprojects.com/en/1.1.x/signals/ 10 | """ 11 | 12 | from __future__ import annotations 13 | 14 | from contextlib import suppress 15 | from typing import Protocol 16 | 17 | 18 | # define a generic observer type 19 | class Observer(Protocol): 20 | def update(self, subject: Subject) -> None: 21 | pass 22 | 23 | 24 | class Subject: 25 | def __init__(self) -> None: 26 | self._observers: list[Observer] = [] 27 | 28 | def attach(self, observer: Observer) -> None: 29 | if observer not in self._observers: 30 | self._observers.append(observer) 31 | 32 | def detach(self, observer: Observer) -> None: 33 | with suppress(ValueError): 34 | self._observers.remove(observer) 35 | 36 | def notify(self, modifier: Observer | None = None) -> None: 37 | for observer in self._observers: 38 | if modifier != observer: 39 | observer.update(self) 40 | 41 | 42 | class Data(Subject): 43 | def __init__(self, name: str = "") -> None: 44 | super().__init__() 45 | self.name = name 46 | self._data = 0 47 | 48 | @property 49 | def data(self) -> int: 50 | return self._data 51 | 52 | @data.setter 53 | def data(self, value: int) -> None: 54 | self._data = value 55 | self.notify() 56 | 57 | 58 | class HexViewer: 59 | def update(self, subject: Data) -> None: 60 | print(f"HexViewer: Subject {subject.name} has data 0x{subject.data:x}") 61 | 62 | 63 | class DecimalViewer: 64 | def update(self, subject: Data) -> None: 65 | print(f"DecimalViewer: Subject {subject.name} has data {subject.data}") 66 | 67 | 68 | def main(): 69 | """ 70 | >>> data1 = Data('Data 1') 71 | >>> data2 = Data('Data 2') 72 | >>> view1 = DecimalViewer() 73 | >>> view2 = HexViewer() 74 | >>> data1.attach(view1) 75 | >>> data1.attach(view2) 76 | >>> data2.attach(view2) 77 | >>> data2.attach(view1) 78 | 79 | >>> data1.data = 10 80 | DecimalViewer: Subject Data 1 has data 10 81 | HexViewer: Subject Data 1 has data 0xa 82 | 83 | >>> data2.data = 15 84 | HexViewer: Subject Data 2 has data 0xf 85 | DecimalViewer: Subject Data 2 has data 15 86 | 87 | >>> data1.data = 3 88 | DecimalViewer: Subject Data 1 has data 3 89 | HexViewer: Subject Data 1 has data 0x3 90 | 91 | >>> data2.data = 5 92 | HexViewer: Subject Data 2 has data 0x5 93 | DecimalViewer: Subject Data 2 has data 5 94 | 95 | # Detach HexViewer from data1 and data2 96 | >>> data1.detach(view2) 97 | >>> data2.detach(view2) 98 | 99 | >>> data1.data = 10 100 | DecimalViewer: Subject Data 1 has data 10 101 | 102 | >>> data2.data = 15 103 | DecimalViewer: Subject Data 2 has data 15 104 | """ 105 | 106 | 107 | if __name__ == "__main__": 108 | import doctest 109 | 110 | doctest.testmod() 111 | -------------------------------------------------------------------------------- /patterns/behavioral/publish_subscribe.py: -------------------------------------------------------------------------------- 1 | """ 2 | Reference: 3 | http://www.slideshare.net/ishraqabd/publish-subscribe-model-overview-13368808 4 | Author: https://github.com/HanWenfang 5 | """ 6 | 7 | from __future__ import annotations 8 | 9 | 10 | class Provider: 11 | def __init__(self) -> None: 12 | self.msg_queue = [] 13 | self.subscribers = {} 14 | 15 | def notify(self, msg: str) -> None: 16 | self.msg_queue.append(msg) 17 | 18 | def subscribe(self, msg: str, subscriber: Subscriber) -> None: 19 | self.subscribers.setdefault(msg, []).append(subscriber) 20 | 21 | def unsubscribe(self, msg: str, subscriber: Subscriber) -> None: 22 | self.subscribers[msg].remove(subscriber) 23 | 24 | def update(self) -> None: 25 | for msg in self.msg_queue: 26 | for sub in self.subscribers.get(msg, []): 27 | sub.run(msg) 28 | self.msg_queue = [] 29 | 30 | 31 | class Publisher: 32 | def __init__(self, msg_center: Provider) -> None: 33 | self.provider = msg_center 34 | 35 | def publish(self, msg: str) -> None: 36 | self.provider.notify(msg) 37 | 38 | 39 | class Subscriber: 40 | def __init__(self, name: str, msg_center: Provider) -> None: 41 | self.name = name 42 | self.provider = msg_center 43 | 44 | def subscribe(self, msg: str) -> None: 45 | self.provider.subscribe(msg, self) 46 | 47 | def unsubscribe(self, msg: str) -> None: 48 | self.provider.unsubscribe(msg, self) 49 | 50 | def run(self, msg: str) -> None: 51 | print(f"{self.name} got {msg}") 52 | 53 | 54 | def main(): 55 | """ 56 | >>> message_center = Provider() 57 | 58 | >>> fftv = Publisher(message_center) 59 | 60 | >>> jim = Subscriber("jim", message_center) 61 | >>> jim.subscribe("cartoon") 62 | >>> jack = Subscriber("jack", message_center) 63 | >>> jack.subscribe("music") 64 | >>> gee = Subscriber("gee", message_center) 65 | >>> gee.subscribe("movie") 66 | >>> vani = Subscriber("vani", message_center) 67 | >>> vani.subscribe("movie") 68 | >>> vani.unsubscribe("movie") 69 | 70 | # Note that no one subscribed to `ads` 71 | # and that vani changed their mind 72 | 73 | >>> fftv.publish("cartoon") 74 | >>> fftv.publish("music") 75 | >>> fftv.publish("ads") 76 | >>> fftv.publish("movie") 77 | >>> fftv.publish("cartoon") 78 | >>> fftv.publish("cartoon") 79 | >>> fftv.publish("movie") 80 | >>> fftv.publish("blank") 81 | 82 | >>> message_center.update() 83 | jim got cartoon 84 | jack got music 85 | gee got movie 86 | jim got cartoon 87 | jim got cartoon 88 | gee got movie 89 | """ 90 | 91 | 92 | if __name__ == "__main__": 93 | import doctest 94 | 95 | doctest.testmod() 96 | -------------------------------------------------------------------------------- /patterns/behavioral/registry.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | 4 | class RegistryHolder(type): 5 | 6 | REGISTRY: Dict[str, "RegistryHolder"] = {} 7 | 8 | def __new__(cls, name, bases, attrs): 9 | new_cls = type.__new__(cls, name, bases, attrs) 10 | """ 11 | Here the name of the class is used as key but it could be any class 12 | parameter. 13 | """ 14 | cls.REGISTRY[new_cls.__name__] = new_cls 15 | return new_cls 16 | 17 | @classmethod 18 | def get_registry(cls): 19 | return dict(cls.REGISTRY) 20 | 21 | 22 | class BaseRegisteredClass(metaclass=RegistryHolder): 23 | """ 24 | Any class that will inherits from BaseRegisteredClass will be included 25 | inside the dict RegistryHolder.REGISTRY, the key being the name of the 26 | class and the associated value, the class itself. 27 | """ 28 | 29 | 30 | def main(): 31 | """ 32 | Before subclassing 33 | >>> sorted(RegistryHolder.REGISTRY) 34 | ['BaseRegisteredClass'] 35 | 36 | >>> class ClassRegistree(BaseRegisteredClass): 37 | ... def __init__(self, *args, **kwargs): 38 | ... pass 39 | 40 | After subclassing 41 | >>> sorted(RegistryHolder.REGISTRY) 42 | ['BaseRegisteredClass', 'ClassRegistree'] 43 | """ 44 | 45 | 46 | if __name__ == "__main__": 47 | import doctest 48 | 49 | doctest.testmod(optionflags=doctest.ELLIPSIS) 50 | -------------------------------------------------------------------------------- /patterns/behavioral/specification.py: -------------------------------------------------------------------------------- 1 | """ 2 | @author: Gordeev Andrey 3 | 4 | *TL;DR 5 | Provides recombination business logic by chaining together using boolean logic. 6 | """ 7 | 8 | from abc import abstractmethod 9 | 10 | 11 | class Specification: 12 | def and_specification(self, candidate): 13 | raise NotImplementedError() 14 | 15 | def or_specification(self, candidate): 16 | raise NotImplementedError() 17 | 18 | def not_specification(self): 19 | raise NotImplementedError() 20 | 21 | @abstractmethod 22 | def is_satisfied_by(self, candidate): 23 | pass 24 | 25 | 26 | class CompositeSpecification(Specification): 27 | @abstractmethod 28 | def is_satisfied_by(self, candidate): 29 | pass 30 | 31 | def and_specification(self, candidate): 32 | return AndSpecification(self, candidate) 33 | 34 | def or_specification(self, candidate): 35 | return OrSpecification(self, candidate) 36 | 37 | def not_specification(self): 38 | return NotSpecification(self) 39 | 40 | 41 | class AndSpecification(CompositeSpecification): 42 | def __init__(self, one, other): 43 | self._one: Specification = one 44 | self._other: Specification = other 45 | 46 | def is_satisfied_by(self, candidate): 47 | return bool( 48 | self._one.is_satisfied_by(candidate) 49 | and self._other.is_satisfied_by(candidate) 50 | ) 51 | 52 | 53 | class OrSpecification(CompositeSpecification): 54 | def __init__(self, one, other): 55 | self._one: Specification = one 56 | self._other: Specification = other 57 | 58 | def is_satisfied_by(self, candidate): 59 | return bool( 60 | self._one.is_satisfied_by(candidate) 61 | or self._other.is_satisfied_by(candidate) 62 | ) 63 | 64 | 65 | class NotSpecification(CompositeSpecification): 66 | def __init__(self, wrapped): 67 | self._wrapped: Specification = wrapped 68 | 69 | def is_satisfied_by(self, candidate): 70 | return bool(not self._wrapped.is_satisfied_by(candidate)) 71 | 72 | 73 | class User: 74 | def __init__(self, super_user=False): 75 | self.super_user = super_user 76 | 77 | 78 | class UserSpecification(CompositeSpecification): 79 | def is_satisfied_by(self, candidate): 80 | return isinstance(candidate, User) 81 | 82 | 83 | class SuperUserSpecification(CompositeSpecification): 84 | def is_satisfied_by(self, candidate): 85 | return getattr(candidate, "super_user", False) 86 | 87 | 88 | def main(): 89 | """ 90 | >>> andrey = User() 91 | >>> ivan = User(super_user=True) 92 | >>> vasiliy = 'not User instance' 93 | 94 | >>> root_specification = UserSpecification().and_specification(SuperUserSpecification()) 95 | 96 | # Is specification satisfied by 97 | >>> root_specification.is_satisfied_by(andrey), 'andrey' 98 | (False, 'andrey') 99 | >>> root_specification.is_satisfied_by(ivan), 'ivan' 100 | (True, 'ivan') 101 | >>> root_specification.is_satisfied_by(vasiliy), 'vasiliy' 102 | (False, 'vasiliy') 103 | """ 104 | 105 | 106 | if __name__ == "__main__": 107 | import doctest 108 | 109 | doctest.testmod() 110 | -------------------------------------------------------------------------------- /patterns/behavioral/state.py: -------------------------------------------------------------------------------- 1 | """ 2 | Implementation of the state pattern 3 | 4 | http://ginstrom.com/scribbles/2007/10/08/design-patterns-python-style/ 5 | 6 | *TL;DR 7 | Implements state as a derived class of the state pattern interface. 8 | Implements state transitions by invoking methods from the pattern's superclass. 9 | """ 10 | 11 | from __future__ import annotations 12 | 13 | 14 | class State: 15 | """Base state. This is to share functionality""" 16 | 17 | def scan(self) -> None: 18 | """Scan the dial to the next station""" 19 | self.pos += 1 20 | if self.pos == len(self.stations): 21 | self.pos = 0 22 | print(f"Scanning... Station is {self.stations[self.pos]} {self.name}") 23 | 24 | 25 | class AmState(State): 26 | def __init__(self, radio: Radio) -> None: 27 | self.radio = radio 28 | self.stations = ["1250", "1380", "1510"] 29 | self.pos = 0 30 | self.name = "AM" 31 | 32 | def toggle_amfm(self) -> None: 33 | print("Switching to FM") 34 | self.radio.state = self.radio.fmstate 35 | 36 | 37 | class FmState(State): 38 | def __init__(self, radio: Radio) -> None: 39 | self.radio = radio 40 | self.stations = ["81.3", "89.1", "103.9"] 41 | self.pos = 0 42 | self.name = "FM" 43 | 44 | def toggle_amfm(self) -> None: 45 | print("Switching to AM") 46 | self.radio.state = self.radio.amstate 47 | 48 | 49 | class Radio: 50 | """A radio. It has a scan button, and an AM/FM toggle switch.""" 51 | 52 | def __init__(self) -> None: 53 | """We have an AM state and an FM state""" 54 | self.amstate = AmState(self) 55 | self.fmstate = FmState(self) 56 | self.state = self.amstate 57 | 58 | def toggle_amfm(self) -> None: 59 | self.state.toggle_amfm() 60 | 61 | def scan(self) -> None: 62 | self.state.scan() 63 | 64 | 65 | def main(): 66 | """ 67 | >>> radio = Radio() 68 | >>> actions = [radio.scan] * 2 + [radio.toggle_amfm] + [radio.scan] * 2 69 | >>> actions *= 2 70 | 71 | >>> for action in actions: 72 | ... action() 73 | Scanning... Station is 1380 AM 74 | Scanning... Station is 1510 AM 75 | Switching to FM 76 | Scanning... Station is 89.1 FM 77 | Scanning... Station is 103.9 FM 78 | Scanning... Station is 81.3 FM 79 | Scanning... Station is 89.1 FM 80 | Switching to AM 81 | Scanning... Station is 1250 AM 82 | Scanning... Station is 1380 AM 83 | """ 84 | 85 | 86 | if __name__ == "__main__": 87 | import doctest 88 | 89 | doctest.testmod() 90 | -------------------------------------------------------------------------------- /patterns/behavioral/strategy.py: -------------------------------------------------------------------------------- 1 | """ 2 | *What is this pattern about? 3 | Define a family of algorithms, encapsulate each one, and make them interchangeable. 4 | Strategy lets the algorithm vary independently from clients that use it. 5 | 6 | *TL;DR 7 | Enables selecting an algorithm at runtime. 8 | """ 9 | 10 | from __future__ import annotations 11 | 12 | from typing import Callable 13 | 14 | 15 | class DiscountStrategyValidator: # Descriptor class for check perform 16 | @staticmethod 17 | def validate(obj: Order, value: Callable) -> bool: 18 | try: 19 | if obj.price - value(obj) < 0: 20 | raise ValueError( 21 | f"Discount cannot be applied due to negative price resulting. {value.__name__}" 22 | ) 23 | except ValueError as ex: 24 | print(str(ex)) 25 | return False 26 | else: 27 | return True 28 | 29 | def __set_name__(self, owner, name: str) -> None: 30 | self.private_name = f"_{name}" 31 | 32 | def __set__(self, obj: Order, value: Callable = None) -> None: 33 | if value and self.validate(obj, value): 34 | setattr(obj, self.private_name, value) 35 | else: 36 | setattr(obj, self.private_name, None) 37 | 38 | def __get__(self, obj: object, objtype: type = None): 39 | return getattr(obj, self.private_name) 40 | 41 | 42 | class Order: 43 | discount_strategy = DiscountStrategyValidator() 44 | 45 | def __init__(self, price: float, discount_strategy: Callable = None) -> None: 46 | self.price: float = price 47 | self.discount_strategy = discount_strategy 48 | 49 | def apply_discount(self) -> float: 50 | if self.discount_strategy: 51 | discount = self.discount_strategy(self) 52 | else: 53 | discount = 0 54 | 55 | return self.price - discount 56 | 57 | def __repr__(self) -> str: 58 | strategy = getattr(self.discount_strategy, "__name__", None) 59 | return f"" 60 | 61 | 62 | def ten_percent_discount(order: Order) -> float: 63 | return order.price * 0.10 64 | 65 | 66 | def on_sale_discount(order: Order) -> float: 67 | return order.price * 0.25 + 20 68 | 69 | 70 | def main(): 71 | """ 72 | >>> order = Order(100, discount_strategy=ten_percent_discount) 73 | >>> print(order) 74 | 75 | >>> print(order.apply_discount()) 76 | 90.0 77 | >>> order = Order(100, discount_strategy=on_sale_discount) 78 | >>> print(order) 79 | 80 | >>> print(order.apply_discount()) 81 | 55.0 82 | >>> order = Order(10, discount_strategy=on_sale_discount) 83 | Discount cannot be applied due to negative price resulting. on_sale_discount 84 | >>> print(order) 85 | 86 | """ 87 | 88 | 89 | if __name__ == "__main__": 90 | import doctest 91 | 92 | doctest.testmod() 93 | -------------------------------------------------------------------------------- /patterns/behavioral/template.py: -------------------------------------------------------------------------------- 1 | """ 2 | An example of the Template pattern in Python 3 | 4 | *TL;DR 5 | Defines the skeleton of a base algorithm, deferring definition of exact 6 | steps to subclasses. 7 | 8 | *Examples in Python ecosystem: 9 | Django class based views: https://docs.djangoproject.com/en/2.1/topics/class-based-views/ 10 | """ 11 | 12 | 13 | def get_text() -> str: 14 | return "plain-text" 15 | 16 | 17 | def get_pdf() -> str: 18 | return "pdf" 19 | 20 | 21 | def get_csv() -> str: 22 | return "csv" 23 | 24 | 25 | def convert_to_text(data: str) -> str: 26 | print("[CONVERT]") 27 | return f"{data} as text" 28 | 29 | 30 | def saver() -> None: 31 | print("[SAVE]") 32 | 33 | 34 | def template_function(getter, converter=False, to_save=False) -> None: 35 | data = getter() 36 | print(f"Got `{data}`") 37 | 38 | if len(data) <= 3 and converter: 39 | data = converter(data) 40 | else: 41 | print("Skip conversion") 42 | 43 | if to_save: 44 | saver() 45 | 46 | print(f"`{data}` was processed") 47 | 48 | 49 | def main(): 50 | """ 51 | >>> template_function(get_text, to_save=True) 52 | Got `plain-text` 53 | Skip conversion 54 | [SAVE] 55 | `plain-text` was processed 56 | 57 | >>> template_function(get_pdf, converter=convert_to_text) 58 | Got `pdf` 59 | [CONVERT] 60 | `pdf as text` was processed 61 | 62 | >>> template_function(get_csv, to_save=True) 63 | Got `csv` 64 | Skip conversion 65 | [SAVE] 66 | `csv` was processed 67 | """ 68 | 69 | 70 | if __name__ == "__main__": 71 | import doctest 72 | 73 | doctest.testmod() 74 | -------------------------------------------------------------------------------- /patterns/behavioral/visitor.py: -------------------------------------------------------------------------------- 1 | """ 2 | http://peter-hoffmann.com/2010/extrinsic-visitor-pattern-python-inheritance.html 3 | 4 | *TL;DR 5 | Separates an algorithm from an object structure on which it operates. 6 | 7 | An interesting recipe could be found in 8 | Brian Jones, David Beazley "Python Cookbook" (2013): 9 | - "8.21. Implementing the Visitor Pattern" 10 | - "8.22. Implementing the Visitor Pattern Without Recursion" 11 | 12 | *Examples in Python ecosystem: 13 | - Python's ast.NodeVisitor: https://github.com/python/cpython/blob/master/Lib/ast.py#L250 14 | which is then being used e.g. in tools like `pyflakes`. 15 | - `Black` formatter tool implements it's own: https://github.com/ambv/black/blob/master/black.py#L718 16 | """ 17 | 18 | 19 | class Node: 20 | pass 21 | 22 | 23 | class A(Node): 24 | pass 25 | 26 | 27 | class B(Node): 28 | pass 29 | 30 | 31 | class C(A, B): 32 | pass 33 | 34 | 35 | class Visitor: 36 | def visit(self, node, *args, **kwargs): 37 | meth = None 38 | for cls in node.__class__.__mro__: 39 | meth_name = "visit_" + cls.__name__ 40 | meth = getattr(self, meth_name, None) 41 | if meth: 42 | break 43 | 44 | if not meth: 45 | meth = self.generic_visit 46 | return meth(node, *args, **kwargs) 47 | 48 | def generic_visit(self, node, *args, **kwargs): 49 | print("generic_visit " + node.__class__.__name__) 50 | 51 | def visit_B(self, node, *args, **kwargs): 52 | print("visit_B " + node.__class__.__name__) 53 | 54 | 55 | def main(): 56 | """ 57 | >>> a, b, c = A(), B(), C() 58 | >>> visitor = Visitor() 59 | 60 | >>> visitor.visit(a) 61 | generic_visit A 62 | 63 | >>> visitor.visit(b) 64 | visit_B B 65 | 66 | >>> visitor.visit(c) 67 | visit_B C 68 | """ 69 | 70 | 71 | if __name__ == "__main__": 72 | import doctest 73 | 74 | doctest.testmod() 75 | -------------------------------------------------------------------------------- /patterns/behavioral/viz/catalog.py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/behavioral/viz/catalog.py.png -------------------------------------------------------------------------------- /patterns/behavioral/viz/chain.py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/behavioral/viz/chain.py.png -------------------------------------------------------------------------------- /patterns/behavioral/viz/chaining_method.py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/behavioral/viz/chaining_method.py.png -------------------------------------------------------------------------------- /patterns/behavioral/viz/command.py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/behavioral/viz/command.py.png -------------------------------------------------------------------------------- /patterns/behavioral/viz/iterator.py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/behavioral/viz/iterator.py.png -------------------------------------------------------------------------------- /patterns/behavioral/viz/mediator.py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/behavioral/viz/mediator.py.png -------------------------------------------------------------------------------- /patterns/behavioral/viz/memento.py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/behavioral/viz/memento.py.png -------------------------------------------------------------------------------- /patterns/behavioral/viz/observer.py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/behavioral/viz/observer.py.png -------------------------------------------------------------------------------- /patterns/behavioral/viz/publish_subscribe.py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/behavioral/viz/publish_subscribe.py.png -------------------------------------------------------------------------------- /patterns/behavioral/viz/registry.py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/behavioral/viz/registry.py.png -------------------------------------------------------------------------------- /patterns/behavioral/viz/specification.py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/behavioral/viz/specification.py.png -------------------------------------------------------------------------------- /patterns/behavioral/viz/state.py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/behavioral/viz/state.py.png -------------------------------------------------------------------------------- /patterns/behavioral/viz/strategy.py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/behavioral/viz/strategy.py.png -------------------------------------------------------------------------------- /patterns/behavioral/viz/template.py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/behavioral/viz/template.py.png -------------------------------------------------------------------------------- /patterns/behavioral/viz/visitor.py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/behavioral/viz/visitor.py.png -------------------------------------------------------------------------------- /patterns/creational/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/creational/__init__.py -------------------------------------------------------------------------------- /patterns/creational/abstract_factory.py: -------------------------------------------------------------------------------- 1 | """ 2 | *What is this pattern about? 3 | 4 | In Java and other languages, the Abstract Factory Pattern serves to provide an interface for 5 | creating related/dependent objects without need to specify their 6 | actual class. 7 | 8 | The idea is to abstract the creation of objects depending on business 9 | logic, platform choice, etc. 10 | 11 | In Python, the interface we use is simply a callable, which is "builtin" interface 12 | in Python, and in normal circumstances we can simply use the class itself as 13 | that callable, because classes are first class objects in Python. 14 | 15 | *What does this example do? 16 | This particular implementation abstracts the creation of a pet and 17 | does so depending on the factory we chose (Dog or Cat, or random_animal) 18 | This works because both Dog/Cat and random_animal respect a common 19 | interface (callable for creation and .speak()). 20 | Now my application can create pets abstractly and decide later, 21 | based on my own criteria, dogs over cats. 22 | 23 | *Where is the pattern used practically? 24 | 25 | *References: 26 | https://sourcemaking.com/design_patterns/abstract_factory 27 | http://ginstrom.com/scribbles/2007/10/08/design-patterns-python-style/ 28 | 29 | *TL;DR 30 | Provides a way to encapsulate a group of individual factories. 31 | """ 32 | 33 | import random 34 | from typing import Type 35 | 36 | 37 | class Pet: 38 | def __init__(self, name: str) -> None: 39 | self.name = name 40 | 41 | def speak(self) -> None: 42 | raise NotImplementedError 43 | 44 | def __str__(self) -> str: 45 | raise NotImplementedError 46 | 47 | 48 | class Dog(Pet): 49 | def speak(self) -> None: 50 | print("woof") 51 | 52 | def __str__(self) -> str: 53 | return f"Dog<{self.name}>" 54 | 55 | 56 | class Cat(Pet): 57 | def speak(self) -> None: 58 | print("meow") 59 | 60 | def __str__(self) -> str: 61 | return f"Cat<{self.name}>" 62 | 63 | 64 | class PetShop: 65 | """A pet shop""" 66 | 67 | def __init__(self, animal_factory: Type[Pet]) -> None: 68 | """pet_factory is our abstract factory. We can set it at will.""" 69 | 70 | self.pet_factory = animal_factory 71 | 72 | def buy_pet(self, name: str) -> Pet: 73 | """Creates and shows a pet using the abstract factory""" 74 | 75 | pet = self.pet_factory(name) 76 | print(f"Here is your lovely {pet}") 77 | return pet 78 | 79 | 80 | # Show pets with various factories 81 | def main() -> None: 82 | """ 83 | # A Shop that sells only cats 84 | >>> cat_shop = PetShop(Cat) 85 | >>> pet = cat_shop.buy_pet("Lucy") 86 | Here is your lovely Cat 87 | >>> pet.speak() 88 | meow 89 | """ 90 | 91 | 92 | if __name__ == "__main__": 93 | animals = [Dog, Cat] 94 | random_animal: Type[Pet] = random.choice(animals) 95 | 96 | shop = PetShop(random_animal) 97 | import doctest 98 | 99 | doctest.testmod() 100 | -------------------------------------------------------------------------------- /patterns/creational/borg.py: -------------------------------------------------------------------------------- 1 | """ 2 | *What is this pattern about? 3 | The Borg pattern (also known as the Monostate pattern) is a way to 4 | implement singleton behavior, but instead of having only one instance 5 | of a class, there are multiple instances that share the same state. In 6 | other words, the focus is on sharing state instead of sharing instance 7 | identity. 8 | 9 | *What does this example do? 10 | To understand the implementation of this pattern in Python, it is 11 | important to know that, in Python, instance attributes are stored in a 12 | attribute dictionary called __dict__. Usually, each instance will have 13 | its own dictionary, but the Borg pattern modifies this so that all 14 | instances have the same dictionary. 15 | In this example, the __shared_state attribute will be the dictionary 16 | shared between all instances, and this is ensured by assigning 17 | __shared_state to the __dict__ variable when initializing a new 18 | instance (i.e., in the __init__ method). Other attributes are usually 19 | added to the instance's attribute dictionary, but, since the attribute 20 | dictionary itself is shared (which is __shared_state), all other 21 | attributes will also be shared. 22 | 23 | *Where is the pattern used practically? 24 | Sharing state is useful in applications like managing database connections: 25 | https://github.com/onetwopunch/pythonDbTemplate/blob/master/database.py 26 | 27 | *References: 28 | - https://fkromer.github.io/python-pattern-references/design/#singleton 29 | - https://learning.oreilly.com/library/view/python-cookbook/0596001673/ch05s23.html 30 | - http://www.aleax.it/5ep.html 31 | 32 | *TL;DR 33 | Provides singleton-like behavior sharing state between instances. 34 | """ 35 | 36 | from typing import Dict 37 | 38 | 39 | class Borg: 40 | _shared_state: Dict[str, str] = {} 41 | 42 | def __init__(self) -> None: 43 | self.__dict__ = self._shared_state 44 | 45 | 46 | class YourBorg(Borg): 47 | def __init__(self, state: str = None) -> None: 48 | super().__init__() 49 | if state: 50 | self.state = state 51 | else: 52 | # initiate the first instance with default state 53 | if not hasattr(self, "state"): 54 | self.state = "Init" 55 | 56 | def __str__(self) -> str: 57 | return self.state 58 | 59 | 60 | def main(): 61 | """ 62 | >>> rm1 = YourBorg() 63 | >>> rm2 = YourBorg() 64 | 65 | >>> rm1.state = 'Idle' 66 | >>> rm2.state = 'Running' 67 | 68 | >>> print('rm1: {0}'.format(rm1)) 69 | rm1: Running 70 | >>> print('rm2: {0}'.format(rm2)) 71 | rm2: Running 72 | 73 | # When the `state` attribute is modified from instance `rm2`, 74 | # the value of `state` in instance `rm1` also changes 75 | >>> rm2.state = 'Zombie' 76 | 77 | >>> print('rm1: {0}'.format(rm1)) 78 | rm1: Zombie 79 | >>> print('rm2: {0}'.format(rm2)) 80 | rm2: Zombie 81 | 82 | # Even though `rm1` and `rm2` share attributes, the instances are not the same 83 | >>> rm1 is rm2 84 | False 85 | 86 | # New instances also get the same shared state 87 | >>> rm3 = YourBorg() 88 | 89 | >>> print('rm1: {0}'.format(rm1)) 90 | rm1: Zombie 91 | >>> print('rm2: {0}'.format(rm2)) 92 | rm2: Zombie 93 | >>> print('rm3: {0}'.format(rm3)) 94 | rm3: Zombie 95 | 96 | # A new instance can explicitly change the state during creation 97 | >>> rm4 = YourBorg('Running') 98 | 99 | >>> print('rm4: {0}'.format(rm4)) 100 | rm4: Running 101 | 102 | # Existing instances reflect that change as well 103 | >>> print('rm3: {0}'.format(rm3)) 104 | rm3: Running 105 | """ 106 | 107 | 108 | if __name__ == "__main__": 109 | import doctest 110 | 111 | doctest.testmod() 112 | -------------------------------------------------------------------------------- /patterns/creational/builder.py: -------------------------------------------------------------------------------- 1 | """ 2 | *What is this pattern about? 3 | It decouples the creation of a complex object and its representation, 4 | so that the same process can be reused to build objects from the same 5 | family. 6 | This is useful when you must separate the specification of an object 7 | from its actual representation (generally for abstraction). 8 | 9 | *What does this example do? 10 | 11 | The first example achieves this by using an abstract base 12 | class for a building, where the initializer (__init__ method) specifies the 13 | steps needed, and the concrete subclasses implement these steps. 14 | 15 | In other programming languages, a more complex arrangement is sometimes 16 | necessary. In particular, you cannot have polymorphic behaviour in a constructor in C++ - 17 | see https://stackoverflow.com/questions/1453131/how-can-i-get-polymorphic-behavior-in-a-c-constructor 18 | - which means this Python technique will not work. The polymorphism 19 | required has to be provided by an external, already constructed 20 | instance of a different class. 21 | 22 | In general, in Python this won't be necessary, but a second example showing 23 | this kind of arrangement is also included. 24 | 25 | *Where is the pattern used practically? 26 | 27 | *References: 28 | https://sourcemaking.com/design_patterns/builder 29 | 30 | *TL;DR 31 | Decouples the creation of a complex object and its representation. 32 | """ 33 | 34 | 35 | # Abstract Building 36 | class Building: 37 | def __init__(self) -> None: 38 | self.build_floor() 39 | self.build_size() 40 | 41 | def build_floor(self): 42 | raise NotImplementedError 43 | 44 | def build_size(self): 45 | raise NotImplementedError 46 | 47 | def __repr__(self) -> str: 48 | return "Floor: {0.floor} | Size: {0.size}".format(self) 49 | 50 | 51 | # Concrete Buildings 52 | class House(Building): 53 | def build_floor(self) -> None: 54 | self.floor = "One" 55 | 56 | def build_size(self) -> None: 57 | self.size = "Big" 58 | 59 | 60 | class Flat(Building): 61 | def build_floor(self) -> None: 62 | self.floor = "More than One" 63 | 64 | def build_size(self) -> None: 65 | self.size = "Small" 66 | 67 | 68 | # In some very complex cases, it might be desirable to pull out the building 69 | # logic into another function (or a method on another class), rather than being 70 | # in the base class '__init__'. (This leaves you in the strange situation where 71 | # a concrete class does not have a useful constructor) 72 | 73 | 74 | class ComplexBuilding: 75 | def __repr__(self) -> str: 76 | return "Floor: {0.floor} | Size: {0.size}".format(self) 77 | 78 | 79 | class ComplexHouse(ComplexBuilding): 80 | def build_floor(self) -> None: 81 | self.floor = "One" 82 | 83 | def build_size(self) -> None: 84 | self.size = "Big and fancy" 85 | 86 | 87 | def construct_building(cls) -> Building: 88 | building = cls() 89 | building.build_floor() 90 | building.build_size() 91 | return building 92 | 93 | 94 | def main(): 95 | """ 96 | >>> house = House() 97 | >>> house 98 | Floor: One | Size: Big 99 | 100 | >>> flat = Flat() 101 | >>> flat 102 | Floor: More than One | Size: Small 103 | 104 | # Using an external constructor function: 105 | >>> complex_house = construct_building(ComplexHouse) 106 | >>> complex_house 107 | Floor: One | Size: Big and fancy 108 | """ 109 | 110 | 111 | if __name__ == "__main__": 112 | import doctest 113 | 114 | doctest.testmod() 115 | -------------------------------------------------------------------------------- /patterns/creational/factory.py: -------------------------------------------------------------------------------- 1 | """*What is this pattern about? 2 | A Factory is an object for creating other objects. 3 | 4 | *What does this example do? 5 | The code shows a way to localize words in two languages: English and 6 | Greek. "get_localizer" is the factory function that constructs a 7 | localizer depending on the language chosen. The localizer object will 8 | be an instance from a different class according to the language 9 | localized. However, the main code does not have to worry about which 10 | localizer will be instantiated, since the method "localize" will be called 11 | in the same way independently of the language. 12 | 13 | *Where can the pattern be used practically? 14 | The Factory Method can be seen in the popular web framework Django: 15 | https://docs.djangoproject.com/en/4.0/topics/forms/formsets/ 16 | For example, different types of forms are created using a formset_factory 17 | 18 | *References: 19 | http://ginstrom.com/scribbles/2007/10/08/design-patterns-python-style/ 20 | 21 | *TL;DR 22 | Creates objects without having to specify the exact class. 23 | """ 24 | 25 | from typing import Dict, Protocol, Type 26 | 27 | 28 | class Localizer(Protocol): 29 | def localize(self, msg: str) -> str: 30 | pass 31 | 32 | 33 | class GreekLocalizer: 34 | """A simple localizer a la gettext""" 35 | 36 | def __init__(self) -> None: 37 | self.translations = {"dog": "σκύλος", "cat": "γάτα"} 38 | 39 | def localize(self, msg: str) -> str: 40 | """We'll punt if we don't have a translation""" 41 | return self.translations.get(msg, msg) 42 | 43 | 44 | class EnglishLocalizer: 45 | """Simply echoes the message""" 46 | 47 | def localize(self, msg: str) -> str: 48 | return msg 49 | 50 | 51 | def get_localizer(language: str = "English") -> Localizer: 52 | """Factory""" 53 | localizers: Dict[str, Type[Localizer]] = { 54 | "English": EnglishLocalizer, 55 | "Greek": GreekLocalizer, 56 | } 57 | 58 | return localizers[language]() 59 | 60 | 61 | def main(): 62 | """ 63 | # Create our localizers 64 | >>> e, g = get_localizer(language="English"), get_localizer(language="Greek") 65 | 66 | # Localize some text 67 | >>> for msg in "dog parrot cat bear".split(): 68 | ... print(e.localize(msg), g.localize(msg)) 69 | dog σκύλος 70 | parrot parrot 71 | cat γάτα 72 | bear bear 73 | """ 74 | 75 | 76 | if __name__ == "__main__": 77 | import doctest 78 | 79 | doctest.testmod() 80 | -------------------------------------------------------------------------------- /patterns/creational/lazy_evaluation.py: -------------------------------------------------------------------------------- 1 | """ 2 | Lazily-evaluated property pattern in Python. 3 | 4 | https://en.wikipedia.org/wiki/Lazy_evaluation 5 | 6 | *References: 7 | bottle 8 | https://github.com/bottlepy/bottle/blob/cafc15419cbb4a6cb748e6ecdccf92893bb25ce5/bottle.py#L270 9 | django 10 | https://github.com/django/django/blob/ffd18732f3ee9e6f0374aff9ccf350d85187fac2/django/utils/functional.py#L19 11 | pip 12 | https://github.com/pypa/pip/blob/cb75cca785629e15efb46c35903827b3eae13481/pip/utils/__init__.py#L821 13 | pyramid 14 | https://github.com/Pylons/pyramid/blob/7909e9503cdfc6f6e84d2c7ace1d3c03ca1d8b73/pyramid/decorator.py#L4 15 | werkzeug 16 | https://github.com/pallets/werkzeug/blob/5a2bf35441006d832ab1ed5a31963cbc366c99ac/werkzeug/utils.py#L35 17 | 18 | *TL;DR 19 | Delays the eval of an expr until its value is needed and avoids repeated evals. 20 | """ 21 | 22 | import functools 23 | 24 | 25 | class lazy_property: 26 | def __init__(self, function): 27 | self.function = function 28 | functools.update_wrapper(self, function) 29 | 30 | def __get__(self, obj, type_): 31 | if obj is None: 32 | return self 33 | val = self.function(obj) 34 | obj.__dict__[self.function.__name__] = val 35 | return val 36 | 37 | 38 | def lazy_property2(fn): 39 | """ 40 | A lazy property decorator. 41 | 42 | The function decorated is called the first time to retrieve the result and 43 | then that calculated result is used the next time you access the value. 44 | """ 45 | attr = "_lazy__" + fn.__name__ 46 | 47 | @property 48 | def _lazy_property(self): 49 | if not hasattr(self, attr): 50 | setattr(self, attr, fn(self)) 51 | return getattr(self, attr) 52 | 53 | return _lazy_property 54 | 55 | 56 | class Person: 57 | def __init__(self, name, occupation): 58 | self.name = name 59 | self.occupation = occupation 60 | self.call_count2 = 0 61 | 62 | @lazy_property 63 | def relatives(self): 64 | # Get all relatives, let's assume that it costs much time. 65 | relatives = "Many relatives." 66 | return relatives 67 | 68 | @lazy_property2 69 | def parents(self): 70 | self.call_count2 += 1 71 | return "Father and mother" 72 | 73 | 74 | def main(): 75 | """ 76 | >>> Jhon = Person('Jhon', 'Coder') 77 | 78 | >>> Jhon.name 79 | 'Jhon' 80 | >>> Jhon.occupation 81 | 'Coder' 82 | 83 | # Before we access `relatives` 84 | >>> sorted(Jhon.__dict__.items()) 85 | [('call_count2', 0), ('name', 'Jhon'), ('occupation', 'Coder')] 86 | 87 | >>> Jhon.relatives 88 | 'Many relatives.' 89 | 90 | # After we've accessed `relatives` 91 | >>> sorted(Jhon.__dict__.items()) 92 | [('call_count2', 0), ..., ('relatives', 'Many relatives.')] 93 | 94 | >>> Jhon.parents 95 | 'Father and mother' 96 | 97 | >>> sorted(Jhon.__dict__.items()) 98 | [('_lazy__parents', 'Father and mother'), ('call_count2', 1), ..., ('relatives', 'Many relatives.')] 99 | 100 | >>> Jhon.parents 101 | 'Father and mother' 102 | 103 | >>> Jhon.call_count2 104 | 1 105 | """ 106 | 107 | 108 | if __name__ == "__main__": 109 | import doctest 110 | 111 | doctest.testmod(optionflags=doctest.ELLIPSIS) 112 | -------------------------------------------------------------------------------- /patterns/creational/pool.py: -------------------------------------------------------------------------------- 1 | """ 2 | *What is this pattern about? 3 | This pattern is used when creating an object is costly (and they are 4 | created frequently) but only a few are used at a time. With a Pool we 5 | can manage those instances we have as of now by caching them. Now it 6 | is possible to skip the costly creation of an object if one is 7 | available in the pool. 8 | A pool allows to 'check out' an inactive object and then to return it. 9 | If none are available the pool creates one to provide without wait. 10 | 11 | *What does this example do? 12 | In this example queue.Queue is used to create the pool (wrapped in a 13 | custom ObjectPool object to use with the with statement), and it is 14 | populated with strings. 15 | As we can see, the first string object put in "yam" is USED by the 16 | with statement. But because it is released back into the pool 17 | afterwards it is reused by the explicit call to sample_queue.get(). 18 | Same thing happens with "sam", when the ObjectPool created inside the 19 | function is deleted (by the GC) and the object is returned. 20 | 21 | *Where is the pattern used practically? 22 | 23 | *References: 24 | http://stackoverflow.com/questions/1514120/python-implementation-of-the-object-pool-design-pattern 25 | https://sourcemaking.com/design_patterns/object_pool 26 | 27 | *TL;DR 28 | Stores a set of initialized objects kept ready to use. 29 | """ 30 | 31 | 32 | class ObjectPool: 33 | def __init__(self, queue, auto_get=False): 34 | self._queue = queue 35 | self.item = self._queue.get() if auto_get else None 36 | 37 | def __enter__(self): 38 | if self.item is None: 39 | self.item = self._queue.get() 40 | return self.item 41 | 42 | def __exit__(self, Type, value, traceback): 43 | if self.item is not None: 44 | self._queue.put(self.item) 45 | self.item = None 46 | 47 | def __del__(self): 48 | if self.item is not None: 49 | self._queue.put(self.item) 50 | self.item = None 51 | 52 | 53 | def main(): 54 | """ 55 | >>> import queue 56 | 57 | >>> def test_object(queue): 58 | ... pool = ObjectPool(queue, True) 59 | ... print('Inside func: {}'.format(pool.item)) 60 | 61 | >>> sample_queue = queue.Queue() 62 | 63 | >>> sample_queue.put('yam') 64 | >>> with ObjectPool(sample_queue) as obj: 65 | ... print('Inside with: {}'.format(obj)) 66 | Inside with: yam 67 | 68 | >>> print('Outside with: {}'.format(sample_queue.get())) 69 | Outside with: yam 70 | 71 | >>> sample_queue.put('sam') 72 | >>> test_object(sample_queue) 73 | Inside func: sam 74 | 75 | >>> print('Outside func: {}'.format(sample_queue.get())) 76 | Outside func: sam 77 | 78 | if not sample_queue.empty(): 79 | print(sample_queue.get()) 80 | """ 81 | 82 | 83 | if __name__ == "__main__": 84 | import doctest 85 | 86 | doctest.testmod() 87 | -------------------------------------------------------------------------------- /patterns/creational/prototype.py: -------------------------------------------------------------------------------- 1 | """ 2 | *What is this pattern about? 3 | This patterns aims to reduce the number of classes required by an 4 | application. Instead of relying on subclasses it creates objects by 5 | copying a prototypical instance at run-time. 6 | 7 | This is useful as it makes it easier to derive new kinds of objects, 8 | when instances of the class have only a few different combinations of 9 | state, and when instantiation is expensive. 10 | 11 | *What does this example do? 12 | When the number of prototypes in an application can vary, it can be 13 | useful to keep a Dispatcher (aka, Registry or Manager). This allows 14 | clients to query the Dispatcher for a prototype before cloning a new 15 | instance. 16 | 17 | Below provides an example of such Dispatcher, which contains three 18 | copies of the prototype: 'default', 'objecta' and 'objectb'. 19 | 20 | *TL;DR 21 | Creates new object instances by cloning prototype. 22 | """ 23 | 24 | from __future__ import annotations 25 | 26 | from typing import Any 27 | 28 | 29 | class Prototype: 30 | def __init__(self, value: str = "default", **attrs: Any) -> None: 31 | self.value = value 32 | self.__dict__.update(attrs) 33 | 34 | def clone(self, **attrs: Any) -> Prototype: 35 | """Clone a prototype and update inner attributes dictionary""" 36 | # Python in Practice, Mark Summerfield 37 | # copy.deepcopy can be used instead of next line. 38 | obj = self.__class__(**self.__dict__) 39 | obj.__dict__.update(attrs) 40 | return obj 41 | 42 | 43 | class PrototypeDispatcher: 44 | def __init__(self): 45 | self._objects = {} 46 | 47 | def get_objects(self) -> dict[str, Prototype]: 48 | """Get all objects""" 49 | return self._objects 50 | 51 | def register_object(self, name: str, obj: Prototype) -> None: 52 | """Register an object""" 53 | self._objects[name] = obj 54 | 55 | def unregister_object(self, name: str) -> None: 56 | """Unregister an object""" 57 | del self._objects[name] 58 | 59 | 60 | def main() -> None: 61 | """ 62 | >>> dispatcher = PrototypeDispatcher() 63 | >>> prototype = Prototype() 64 | 65 | >>> d = prototype.clone() 66 | >>> a = prototype.clone(value='a-value', category='a') 67 | >>> b = a.clone(value='b-value', is_checked=True) 68 | >>> dispatcher.register_object('objecta', a) 69 | >>> dispatcher.register_object('objectb', b) 70 | >>> dispatcher.register_object('default', d) 71 | 72 | >>> [{n: p.value} for n, p in dispatcher.get_objects().items()] 73 | [{'objecta': 'a-value'}, {'objectb': 'b-value'}, {'default': 'default'}] 74 | 75 | >>> print(b.category, b.is_checked) 76 | a True 77 | """ 78 | 79 | 80 | if __name__ == "__main__": 81 | import doctest 82 | 83 | doctest.testmod() 84 | -------------------------------------------------------------------------------- /patterns/creational/viz/abstract_factory.py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/creational/viz/abstract_factory.py.png -------------------------------------------------------------------------------- /patterns/creational/viz/borg.py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/creational/viz/borg.py.png -------------------------------------------------------------------------------- /patterns/creational/viz/builder.py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/creational/viz/builder.py.png -------------------------------------------------------------------------------- /patterns/creational/viz/factory_method.py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/creational/viz/factory_method.py.png -------------------------------------------------------------------------------- /patterns/creational/viz/lazy_evaluation.py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/creational/viz/lazy_evaluation.py.png -------------------------------------------------------------------------------- /patterns/creational/viz/pool.py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/creational/viz/pool.py.png -------------------------------------------------------------------------------- /patterns/creational/viz/prototype.py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/creational/viz/prototype.py.png -------------------------------------------------------------------------------- /patterns/dependency_injection.py: -------------------------------------------------------------------------------- 1 | """ 2 | Dependency Injection (DI) is a technique whereby one object supplies the dependencies (services) 3 | to another object (client). 4 | It allows to decouple objects: no need to change client code simply because an object it depends on 5 | needs to be changed to a different one. (Open/Closed principle) 6 | 7 | Port of the Java example of Dependency Injection" in 8 | "xUnit Test Patterns - Refactoring Test Code" by Gerard Meszaros 9 | (ISBN-10: 0131495054, ISBN-13: 978-0131495050) 10 | 11 | In the following example `time_provider` (service) is embedded into TimeDisplay (client). 12 | If such service performed an expensive operation you would like to substitute or mock it in tests. 13 | 14 | class TimeDisplay(object): 15 | 16 | def __init__(self): 17 | self.time_provider = datetime.datetime.now 18 | 19 | def get_current_time_as_html_fragment(self): 20 | current_time = self.time_provider() 21 | current_time_as_html_fragment = "{}".format(current_time) 22 | return current_time_as_html_fragment 23 | 24 | """ 25 | 26 | import datetime 27 | from typing import Callable 28 | 29 | 30 | class ConstructorInjection: 31 | def __init__(self, time_provider: Callable) -> None: 32 | self.time_provider = time_provider 33 | 34 | def get_current_time_as_html_fragment(self) -> str: 35 | current_time = self.time_provider() 36 | current_time_as_html_fragment = '{}'.format( 37 | current_time 38 | ) 39 | return current_time_as_html_fragment 40 | 41 | 42 | class ParameterInjection: 43 | def __init__(self) -> None: 44 | pass 45 | 46 | def get_current_time_as_html_fragment(self, time_provider: Callable) -> str: 47 | current_time = time_provider() 48 | current_time_as_html_fragment = '{}'.format( 49 | current_time 50 | ) 51 | return current_time_as_html_fragment 52 | 53 | 54 | class SetterInjection: 55 | """Setter Injection""" 56 | 57 | def __init__(self): 58 | pass 59 | 60 | def set_time_provider(self, time_provider: Callable): 61 | self.time_provider = time_provider 62 | 63 | def get_current_time_as_html_fragment(self): 64 | current_time = self.time_provider() 65 | current_time_as_html_fragment = '{}'.format( 66 | current_time 67 | ) 68 | return current_time_as_html_fragment 69 | 70 | 71 | def production_code_time_provider() -> str: 72 | """ 73 | Production code version of the time provider (just a wrapper for formatting 74 | datetime for this example). 75 | """ 76 | current_time = datetime.datetime.now() 77 | current_time_formatted = f"{current_time.hour}:{current_time.minute}" 78 | return current_time_formatted 79 | 80 | 81 | def midnight_time_provider() -> str: 82 | """Hard-coded stub""" 83 | return "24:01" 84 | 85 | 86 | def main(): 87 | """ 88 | >>> time_with_ci1 = ConstructorInjection(midnight_time_provider) 89 | >>> time_with_ci1.get_current_time_as_html_fragment() 90 | '24:01' 91 | 92 | >>> time_with_ci2 = ConstructorInjection(production_code_time_provider) 93 | >>> time_with_ci2.get_current_time_as_html_fragment() 94 | '...' 95 | 96 | >>> time_with_pi = ParameterInjection() 97 | >>> time_with_pi.get_current_time_as_html_fragment(midnight_time_provider) 98 | '24:01' 99 | 100 | >>> time_with_si = SetterInjection() 101 | 102 | >>> time_with_si.get_current_time_as_html_fragment() 103 | Traceback (most recent call last): 104 | ... 105 | AttributeError: 'SetterInjection' object has no attribute 'time_provider' 106 | 107 | >>> time_with_si.set_time_provider(midnight_time_provider) 108 | >>> time_with_si.get_current_time_as_html_fragment() 109 | '24:01' 110 | """ 111 | 112 | 113 | if __name__ == "__main__": 114 | import doctest 115 | 116 | doctest.testmod(optionflags=doctest.ELLIPSIS) 117 | -------------------------------------------------------------------------------- /patterns/fundamental/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/fundamental/__init__.py -------------------------------------------------------------------------------- /patterns/fundamental/delegation_pattern.py: -------------------------------------------------------------------------------- 1 | """ 2 | Reference: https://en.wikipedia.org/wiki/Delegation_pattern 3 | Author: https://github.com/IuryAlves 4 | 5 | *TL;DR 6 | Allows object composition to achieve the same code reuse as inheritance. 7 | """ 8 | 9 | from __future__ import annotations 10 | 11 | from typing import Any, Callable 12 | 13 | 14 | class Delegator: 15 | """ 16 | >>> delegator = Delegator(Delegate()) 17 | >>> delegator.p1 18 | 123 19 | >>> delegator.p2 20 | Traceback (most recent call last): 21 | ... 22 | AttributeError: 'Delegate' object has no attribute 'p2'. Did you mean: 'p1'? 23 | >>> delegator.do_something("nothing") 24 | 'Doing nothing' 25 | >>> delegator.do_something("something", kw=", faif!") 26 | 'Doing something, faif!' 27 | >>> delegator.do_anything() 28 | Traceback (most recent call last): 29 | ... 30 | AttributeError: 'Delegate' object has no attribute 'do_anything'. Did you mean: 'do_something'? 31 | """ 32 | 33 | def __init__(self, delegate: Delegate) -> None: 34 | self.delegate = delegate 35 | 36 | def __getattr__(self, name: str) -> Any | Callable: 37 | attr = getattr(self.delegate, name) 38 | 39 | if not callable(attr): 40 | return attr 41 | 42 | def wrapper(*args, **kwargs): 43 | return attr(*args, **kwargs) 44 | 45 | return wrapper 46 | 47 | 48 | class Delegate: 49 | def __init__(self) -> None: 50 | self.p1 = 123 51 | 52 | def do_something(self, something: str, kw=None) -> str: 53 | return f"Doing {something}{kw or ''}" 54 | 55 | 56 | if __name__ == "__main__": 57 | import doctest 58 | 59 | doctest.testmod() 60 | -------------------------------------------------------------------------------- /patterns/fundamental/viz/delegation_pattern.py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/fundamental/viz/delegation_pattern.py.png -------------------------------------------------------------------------------- /patterns/other/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/other/__init__.py -------------------------------------------------------------------------------- /patterns/other/blackboard.py: -------------------------------------------------------------------------------- 1 | """ 2 | @author: Eugene Duboviy | github.com/duboviy 3 | 4 | In Blackboard pattern several specialised sub-systems (knowledge sources) 5 | assemble their knowledge to build a possibly partial or approximate solution. 6 | In this way, the sub-systems work together to solve the problem, 7 | where the solution is the sum of its parts. 8 | 9 | https://en.wikipedia.org/wiki/Blackboard_system 10 | """ 11 | 12 | from abc import ABC, abstractmethod 13 | import random 14 | 15 | 16 | class AbstractExpert(ABC): 17 | """Abstract class for experts in the blackboard system.""" 18 | @abstractmethod 19 | def __init__(self, blackboard) -> None: 20 | self.blackboard = blackboard 21 | 22 | @property 23 | @abstractmethod 24 | def is_eager_to_contribute(self) -> int: 25 | raise NotImplementedError("Must provide implementation in subclass.") 26 | 27 | @abstractmethod 28 | def contribute(self) -> None: 29 | raise NotImplementedError("Must provide implementation in subclass.") 30 | 31 | 32 | class Blackboard: 33 | """The blackboard system that holds the common state.""" 34 | def __init__(self) -> None: 35 | self.experts: list = [] 36 | self.common_state = { 37 | "problems": 0, 38 | "suggestions": 0, 39 | "contributions": [], 40 | "progress": 0, # percentage, if 100 -> task is finished 41 | } 42 | 43 | def add_expert(self, expert: AbstractExpert) -> None: 44 | self.experts.append(expert) 45 | 46 | 47 | class Controller: 48 | """The controller that manages the blackboard system.""" 49 | def __init__(self, blackboard: Blackboard) -> None: 50 | self.blackboard = blackboard 51 | 52 | def run_loop(self): 53 | """ 54 | This function is a loop that runs until the progress reaches 100. 55 | It checks if an expert is eager to contribute and then calls its contribute method. 56 | """ 57 | while self.blackboard.common_state["progress"] < 100: 58 | for expert in self.blackboard.experts: 59 | if expert.is_eager_to_contribute: 60 | expert.contribute() 61 | return self.blackboard.common_state["contributions"] 62 | 63 | 64 | class Student(AbstractExpert): 65 | """Concrete class for a student expert.""" 66 | def __init__(self, blackboard) -> None: 67 | super().__init__(blackboard) 68 | 69 | @property 70 | def is_eager_to_contribute(self) -> bool: 71 | return True 72 | 73 | def contribute(self) -> None: 74 | self.blackboard.common_state["problems"] += random.randint(1, 10) 75 | self.blackboard.common_state["suggestions"] += random.randint(1, 10) 76 | self.blackboard.common_state["contributions"] += [self.__class__.__name__] 77 | self.blackboard.common_state["progress"] += random.randint(1, 2) 78 | 79 | 80 | class Scientist(AbstractExpert): 81 | """Concrete class for a scientist expert.""" 82 | def __init__(self, blackboard) -> None: 83 | super().__init__(blackboard) 84 | 85 | @property 86 | def is_eager_to_contribute(self) -> int: 87 | return random.randint(0, 1) 88 | 89 | def contribute(self) -> None: 90 | self.blackboard.common_state["problems"] += random.randint(10, 20) 91 | self.blackboard.common_state["suggestions"] += random.randint(10, 20) 92 | self.blackboard.common_state["contributions"] += [self.__class__.__name__] 93 | self.blackboard.common_state["progress"] += random.randint(10, 30) 94 | 95 | 96 | class Professor(AbstractExpert): 97 | def __init__(self, blackboard) -> None: 98 | super().__init__(blackboard) 99 | 100 | @property 101 | def is_eager_to_contribute(self) -> bool: 102 | return True if self.blackboard.common_state["problems"] > 100 else False 103 | 104 | def contribute(self) -> None: 105 | self.blackboard.common_state["problems"] += random.randint(1, 2) 106 | self.blackboard.common_state["suggestions"] += random.randint(10, 20) 107 | self.blackboard.common_state["contributions"] += [self.__class__.__name__] 108 | self.blackboard.common_state["progress"] += random.randint(10, 100) 109 | 110 | 111 | def main(): 112 | """ 113 | >>> blackboard = Blackboard() 114 | >>> blackboard.add_expert(Student(blackboard)) 115 | >>> blackboard.add_expert(Scientist(blackboard)) 116 | >>> blackboard.add_expert(Professor(blackboard)) 117 | 118 | >>> c = Controller(blackboard) 119 | >>> contributions = c.run_loop() 120 | 121 | >>> from pprint import pprint 122 | >>> pprint(contributions) 123 | ['Student', 124 | 'Scientist', 125 | 'Student', 126 | 'Scientist', 127 | 'Student', 128 | 'Scientist', 129 | 'Professor'] 130 | """ 131 | 132 | 133 | if __name__ == "__main__": 134 | random.seed(1234) # for deterministic doctest outputs 135 | import doctest 136 | 137 | doctest.testmod() 138 | -------------------------------------------------------------------------------- /patterns/other/graph_search.py: -------------------------------------------------------------------------------- 1 | class GraphSearch: 2 | """Graph search emulation in python, from source 3 | http://www.python.org/doc/essays/graphs/ 4 | 5 | dfs stands for Depth First Search 6 | bfs stands for Breadth First Search""" 7 | 8 | def __init__(self, graph): 9 | self.graph = graph 10 | 11 | def find_path_dfs(self, start, end, path=None): 12 | path = path or [] 13 | 14 | path.append(start) 15 | if start == end: 16 | return path 17 | for node in self.graph.get(start, []): 18 | if node not in path: 19 | newpath = self.find_path_dfs(node, end, path[:]) 20 | if newpath: 21 | return newpath 22 | 23 | def find_all_paths_dfs(self, start, end, path=None): 24 | path = path or [] 25 | path.append(start) 26 | if start == end: 27 | return [path] 28 | paths = [] 29 | for node in self.graph.get(start, []): 30 | if node not in path: 31 | newpaths = self.find_all_paths_dfs(node, end, path[:]) 32 | paths.extend(newpaths) 33 | return paths 34 | 35 | def find_shortest_path_dfs(self, start, end, path=None): 36 | path = path or [] 37 | path.append(start) 38 | 39 | if start == end: 40 | return path 41 | shortest = None 42 | for node in self.graph.get(start, []): 43 | if node not in path: 44 | newpath = self.find_shortest_path_dfs(node, end, path[:]) 45 | if newpath: 46 | if not shortest or len(newpath) < len(shortest): 47 | shortest = newpath 48 | return shortest 49 | 50 | def find_shortest_path_bfs(self, start, end): 51 | """ 52 | Finds the shortest path between two nodes in a graph using breadth-first search. 53 | 54 | :param start: The node to start from. 55 | :type start: str or int 56 | :param end: The node to find the shortest path to. 57 | :type end: str or int 58 | 59 | :returns queue_path_to_end, dist_to[end]: A list of nodes 60 | representing the shortest path from `start` to `end`, and a dictionary 61 | mapping each node in the graph (except for `start`) with its distance from it 62 | (in terms of hops). If no such path exists, returns an empty list and an empty 63 | dictionary instead. 64 | """ 65 | queue = [start] 66 | dist_to = {start: 0} 67 | edge_to = {} 68 | 69 | if start == end: 70 | return queue 71 | 72 | while len(queue): 73 | value = queue.pop(0) 74 | for node in self.graph[value]: 75 | if node not in dist_to.keys(): 76 | edge_to[node] = value 77 | dist_to[node] = dist_to[value] + 1 78 | queue.append(node) 79 | if end in edge_to.keys(): 80 | path = [] 81 | node = end 82 | while dist_to[node] != 0: 83 | path.insert(0, node) 84 | node = edge_to[node] 85 | path.insert(0, start) 86 | return path 87 | 88 | 89 | def main(): 90 | """ 91 | # example of graph usage 92 | >>> graph = { 93 | ... 'A': ['B', 'C'], 94 | ... 'B': ['C', 'D'], 95 | ... 'C': ['D', 'G'], 96 | ... 'D': ['C'], 97 | ... 'E': ['F'], 98 | ... 'F': ['C'], 99 | ... 'G': ['E'], 100 | ... 'H': ['C'] 101 | ... } 102 | 103 | # initialization of new graph search object 104 | >>> graph_search = GraphSearch(graph) 105 | 106 | >>> print(graph_search.find_path_dfs('A', 'D')) 107 | ['A', 'B', 'C', 'D'] 108 | 109 | # start the search somewhere in the middle 110 | >>> print(graph_search.find_path_dfs('G', 'F')) 111 | ['G', 'E', 'F'] 112 | 113 | # unreachable node 114 | >>> print(graph_search.find_path_dfs('C', 'H')) 115 | None 116 | 117 | # non existing node 118 | >>> print(graph_search.find_path_dfs('C', 'X')) 119 | None 120 | 121 | >>> print(graph_search.find_all_paths_dfs('A', 'D')) 122 | [['A', 'B', 'C', 'D'], ['A', 'B', 'D'], ['A', 'C', 'D']] 123 | >>> print(graph_search.find_shortest_path_dfs('A', 'D')) 124 | ['A', 'B', 'D'] 125 | >>> print(graph_search.find_shortest_path_dfs('A', 'F')) 126 | ['A', 'C', 'G', 'E', 'F'] 127 | 128 | >>> print(graph_search.find_shortest_path_bfs('A', 'D')) 129 | ['A', 'B', 'D'] 130 | >>> print(graph_search.find_shortest_path_bfs('A', 'F')) 131 | ['A', 'C', 'G', 'E', 'F'] 132 | 133 | # start the search somewhere in the middle 134 | >>> print(graph_search.find_shortest_path_bfs('G', 'F')) 135 | ['G', 'E', 'F'] 136 | 137 | # unreachable node 138 | >>> print(graph_search.find_shortest_path_bfs('A', 'H')) 139 | None 140 | 141 | # non existing node 142 | >>> print(graph_search.find_shortest_path_bfs('A', 'X')) 143 | None 144 | """ 145 | 146 | 147 | if __name__ == "__main__": 148 | import doctest 149 | 150 | doctest.testmod() 151 | -------------------------------------------------------------------------------- /patterns/other/hsm/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/other/hsm/__init__.py -------------------------------------------------------------------------------- /patterns/other/hsm/classes_hsm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/other/hsm/classes_hsm.png -------------------------------------------------------------------------------- /patterns/other/hsm/classes_test_hsm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/other/hsm/classes_test_hsm.png -------------------------------------------------------------------------------- /patterns/other/hsm/hsm.py: -------------------------------------------------------------------------------- 1 | """ 2 | Implementation of the HSM (hierarchical state machine) or 3 | NFSM (nested finite state machine) C++ example from 4 | http://www.eventhelix.com/RealtimeMantra/HierarchicalStateMachine.htm#.VwqLVEL950w 5 | in Python 6 | 7 | - single source 'message type' for state transition changes 8 | - message type considered, messages (comment) not considered to avoid complexity 9 | """ 10 | 11 | 12 | class UnsupportedMessageType(BaseException): 13 | pass 14 | 15 | 16 | class UnsupportedState(BaseException): 17 | pass 18 | 19 | 20 | class UnsupportedTransition(BaseException): 21 | pass 22 | 23 | 24 | class HierachicalStateMachine: 25 | def __init__(self): 26 | self._active_state = Active(self) # Unit.Inservice.Active() 27 | self._standby_state = Standby(self) # Unit.Inservice.Standby() 28 | self._suspect_state = Suspect(self) # Unit.OutOfService.Suspect() 29 | self._failed_state = Failed(self) # Unit.OutOfService.Failed() 30 | self._current_state = self._standby_state 31 | self.states = { 32 | "active": self._active_state, 33 | "standby": self._standby_state, 34 | "suspect": self._suspect_state, 35 | "failed": self._failed_state, 36 | } 37 | self.message_types = { 38 | "fault trigger": self._current_state.on_fault_trigger, 39 | "switchover": self._current_state.on_switchover, 40 | "diagnostics passed": self._current_state.on_diagnostics_passed, 41 | "diagnostics failed": self._current_state.on_diagnostics_failed, 42 | "operator inservice": self._current_state.on_operator_inservice, 43 | } 44 | 45 | def _next_state(self, state): 46 | try: 47 | self._current_state = self.states[state] 48 | except KeyError: 49 | raise UnsupportedState 50 | 51 | def _send_diagnostics_request(self): 52 | return "send diagnostic request" 53 | 54 | def _raise_alarm(self): 55 | return "raise alarm" 56 | 57 | def _clear_alarm(self): 58 | return "clear alarm" 59 | 60 | def _perform_switchover(self): 61 | return "perform switchover" 62 | 63 | def _send_switchover_response(self): 64 | return "send switchover response" 65 | 66 | def _send_operator_inservice_response(self): 67 | return "send operator inservice response" 68 | 69 | def _send_diagnostics_failure_report(self): 70 | return "send diagnostics failure report" 71 | 72 | def _send_diagnostics_pass_report(self): 73 | return "send diagnostics pass report" 74 | 75 | def _abort_diagnostics(self): 76 | return "abort diagnostics" 77 | 78 | def _check_mate_status(self): 79 | return "check mate status" 80 | 81 | def on_message(self, message_type): # message ignored 82 | if message_type in self.message_types.keys(): 83 | self.message_types[message_type]() 84 | else: 85 | raise UnsupportedMessageType 86 | 87 | 88 | class Unit: 89 | def __init__(self, HierachicalStateMachine): 90 | self.hsm = HierachicalStateMachine 91 | 92 | def on_switchover(self): 93 | raise UnsupportedTransition 94 | 95 | def on_fault_trigger(self): 96 | raise UnsupportedTransition 97 | 98 | def on_diagnostics_failed(self): 99 | raise UnsupportedTransition 100 | 101 | def on_diagnostics_passed(self): 102 | raise UnsupportedTransition 103 | 104 | def on_operator_inservice(self): 105 | raise UnsupportedTransition 106 | 107 | 108 | class Inservice(Unit): 109 | def __init__(self, HierachicalStateMachine): 110 | self._hsm = HierachicalStateMachine 111 | 112 | def on_fault_trigger(self): 113 | self._hsm._next_state("suspect") 114 | self._hsm._send_diagnostics_request() 115 | self._hsm._raise_alarm() 116 | 117 | def on_switchover(self): 118 | self._hsm._perform_switchover() 119 | self._hsm._check_mate_status() 120 | self._hsm._send_switchover_response() 121 | 122 | 123 | class Active(Inservice): 124 | def __init__(self, HierachicalStateMachine): 125 | self._hsm = HierachicalStateMachine 126 | 127 | def on_fault_trigger(self): 128 | super().perform_switchover() 129 | super().on_fault_trigger() 130 | 131 | def on_switchover(self): 132 | self._hsm.on_switchover() # message ignored 133 | self._hsm.next_state("standby") 134 | 135 | 136 | class Standby(Inservice): 137 | def __init__(self, HierachicalStateMachine): 138 | self._hsm = HierachicalStateMachine 139 | 140 | def on_switchover(self): 141 | super().on_switchover() # message ignored 142 | self._hsm._next_state("active") 143 | 144 | 145 | class OutOfService(Unit): 146 | def __init__(self, HierachicalStateMachine): 147 | self._hsm = HierachicalStateMachine 148 | 149 | def on_operator_inservice(self): 150 | self._hsm.on_switchover() # message ignored 151 | self._hsm.send_operator_inservice_response() 152 | self._hsm.next_state("suspect") 153 | 154 | 155 | class Suspect(OutOfService): 156 | def __init__(self, HierachicalStateMachine): 157 | self._hsm = HierachicalStateMachine 158 | 159 | def on_diagnostics_failed(self): 160 | super().send_diagnostics_failure_report() 161 | super().next_state("failed") 162 | 163 | def on_diagnostics_passed(self): 164 | super().send_diagnostics_pass_report() 165 | super().clear_alarm() # loss of redundancy alarm 166 | super().next_state("standby") 167 | 168 | def on_operator_inservice(self): 169 | super().abort_diagnostics() 170 | super().on_operator_inservice() # message ignored 171 | 172 | 173 | class Failed(OutOfService): 174 | """No need to override any method.""" 175 | 176 | def __init__(self, HierachicalStateMachine): 177 | self._hsm = HierachicalStateMachine 178 | -------------------------------------------------------------------------------- /patterns/structural/3-tier.py: -------------------------------------------------------------------------------- 1 | """ 2 | *TL;DR 3 | Separates presentation, application processing, and data management functions. 4 | """ 5 | 6 | from typing import Dict, KeysView, Optional, Union 7 | 8 | 9 | class Data: 10 | """Data Store Class""" 11 | 12 | products = { 13 | "milk": {"price": 1.50, "quantity": 10}, 14 | "eggs": {"price": 0.20, "quantity": 100}, 15 | "cheese": {"price": 2.00, "quantity": 10}, 16 | } 17 | 18 | def __get__(self, obj, klas): 19 | 20 | print("(Fetching from Data Store)") 21 | return {"products": self.products} 22 | 23 | 24 | class BusinessLogic: 25 | """Business logic holding data store instances""" 26 | 27 | data = Data() 28 | 29 | def product_list(self) -> KeysView[str]: 30 | return self.data["products"].keys() 31 | 32 | def product_information( 33 | self, product: str 34 | ) -> Optional[Dict[str, Union[int, float]]]: 35 | return self.data["products"].get(product, None) 36 | 37 | 38 | class Ui: 39 | """UI interaction class""" 40 | 41 | def __init__(self) -> None: 42 | self.business_logic = BusinessLogic() 43 | 44 | def get_product_list(self) -> None: 45 | print("PRODUCT LIST:") 46 | for product in self.business_logic.product_list(): 47 | print(product) 48 | print("") 49 | 50 | def get_product_information(self, product: str) -> None: 51 | product_info = self.business_logic.product_information(product) 52 | if product_info: 53 | print("PRODUCT INFORMATION:") 54 | print( 55 | f"Name: {product.title()}, " 56 | + f"Price: {product_info.get('price', 0):.2f}, " 57 | + f"Quantity: {product_info.get('quantity', 0):}" 58 | ) 59 | else: 60 | print(f"That product '{product}' does not exist in the records") 61 | 62 | 63 | def main(): 64 | """ 65 | >>> ui = Ui() 66 | >>> ui.get_product_list() 67 | PRODUCT LIST: 68 | (Fetching from Data Store) 69 | milk 70 | eggs 71 | cheese 72 | 73 | 74 | >>> ui.get_product_information("cheese") 75 | (Fetching from Data Store) 76 | PRODUCT INFORMATION: 77 | Name: Cheese, Price: 2.00, Quantity: 10 78 | 79 | >>> ui.get_product_information("eggs") 80 | (Fetching from Data Store) 81 | PRODUCT INFORMATION: 82 | Name: Eggs, Price: 0.20, Quantity: 100 83 | 84 | >>> ui.get_product_information("milk") 85 | (Fetching from Data Store) 86 | PRODUCT INFORMATION: 87 | Name: Milk, Price: 1.50, Quantity: 10 88 | 89 | >>> ui.get_product_information("arepas") 90 | (Fetching from Data Store) 91 | That product 'arepas' does not exist in the records 92 | """ 93 | 94 | 95 | if __name__ == "__main__": 96 | import doctest 97 | 98 | doctest.testmod() 99 | -------------------------------------------------------------------------------- /patterns/structural/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/structural/__init__.py -------------------------------------------------------------------------------- /patterns/structural/adapter.py: -------------------------------------------------------------------------------- 1 | """ 2 | *What is this pattern about? 3 | The Adapter pattern provides a different interface for a class. We can 4 | think about it as a cable adapter that allows you to charge a phone 5 | somewhere that has outlets in a different shape. Following this idea, 6 | the Adapter pattern is useful to integrate classes that couldn't be 7 | integrated due to their incompatible interfaces. 8 | 9 | *What does this example do? 10 | 11 | The example has classes that represent entities (Dog, Cat, Human, Car) 12 | that make different noises. The Adapter class provides a different 13 | interface to the original methods that make such noises. So the 14 | original interfaces (e.g., bark and meow) are available under a 15 | different name: make_noise. 16 | 17 | *Where is the pattern used practically? 18 | The Grok framework uses adapters to make objects work with a 19 | particular API without modifying the objects themselves: 20 | http://grok.zope.org/doc/current/grok_overview.html#adapters 21 | 22 | *References: 23 | http://ginstrom.com/scribbles/2008/11/06/generic-adapter-class-in-python/ 24 | https://sourcemaking.com/design_patterns/adapter 25 | http://python-3-patterns-idioms-test.readthedocs.io/en/latest/ChangeInterface.html#adapter 26 | 27 | *TL;DR 28 | Allows the interface of an existing class to be used as another interface. 29 | """ 30 | 31 | from typing import Callable, TypeVar 32 | 33 | T = TypeVar("T") 34 | 35 | 36 | class Dog: 37 | def __init__(self) -> None: 38 | self.name = "Dog" 39 | 40 | def bark(self) -> str: 41 | return "woof!" 42 | 43 | 44 | class Cat: 45 | def __init__(self) -> None: 46 | self.name = "Cat" 47 | 48 | def meow(self) -> str: 49 | return "meow!" 50 | 51 | 52 | class Human: 53 | def __init__(self) -> None: 54 | self.name = "Human" 55 | 56 | def speak(self) -> str: 57 | return "'hello'" 58 | 59 | 60 | class Car: 61 | def __init__(self) -> None: 62 | self.name = "Car" 63 | 64 | def make_noise(self, octane_level: int) -> str: 65 | return f"vroom{'!' * octane_level}" 66 | 67 | 68 | class Adapter: 69 | """Adapts an object by replacing methods. 70 | 71 | Usage 72 | ------ 73 | dog = Dog() 74 | dog = Adapter(dog, make_noise=dog.bark) 75 | """ 76 | 77 | def __init__(self, obj: T, **adapted_methods: Callable): 78 | """We set the adapted methods in the object's dict.""" 79 | self.obj = obj 80 | self.__dict__.update(adapted_methods) 81 | 82 | def __getattr__(self, attr): 83 | """All non-adapted calls are passed to the object.""" 84 | return getattr(self.obj, attr) 85 | 86 | def original_dict(self): 87 | """Print original object dict.""" 88 | return self.obj.__dict__ 89 | 90 | 91 | def main(): 92 | """ 93 | >>> objects = [] 94 | >>> dog = Dog() 95 | >>> print(dog.__dict__) 96 | {'name': 'Dog'} 97 | 98 | >>> objects.append(Adapter(dog, make_noise=dog.bark)) 99 | 100 | >>> objects[0].__dict__['obj'], objects[0].__dict__['make_noise'] 101 | (<...Dog object at 0x...>, >) 102 | 103 | >>> print(objects[0].original_dict()) 104 | {'name': 'Dog'} 105 | 106 | >>> cat = Cat() 107 | >>> objects.append(Adapter(cat, make_noise=cat.meow)) 108 | >>> human = Human() 109 | >>> objects.append(Adapter(human, make_noise=human.speak)) 110 | >>> car = Car() 111 | >>> objects.append(Adapter(car, make_noise=lambda: car.make_noise(3))) 112 | 113 | >>> for obj in objects: 114 | ... print("A {0} goes {1}".format(obj.name, obj.make_noise())) 115 | A Dog goes woof! 116 | A Cat goes meow! 117 | A Human goes 'hello' 118 | A Car goes vroom!!! 119 | """ 120 | 121 | 122 | if __name__ == "__main__": 123 | import doctest 124 | 125 | doctest.testmod(optionflags=doctest.ELLIPSIS) 126 | -------------------------------------------------------------------------------- /patterns/structural/bridge.py: -------------------------------------------------------------------------------- 1 | """ 2 | *References: 3 | http://en.wikibooks.org/wiki/Computer_Science_Design_Patterns/Bridge_Pattern#Python 4 | 5 | *TL;DR 6 | Decouples an abstraction from its implementation. 7 | """ 8 | 9 | 10 | # ConcreteImplementor 1/2 11 | class DrawingAPI1: 12 | def draw_circle(self, x, y, radius): 13 | print(f"API1.circle at {x}:{y} radius {radius}") 14 | 15 | 16 | # ConcreteImplementor 2/2 17 | class DrawingAPI2: 18 | def draw_circle(self, x, y, radius): 19 | print(f"API2.circle at {x}:{y} radius {radius}") 20 | 21 | 22 | # Refined Abstraction 23 | class CircleShape: 24 | def __init__(self, x, y, radius, drawing_api): 25 | self._x = x 26 | self._y = y 27 | self._radius = radius 28 | self._drawing_api = drawing_api 29 | 30 | # low-level i.e. Implementation specific 31 | def draw(self): 32 | self._drawing_api.draw_circle(self._x, self._y, self._radius) 33 | 34 | # high-level i.e. Abstraction specific 35 | def scale(self, pct): 36 | self._radius *= pct 37 | 38 | 39 | def main(): 40 | """ 41 | >>> shapes = (CircleShape(1, 2, 3, DrawingAPI1()), CircleShape(5, 7, 11, DrawingAPI2())) 42 | 43 | >>> for shape in shapes: 44 | ... shape.scale(2.5) 45 | ... shape.draw() 46 | API1.circle at 1:2 radius 7.5 47 | API2.circle at 5:7 radius 27.5 48 | """ 49 | 50 | 51 | if __name__ == "__main__": 52 | import doctest 53 | 54 | doctest.testmod() 55 | -------------------------------------------------------------------------------- /patterns/structural/composite.py: -------------------------------------------------------------------------------- 1 | """ 2 | *What is this pattern about? 3 | The composite pattern describes a group of objects that is treated the 4 | same way as a single instance of the same type of object. The intent of 5 | a composite is to "compose" objects into tree structures to represent 6 | part-whole hierarchies. Implementing the composite pattern lets clients 7 | treat individual objects and compositions uniformly. 8 | 9 | *What does this example do? 10 | The example implements a graphic class,which can be either an ellipse 11 | or a composition of several graphics. Every graphic can be printed. 12 | 13 | *Where is the pattern used practically? 14 | In graphics editors a shape can be basic or complex. An example of a 15 | simple shape is a line, where a complex shape is a rectangle which is 16 | made of four line objects. Since shapes have many operations in common 17 | such as rendering the shape to screen, and since shapes follow a 18 | part-whole hierarchy, composite pattern can be used to enable the 19 | program to deal with all shapes uniformly. 20 | 21 | *References: 22 | https://en.wikipedia.org/wiki/Composite_pattern 23 | https://infinitescript.com/2014/10/the-23-gang-of-three-design-patterns/ 24 | 25 | *TL;DR 26 | Describes a group of objects that is treated as a single instance. 27 | """ 28 | 29 | from abc import ABC, abstractmethod 30 | from typing import List 31 | 32 | 33 | class Graphic(ABC): 34 | @abstractmethod 35 | def render(self) -> None: 36 | raise NotImplementedError("You should implement this!") 37 | 38 | 39 | class CompositeGraphic(Graphic): 40 | def __init__(self) -> None: 41 | self.graphics: List[Graphic] = [] 42 | 43 | def render(self) -> None: 44 | for graphic in self.graphics: 45 | graphic.render() 46 | 47 | def add(self, graphic: Graphic) -> None: 48 | self.graphics.append(graphic) 49 | 50 | def remove(self, graphic: Graphic) -> None: 51 | self.graphics.remove(graphic) 52 | 53 | 54 | class Ellipse(Graphic): 55 | def __init__(self, name: str) -> None: 56 | self.name = name 57 | 58 | def render(self) -> None: 59 | print(f"Ellipse: {self.name}") 60 | 61 | 62 | def main(): 63 | """ 64 | >>> ellipse1 = Ellipse("1") 65 | >>> ellipse2 = Ellipse("2") 66 | >>> ellipse3 = Ellipse("3") 67 | >>> ellipse4 = Ellipse("4") 68 | 69 | >>> graphic1 = CompositeGraphic() 70 | >>> graphic2 = CompositeGraphic() 71 | 72 | >>> graphic1.add(ellipse1) 73 | >>> graphic1.add(ellipse2) 74 | >>> graphic1.add(ellipse3) 75 | >>> graphic2.add(ellipse4) 76 | 77 | >>> graphic = CompositeGraphic() 78 | 79 | >>> graphic.add(graphic1) 80 | >>> graphic.add(graphic2) 81 | 82 | >>> graphic.render() 83 | Ellipse: 1 84 | Ellipse: 2 85 | Ellipse: 3 86 | Ellipse: 4 87 | """ 88 | 89 | 90 | if __name__ == "__main__": 91 | import doctest 92 | 93 | doctest.testmod() 94 | -------------------------------------------------------------------------------- /patterns/structural/decorator.py: -------------------------------------------------------------------------------- 1 | """ 2 | *What is this pattern about? 3 | The Decorator pattern is used to dynamically add a new feature to an 4 | object without changing its implementation. It differs from 5 | inheritance because the new feature is added only to that particular 6 | object, not to the entire subclass. 7 | 8 | *What does this example do? 9 | This example shows a way to add formatting options (boldface and 10 | italic) to a text by appending the corresponding tags ( and 11 | ). Also, we can see that decorators can be applied one after the other, 12 | since the original text is passed to the bold wrapper, which in turn 13 | is passed to the italic wrapper. 14 | 15 | *Where is the pattern used practically? 16 | The Grok framework uses decorators to add functionalities to methods, 17 | like permissions or subscription to an event: 18 | http://grok.zope.org/doc/current/reference/decorators.html 19 | 20 | *References: 21 | https://sourcemaking.com/design_patterns/decorator 22 | 23 | *TL;DR 24 | Adds behaviour to object without affecting its class. 25 | """ 26 | 27 | 28 | class TextTag: 29 | """Represents a base text tag""" 30 | 31 | def __init__(self, text: str) -> None: 32 | self._text = text 33 | 34 | def render(self) -> str: 35 | return self._text 36 | 37 | 38 | class BoldWrapper(TextTag): 39 | """Wraps a tag in """ 40 | 41 | def __init__(self, wrapped: TextTag) -> None: 42 | self._wrapped = wrapped 43 | 44 | def render(self) -> str: 45 | return f"{self._wrapped.render()}" 46 | 47 | 48 | class ItalicWrapper(TextTag): 49 | """Wraps a tag in """ 50 | 51 | def __init__(self, wrapped: TextTag) -> None: 52 | self._wrapped = wrapped 53 | 54 | def render(self) -> str: 55 | return f"{self._wrapped.render()}" 56 | 57 | 58 | def main(): 59 | """ 60 | >>> simple_hello = TextTag("hello, world!") 61 | >>> special_hello = ItalicWrapper(BoldWrapper(simple_hello)) 62 | 63 | >>> print("before:", simple_hello.render()) 64 | before: hello, world! 65 | 66 | >>> print("after:", special_hello.render()) 67 | after: hello, world! 68 | """ 69 | 70 | 71 | if __name__ == "__main__": 72 | import doctest 73 | 74 | doctest.testmod() 75 | -------------------------------------------------------------------------------- /patterns/structural/facade.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example from https://en.wikipedia.org/wiki/Facade_pattern#Python 3 | 4 | 5 | *What is this pattern about? 6 | The Facade pattern is a way to provide a simpler unified interface to 7 | a more complex system. It provides an easier way to access functions 8 | of the underlying system by providing a single entry point. 9 | This kind of abstraction is seen in many real life situations. For 10 | example, we can turn on a computer by just pressing a button, but in 11 | fact there are many procedures and operations done when that happens 12 | (e.g., loading programs from disk to memory). In this case, the button 13 | serves as an unified interface to all the underlying procedures to 14 | turn on a computer. 15 | 16 | *Where is the pattern used practically? 17 | This pattern can be seen in the Python standard library when we use 18 | the isdir function. Although a user simply uses this function to know 19 | whether a path refers to a directory, the system makes a few 20 | operations and calls other modules (e.g., os.stat) to give the result. 21 | 22 | *References: 23 | https://sourcemaking.com/design_patterns/facade 24 | https://fkromer.github.io/python-pattern-references/design/#facade 25 | http://python-3-patterns-idioms-test.readthedocs.io/en/latest/ChangeInterface.html#facade 26 | 27 | *TL;DR 28 | Provides a simpler unified interface to a complex system. 29 | """ 30 | 31 | 32 | # Complex computer parts 33 | class CPU: 34 | """ 35 | Simple CPU representation. 36 | """ 37 | 38 | def freeze(self) -> None: 39 | print("Freezing processor.") 40 | 41 | def jump(self, position: str) -> None: 42 | print("Jumping to:", position) 43 | 44 | def execute(self) -> None: 45 | print("Executing.") 46 | 47 | 48 | class Memory: 49 | """ 50 | Simple memory representation. 51 | """ 52 | 53 | def load(self, position: str, data: str) -> None: 54 | print(f"Loading from {position} data: '{data}'.") 55 | 56 | 57 | class SolidStateDrive: 58 | """ 59 | Simple solid state drive representation. 60 | """ 61 | 62 | def read(self, lba: str, size: str) -> str: 63 | return f"Some data from sector {lba} with size {size}" 64 | 65 | 66 | class ComputerFacade: 67 | """ 68 | Represents a facade for various computer parts. 69 | """ 70 | 71 | def __init__(self): 72 | self.cpu = CPU() 73 | self.memory = Memory() 74 | self.ssd = SolidStateDrive() 75 | 76 | def start(self): 77 | self.cpu.freeze() 78 | self.memory.load("0x00", self.ssd.read("100", "1024")) 79 | self.cpu.jump("0x00") 80 | self.cpu.execute() 81 | 82 | 83 | def main(): 84 | """ 85 | >>> computer_facade = ComputerFacade() 86 | >>> computer_facade.start() 87 | Freezing processor. 88 | Loading from 0x00 data: 'Some data from sector 100 with size 1024'. 89 | Jumping to: 0x00 90 | Executing. 91 | """ 92 | 93 | 94 | if __name__ == "__main__": 95 | import doctest 96 | 97 | doctest.testmod(optionflags=doctest.ELLIPSIS) 98 | -------------------------------------------------------------------------------- /patterns/structural/flyweight.py: -------------------------------------------------------------------------------- 1 | """ 2 | *What is this pattern about? 3 | This pattern aims to minimise the number of objects that are needed by 4 | a program at run-time. A Flyweight is an object shared by multiple 5 | contexts, and is indistinguishable from an object that is not shared. 6 | 7 | The state of a Flyweight should not be affected by it's context, this 8 | is known as its intrinsic state. The decoupling of the objects state 9 | from the object's context, allows the Flyweight to be shared. 10 | 11 | *What does this example do? 12 | The example below sets-up an 'object pool' which stores initialised 13 | objects. When a 'Card' is created it first checks to see if it already 14 | exists instead of creating a new one. This aims to reduce the number of 15 | objects initialised by the program. 16 | 17 | *References: 18 | http://codesnipers.com/?q=python-flyweights 19 | https://python-patterns.guide/gang-of-four/flyweight/ 20 | 21 | *Examples in Python ecosystem: 22 | https://docs.python.org/3/library/sys.html#sys.intern 23 | 24 | *TL;DR 25 | Minimizes memory usage by sharing data with other similar objects. 26 | """ 27 | 28 | import weakref 29 | 30 | 31 | class Card: 32 | """The Flyweight""" 33 | 34 | # Could be a simple dict. 35 | # With WeakValueDictionary garbage collection can reclaim the object 36 | # when there are no other references to it. 37 | _pool: weakref.WeakValueDictionary = weakref.WeakValueDictionary() 38 | 39 | def __new__(cls, value, suit): 40 | # If the object exists in the pool - just return it 41 | obj = cls._pool.get(value + suit) 42 | # otherwise - create new one (and add it to the pool) 43 | if obj is None: 44 | obj = object.__new__(Card) 45 | cls._pool[value + suit] = obj 46 | # This row does the part we usually see in `__init__` 47 | obj.value, obj.suit = value, suit 48 | return obj 49 | 50 | # If you uncomment `__init__` and comment-out `__new__` - 51 | # Card becomes normal (non-flyweight). 52 | # def __init__(self, value, suit): 53 | # self.value, self.suit = value, suit 54 | 55 | def __repr__(self): 56 | return f"" 57 | 58 | 59 | def main(): 60 | """ 61 | >>> c1 = Card('9', 'h') 62 | >>> c2 = Card('9', 'h') 63 | >>> c1, c2 64 | (, ) 65 | >>> c1 == c2 66 | True 67 | >>> c1 is c2 68 | True 69 | 70 | >>> c1.new_attr = 'temp' 71 | >>> c3 = Card('9', 'h') 72 | >>> hasattr(c3, 'new_attr') 73 | True 74 | 75 | >>> Card._pool.clear() 76 | >>> c4 = Card('9', 'h') 77 | >>> hasattr(c4, 'new_attr') 78 | False 79 | """ 80 | 81 | 82 | if __name__ == "__main__": 83 | import doctest 84 | 85 | doctest.testmod() 86 | -------------------------------------------------------------------------------- /patterns/structural/flyweight_with_metaclass.py: -------------------------------------------------------------------------------- 1 | import weakref 2 | 3 | 4 | class FlyweightMeta(type): 5 | def __new__(mcs, name, parents, dct): 6 | """ 7 | Set up object pool 8 | 9 | :param name: class name 10 | :param parents: class parents 11 | :param dct: dict: includes class attributes, class methods, 12 | static methods, etc 13 | :return: new class 14 | """ 15 | dct["pool"] = weakref.WeakValueDictionary() 16 | return super().__new__(mcs, name, parents, dct) 17 | 18 | @staticmethod 19 | def _serialize_params(cls, *args, **kwargs): 20 | """ 21 | Serialize input parameters to a key. 22 | Simple implementation is just to serialize it as a string 23 | """ 24 | args_list = list(map(str, args)) 25 | args_list.extend([str(kwargs), cls.__name__]) 26 | key = "".join(args_list) 27 | return key 28 | 29 | def __call__(cls, *args, **kwargs): 30 | key = FlyweightMeta._serialize_params(cls, *args, **kwargs) 31 | pool = getattr(cls, "pool", {}) 32 | 33 | instance = pool.get(key) 34 | if instance is None: 35 | instance = super().__call__(*args, **kwargs) 36 | pool[key] = instance 37 | return instance 38 | 39 | 40 | class Card2(metaclass=FlyweightMeta): 41 | def __init__(self, *args, **kwargs): 42 | # print('Init {}: {}'.format(self.__class__, (args, kwargs))) 43 | pass 44 | 45 | 46 | if __name__ == "__main__": 47 | instances_pool = getattr(Card2, "pool") 48 | cm1 = Card2("10", "h", a=1) 49 | cm2 = Card2("10", "h", a=1) 50 | cm3 = Card2("10", "h", a=2) 51 | 52 | assert (cm1 == cm2) and (cm1 != cm3) 53 | assert (cm1 is cm2) and (cm1 is not cm3) 54 | assert len(instances_pool) == 2 55 | 56 | del cm1 57 | assert len(instances_pool) == 2 58 | 59 | del cm2 60 | assert len(instances_pool) == 1 61 | 62 | del cm3 63 | assert len(instances_pool) == 0 64 | -------------------------------------------------------------------------------- /patterns/structural/front_controller.py: -------------------------------------------------------------------------------- 1 | """ 2 | @author: Gordeev Andrey 3 | 4 | *TL;DR 5 | Provides a centralized entry point that controls and manages request handling. 6 | """ 7 | 8 | from __future__ import annotations 9 | 10 | from typing import Any 11 | 12 | 13 | class MobileView: 14 | def show_index_page(self) -> None: 15 | print("Displaying mobile index page") 16 | 17 | 18 | class TabletView: 19 | def show_index_page(self) -> None: 20 | print("Displaying tablet index page") 21 | 22 | 23 | class Dispatcher: 24 | def __init__(self) -> None: 25 | self.mobile_view = MobileView() 26 | self.tablet_view = TabletView() 27 | 28 | def dispatch(self, request: Request) -> None: 29 | """ 30 | This function is used to dispatch the request based on the type of device. 31 | If it is a mobile, then mobile view will be called and if it is a tablet, 32 | then tablet view will be called. 33 | Otherwise, an error message will be printed saying that cannot dispatch the request. 34 | """ 35 | if request.type == Request.mobile_type: 36 | self.mobile_view.show_index_page() 37 | elif request.type == Request.tablet_type: 38 | self.tablet_view.show_index_page() 39 | else: 40 | print("Cannot dispatch the request") 41 | 42 | 43 | class RequestController: 44 | """front controller""" 45 | 46 | def __init__(self) -> None: 47 | self.dispatcher = Dispatcher() 48 | 49 | def dispatch_request(self, request: Any) -> None: 50 | """ 51 | This function takes a request object and sends it to the dispatcher. 52 | """ 53 | if isinstance(request, Request): 54 | self.dispatcher.dispatch(request) 55 | else: 56 | print("request must be a Request object") 57 | 58 | 59 | class Request: 60 | """request""" 61 | 62 | mobile_type = "mobile" 63 | tablet_type = "tablet" 64 | 65 | def __init__(self, request): 66 | self.type = None 67 | request = request.lower() 68 | if request == self.mobile_type: 69 | self.type = self.mobile_type 70 | elif request == self.tablet_type: 71 | self.type = self.tablet_type 72 | 73 | 74 | def main(): 75 | """ 76 | >>> front_controller = RequestController() 77 | 78 | >>> front_controller.dispatch_request(Request('mobile')) 79 | Displaying mobile index page 80 | 81 | >>> front_controller.dispatch_request(Request('tablet')) 82 | Displaying tablet index page 83 | 84 | >>> front_controller.dispatch_request(Request('desktop')) 85 | Cannot dispatch the request 86 | 87 | >>> front_controller.dispatch_request('mobile') 88 | request must be a Request object 89 | """ 90 | 91 | 92 | if __name__ == "__main__": 93 | import doctest 94 | 95 | doctest.testmod() 96 | -------------------------------------------------------------------------------- /patterns/structural/mvc.py: -------------------------------------------------------------------------------- 1 | """ 2 | *TL;DR 3 | Separates data in GUIs from the ways it is presented, and accepted. 4 | """ 5 | 6 | from abc import ABC, abstractmethod 7 | from inspect import signature 8 | from sys import argv 9 | from typing import Any 10 | 11 | 12 | class Model(ABC): 13 | """The Model is the data layer of the application.""" 14 | @abstractmethod 15 | def __iter__(self) -> Any: 16 | pass 17 | 18 | @abstractmethod 19 | def get(self, item: str) -> dict: 20 | """Returns an object with a .items() call method 21 | that iterates over key,value pairs of its information.""" 22 | pass 23 | 24 | @property 25 | @abstractmethod 26 | def item_type(self) -> str: 27 | pass 28 | 29 | 30 | class ProductModel(Model): 31 | """The Model is the data layer of the application.""" 32 | class Price(float): 33 | """A polymorphic way to pass a float with a particular 34 | __str__ functionality.""" 35 | 36 | def __str__(self) -> str: 37 | return f"{self:.2f}" 38 | 39 | products = { 40 | "milk": {"price": Price(1.50), "quantity": 10}, 41 | "eggs": {"price": Price(0.20), "quantity": 100}, 42 | "cheese": {"price": Price(2.00), "quantity": 10}, 43 | } 44 | 45 | item_type = "product" 46 | 47 | def __iter__(self) -> Any: 48 | yield from self.products 49 | 50 | def get(self, product: str) -> dict: 51 | try: 52 | return self.products[product] 53 | except KeyError as e: 54 | raise KeyError(str(e) + " not in the model's item list.") 55 | 56 | 57 | class View(ABC): 58 | """The View is the presentation layer of the application.""" 59 | @abstractmethod 60 | def show_item_list(self, item_type: str, item_list: list) -> None: 61 | pass 62 | 63 | @abstractmethod 64 | def show_item_information(self, item_type: str, item_name: str, item_info: dict) -> None: 65 | """Will look for item information by iterating over key,value pairs 66 | yielded by item_info.items()""" 67 | pass 68 | 69 | @abstractmethod 70 | def item_not_found(self, item_type: str, item_name: str) -> None: 71 | pass 72 | 73 | 74 | class ConsoleView(View): 75 | """The View is the presentation layer of the application.""" 76 | def show_item_list(self, item_type: str, item_list: list) -> None: 77 | print(item_type.upper() + " LIST:") 78 | for item in item_list: 79 | print(item) 80 | print("") 81 | 82 | @staticmethod 83 | def capitalizer(string: str) -> str: 84 | """Capitalizes the first letter of a string and lowercases the rest.""" 85 | return string[0].upper() + string[1:].lower() 86 | 87 | def show_item_information(self, item_type: str, item_name: str, item_info: dict) -> None: 88 | """Will look for item information by iterating over key,value pairs""" 89 | print(item_type.upper() + " INFORMATION:") 90 | printout = "Name: %s" % item_name 91 | for key, value in item_info.items(): 92 | printout += ", " + self.capitalizer(str(key)) + ": " + str(value) 93 | printout += "\n" 94 | print(printout) 95 | 96 | def item_not_found(self, item_type: str, item_name: str) -> None: 97 | print(f'That {item_type} "{item_name}" does not exist in the records') 98 | 99 | 100 | class Controller: 101 | """The Controller is the intermediary between the Model and the View.""" 102 | def __init__(self, model_class: Model, view_class: View) -> None: 103 | self.model: Model = model_class 104 | self.view: View = view_class 105 | 106 | def show_items(self) -> None: 107 | items = list(self.model) 108 | item_type = self.model.item_type 109 | self.view.show_item_list(item_type, items) 110 | 111 | def show_item_information(self, item_name: str) -> None: 112 | """ 113 | Show information about a {item_type} item. 114 | :param str item_name: the name of the {item_type} item to show information about 115 | """ 116 | item_type: str = self.model.item_type 117 | try: 118 | item_info: dict = self.model.get(item_name) 119 | except Exception: 120 | self.view.item_not_found(item_type, item_name) 121 | else: 122 | self.view.show_item_information(item_type, item_name, item_info) 123 | 124 | 125 | class Router: 126 | """The Router is the entry point of the application.""" 127 | def __init__(self): 128 | self.routes = {} 129 | 130 | def register( 131 | self, 132 | path: str, 133 | controller_class: type[Controller], 134 | model_class: type[Model], 135 | view_class: type[View]) -> None: 136 | model_instance: Model = model_class() 137 | view_instance: View = view_class() 138 | self.routes[path] = controller_class(model_instance, view_instance) 139 | 140 | def resolve(self, path: str) -> Controller: 141 | if self.routes.get(path): 142 | controller: Controller = self.routes[path] 143 | return controller 144 | else: 145 | raise KeyError(f"No controller registered for path '{path}'") 146 | 147 | 148 | def main(): 149 | """ 150 | >>> model = ProductModel() 151 | >>> view = ConsoleView() 152 | >>> controller = Controller(model, view) 153 | 154 | >>> controller.show_items() 155 | PRODUCT LIST: 156 | milk 157 | eggs 158 | cheese 159 | 160 | 161 | >>> controller.show_item_information("cheese") 162 | PRODUCT INFORMATION: 163 | Name: cheese, Price: 2.00, Quantity: 10 164 | 165 | 166 | >>> controller.show_item_information("eggs") 167 | PRODUCT INFORMATION: 168 | Name: eggs, Price: 0.20, Quantity: 100 169 | 170 | 171 | >>> controller.show_item_information("milk") 172 | PRODUCT INFORMATION: 173 | Name: milk, Price: 1.50, Quantity: 10 174 | 175 | 176 | >>> controller.show_item_information("arepas") 177 | That product "arepas" does not exist in the records 178 | """ 179 | 180 | 181 | if __name__ == "__main__": 182 | router = Router() 183 | router.register("products", Controller, ProductModel, ConsoleView) 184 | controller: Controller = router.resolve(argv[1]) 185 | 186 | action: str = str(argv[2]) if len(argv) > 2 else "" 187 | args: str = ' '.join(map(str, argv[3:])) if len(argv) > 3 else "" 188 | 189 | if hasattr(controller, action): 190 | command = getattr(controller, action) 191 | sig = signature(command) 192 | 193 | if len(sig.parameters) > 0: 194 | if args: 195 | command(args) 196 | else: 197 | print("Command requires arguments.") 198 | else: 199 | command() 200 | else: 201 | print(f"Command {action} not found in the controller.") 202 | 203 | import doctest 204 | doctest.testmod() 205 | -------------------------------------------------------------------------------- /patterns/structural/proxy.py: -------------------------------------------------------------------------------- 1 | """ 2 | *What is this pattern about? 3 | Proxy is used in places where you want to add functionality to a class without 4 | changing its interface. The main class is called `Real Subject`. A client should 5 | use the proxy or the real subject without any code change, so both must have the 6 | same interface. Logging and controlling access to the real subject are some of 7 | the proxy pattern usages. 8 | 9 | *References: 10 | https://refactoring.guru/design-patterns/proxy/python/example 11 | https://python-3-patterns-idioms-test.readthedocs.io/en/latest/Fronting.html 12 | 13 | *TL;DR 14 | Add functionality or logic (e.g. logging, caching, authorization) to a resource 15 | without changing its interface. 16 | """ 17 | 18 | from typing import Union 19 | 20 | 21 | class Subject: 22 | """ 23 | As mentioned in the document, interfaces of both RealSubject and Proxy should 24 | be the same, because the client should be able to use RealSubject or Proxy with 25 | no code change. 26 | 27 | Not all times this interface is necessary. The point is the client should be 28 | able to use RealSubject or Proxy interchangeably with no change in code. 29 | """ 30 | 31 | def do_the_job(self, user: str) -> None: 32 | raise NotImplementedError() 33 | 34 | 35 | class RealSubject(Subject): 36 | """ 37 | This is the main job doer. External services like payment gateways can be a 38 | good example. 39 | """ 40 | 41 | def do_the_job(self, user: str) -> None: 42 | print(f"I am doing the job for {user}") 43 | 44 | 45 | class Proxy(Subject): 46 | def __init__(self) -> None: 47 | self._real_subject = RealSubject() 48 | 49 | def do_the_job(self, user: str) -> None: 50 | """ 51 | logging and controlling access are some examples of proxy usages. 52 | """ 53 | 54 | print(f"[log] Doing the job for {user} is requested.") 55 | 56 | if user == "admin": 57 | self._real_subject.do_the_job(user) 58 | else: 59 | print("[log] I can do the job just for `admins`.") 60 | 61 | 62 | def client(job_doer: Union[RealSubject, Proxy], user: str) -> None: 63 | job_doer.do_the_job(user) 64 | 65 | 66 | def main(): 67 | """ 68 | >>> proxy = Proxy() 69 | 70 | >>> real_subject = RealSubject() 71 | 72 | >>> client(proxy, 'admin') 73 | [log] Doing the job for admin is requested. 74 | I am doing the job for admin 75 | 76 | >>> client(proxy, 'anonymous') 77 | [log] Doing the job for anonymous is requested. 78 | [log] I can do the job just for `admins`. 79 | 80 | >>> client(real_subject, 'admin') 81 | I am doing the job for admin 82 | 83 | >>> client(real_subject, 'anonymous') 84 | I am doing the job for anonymous 85 | """ 86 | 87 | 88 | if __name__ == "__main__": 89 | import doctest 90 | 91 | doctest.testmod() 92 | -------------------------------------------------------------------------------- /patterns/structural/viz/3-tier.py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/structural/viz/3-tier.py.png -------------------------------------------------------------------------------- /patterns/structural/viz/adapter.py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/structural/viz/adapter.py.png -------------------------------------------------------------------------------- /patterns/structural/viz/bridge.py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/structural/viz/bridge.py.png -------------------------------------------------------------------------------- /patterns/structural/viz/composite.py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/structural/viz/composite.py.png -------------------------------------------------------------------------------- /patterns/structural/viz/decorator.py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/structural/viz/decorator.py.png -------------------------------------------------------------------------------- /patterns/structural/viz/facade.py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/structural/viz/facade.py.png -------------------------------------------------------------------------------- /patterns/structural/viz/flyweight.py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/structural/viz/flyweight.py.png -------------------------------------------------------------------------------- /patterns/structural/viz/front_controller.py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/structural/viz/front_controller.py.png -------------------------------------------------------------------------------- /patterns/structural/viz/mvc.py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/structural/viz/mvc.py.png -------------------------------------------------------------------------------- /patterns/structural/viz/proxy.py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/patterns/structural/viz/proxy.py.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=42", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "patterns" 7 | description = "A collection of design patterns and idioms in Python." 8 | version = "0.1.0" 9 | readme = "README.md" 10 | requires-python = ">=3.9" 11 | license = {text = "MIT"} 12 | classifiers = [ 13 | "Programming Language :: Python :: 3.9", 14 | "Programming Language :: Python :: 3.10", 15 | "Programming Language :: Python :: 3.11", 16 | "Programming Language :: Python :: 3.12", 17 | "Programming Language :: Python :: 3.13", 18 | ] 19 | 20 | [project.optional-dependencies] 21 | dev = ["pytest", "pytest-cov", "pytest-randomly", "flake8", "mypy", "coverage"] 22 | 23 | [tool.setuptools] 24 | packages = ["patterns"] 25 | 26 | [tool.pytest.ini_options] 27 | filterwarnings = [ 28 | "ignore::Warning:.*test class 'TestRunner'.*" 29 | ] 30 | # Adding settings from tox.ini for pytest 31 | testpaths = ["tests"] 32 | #testpaths = ["tests", "patterns"] 33 | python_files = ["test_*.py", "*_test.py"] 34 | # Enable doctest discovery in patterns directory 35 | addopts = "--doctest-modules --randomly-seed=1234 --cov=patterns --cov-report=term-missing" 36 | doctest_optionflags = ["ELLIPSIS", "NORMALIZE_WHITESPACE"] 37 | log_level = "INFO" 38 | 39 | [tool.coverage.run] 40 | branch = true 41 | source = ["./"] 42 | #source = ["patterns"] 43 | # Ensure coverage data is collected properly 44 | relative_files = true 45 | parallel = true 46 | dynamic_context = "test_function" 47 | data_file = ".coverage" 48 | 49 | [tool.coverage.report] 50 | # Regexes for lines to exclude from consideration 51 | exclude_lines = [ 52 | "def __repr__", 53 | "if self\\.debug", 54 | "raise AssertionError", 55 | "raise NotImplementedError", 56 | "if 0:", 57 | "if __name__ == .__main__.:", 58 | "@(abc\\.)?abstractmethod" 59 | ] 60 | ignore_errors = true 61 | 62 | [tool.coverage.html] 63 | directory = "coverage_html_report" 64 | 65 | [tool.mypy] 66 | python_version = "3.12" 67 | ignore_missing_imports = true 68 | 69 | [tool.flake8] 70 | max-line-length = 120 71 | ignore = ["E266", "E731", "W503"] 72 | exclude = ["venv*"] 73 | 74 | [tool.tox] 75 | legacy_tox_ini = """ 76 | [tox] 77 | envlist = py312,cov-report 78 | skip_missing_interpreters = true 79 | usedevelop = true 80 | 81 | #[testenv] 82 | #setenv = 83 | # COVERAGE_FILE = .coverage.{envname} 84 | #deps = 85 | # -r requirements-dev.txt 86 | #commands = 87 | # flake8 --exclude="venv/,.tox/" patterns/ 88 | # coverage run -m pytest --randomly-seed=1234 --doctest-modules patterns/ 89 | # coverage run -m pytest -s -vv --cov=patterns/ --log-level=INFO tests/ 90 | 91 | #[testenv:cov-report] 92 | #setenv = 93 | # COVERAGE_FILE = .coverage 94 | #deps = coverage 95 | #commands = 96 | # coverage combine 97 | # coverage report 98 | #""" -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | mypy 2 | pyupgrade 3 | pytest>=6.2.0 4 | pytest-cov>=2.11.0 5 | pytest-randomly>=3.1.0 6 | black>=25.1.0 7 | isort>=5.7.0 8 | flake8>=7.1.0 9 | tox>=4.25.0 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | setup( 4 | name="patterns", 5 | packages=find_packages(), 6 | description="A collection of design patterns and idioms in Python.", 7 | classifiers=[ 8 | "Programming Language :: Python :: 3.9", 9 | "Programming Language :: Python :: 3.10", 10 | "Programming Language :: Python :: 3.11", 11 | "Programming Language :: Python :: 3.12", 12 | "Programming Language :: Python :: 3.13", 13 | ], 14 | ) 15 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faif/python-patterns/879ac0107f7f0005767d0e67c1555f54515c10ae/tests/__init__.py -------------------------------------------------------------------------------- /tests/behavioral/test_observer.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock, patch 2 | 3 | import pytest 4 | 5 | from patterns.behavioral.observer import Data, DecimalViewer, HexViewer 6 | 7 | 8 | @pytest.fixture 9 | def observable(): 10 | return Data("some data") 11 | 12 | 13 | def test_attach_detach(observable): 14 | decimal_viewer = DecimalViewer() 15 | assert len(observable._observers) == 0 16 | 17 | observable.attach(decimal_viewer) 18 | assert decimal_viewer in observable._observers 19 | 20 | observable.detach(decimal_viewer) 21 | assert decimal_viewer not in observable._observers 22 | 23 | 24 | def test_one_data_change_notifies_each_observer_once(observable): 25 | observable.attach(DecimalViewer()) 26 | observable.attach(HexViewer()) 27 | 28 | with patch( 29 | "patterns.behavioral.observer.DecimalViewer.update", new_callable=Mock() 30 | ) as mocked_update: 31 | assert mocked_update.call_count == 0 32 | observable.data = 10 33 | assert mocked_update.call_count == 1 34 | -------------------------------------------------------------------------------- /tests/behavioral/test_publish_subscribe.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import call, patch 3 | 4 | from patterns.behavioral.publish_subscribe import Provider, Publisher, Subscriber 5 | 6 | 7 | class TestProvider(unittest.TestCase): 8 | """ 9 | Integration tests ~ provider class with as little mocking as possible. 10 | """ 11 | 12 | def test_subscriber_shall_be_attachable_to_subscriptions(cls): 13 | subscription = "sub msg" 14 | pro = Provider() 15 | cls.assertEqual(len(pro.subscribers), 0) 16 | sub = Subscriber("sub name", pro) 17 | sub.subscribe(subscription) 18 | cls.assertEqual(len(pro.subscribers[subscription]), 1) 19 | 20 | def test_subscriber_shall_be_detachable_from_subscriptions(cls): 21 | subscription = "sub msg" 22 | pro = Provider() 23 | sub = Subscriber("sub name", pro) 24 | sub.subscribe(subscription) 25 | cls.assertEqual(len(pro.subscribers[subscription]), 1) 26 | sub.unsubscribe(subscription) 27 | cls.assertEqual(len(pro.subscribers[subscription]), 0) 28 | 29 | def test_publisher_shall_append_subscription_message_to_queue(cls): 30 | """msg_queue ~ Provider.notify(msg) ~ Publisher.publish(msg)""" 31 | expected_msg = "expected msg" 32 | pro = Provider() 33 | pub = Publisher(pro) 34 | Subscriber("sub name", pro) 35 | cls.assertEqual(len(pro.msg_queue), 0) 36 | pub.publish(expected_msg) 37 | cls.assertEqual(len(pro.msg_queue), 1) 38 | cls.assertEqual(pro.msg_queue[0], expected_msg) 39 | 40 | def test_provider_shall_update_affected_subscribers_with_published_subscription( 41 | cls, 42 | ): 43 | pro = Provider() 44 | pub = Publisher(pro) 45 | sub1 = Subscriber("sub 1 name", pro) 46 | sub1.subscribe("sub 1 msg 1") 47 | sub1.subscribe("sub 1 msg 2") 48 | sub2 = Subscriber("sub 2 name", pro) 49 | sub2.subscribe("sub 2 msg 1") 50 | sub2.subscribe("sub 2 msg 2") 51 | with patch.object(sub1, "run") as mock_subscriber1_run, patch.object( 52 | sub2, "run" 53 | ) as mock_subscriber2_run: 54 | pro.update() 55 | cls.assertEqual(mock_subscriber1_run.call_count, 0) 56 | cls.assertEqual(mock_subscriber2_run.call_count, 0) 57 | pub.publish("sub 1 msg 1") 58 | pub.publish("sub 1 msg 2") 59 | pub.publish("sub 2 msg 1") 60 | pub.publish("sub 2 msg 2") 61 | with patch.object(sub1, "run") as mock_subscriber1_run, patch.object( 62 | sub2, "run" 63 | ) as mock_subscriber2_run: 64 | pro.update() 65 | expected_sub1_calls = [call("sub 1 msg 1"), call("sub 1 msg 2")] 66 | mock_subscriber1_run.assert_has_calls(expected_sub1_calls) 67 | expected_sub2_calls = [call("sub 2 msg 1"), call("sub 2 msg 2")] 68 | mock_subscriber2_run.assert_has_calls(expected_sub2_calls) 69 | -------------------------------------------------------------------------------- /tests/behavioral/test_state.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from patterns.behavioral.state import Radio 4 | 5 | 6 | @pytest.fixture 7 | def radio(): 8 | return Radio() 9 | 10 | 11 | def test_initial_state(radio): 12 | assert radio.state.name == "AM" 13 | 14 | 15 | def test_initial_am_station(radio): 16 | initial_pos = radio.state.pos 17 | assert radio.state.stations[initial_pos] == "1250" 18 | 19 | 20 | def test_toggle_amfm(radio): 21 | assert radio.state.name == "AM" 22 | 23 | radio.toggle_amfm() 24 | assert radio.state.name == "FM" 25 | 26 | radio.toggle_amfm() 27 | assert radio.state.name == "AM" 28 | -------------------------------------------------------------------------------- /tests/behavioral/test_strategy.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from patterns.behavioral.strategy import Order, on_sale_discount, ten_percent_discount 4 | 5 | 6 | @pytest.fixture 7 | def order(): 8 | return Order(100) 9 | 10 | 11 | @pytest.mark.parametrize( 12 | "func, discount", [(ten_percent_discount, 10.0), (on_sale_discount, 45.0)] 13 | ) 14 | def test_discount_function_return(func, order, discount): 15 | assert func(order) == discount 16 | 17 | 18 | @pytest.mark.parametrize( 19 | "func, price", [(ten_percent_discount, 100), (on_sale_discount, 100)] 20 | ) 21 | def test_order_discount_strategy_validate_success(func, price): 22 | order = Order(price, func) 23 | 24 | assert order.price == price 25 | assert order.discount_strategy == func 26 | 27 | 28 | def test_order_discount_strategy_validate_error(): 29 | order = Order(10, discount_strategy=on_sale_discount) 30 | 31 | assert order.discount_strategy is None 32 | 33 | 34 | @pytest.mark.parametrize( 35 | "func, price, discount", 36 | [(ten_percent_discount, 100, 90.0), (on_sale_discount, 100, 55.0)], 37 | ) 38 | def test_discount_apply_success(func, price, discount): 39 | order = Order(price, func) 40 | 41 | assert order.apply_discount() == discount 42 | -------------------------------------------------------------------------------- /tests/creational/test_abstract_factory.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch 3 | 4 | from patterns.creational.abstract_factory import Dog, PetShop 5 | 6 | 7 | class TestPetShop(unittest.TestCase): 8 | def test_dog_pet_shop_shall_show_dog_instance(self): 9 | dog_pet_shop = PetShop(Dog) 10 | with patch.object(Dog, "speak") as mock_Dog_speak: 11 | pet = dog_pet_shop.buy_pet("") 12 | pet.speak() 13 | self.assertEqual(mock_Dog_speak.call_count, 1) 14 | -------------------------------------------------------------------------------- /tests/creational/test_borg.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from patterns.creational.borg import Borg, YourBorg 4 | 5 | 6 | class BorgTest(unittest.TestCase): 7 | def setUp(self): 8 | self.b1 = Borg() 9 | self.b2 = Borg() 10 | # creating YourBorg instance implicitly sets the state attribute 11 | # for all borg instances. 12 | self.ib1 = YourBorg() 13 | 14 | def tearDown(self): 15 | self.ib1.state = "Init" 16 | 17 | def test_initial_borg_state_shall_be_init(self): 18 | b = Borg() 19 | self.assertEqual(b.state, "Init") 20 | 21 | def test_changing_instance_attribute_shall_change_borg_state(self): 22 | self.b1.state = "Running" 23 | self.assertEqual(self.b1.state, "Running") 24 | self.assertEqual(self.b2.state, "Running") 25 | self.assertEqual(self.ib1.state, "Running") 26 | 27 | def test_instances_shall_have_own_ids(self): 28 | self.assertNotEqual(id(self.b1), id(self.b2), id(self.ib1)) 29 | -------------------------------------------------------------------------------- /tests/creational/test_builder.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from patterns.creational.builder import ComplexHouse, Flat, House, construct_building 4 | 5 | 6 | class TestSimple(unittest.TestCase): 7 | def test_house(self): 8 | house = House() 9 | self.assertEqual(house.size, "Big") 10 | self.assertEqual(house.floor, "One") 11 | 12 | def test_flat(self): 13 | flat = Flat() 14 | self.assertEqual(flat.size, "Small") 15 | self.assertEqual(flat.floor, "More than One") 16 | 17 | 18 | class TestComplex(unittest.TestCase): 19 | def test_house(self): 20 | house = construct_building(ComplexHouse) 21 | self.assertEqual(house.size, "Big and fancy") 22 | self.assertEqual(house.floor, "One") 23 | -------------------------------------------------------------------------------- /tests/creational/test_lazy.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from patterns.creational.lazy_evaluation import Person 4 | 5 | 6 | class TestDynamicExpanding(unittest.TestCase): 7 | def setUp(self): 8 | self.John = Person("John", "Coder") 9 | 10 | def test_innate_properties(self): 11 | self.assertDictEqual( 12 | {"name": "John", "occupation": "Coder", "call_count2": 0}, 13 | self.John.__dict__, 14 | ) 15 | 16 | def test_relatives_not_in_properties(self): 17 | self.assertNotIn("relatives", self.John.__dict__) 18 | 19 | def test_extended_properties(self): 20 | print(f"John's relatives: {self.John.relatives}") 21 | self.assertDictEqual( 22 | { 23 | "name": "John", 24 | "occupation": "Coder", 25 | "relatives": "Many relatives.", 26 | "call_count2": 0, 27 | }, 28 | self.John.__dict__, 29 | ) 30 | 31 | def test_relatives_after_access(self): 32 | print(f"John's relatives: {self.John.relatives}") 33 | self.assertIn("relatives", self.John.__dict__) 34 | 35 | def test_parents(self): 36 | for _ in range(2): 37 | self.assertEqual(self.John.parents, "Father and mother") 38 | self.assertEqual(self.John.call_count2, 1) 39 | -------------------------------------------------------------------------------- /tests/creational/test_pool.py: -------------------------------------------------------------------------------- 1 | import queue 2 | import unittest 3 | 4 | from patterns.creational.pool import ObjectPool 5 | 6 | 7 | class TestPool(unittest.TestCase): 8 | def setUp(self): 9 | self.sample_queue = queue.Queue() 10 | self.sample_queue.put("first") 11 | self.sample_queue.put("second") 12 | 13 | def test_items_recoil(self): 14 | with ObjectPool(self.sample_queue, True) as pool: 15 | self.assertEqual(pool, "first") 16 | self.assertTrue(self.sample_queue.get() == "second") 17 | self.assertFalse(self.sample_queue.empty()) 18 | self.assertTrue(self.sample_queue.get() == "first") 19 | self.assertTrue(self.sample_queue.empty()) 20 | 21 | def test_frozen_pool(self): 22 | with ObjectPool(self.sample_queue) as pool: 23 | self.assertEqual(pool, "first") 24 | self.assertEqual(pool, "first") 25 | self.assertTrue(self.sample_queue.get() == "second") 26 | self.assertFalse(self.sample_queue.empty()) 27 | self.assertTrue(self.sample_queue.get() == "first") 28 | self.assertTrue(self.sample_queue.empty()) 29 | 30 | 31 | class TestNaitivePool(unittest.TestCase): 32 | """def test_object(queue): 33 | queue_object = QueueObject(queue, True) 34 | print('Inside func: {}'.format(queue_object.object))""" 35 | 36 | def test_pool_behavior_with_single_object_inside(self): 37 | sample_queue = queue.Queue() 38 | sample_queue.put("yam") 39 | with ObjectPool(sample_queue) as obj: 40 | # print('Inside with: {}'.format(obj)) 41 | self.assertEqual(obj, "yam") 42 | self.assertFalse(sample_queue.empty()) 43 | self.assertTrue(sample_queue.get() == "yam") 44 | self.assertTrue(sample_queue.empty()) 45 | 46 | # sample_queue.put('sam') 47 | # test_object(sample_queue) 48 | # print('Outside func: {}'.format(sample_queue.get())) 49 | 50 | # if not sample_queue.empty(): 51 | -------------------------------------------------------------------------------- /tests/creational/test_prototype.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from patterns.creational.prototype import Prototype, PrototypeDispatcher 4 | 5 | 6 | class TestPrototypeFeatures(unittest.TestCase): 7 | def setUp(self): 8 | self.prototype = Prototype() 9 | 10 | def test_cloning_propperty_innate_values(self): 11 | sample_object_1 = self.prototype.clone() 12 | sample_object_2 = self.prototype.clone() 13 | self.assertEqual(sample_object_1.value, sample_object_2.value) 14 | 15 | def test_extended_property_values_cloning(self): 16 | sample_object_1 = self.prototype.clone() 17 | sample_object_1.some_value = "test string" 18 | sample_object_2 = self.prototype.clone() 19 | self.assertRaises(AttributeError, lambda: sample_object_2.some_value) 20 | 21 | def test_cloning_propperty_assigned_values(self): 22 | sample_object_1 = self.prototype.clone() 23 | sample_object_2 = self.prototype.clone(value="re-assigned") 24 | self.assertNotEqual(sample_object_1.value, sample_object_2.value) 25 | 26 | 27 | class TestDispatcherFeatures(unittest.TestCase): 28 | def setUp(self): 29 | self.dispatcher = PrototypeDispatcher() 30 | self.prototype = Prototype() 31 | c = self.prototype.clone() 32 | a = self.prototype.clone(value="a-value", ext_value="E") 33 | b = self.prototype.clone(value="b-value", diff=True) 34 | self.dispatcher.register_object("A", a) 35 | self.dispatcher.register_object("B", b) 36 | self.dispatcher.register_object("C", c) 37 | 38 | def test_batch_retrieving(self): 39 | self.assertEqual(len(self.dispatcher.get_objects()), 3) 40 | 41 | def test_particular_properties_retrieving(self): 42 | self.assertEqual(self.dispatcher.get_objects()["A"].value, "a-value") 43 | self.assertEqual(self.dispatcher.get_objects()["B"].value, "b-value") 44 | self.assertEqual(self.dispatcher.get_objects()["C"].value, "default") 45 | 46 | def test_extended_properties_retrieving(self): 47 | self.assertEqual(self.dispatcher.get_objects()["A"].ext_value, "E") 48 | self.assertTrue(self.dispatcher.get_objects()["B"].diff) 49 | -------------------------------------------------------------------------------- /tests/structural/test_adapter.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from patterns.structural.adapter import Adapter, Car, Cat, Dog, Human 4 | 5 | 6 | class ClassTest(unittest.TestCase): 7 | def setUp(self): 8 | self.dog = Dog() 9 | self.cat = Cat() 10 | self.human = Human() 11 | self.car = Car() 12 | 13 | def test_dog_shall_bark(self): 14 | noise = self.dog.bark() 15 | expected_noise = "woof!" 16 | self.assertEqual(noise, expected_noise) 17 | 18 | def test_cat_shall_meow(self): 19 | noise = self.cat.meow() 20 | expected_noise = "meow!" 21 | self.assertEqual(noise, expected_noise) 22 | 23 | def test_human_shall_speak(self): 24 | noise = self.human.speak() 25 | expected_noise = "'hello'" 26 | self.assertEqual(noise, expected_noise) 27 | 28 | def test_car_shall_make_loud_noise(self): 29 | noise = self.car.make_noise(1) 30 | expected_noise = "vroom!" 31 | self.assertEqual(noise, expected_noise) 32 | 33 | def test_car_shall_make_very_loud_noise(self): 34 | noise = self.car.make_noise(10) 35 | expected_noise = "vroom!!!!!!!!!!" 36 | self.assertEqual(noise, expected_noise) 37 | 38 | 39 | class AdapterTest(unittest.TestCase): 40 | def test_dog_adapter_shall_make_noise(self): 41 | dog = Dog() 42 | dog_adapter = Adapter(dog, make_noise=dog.bark) 43 | noise = dog_adapter.make_noise() 44 | expected_noise = "woof!" 45 | self.assertEqual(noise, expected_noise) 46 | 47 | def test_cat_adapter_shall_make_noise(self): 48 | cat = Cat() 49 | cat_adapter = Adapter(cat, make_noise=cat.meow) 50 | noise = cat_adapter.make_noise() 51 | expected_noise = "meow!" 52 | self.assertEqual(noise, expected_noise) 53 | 54 | def test_human_adapter_shall_make_noise(self): 55 | human = Human() 56 | human_adapter = Adapter(human, make_noise=human.speak) 57 | noise = human_adapter.make_noise() 58 | expected_noise = "'hello'" 59 | self.assertEqual(noise, expected_noise) 60 | 61 | def test_car_adapter_shall_make_loud_noise(self): 62 | car = Car() 63 | car_adapter = Adapter(car, make_noise=car.make_noise) 64 | noise = car_adapter.make_noise(1) 65 | expected_noise = "vroom!" 66 | self.assertEqual(noise, expected_noise) 67 | 68 | def test_car_adapter_shall_make_very_loud_noise(self): 69 | car = Car() 70 | car_adapter = Adapter(car, make_noise=car.make_noise) 71 | noise = car_adapter.make_noise(10) 72 | expected_noise = "vroom!!!!!!!!!!" 73 | 74 | self.assertEqual(noise, expected_noise) 75 | -------------------------------------------------------------------------------- /tests/structural/test_bridge.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch 3 | 4 | from patterns.structural.bridge import CircleShape, DrawingAPI1, DrawingAPI2 5 | 6 | 7 | class BridgeTest(unittest.TestCase): 8 | def test_bridge_shall_draw_with_concrete_api_implementation(cls): 9 | ci1 = DrawingAPI1() 10 | ci2 = DrawingAPI2() 11 | with patch.object(ci1, "draw_circle") as mock_ci1_draw_circle, patch.object( 12 | ci2, "draw_circle" 13 | ) as mock_ci2_draw_circle: 14 | sh1 = CircleShape(1, 2, 3, ci1) 15 | sh1.draw() 16 | cls.assertEqual(mock_ci1_draw_circle.call_count, 1) 17 | sh2 = CircleShape(1, 2, 3, ci2) 18 | sh2.draw() 19 | cls.assertEqual(mock_ci2_draw_circle.call_count, 1) 20 | 21 | def test_bridge_shall_scale_both_api_circles_with_own_implementation(cls): 22 | SCALE_FACTOR = 2 23 | CIRCLE1_RADIUS = 3 24 | EXPECTED_CIRCLE1_RADIUS = 6 25 | CIRCLE2_RADIUS = CIRCLE1_RADIUS * CIRCLE1_RADIUS 26 | EXPECTED_CIRCLE2_RADIUS = CIRCLE2_RADIUS * SCALE_FACTOR 27 | 28 | ci1 = DrawingAPI1() 29 | ci2 = DrawingAPI2() 30 | sh1 = CircleShape(1, 2, CIRCLE1_RADIUS, ci1) 31 | sh2 = CircleShape(1, 2, CIRCLE2_RADIUS, ci2) 32 | sh1.scale(SCALE_FACTOR) 33 | sh2.scale(SCALE_FACTOR) 34 | cls.assertEqual(sh1._radius, EXPECTED_CIRCLE1_RADIUS) 35 | cls.assertEqual(sh2._radius, EXPECTED_CIRCLE2_RADIUS) 36 | with patch.object(sh1, "scale") as mock_sh1_scale_circle, patch.object( 37 | sh2, "scale" 38 | ) as mock_sh2_scale_circle: 39 | sh1.scale(2) 40 | sh2.scale(2) 41 | cls.assertEqual(mock_sh1_scale_circle.call_count, 1) 42 | cls.assertEqual(mock_sh2_scale_circle.call_count, 1) 43 | -------------------------------------------------------------------------------- /tests/structural/test_decorator.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from patterns.structural.decorator import BoldWrapper, ItalicWrapper, TextTag 4 | 5 | 6 | class TestTextWrapping(unittest.TestCase): 7 | def setUp(self): 8 | self.raw_string = TextTag("raw but not cruel") 9 | 10 | def test_italic(self): 11 | self.assertEqual( 12 | ItalicWrapper(self.raw_string).render(), "raw but not cruel" 13 | ) 14 | 15 | def test_bold(self): 16 | self.assertEqual( 17 | BoldWrapper(self.raw_string).render(), "raw but not cruel" 18 | ) 19 | 20 | def test_mixed_bold_and_italic(self): 21 | self.assertEqual( 22 | BoldWrapper(ItalicWrapper(self.raw_string)).render(), 23 | "raw but not cruel", 24 | ) 25 | -------------------------------------------------------------------------------- /tests/structural/test_proxy.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import unittest 3 | from io import StringIO 4 | 5 | from patterns.structural.proxy import Proxy, client 6 | 7 | 8 | class ProxyTest(unittest.TestCase): 9 | @classmethod 10 | def setUpClass(cls): 11 | """Class scope setup.""" 12 | cls.proxy = Proxy() 13 | 14 | def setUp(cls): 15 | """Function/test case scope setup.""" 16 | cls.output = StringIO() 17 | cls.saved_stdout = sys.stdout 18 | sys.stdout = cls.output 19 | 20 | def tearDown(cls): 21 | """Function/test case scope teardown.""" 22 | cls.output.close() 23 | sys.stdout = cls.saved_stdout 24 | 25 | def test_do_the_job_for_admin_shall_pass(self): 26 | client(self.proxy, "admin") 27 | assert self.output.getvalue() == ( 28 | "[log] Doing the job for admin is requested.\n" 29 | "I am doing the job for admin\n" 30 | ) 31 | 32 | def test_do_the_job_for_anonymous_shall_reject(self): 33 | client(self.proxy, "anonymous") 34 | assert self.output.getvalue() == ( 35 | "[log] Doing the job for anonymous is requested.\n" 36 | "[log] I can do the job just for `admins`.\n" 37 | ) 38 | -------------------------------------------------------------------------------- /tests/test_hsm.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch 3 | 4 | from patterns.other.hsm.hsm import ( 5 | Active, 6 | HierachicalStateMachine, 7 | Standby, 8 | Suspect, 9 | UnsupportedMessageType, 10 | UnsupportedState, 11 | UnsupportedTransition, 12 | ) 13 | 14 | 15 | class HsmMethodTest(unittest.TestCase): 16 | @classmethod 17 | def setUpClass(cls): 18 | cls.hsm = HierachicalStateMachine() 19 | 20 | def test_initial_state_shall_be_standby(cls): 21 | cls.assertEqual(isinstance(cls.hsm._current_state, Standby), True) 22 | 23 | def test_unsupported_state_shall_raise_exception(cls): 24 | with cls.assertRaises(UnsupportedState): 25 | cls.hsm._next_state("missing") 26 | 27 | def test_unsupported_message_type_shall_raise_exception(cls): 28 | with cls.assertRaises(UnsupportedMessageType): 29 | cls.hsm.on_message("trigger") 30 | 31 | def test_calling_next_state_shall_change_current_state(cls): 32 | cls.hsm._current_state = Standby # initial state 33 | cls.hsm._next_state("active") 34 | cls.assertEqual(isinstance(cls.hsm._current_state, Active), True) 35 | cls.hsm._current_state = Standby(cls.hsm) # initial state 36 | 37 | def test_method_perform_switchover_shall_return_specifically(cls): 38 | """Exemplary HierachicalStateMachine method test. 39 | (here: _perform_switchover()). Add additional test cases...""" 40 | return_value = cls.hsm._perform_switchover() 41 | expected_return_value = "perform switchover" 42 | cls.assertEqual(return_value, expected_return_value) 43 | 44 | 45 | class StandbyStateTest(unittest.TestCase): 46 | """Exemplary 2nd level state test class (here: Standby state). Add missing 47 | state test classes...""" 48 | 49 | @classmethod 50 | def setUpClass(cls): 51 | cls.hsm = HierachicalStateMachine() 52 | 53 | def setUp(cls): 54 | cls.hsm._current_state = Standby(cls.hsm) 55 | 56 | def test_given_standby_on_message_switchover_shall_set_active(cls): 57 | cls.hsm.on_message("switchover") 58 | cls.assertEqual(isinstance(cls.hsm._current_state, Active), True) 59 | 60 | def test_given_standby_on_message_switchover_shall_call_hsm_methods(cls): 61 | with patch.object( 62 | cls.hsm, "_perform_switchover" 63 | ) as mock_perform_switchover, patch.object( 64 | cls.hsm, "_check_mate_status" 65 | ) as mock_check_mate_status, patch.object( 66 | cls.hsm, "_send_switchover_response" 67 | ) as mock_send_switchover_response, patch.object( 68 | cls.hsm, "_next_state" 69 | ) as mock_next_state: 70 | cls.hsm.on_message("switchover") 71 | cls.assertEqual(mock_perform_switchover.call_count, 1) 72 | cls.assertEqual(mock_check_mate_status.call_count, 1) 73 | cls.assertEqual(mock_send_switchover_response.call_count, 1) 74 | cls.assertEqual(mock_next_state.call_count, 1) 75 | 76 | def test_given_standby_on_message_fault_trigger_shall_set_suspect(cls): 77 | cls.hsm.on_message("fault trigger") 78 | cls.assertEqual(isinstance(cls.hsm._current_state, Suspect), True) 79 | 80 | def test_given_standby_on_message_diagnostics_failed_shall_raise_exception_and_keep_in_state( 81 | cls, 82 | ): 83 | with cls.assertRaises(UnsupportedTransition): 84 | cls.hsm.on_message("diagnostics failed") 85 | cls.assertEqual(isinstance(cls.hsm._current_state, Standby), True) 86 | 87 | def test_given_standby_on_message_diagnostics_passed_shall_raise_exception_and_keep_in_state( 88 | cls, 89 | ): 90 | with cls.assertRaises(UnsupportedTransition): 91 | cls.hsm.on_message("diagnostics passed") 92 | cls.assertEqual(isinstance(cls.hsm._current_state, Standby), True) 93 | 94 | def test_given_standby_on_message_operator_inservice_shall_raise_exception_and_keep_in_state( 95 | cls, 96 | ): 97 | with cls.assertRaises(UnsupportedTransition): 98 | cls.hsm.on_message("operator inservice") 99 | cls.assertEqual(isinstance(cls.hsm._current_state, Standby), True) 100 | --------------------------------------------------------------------------------