diff --git a/.codespellignore b/.codespellignore new file mode 100644 index 00000000..d272a2e1 --- /dev/null +++ b/.codespellignore @@ -0,0 +1,14 @@ +__pycache__ +*.pyc +.idea +*.egg-info/ +.tox/ +env/ +venv/ +.env +.venv +.vscode/ +.python-version +.coverage +build/ +dist/ \ No newline at end of file diff --git a/.github/workflows/lint_pr.yml b/.github/workflows/lint_pr.yml new file mode 100644 index 00000000..f18e5c2e --- /dev/null +++ b/.github/workflows/lint_pr.yml @@ -0,0 +1,286 @@ +name: lint_pull_request +on: [pull_request, push] +jobs: + check_changes: + runs-on: ubuntu-24.04 + outputs: + has_python_changes: ${{ steps.changed-files.outputs.has_python_changes }} + files: ${{ steps.changed-files.outputs.files }} + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # To get all history for git diff commands + + - name: Get changed Python files + id: changed-files + run: | + if [ "${{ github.event_name }}" == "pull_request" ]; then + # For PRs, compare against base branch + CHANGED_FILES=$(git diff --name-only --diff-filter=ACMRT origin/${{ github.base_ref }} HEAD | grep '\.py$' | grep -v "^setup\.py$" || echo "") + # Check if setup.py specifically changed + SETUP_PY_CHANGED=$(git diff --name-only --diff-filter=ACMRT origin/${{ github.base_ref }} HEAD | grep "^setup\.py$" || echo "") + if [ ! -z "$SETUP_PY_CHANGED" ]; then + CHANGED_FILES="$CHANGED_FILES $SETUP_PY_CHANGED" + fi + else + # For pushes, use the before/after SHAs + CHANGED_FILES=$(git diff --name-only --diff-filter=ACMRT ${{ github.event.before }} ${{ github.event.after }} | grep '\.py$' | grep -v "^setup\.py$" || echo "") + # Check if setup.py specifically changed + SETUP_PY_CHANGED=$(git diff --name-only --diff-filter=ACMRT ${{ github.event.before }} ${{ github.event.after }} | grep "^setup\.py$" || echo "") + if [ ! -z "$SETUP_PY_CHANGED" ]; then + CHANGED_FILES="$CHANGED_FILES $SETUP_PY_CHANGED" + fi + fi + + # Check if any Python files were changed and set the output accordingly + if [ -z "$CHANGED_FILES" ]; then + echo "No Python files changed" + echo "has_python_changes=false" >> $GITHUB_OUTPUT + echo "files=" >> $GITHUB_OUTPUT + else + echo "Changed Python files: $CHANGED_FILES" + echo "has_python_changes=true" >> $GITHUB_OUTPUT + echo "files=$CHANGED_FILES" >> $GITHUB_OUTPUT + fi + + - name: PR information + if: ${{ github.event_name == 'pull_request' }} + run: | + if [[ "${{ steps.changed-files.outputs.has_python_changes }}" == "true" ]]; then + echo "This PR contains Python changes that will be linted." + else + echo "This PR contains no Python changes, but still requires manual approval." + fi + + lint: + needs: check_changes + if: ${{ needs.check_changes.outputs.has_python_changes == 'true' }} + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + tool: [flake8, format, mypy, pytest, pyupgrade, tox] + steps: + # Additional check to ensure we have Python files before proceeding + - name: Verify Python changes + run: | + if [[ "${{ needs.check_changes.outputs.has_python_changes }}" != "true" ]]; then + echo "No Python files were changed. Skipping linting." + exit 0 + fi + + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - uses: actions/setup-python@v4 + with: + python-version: 3.12 + + - uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('requirements-dev.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-dev.txt + + # Flake8 linting + - name: Lint with flake8 + if: ${{ matrix.tool == 'flake8' }} + id: flake8 + run: | + echo "Linting files: ${{ needs.check_changes.outputs.files }}" + flake8 ${{ needs.check_changes.outputs.files }} --count --show-source --statistics + + # Format checking with isort and black + - name: Format check + if: ${{ matrix.tool == 'format' }} + id: format + run: | + echo "Checking format with isort for: ${{ needs.check_changes.outputs.files }}" + isort --profile black --check ${{ needs.check_changes.outputs.files }} + echo "Checking format with black for: ${{ needs.check_changes.outputs.files }}" + black --check ${{ needs.check_changes.outputs.files }} + + # Type checking with mypy + - name: Type check with mypy + if: ${{ matrix.tool == 'mypy' }} + id: mypy + run: | + echo "Type checking: ${{ needs.check_changes.outputs.files }}" + mypy --ignore-missing-imports ${{ needs.check_changes.outputs.files }} + + # Run tests with pytest + - name: Run tests with pytest + if: ${{ matrix.tool == 'pytest' }} + id: pytest + run: | + echo "Running pytest discovery..." + python -m pytest --collect-only -v + + # First run any test files that correspond to changed files + echo "Running tests for changed files..." + changed_files="${{ needs.check_changes.outputs.files }}" + + # Extract module paths from changed files + modules=() + for file in $changed_files; do + # Convert file path to module path (remove .py and replace / with .) + if [[ $file == patterns/* ]]; then + module_path=${file%.py} + module_path=${module_path//\//.} + modules+=("$module_path") + fi + done + + # Run tests for each module + for module in "${modules[@]}"; do + echo "Testing module: $module" + python -m pytest -xvs tests/ -k "$module" || true + done + + # Then run doctests on the changed files + echo "Running doctests for changed files..." + for file in $changed_files; do + if [[ $file == *.py ]]; then + echo "Running doctest for $file" + python -m pytest --doctest-modules -v $file || true + fi + done + + # Check Python version compatibility + - name: Check Python version compatibility + if: ${{ matrix.tool == 'pyupgrade' }} + id: pyupgrade + run: pyupgrade --py312-plus ${{ needs.check_changes.outputs.files }} + + # Run tox + - name: Run tox + if: ${{ matrix.tool == 'tox' }} + id: tox + run: | + echo "Running tox integration for changed files..." + changed_files="${{ needs.check_changes.outputs.files }}" + + # Create a temporary tox configuration that extends the original one + echo "[tox]" > tox_pr.ini + echo "envlist = py312" >> tox_pr.ini + echo "skip_missing_interpreters = true" >> tox_pr.ini + + echo "[testenv]" >> tox_pr.ini + echo "setenv =" >> tox_pr.ini + echo " COVERAGE_FILE = .coverage.{envname}" >> tox_pr.ini + echo "deps =" >> tox_pr.ini + echo " -r requirements-dev.txt" >> tox_pr.ini + echo "allowlist_externals =" >> tox_pr.ini + echo " pytest" >> tox_pr.ini + echo " coverage" >> tox_pr.ini + echo " python" >> tox_pr.ini + echo "commands =" >> tox_pr.ini + + # Check if we have any implementation files that changed + pattern_files=0 + test_files=0 + + for file in $changed_files; do + if [[ $file == patterns/* ]]; then + pattern_files=1 + elif [[ $file == tests/* ]]; then + test_files=1 + fi + done + + # Only run targeted tests, no baseline + echo " # Run specific tests for changed files" >> tox_pr.ini + + has_tests=false + + # Add coverage-focused test commands + for file in $changed_files; do + if [[ $file == *.py ]]; then + # Run coverage tests for implementation files + if [[ $file == patterns/* ]]; then + module_name=$(basename $file .py) + + # Get the pattern type (behavioral, structural, etc.) + if [[ $file == patterns/behavioral/* ]]; then + pattern_dir="behavioral" + elif [[ $file == patterns/creational/* ]]; then + pattern_dir="creational" + elif [[ $file == patterns/structural/* ]]; then + pattern_dir="structural" + elif [[ $file == patterns/fundamental/* ]]; then + pattern_dir="fundamental" + elif [[ $file == patterns/other/* ]]; then + pattern_dir="other" + else + pattern_dir="" + fi + + echo " # Testing $file" >> tox_pr.ini + + # Check if specific test exists + if [ -n "$pattern_dir" ]; then + test_path="tests/${pattern_dir}/test_${module_name}.py" + 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 + + # Also try to find any test that might include this module + echo " coverage run -m pytest -xvs --cov=patterns --cov-append tests/${pattern_dir}/ -k \"${module_name}\" --no-header" >> tox_pr.ini + fi + + # Run doctests for the file + echo " coverage run -m pytest --doctest-modules -v --cov=patterns --cov-append $file" >> tox_pr.ini + + has_tests=true + fi + + # Run test files directly if modified + if [[ $file == tests/* ]]; then + echo " coverage run -m pytest -xvs --cov=patterns --cov-append $file" >> tox_pr.ini + has_tests=true + fi + fi + done + + # If we didn't find any specific tests to run, mention it + if [ "$has_tests" = false ]; then + echo " python -c \"print('No specific tests found for changed files. Consider adding tests.')\"" >> tox_pr.ini + # Add a minimal test to avoid failure, but ensure it generates coverage data + echo " coverage run -m pytest -xvs --cov=patterns --cov-append -k \"not integration\" --no-header" >> tox_pr.ini + fi + + # Add coverage report command + echo " coverage combine" >> tox_pr.ini + echo " coverage report -m" >> tox_pr.ini + + # Run tox with the custom configuration + echo "Running tox with custom PR configuration..." + echo "======================== TOX CONFIG ========================" + cat tox_pr.ini + echo "===========================================================" + tox -c tox_pr.ini + + summary: + needs: [check_changes, lint] + # Run summary in all cases, regardless of whether lint job ran + if: ${{ always() }} + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v3 + + - name: Summarize results + run: | + echo "## Pull Request Lint Results" >> $GITHUB_STEP_SUMMARY + if [[ "${{ needs.check_changes.outputs.has_python_changes }}" == "true" ]]; then + echo "Linting has completed for all Python files changed in this PR." >> $GITHUB_STEP_SUMMARY + echo "See individual job logs for detailed results." >> $GITHUB_STEP_SUMMARY + else + echo "No Python files were changed in this PR. Linting was skipped." >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + echo "⚠️ **Note:** This PR still requires manual approval regardless of linting results." >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/lint_python.yml b/.github/workflows/lint_python.yml new file mode 100644 index 00000000..19d6c078 --- /dev/null +++ b/.github/workflows/lint_python.yml @@ -0,0 +1,36 @@ +name: lint_python +on: [pull_request, push] +jobs: + lint_python: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: 3.12 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-dev.txt + - name: Lint with flake8 + run: flake8 ./patterns --count --show-source --statistics + continue-on-error: true + - name: Format check with isort and black + run: | + isort --profile black --check ./patterns + black --check ./patterns + continue-on-error: true + - name: Type check with mypy + run: mypy --ignore-missing-imports ./patterns || true + continue-on-error: true + - name: Run tests with pytest + run: | + pytest ./patterns + pytest --doctest-modules ./patterns || true + continue-on-error: true + - name: Check Python version compatibility + run: shopt -s globstar && pyupgrade --py312-plus ./patterns/**/*.py + continue-on-error: true + - name: Run tox + run: tox + continue-on-error: true diff --git a/.gitignore b/.gitignore index bee8a64b..d272a2e1 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,14 @@ __pycache__ +*.pyc +.idea +*.egg-info/ +.tox/ +env/ +venv/ +.env +.venv +.vscode/ +.python-version +.coverage +build/ +dist/ \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..dfeece70 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,20 @@ +os: linux +dist: noble +language: python + +jobs: + include: + - python: "3.12" + env: TOXENV=py312 + +cache: + - pip + +install: + - pip install codecov tox + +script: + - tox + +after_success: + - codecov diff --git a/3-tier.py b/3-tier.py deleted file mode 100644 index a1cd30c1..00000000 --- a/3-tier.py +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - - -class Data(object): - """ Data Store Class """ - - products = { - 'milk': {'price': 1.50, 'quantity': 10}, - 'eggs': {'price': 0.20, 'quantity': 100}, - 'cheese': {'price': 2.00, 'quantity': 10} - } - - def __get__(self, obj, klas): - print ("(Fetching from Data Store)") - return {'products': self.products} - - -class BusinessLogic(object): - - """ Business logic holding data store instances """ - - data = Data() - - def product_list(self): - return self.data['products'].keys() - - def product_information(self, product): - return self.data['products'].get(product, None) - - -class Ui(object): - """ UI interaction class """ - - def __init__(self): - self.business_logic = BusinessLogic() - - def get_product_list(self): - print('PRODUCT LIST:') - for product in self.business_logic.product_list(): - print(product) - print('') - - def get_product_information(self, product): - product_info = self.business_logic.product_information(product) - if product_info: - print('PRODUCT INFORMATION:') - print('Name: {0}, Price: {1:.2f}, Quantity: {2:}'.format( - product.title(), product_info.get('price', 0), - product_info.get('quantity', 0))) - else: - print('That product "{0}" does not exist in the records'.format( - product)) - - -def main(): - ui = Ui() - ui.get_product_list() - ui.get_product_information('cheese') - ui.get_product_information('eggs') - ui.get_product_information('milk') - ui.get_product_information('arepas') - -if __name__ == '__main__': - main() - -### OUTPUT ### -# PRODUCT LIST: -# (Fetching from Data Store) -# cheese -# eggs -# milk -# -# (Fetching from Data Store) -# PRODUCT INFORMATION: -# Name: Cheese, Price: 2.00, Quantity: 10 -# (Fetching from Data Store) -# PRODUCT INFORMATION: -# Name: Eggs, Price: 0.20, Quantity: 100 -# (Fetching from Data Store) -# PRODUCT INFORMATION: -# Name: Milk, Price: 1.50, Quantity: 10 -# (Fetching from Data Store) -# That product "arepas" does not exist in the records diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..92ba244a --- /dev/null +++ b/Makefile @@ -0,0 +1,87 @@ +# REDNAFI +# This only works with embedded venv not virtualenv +# Install venv: python3.8 -m venv venv +# Activate venv: source venv/bin/activate + +# Usage (line =black line length, path = action path, ignore= exclude folders) +# ------ +# make pylinter [make pylinter line=88 path=.] +# make pyupgrade + +path := . +line := 88 +ignore := *env + +all: + @echo + +.PHONY: checkvenv +checkvenv: +# raises error if environment is not active +ifeq ("$(VIRTUAL_ENV)","") + @echo "Venv is not activated!" + @echo "Activate venv first." + @echo + exit 1 +endif + +.PHONY: pyupgrade +pyupgrade: checkvenv +# checks if pip-tools is installed +ifeq ("$(wildcard venv/bin/pip-compile)","") + @echo "Installing Pip-tools..." + @pip install pip-tools +endif + +ifeq ("$(wildcard venv/bin/pip-sync)","") + @echo "Installing Pip-tools..." + @pip install pip-tools +endif + +# pip-tools + # @pip-compile --upgrade requirements-dev.txt + @pip-sync requirements-dev.txt + + +.PHONY: pylinter +pylinter: checkvenv +# checks if black is installed +ifeq ("$(wildcard venv/bin/black)","") + @echo "Installing Black..." + @pip install black +endif + +# checks if isort is installed +ifeq ("$(wildcard venv/bin/isort)","") + @echo "Installing Isort..." + @pip install isort +endif + +# checks if flake8 is installed +ifeq ("$(wildcard venv/bin/flake8)","") + @echo -e "Installing flake8..." + @pip install flake8 + @echo +endif + +# black + @echo "Applying Black" + @echo "----------------\n" + @black --line-length $(line) --exclude $(ignore) $(path) + @echo + +# isort + @echo "Applying Isort" + @echo "----------------\n" + @isort --atomic --profile black $(path) + @echo + +# flake8 + @echo "Applying Flake8" + @echo "----------------\n" + @flake8 --max-line-length "$(line)" \ + --max-complexity "18" \ + --select "B,C,E,F,W,T4,B9" \ + --ignore "E203,E266,E501,W503,F403,F401,E402" \ + --exclude ".git,__pycache__,old, build, \ + dist, venv, .tox" $(path) diff --git a/README.md b/README.md index 98550102..05222bc9 100644 --- a/README.md +++ b/README.md @@ -3,40 +3,119 @@ python-patterns A collection of design patterns and idioms in Python. -When an implementation is added or modified, be sure to update this file and -rerun `append_output.sh` (eg. ./append_output.sh borg.py) to keep the output -comments at the bottom up to date. +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. -Current Patterns: +Current Patterns +---------------- + +__Creational Patterns__: + +| Pattern | Description | +|:-------:| ----------- | +| [abstract_factory](patterns/creational/abstract_factory.py) | use a generic function with specific factories | +| [borg](patterns/creational/borg.py) | a singleton with shared-state among instances | +| [builder](patterns/creational/builder.py) | instead of using multiple constructors, builder object receives parameters and returns constructed objects | +| [factory](patterns/creational/factory.py) | delegate a specialized function/method to create instances | +| [lazy_evaluation](patterns/creational/lazy_evaluation.py) | lazily-evaluated property pattern in Python | +| [pool](patterns/creational/pool.py) | preinstantiate and maintain a group of instances of the same type | +| [prototype](patterns/creational/prototype.py) | use a factory and clones of a prototype for new instances (if instantiation is expensive) | + +__Structural Patterns__: + +| Pattern | Description | +|:-------:| ----------- | +| [3-tier](patterns/structural/3-tier.py) | data<->business logic<->presentation separation (strict relationships) | +| [adapter](patterns/structural/adapter.py) | adapt one interface to another using a white-list | +| [bridge](patterns/structural/bridge.py) | a client-provider middleman to soften interface changes | +| [composite](patterns/structural/composite.py) | lets clients treat individual objects and compositions uniformly | +| [decorator](patterns/structural/decorator.py) | wrap functionality with other functionality in order to affect outputs | +| [facade](patterns/structural/facade.py) | use one class as an API to a number of others | +| [flyweight](patterns/structural/flyweight.py) | transparently reuse existing instances of objects with similar/identical state | +| [front_controller](patterns/structural/front_controller.py) | single handler requests coming to the application | +| [mvc](patterns/structural/mvc.py) | model<->view<->controller (non-strict relationships) | +| [proxy](patterns/structural/proxy.py) | an object funnels operations to something else | + +__Behavioral Patterns__: + +| Pattern | Description | +|:-------:| ----------- | +| [chain_of_responsibility](patterns/behavioral/chain_of_responsibility.py) | apply a chain of successive handlers to try and process the data | +| [catalog](patterns/behavioral/catalog.py) | general methods will call different specialized methods based on construction parameter | +| [chaining_method](patterns/behavioral/chaining_method.py) | continue callback next object method | +| [command](patterns/behavioral/command.py) | bundle a command and arguments to call later | +| [iterator](patterns/behavioral/iterator.py) | traverse a container and access the container's elements | +| [iterator](patterns/behavioral/iterator_alt.py) (alt. impl.)| traverse a container and access the container's elements | +| [mediator](patterns/behavioral/mediator.py) | an object that knows how to connect other objects and act as a proxy | +| [memento](patterns/behavioral/memento.py) | generate an opaque token that can be used to go back to a previous state | +| [observer](patterns/behavioral/observer.py) | provide a callback for notification of events/changes to data | +| [publish_subscribe](patterns/behavioral/publish_subscribe.py) | a source syndicates events/data to 0+ registered listeners | +| [registry](patterns/behavioral/registry.py) | keep track of all subclasses of a given class | +| [specification](patterns/behavioral/specification.py) | business rules can be recombined by chaining the business rules together using boolean logic | +| [state](patterns/behavioral/state.py) | logic is organized into a discrete number of potential states and the next state that can be transitioned to | +| [strategy](patterns/behavioral/strategy.py) | selectable operations over the same data | +| [template](patterns/behavioral/template.py) | an object imposes a structure but takes pluggable components | +| [visitor](patterns/behavioral/visitor.py) | invoke a callback for all items of a collection | + +__Design for Testability Patterns__: + +| Pattern | Description | +|:-------:| ----------- | +| [dependency_injection](patterns/dependency_injection.py) | 3 variants of dependency injection | + +__Fundamental Patterns__: + +| Pattern | Description | +|:-------:| ----------- | +| [delegation_pattern](patterns/fundamental/delegation_pattern.py) | an object handles a request by delegating to a second object (the delegate) | + +__Others__: | Pattern | Description | |:-------:| ----------- | -| [3-tier](3-tier.py) | data<->business logic<->presentation separation (strict relationships) | -| [abstract_factory](abstract_factory.py) | use a generic function with specific factories | -| [adapter](adapter.py) | adapt one interface to another using a whitelist | -| [borg](borg.py) | a singleton with shared-state among instances | -| [bridge](bridge.py) | a client-provider middleman to soften interface changes | -| [builder](builder.py) | call many little discrete methods rather than having a huge number of constructor parameters | -| [catalog](catalog.py) | general methods will call different specialized methods based on construction parameter | -| [chain](chain.py) | apply a chain of successive handlers to try and process the data | -| [chaining_method](chaining_method.py) | continue callback next object method | -| [command](command.py) | bundle a command and arguments to call later | -| [composite](composite.py) | encapsulate and provide access to a number of different objects | -| [decorator](decorator.py) | wrap functionality with other functionality in order to affect outputs | -| [facade](facade.py) | use one class as an API to a number of others | -| [factory_method](factory_method.py) | delegate a specialized function/method to create instances | -| [flyweight](flyweight.py) | transparently reuse existing instances of objects with similar/identical state | -| [graph_search](graph_search.py) | (graphing algorithms, not design patterns) | -| [lazy_evaluation](lazy_evaluation.py) | lazily-evaluated property pattern in Python | -| [mediator](mediator.py) | an object that knows how to connect other objects and act as a proxy | -| [memento](memento.py) | generate an opaque token that can be used to go back to a previous state | -| [mvc](mvc.py) | model<->view<->controller (non-strict relationships) | -| [observer](observer.py) | provide a callback for notification of events/changes to data | -| [pool](pool.py) | preinstantiate and maintain a group of instances of the same type | -| [prototype](prototype.py) | use a factory and clones of a prototype for new instances (if instantiation is expensive) | -| [proxy](proxy.py) | an object funnels operations to something else | -| [publish_subscribe](publish_subscribe.py) | a source syndicates events/data to 0+ registered listeners | -| [state](state.py) | logic is org'd into a discrete number of potential states and the next state that can be transitioned to | -| [strategy](strategy.py) | selectable operations over the same data | -| [template](template.py) | an object imposes a structure but takes pluggable components | -| [visitor](visitor.py) | invoke a callback for all items of a collection | +| [blackboard](patterns/other/blackboard.py) | architectural model, assemble different sub-system knowledge to build a solution, AI approach - non gang of four pattern | +| [graph_search](patterns/other/graph_search.py) | graphing algorithms - non gang of four pattern | +| [hsm](patterns/other/hsm/hsm.py) | hierarchical state machine - non gang of four pattern | + + +Videos +------ +[Design Patterns in Python by Peter Ullrich](https://www.youtube.com/watch?v=bsyjSW46TDg) + +[Sebastian Buczyński - Why you don't need design patterns in Python?](https://www.youtube.com/watch?v=G5OeYHCJuv0) + +[You Don't Need That!](https://www.youtube.com/watch?v=imW-trt0i9I) + +[Pluggable Libs Through Design Patterns](https://www.youtube.com/watch?v=PfgEU3W0kyU) + + +Contributing +------------ +When an implementation is added or modified, please review the following guidelines: + +##### Docstrings +Add module level description in form of a docstring with links to corresponding references or other useful information. + +Add "Examples in Python ecosystem" section if you know some. It shows how patterns could be applied to real-world problems. + +[facade.py](patterns/structural/facade.py) has a good example of detailed description, +but sometimes the shorter one as in [template.py](patterns/behavioral/template.py) would suffice. + +##### Python 2 compatibility +To see Python 2 compatible versions of some patterns please check-out the [legacy](https://github.com/faif/python-patterns/tree/legacy) tag. + +##### Update README +When everything else is done - update corresponding part of README. + +##### Travis CI +Please run the following before submitting a patch +- `black .` This lints your code. + +Then either: +- `tox` or `tox -e ci37` This runs unit tests. see tox.ini for further details. +- 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. + +You can also run `flake8` or `pytest` commands manually. Examples can be found in `tox.ini`. + +## Contributing via issue triage [![Open Source Helpers](https://www.codetriage.com/faif/python-patterns/badges/users.svg)](https://www.codetriage.com/faif/python-patterns) + +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). diff --git a/abstract_factory.py b/abstract_factory.py deleted file mode 100644 index ea7d0294..00000000 --- a/abstract_factory.py +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# http://ginstrom.com/scribbles/2007/10/08/design-patterns-python-style/ - -"""Implementation of the abstract factory pattern""" - -import random - - -class PetShop: - - """A pet shop""" - - def __init__(self, animal_factory=None): - """pet_factory is our abstract factory. We can set it at will.""" - - self.pet_factory = animal_factory - - def show_pet(self): - """Creates and shows a pet using the abstract factory""" - - pet = self.pet_factory.get_pet() - print("We have a lovely {}".format(pet)) - print("It says {}".format(pet.speak())) - print("We also have {}".format(self.pet_factory.get_food())) - - -# Stuff that our factory makes - -class Dog: - - def speak(self): - return "woof" - - def __str__(self): - return "Dog" - - -class Cat: - - def speak(self): - return "meow" - - def __str__(self): - return "Cat" - - -# Factory classes - -class DogFactory: - - def get_pet(self): - return Dog() - - def get_food(self): - return "dog food" - - -class CatFactory: - - def get_pet(self): - return Cat() - - def get_food(self): - return "cat food" - - -# Create the proper family -def get_factory(): - """Let's be dynamic!""" - return random.choice([DogFactory, CatFactory])() - - -# Show pets with various factories -if __name__ == "__main__": - for i in range(3): - shop = PetShop(get_factory()) - shop.show_pet() - print("=" * 20) - -### OUTPUT ### -# We have a lovely Dog -# It says woof -# We also have dog food -# ==================== -# We have a lovely Dog -# It says woof -# We also have dog food -# ==================== -# We have a lovely Cat -# It says meow -# We also have cat food -# ==================== diff --git a/adapter.py b/adapter.py deleted file mode 100644 index 65f03cbb..00000000 --- a/adapter.py +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -"""http://ginstrom.com/scribbles/2008/11/06/generic-adapter-class-in-python/""" - - -class Dog(object): - def __init__(self): - self.name = "Dog" - - def bark(self): - return "woof!" - - -class Cat(object): - def __init__(self): - self.name = "Cat" - - def meow(self): - return "meow!" - - -class Human(object): - def __init__(self): - self.name = "Human" - - def speak(self): - return "'hello'" - - -class Car(object): - def __init__(self): - self.name = "Car" - - def make_noise(self, octane_level): - return "vroom{0}".format("!" * octane_level) - - -class Adapter(object): - - """ - Adapts an object by replacing methods. - Usage: - dog = Dog - dog = Adapter(dog, dict(make_noise=dog.bark)) - - >>> objects = [] - >>> dog = Dog() - >>> objects.append(Adapter(dog, make_noise=dog.bark)) - >>> cat = Cat() - >>> objects.append(Adapter(cat, make_noise=cat.meow)) - >>> human = Human() - >>> objects.append(Adapter(human, make_noise=human.speak)) - >>> car = Car() - >>> car_noise = lambda: car.make_noise(3) - >>> objects.append(Adapter(car, make_noise=car_noise)) - - >>> for obj in objects: - ... print('A {} goes {}'.format(obj.name, obj.make_noise())) - A Dog goes woof! - A Cat goes meow! - A Human goes 'hello' - A Car goes vroom!!! - """ - - def __init__(self, obj, **adapted_methods): - """We set the adapted methods in the object's dict""" - self.obj = obj - self.__dict__.update(adapted_methods) - - def __getattr__(self, attr): - """All non-adapted calls are passed to the object""" - return getattr(self.obj, attr) - - -def main(): - objects = [] - dog = Dog() - objects.append(Adapter(dog, make_noise=dog.bark)) - cat = Cat() - objects.append(Adapter(cat, make_noise=cat.meow)) - human = Human() - objects.append(Adapter(human, make_noise=human.speak)) - car = Car() - objects.append(Adapter(car, make_noise=lambda: car.make_noise(3))) - - for obj in objects: - print("A {0} goes {1}".format(obj.name, obj.make_noise())) - - -if __name__ == "__main__": - main() - -### OUTPUT ### -# A Dog goes woof! -# A Cat goes meow! -# A Human goes 'hello' -# A Car goes vroom!!! diff --git a/append_output.sh b/append_output.sh deleted file mode 100755 index a3b7948b..00000000 --- a/append_output.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -set -e - -src=$(sed -n -e '/### OUTPUT ###/,$!p' "$1") -output=$(python "$1" | sed 's/^/# /') - -# These are done separately to avoid having to insert a newline, which causes -# problems when the text itself has '\n' in strings -echo "$src" > $1 -echo -e "\n### OUTPUT ###" >> $1 -echo "$output" >> $1 diff --git a/borg.py b/borg.py deleted file mode 100644 index 0c464022..00000000 --- a/borg.py +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - - -class Borg: - __shared_state = {} - - def __init__(self): - self.__dict__ = self.__shared_state - self.state = 'Init' - - def __str__(self): - return self.state - - -class YourBorg(Borg): - pass - -if __name__ == '__main__': - rm1 = Borg() - rm2 = Borg() - - rm1.state = 'Idle' - rm2.state = 'Running' - - print('rm1: {0}'.format(rm1)) - print('rm2: {0}'.format(rm2)) - - rm2.state = 'Zombie' - - print('rm1: {0}'.format(rm1)) - print('rm2: {0}'.format(rm2)) - - print('rm1 id: {0}'.format(id(rm1))) - print('rm2 id: {0}'.format(id(rm2))) - - rm3 = YourBorg() - - print('rm1: {0}'.format(rm1)) - print('rm2: {0}'.format(rm2)) - print('rm3: {0}'.format(rm3)) - -### OUTPUT ### -# rm1: Running -# rm2: Running -# rm1: Zombie -# rm2: Zombie -# rm1 id: 140732837899224 -# rm2 id: 140732837899296 -# rm1: Init -# rm2: Init -# rm3: Init diff --git a/bridge.py b/bridge.py deleted file mode 100644 index 1c3c0747..00000000 --- a/bridge.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -"""http://en.wikibooks.org/wiki/Computer_Science_Design_Patterns/Bridge_Pattern#Python""" - - -# ConcreteImplementor 1/2 -class DrawingAPI1(object): - - def draw_circle(self, x, y, radius): - print('API1.circle at {}:{} radius {}'.format(x, y, radius)) - - -# ConcreteImplementor 2/2 -class DrawingAPI2(object): - - def draw_circle(self, x, y, radius): - print('API2.circle at {}:{} radius {}'.format(x, y, radius)) - - -# Refined Abstraction -class CircleShape(object): - - def __init__(self, x, y, radius, drawing_api): - self._x = x - self._y = y - self._radius = radius - self._drawing_api = drawing_api - - # low-level i.e. Implementation specific - def draw(self): - self._drawing_api.draw_circle(self._x, self._y, self._radius) - - # high-level i.e. Abstraction specific - def scale(self, pct): - self._radius *= pct - - -def main(): - shapes = ( - CircleShape(1, 2, 3, DrawingAPI1()), - CircleShape(5, 7, 11, DrawingAPI2()) - ) - - for shape in shapes: - shape.scale(2.5) - shape.draw() - - -if __name__ == '__main__': - main() - -### OUTPUT ### -# API1.circle at 1:2 radius 7.5 -# API2.circle at 5:7 radius 27.5 diff --git a/builder.py b/builder.py deleted file mode 100644 index 3b04412d..00000000 --- a/builder.py +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/python -# -*- coding : utf-8 -*- - -""" -@author: Diogenes Augusto Fernandes Herminio -https://gist.github.com/420905#file_builder_python.py -""" - - -# Director -class Director(object): - - def __init__(self): - self.builder = None - - def construct_building(self): - self.builder.new_building() - self.builder.build_floor() - self.builder.build_size() - - def get_building(self): - return self.builder.building - - -# Abstract Builder -class Builder(object): - - def __init__(self): - self.building = None - - def new_building(self): - self.building = Building() - - -# Concrete Builder -class BuilderHouse(Builder): - - def build_floor(self): - self.building.floor = 'One' - - def build_size(self): - self.building.size = 'Big' - - -class BuilderFlat(Builder): - - def build_floor(self): - self.building.floor = 'More than One' - - def build_size(self): - self.building.size = 'Small' - - -# Product -class Building(object): - - def __init__(self): - self.floor = None - self.size = None - - def __repr__(self): - return 'Floor: {0.floor} | Size: {0.size}'.format(self) - - -# Client -if __name__ == "__main__": - director = Director() - director.builder = BuilderHouse() - director.construct_building() - building = director.get_building() - print(building) - director.builder = BuilderFlat() - director.construct_building() - building = director.get_building() - print(building) - -### OUTPUT ### -# Floor: One | Size: Big -# Floor: More than One | Size: Small diff --git a/catalog.py b/catalog.py deleted file mode 100644 index 8bfdf921..00000000 --- a/catalog.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -A class that uses different static function depending of a parameter passed in -init. Note the use of a single dictionary instead of multiple conditions -""" -__author__ = "Ibrahim Diop " -__gist__ = "" - - -class Catalog(): - - """ - catalog of multiple static methods that are executed depending on an init - parameter - """ - - def __init__(self, param): - - # dictionary that will be used to determine which static method is - # to be executed but that will be also used to store possible param - # value - self._static_method_choices = {'param_value_1': self._static_method_1, - 'param_value_2': self._static_method_2} - - # simple test to validate param value - if param in self._static_method_choices.keys(): - self.param = param - else: - raise ValueError("Invalid Value for Param: {0}".format(param)) - - @staticmethod - def _static_method_1(): - print("executed method 1!") - - @staticmethod - def _static_method_2(): - print("executed method 2!") - - def main_method(self): - """ - will execute either _static_method_1 or _static_method_2 - depending on self.param value - """ - self._static_method_choices[self.param]() - - -def main(): - """ - >>> c = Catalog('param_value_1').main_method() - executed method 1! - >>> Catalog('param_value_2').main_method() - executed method 2! - """ - - test = Catalog('param_value_2') - test.main_method() - -if __name__ == "__main__": - main() - -### OUTPUT ### -# executed method 2! diff --git a/chain.py b/chain.py deleted file mode 100644 index 6698f8f9..00000000 --- a/chain.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -"""http://www.testingperspective.com/wiki/doku.php/collaboration/chetan/designpatternsinpython/chain-of-responsibilitypattern""" - -class Handler: - def __init__(self,successor): - self._successor = successor; - def handle(self,request): - i = self._handle(request) - if not i: - self._successor.handle(request) - def _handle(self, request): - raise NotImplementedError('Must provide implementation in subclass.') - - -class ConcreteHandler1(Handler): - - def _handle(self, request): - if 0 < request <= 10: - print('request {} handled in handler 1'.format(request)) - return True - -class ConcreteHandler2(Handler): - - def _handle(self, request): - if 10 < request <= 20: - print('request {} handled in handler 2'.format(request)) - return True - -class ConcreteHandler3(Handler): - - def _handle(self, request): - if 20 < request <= 30: - print('request {} handled in handler 3'.format(request)) - return True -class DefaultHandler(Handler): - - def _handle(self, request): - print('end of chain, no handler for {}'.format(request)) - return True - - -class Client: - def __init__(self): - self.handler = ConcreteHandler1(ConcreteHandler3(ConcreteHandler2(DefaultHandler(None)))) - def delegate(self, requests): - for request in requests: - self.handler.handle(request) - - -if __name__ == "__main__": - client = Client() - requests = [2, 5, 14, 22, 18, 3, 35, 27, 20] - client.delegate(requests) - -### OUTPUT ### -# request 2 handled in handler 1 -# request 5 handled in handler 1 -# request 14 handled in handler 2 -# request 22 handled in handler 3 -# request 18 handled in handler 2 -# request 3 handled in handler 1 -# end of chain, no handler for 35 -# request 27 handled in handler 3 -# request 20 handled in handler 2 diff --git a/chaining_method.py b/chaining_method.py deleted file mode 100644 index e0374761..00000000 --- a/chaining_method.py +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -class Person(object): - - def __init__(self, name, action): - self.name = name - self.action = action - - def do_action(self): - print(self.name, self.action.name, end=' ') - return self.action - -class Action(object): - - def __init__(self, name): - self.name = name - - def amount(self, val): - print(val, end=' ') - return self - - def stop(self): - print('then stop') - -if __name__ == '__main__': - - move = Action('move') - person = Person('Jack', move) - person.do_action().amount('5m').stop() - -### OUTPUT ### -# Jack move 5m then stop diff --git a/command.py b/command.py deleted file mode 100644 index 727f42cb..00000000 --- a/command.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -import os - - -class MoveFileCommand(object): - - def __init__(self, src, dest): - self.src = src - self.dest = dest - - def execute(self): - print('renaming {} to {}'.format(self.src, self.dest)) - os.rename(self.src, self.dest) - - def undo(self): - print('renaming {} to {}'.format(self.dest, self.src)) - os.rename(self.dest, self.src) - - -def main(): - command_stack = [] - - # commands are just pushed into the command stack - command_stack.append(MoveFileCommand('foo.txt', 'bar.txt')) - command_stack.append(MoveFileCommand('bar.txt', 'baz.txt')) - - # they can be executed later on - for cmd in command_stack: - cmd.execute() - - # and can also be undone at will - for cmd in reversed(command_stack): - cmd.undo() - -if __name__ == "__main__": - main() - -### OUTPUT ### -# renaming foo.txt to bar.txt -# renaming bar.txt to baz.txt -# renaming baz.txt to bar.txt -# renaming bar.txt to foo.txt diff --git a/composite.py b/composite.py deleted file mode 100644 index 9b8cc56f..00000000 --- a/composite.py +++ /dev/null @@ -1,340 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -A class which defines a composite object which can store -hieararchical dictionaries with names. - -This class is same as a hiearchical dictionary, but it -provides methods to add/access/modify children by name, -like a Composite. - -Created Anand B Pillai - -""" -__author__ = "Anand B Pillai" -__maintainer__ = "Anand B Pillai" -__version__ = "0.2" - - -def normalize(val): - """ Normalize a string so that it can be used as an attribute - to a Python object """ - - if val.find('-') != -1: - val = val.replace('-', '_') - - return val - - -def denormalize(val): - """ De-normalize a string """ - - if val.find('_') != -1: - val = val.replace('_', '-') - - return val - - -class SpecialDict(dict): - - """ A dictionary type which allows direct attribute - access to its keys """ - - def __getattr__(self, name): - - if name in self.__dict__: - return self.__dict__[name] - elif name in self: - return self.get(name) - else: - # Check for denormalized name - name = denormalize(name) - if name in self: - return self.get(name) - else: - raise AttributeError('no attribute named %s' % name) - - def __setattr__(self, name, value): - - if name in self.__dict__: - self.__dict__[name] = value - elif name in self: - self[name] = value - else: - # Check for denormalized name - name2 = denormalize(name) - if name2 in self: - self[name2] = value - else: - # New attribute - self[name] = value - - -class CompositeDict(SpecialDict): - - """ A class which works like a hierarchical dictionary. - This class is based on the Composite design-pattern """ - - ID = 0 - - def __init__(self, name=''): - - if name: - self._name = name - else: - self._name = ''.join(('id#', str(self.__class__.ID))) - self.__class__.ID += 1 - - self._children = [] - # Link back to father - self._father = None - self[self._name] = SpecialDict() - - def __getattr__(self, name): - - if name in self.__dict__: - return self.__dict__[name] - elif name in self: - return self.get(name) - else: - # Check for denormalized name - name = denormalize(name) - if name in self: - return self.get(name) - else: - # Look in children list - child = self.findChild(name) - if child: - return child - else: - attr = getattr(self[self._name], name) - if attr: - return attr - - raise AttributeError('no attribute named %s' % name) - - def isRoot(self): - """ Return whether I am a root component or not """ - - # If I don't have a parent, I am root - return not self._father - - def isLeaf(self): - """ Return whether I am a leaf component or not """ - - # I am a leaf if I have no children - return not self._children - - def getName(self): - """ Return the name of this ConfigInfo object """ - - return self._name - - def getIndex(self, child): - """ Return the index of the child ConfigInfo object 'child' """ - - if child in self._children: - return self._children.index(child) - else: - return -1 - - def getDict(self): - """ Return the contained dictionary """ - - return self[self._name] - - def getProperty(self, child, key): - """ Return the value for the property for child - 'child' with key 'key' """ - - # First get the child's dictionary - childDict = self.getInfoDict(child) - if childDict: - return childDict.get(key, None) - - def setProperty(self, child, key, value): - """ Set the value for the property 'key' for - the child 'child' to 'value' """ - - # First get the child's dictionary - childDict = self.getInfoDict(child) - if childDict: - childDict[key] = value - - def getChildren(self): - """ Return the list of immediate children of this object """ - - return self._children - - def getAllChildren(self): - """ Return the list of all children of this object """ - - l = [] - for child in self._children: - l.append(child) - l.extend(child.getAllChildren()) - - return l - - def getChild(self, name): - """ Return the immediate child object with the given name """ - - for child in self._children: - if child.getName() == name: - return child - - def findChild(self, name): - """ Return the child with the given name from the tree """ - - # Note - this returns the first child of the given name - # any other children with similar names down the tree - # is not considered. - - for child in self.getAllChildren(): - if child.getName() == name: - return child - - def findChildren(self, name): - """ Return a list of children with the given name from the tree """ - - # Note: this returns a list of all the children of a given - # name, irrespective of the depth of look-up. - - children = [] - - for child in self.getAllChildren(): - if child.getName() == name: - children.append(child) - - return children - - def getPropertyDict(self): - """ Return the property dictionary """ - - d = self.getChild('__properties') - if d: - return d.getDict() - else: - return {} - - def getParent(self): - """ Return the person who created me """ - - return self._father - - def __setChildDict(self, child): - """ Private method to set the dictionary of the child - object 'child' in the internal dictionary """ - - d = self[self._name] - d[child.getName()] = child.getDict() - - def setParent(self, father): - """ Set the parent object of myself """ - - # This should be ideally called only once - # by the father when creating the child :-) - # though it is possible to change parenthood - # when a new child is adopted in the place - # of an existing one - in that case the existing - # child is orphaned - see addChild and addChild2 - # methods ! - self._father = father - - def setName(self, name): - """ Set the name of this ConfigInfo object to 'name' """ - - self._name = name - - def setDict(self, d): - """ Set the contained dictionary """ - - self[self._name] = d.copy() - - def setAttribute(self, name, value): - """ Set a name value pair in the contained dictionary """ - - self[self._name][name] = value - - def getAttribute(self, name): - """ Return value of an attribute from the contained dictionary """ - - return self[self._name][name] - - def addChild(self, name, force=False): - """ Add a new child 'child' with the name 'name'. - If the optional flag 'force' is set to True, the - child object is overwritten if it is already there. - - This function returns the child object, whether - new or existing """ - - if type(name) != str: - raise ValueError('Argument should be a string!') - - child = self.getChild(name) - if child: - # print('Child %s present!' % name) - # Replace it if force==True - if force: - index = self.getIndex(child) - if index != -1: - child = self.__class__(name) - self._children[index] = child - child.setParent(self) - - self.__setChildDict(child) - return child - else: - child = self.__class__(name) - child.setParent(self) - - self._children.append(child) - self.__setChildDict(child) - - return child - - def addChild2(self, child): - """ Add the child object 'child'. If it is already present, - it is overwritten by default """ - - currChild = self.getChild(child.getName()) - if currChild: - index = self.getIndex(currChild) - if index != -1: - self._children[index] = child - child.setParent(self) - # Unset the existing child's parent - currChild.setParent(None) - del currChild - - self.__setChildDict(child) - else: - child.setParent(self) - self._children.append(child) - self.__setChildDict(child) - - -if __name__ == "__main__": - window = CompositeDict('Window') - frame = window.addChild('Frame') - tfield = frame.addChild('Text Field') - tfield.setAttribute('size', '20') - - btn = frame.addChild('Button1') - btn.setAttribute('label', 'Submit') - - btn = frame.addChild('Button2') - btn.setAttribute('label', 'Browse') - - # print(window) - # print(window.Frame) - # print(window.Frame.Button1) - # print(window.Frame.Button2) - print(window.Frame.Button1.label) - print(window.Frame.Button2.label) - -### OUTPUT ### -# Submit -# Browse diff --git a/config_backup/.coveragerc b/config_backup/.coveragerc new file mode 100644 index 00000000..98306ea9 --- /dev/null +++ b/config_backup/.coveragerc @@ -0,0 +1,25 @@ +[run] +branch = True + +[report] +; Regexes for lines to exclude from consideration +exclude_also = + ; Don't complain about missing debug-only code: + def __repr__ + if self\.debug + + ; Don't complain if tests don't hit defensive assertion code: + raise AssertionError + raise NotImplementedError + + ; Don't complain if non-runnable code isn't run: + if 0: + if __name__ == .__main__.: + + ; Don't complain about abstract methods, they aren't run: + @(abc\.)?abstractmethod + +ignore_errors = True + +[html] +directory = coverage_html_report \ No newline at end of file diff --git a/config_backup/setup.cfg b/config_backup/setup.cfg new file mode 100644 index 00000000..e109555b --- /dev/null +++ b/config_backup/setup.cfg @@ -0,0 +1,13 @@ +[flake8] +max-line-length = 120 +ignore = E266 E731 W503 +exclude = venv* + +[tool:pytest] +filterwarnings = + ; ignore TestRunner class from facade example + ignore:.*test class 'TestRunner'.*:Warning + +[mypy] +python_version = 3.12 +ignore_missing_imports = True diff --git a/config_backup/tox.ini b/config_backup/tox.ini new file mode 100644 index 00000000..36e2577e --- /dev/null +++ b/config_backup/tox.ini @@ -0,0 +1,28 @@ +[tox] +envlist = py312,cov-report +skip_missing_interpreters = true +usedevelop = true + +[testenv] +setenv = + COVERAGE_FILE = .coverage.{envname} +deps = + -r requirements-dev.txt +allowlist_externals = + pytest + flake8 + mypy +commands = + flake8 --exclude="venv/,.tox/" patterns/ + ; `randomly-seed` option from `pytest-randomly` helps with deterministic outputs for examples like `other/blackboard.py` + pytest --randomly-seed=1234 --doctest-modules patterns/ + pytest -s -vv --cov=patterns/ --log-level=INFO tests/ + + +[testenv:cov-report] +setenv = + COVERAGE_FILE = .coverage +deps = coverage +commands = + coverage combine + coverage report diff --git a/decorator.py b/decorator.py deleted file mode 100644 index ce96f315..00000000 --- a/decorator.py +++ /dev/null @@ -1,31 +0,0 @@ -"""https://docs.python.org/2/library/functools.html#functools.wraps""" -"""https://stackoverflow.com/questions/739654/how-can-i-make-a-chain-of-function-decorators-in-python/739665#739665""" - -from functools import wraps - - -def makebold(fn): - @wraps(fn) - def wrapped(): - return "" + fn() + "" - return wrapped - - -def makeitalic(fn): - @wraps(fn) - def wrapped(): - return "" + fn() + "" - return wrapped - - -@makebold -@makeitalic -def hello(): - """a decorated hello world""" - return "hello world" - -if __name__ == '__main__': - print('result:{} name:{} doc:{}'.format(hello(), hello.__name__, hello.__doc__)) - -### OUTPUT ### -# result:hello world name:hello doc:a decorated hello world diff --git a/facade.py b/facade.py deleted file mode 100644 index 77197dc3..00000000 --- a/facade.py +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -import time - -SLEEP = 0.5 - - -# Complex Parts -class TC1: - - def run(self): - print("###### In Test 1 ######") - time.sleep(SLEEP) - print("Setting up") - time.sleep(SLEEP) - print("Running test") - time.sleep(SLEEP) - print("Tearing down") - time.sleep(SLEEP) - print("Test Finished\n") - - -class TC2: - - def run(self): - print("###### In Test 2 ######") - time.sleep(SLEEP) - print("Setting up") - time.sleep(SLEEP) - print("Running test") - time.sleep(SLEEP) - print("Tearing down") - time.sleep(SLEEP) - print("Test Finished\n") - - -class TC3: - - def run(self): - print("###### In Test 3 ######") - time.sleep(SLEEP) - print("Setting up") - time.sleep(SLEEP) - print("Running test") - time.sleep(SLEEP) - print("Tearing down") - time.sleep(SLEEP) - print("Test Finished\n") - - -# Facade -class TestRunner: - - def __init__(self): - self.tc1 = TC1() - self.tc2 = TC2() - self.tc3 = TC3() - self.tests = [i for i in (self.tc1, self.tc2, self.tc3)] - - def runAll(self): - [i.run() for i in self.tests] - - -# Client -if __name__ == '__main__': - testrunner = TestRunner() - testrunner.runAll() - -### OUTPUT ### -# ###### In Test 1 ###### -# Setting up -# Running test -# Tearing down -# Test Finished -# -# ###### In Test 2 ###### -# Setting up -# Running test -# Tearing down -# Test Finished -# -# ###### In Test 3 ###### -# Setting up -# Running test -# Tearing down -# Test Finished -# diff --git a/factory_method.py b/factory_method.py deleted file mode 100644 index c21e3960..00000000 --- a/factory_method.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -"""http://ginstrom.com/scribbles/2007/10/08/design-patterns-python-style/""" - - -class GreekGetter: - - """A simple localizer a la gettext""" - - def __init__(self): - self.trans = dict(dog="σκύλος", cat="γάτα") - - def get(self, msgid): - """We'll punt if we don't have a translation""" - try: - return self.trans[msgid] - except KeyError: - return str(msgid) - - -class EnglishGetter: - - """Simply echoes the msg ids""" - - def get(self, msgid): - return str(msgid) - - -def get_localizer(language="English"): - """The factory method""" - languages = dict(English=EnglishGetter, Greek=GreekGetter) - return languages[language]() - -# Create our localizers -e, g = get_localizer(language="English"), get_localizer(language="Greek") -# Localize some text -for msgid in "dog parrot cat bear".split(): - print(e.get(msgid), g.get(msgid)) - -### OUTPUT ### -# dog σκύλος -# parrot parrot -# cat γάτα -# bear bear diff --git a/flyweight.py b/flyweight.py deleted file mode 100644 index efa48981..00000000 --- a/flyweight.py +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -"""http://codesnipers.com/?q=python-flyweights""" - -import weakref - - -class Card(object): - - """The object pool. Has builtin reference counting""" - _CardPool = weakref.WeakValueDictionary() - - """Flyweight implementation. If the object exists in the - pool just return it (instead of creating a new one)""" - def __new__(cls, value, suit): - obj = Card._CardPool.get(value + suit, None) - if not obj: - obj = object.__new__(cls) - Card._CardPool[value + suit] = obj - obj.value, obj.suit = value, suit - return obj - - # def __init__(self, value, suit): - # self.value, self.suit = value, suit - - def __repr__(self): - return "" % (self.value, self.suit) - - -if __name__ == '__main__': - # comment __new__ and uncomment __init__ to see the difference - c1 = Card('9', 'h') - c2 = Card('9', 'h') - print(c1, c2) - print(c1 == c2) - print(id(c1), id(c2)) - -### OUTPUT ### -# -# True -# 140368617673296 140368617673296 diff --git a/graph_search.py b/graph_search.py deleted file mode 100644 index e76d2e39..00000000 --- a/graph_search.py +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - - -class GraphSearch: - - """Graph search emulation in python, from source - http://www.python.org/doc/essays/graphs/""" - - def __init__(self, graph): - self.graph = graph - - def find_path(self, start, end, path=None): - self.start = start - self.end = end - self.path = path if path else [] - - self.path += [self.start] - if self.start == self.end: - return self.path - if self.start not in self.graph: - return None - for node in self.graph[self.start]: - if node not in self.path: - newpath = self.find_path(node, self.end, self.path) - if newpath: - return newpath - return None - - def find_all_path(self, start, end, path=None): - self.start = start - self.end = end - _path = path if path else [] - _path += [self.start] - if self.start == self.end: - return [_path] - if self.start not in self.graph: - return [] - paths = [] - for node in self.graph[self.start]: - if node not in _path: - newpaths = self.find_all_path(node, self.end, _path[:]) - for newpath in newpaths: - paths.append(newpath) - return paths - - def find_shortest_path(self, start, end, path=None): - self.start = start - self.end = end - _path = path if path else [] - - _path += [self.start] - if self.start == self.end: - return _path - if self.start not in self.graph: - return None - shortest = None - for node in self.graph[self.start]: - if node not in _path: - newpath = self.find_shortest_path(node, self.end, _path[:]) - if newpath: - if not shortest or len(newpath) < len(shortest): - shortest = newpath - return shortest - -# example of graph usage -graph = {'A': ['B', 'C'], - 'B': ['C', 'D'], - 'C': ['D'], - 'D': ['C'], - 'E': ['F'], - 'F': ['C'] - } - -# initialization of new graph search object -graph1 = GraphSearch(graph) - - -print(graph1.find_path('A', 'D')) -print(graph1.find_all_path('A', 'D')) -print(graph1.find_shortest_path('A', 'D')) - -### OUTPUT ### -# ['A', 'B', 'C', 'D'] -# [['A', 'B', 'C', 'D'], ['A', 'B', 'D'], ['A', 'C', 'D']] -# ['A', 'B', 'D'] diff --git a/iterator.py b/iterator.py deleted file mode 100644 index 74d67138..00000000 --- a/iterator.py +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -"""http://ginstrom.com/scribbles/2007/10/08/design-patterns-python-style/ - -Implementation of the iterator pattern with a generator""" - -from __future__ import print_function - - -def count_to(count): - """Counts by word numbers, up to a maximum of five""" - numbers = ["one", "two", "three", "four", "five"] - # enumerate() returns a tuple containing a count (from start which - # defaults to 0) and the values obtained from iterating over sequence - for pos, number in zip(range(count), numbers): - yield number - -# Test the generator -count_to_two = lambda: count_to(2) -count_to_five = lambda: count_to(5) - -print('Counting to two...') -for number in count_to_two(): - print(number, end=' ') - -print() - -print('Counting to five...') -for number in count_to_five(): - print(number, end=' ') - -print() - -### OUTPUT ### -# Counting to two... -# one two -# Counting to five... -# one two three four five diff --git a/lazy_evaluation.py b/lazy_evaluation.py deleted file mode 100644 index 5f364a65..00000000 --- a/lazy_evaluation.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -Lazily-evaluated property pattern in Python. - -https://en.wikipedia.org/wiki/Lazy_evaluation -http://stevenloria.com/lazy-evaluated-properties-in-python/ -""" - - -def lazy_property(fn): - """Decorator that makes a property lazy-evaluated.""" - attr_name = '_lazy_' + fn.__name__ - - @property - def _lazy_property(self): - if not hasattr(self, attr_name): - setattr(self, attr_name, fn(self)) - return getattr(self, attr_name) - return _lazy_property - - -class Person(object): - def __init__(self, name, occupation): - self.name = name - self.occupation = occupation - - @lazy_property - def relatives(self): - # Get all relatives, let's assume that it costs much time. - relatives = "Many relatives." - return relatives - - -def main(): - Jhon = Person('Jhon', 'Coder') - print("Name: {0} Occupation: {1}".format(Jhon.name, Jhon.occupation)) - print("Before we access `relatives`:") - print(Jhon.__dict__) - print("Jhon's relatives: {0}".format(Jhon.relatives)) - print("After we've accessed `relatives`:") - print(Jhon.__dict__) - - -if __name__ == '__main__': - main() - -### OUTPUT ### -# Name: Jhon Occupation: Coder -# Before we access `relatives`: -# {'name': 'Jhon', 'occupation': 'Coder'} -# Jhon's relatives: Many relatives. -# After we've accessed `relatives`: -# {'_lazy_relatives': 'Many relatives.', 'name': 'Jhon', 'occupation': 'Coder'} diff --git a/lint.sh b/lint.sh new file mode 100755 index 00000000..a7eebda1 --- /dev/null +++ b/lint.sh @@ -0,0 +1,16 @@ +#! /bin/bash + +pip install --upgrade pip +pip install black codespell flake8 isort mypy pytest pyupgrade tox +pip install -e . + +source_dir="./patterns" + +codespell --quiet-level=2 ./patterns # --ignore-words-list="" --skip="" +flake8 "${source_dir}" --count --show-source --statistics +isort --profile black "${source_dir}" +tox +mypy --ignore-missing-imports "${source_dir}" || true +pytest "${source_dir}" +pytest --doctest-modules "${source_dir}" || true +shopt -s globstar && pyupgrade --py312-plus ${source_dir}/*.py diff --git a/mediator.py b/mediator.py deleted file mode 100644 index 82a2886f..00000000 --- a/mediator.py +++ /dev/null @@ -1,143 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -"""http://dpip.testingperspective.com/?p=28""" - -import random -import time - - -class TC: - - def __init__(self): - self._tm = None - self._bProblem = 0 - - def setup(self): - print("Setting up the Test") - time.sleep(0.1) - self._tm.prepareReporting() - - def execute(self): - if not self._bProblem: - print("Executing the test") - time.sleep(0.1) - else: - print("Problem in setup. Test not executed.") - - def tearDown(self): - if not self._bProblem: - print("Tearing down") - time.sleep(0.1) - self._tm.publishReport() - else: - print("Test not executed. No tear down required.") - - def setTM(self, tm): - self._tm = tm - - def setProblem(self, value): - self._bProblem = value - - -class Reporter: - - def __init__(self): - self._tm = None - - def prepare(self): - print("Reporter Class is preparing to report the results") - time.sleep(0.1) - - def report(self): - print("Reporting the results of Test") - time.sleep(0.1) - - def setTM(self, tm): - self._tm = tm - - -class DB: - - def __init__(self): - self._tm = None - - def insert(self): - print("Inserting the execution begin status in the Database") - time.sleep(0.1) - # Following code is to simulate a communication from DB to TC - if random.randrange(1, 4) == 3: - return -1 - - def update(self): - print("Updating the test results in Database") - time.sleep(0.1) - - def setTM(self, tm): - self._tm = tm - - -class TestManager: - - def __init__(self): - self._reporter = None - self._db = None - self._tc = None - - def prepareReporting(self): - rvalue = self._db.insert() - if rvalue == -1: - self._tc.setProblem(1) - self._reporter.prepare() - - def setReporter(self, reporter): - self._reporter = reporter - - def setDB(self, db): - self._db = db - - def publishReport(self): - self._db.update() - self._reporter.report() - - def setTC(self, tc): - self._tc = tc - - -if __name__ == '__main__': - reporter = Reporter() - db = DB() - tm = TestManager() - tm.setReporter(reporter) - tm.setDB(db) - reporter.setTM(tm) - db.setTM(tm) - # For simplification we are looping on the same test. - # Practically, it could be about various unique test classes and their - # objects - for i in range(3): - tc = TC() - tc.setTM(tm) - tm.setTC(tc) - tc.setup() - tc.execute() - tc.tearDown() - -### OUTPUT ### -# Setting up the Test -# Inserting the execution begin status in the Database -# Executing the test -# Tearing down -# Updating the test results in Database -# Reporting the results of Test -# Setting up the Test -# Inserting the execution begin status in the Database -# Reporter Class is preparing to report the results -# Problem in setup. Test not executed. -# Test not executed. No tear down required. -# Setting up the Test -# Inserting the execution begin status in the Database -# Executing the test -# Tearing down -# Updating the test results in Database -# Reporting the results of Test diff --git a/memento.py b/memento.py deleted file mode 100644 index 42034658..00000000 --- a/memento.py +++ /dev/null @@ -1,127 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -"""http://code.activestate.com/recipes/413838-memento-closure/""" - -import copy - - -def Memento(obj, deep=False): - state = (copy.copy, copy.deepcopy)[bool(deep)](obj.__dict__) - - def Restore(): - obj.__dict__.clear() - obj.__dict__.update(state) - return Restore - - -class Transaction: - - """A transaction guard. This is really just - syntactic suggar arount a memento closure. - """ - deep = False - - def __init__(self, *targets): - self.targets = targets - self.Commit() - - def Commit(self): - self.states = [Memento(target, self.deep) for target in self.targets] - - def Rollback(self): - for st in self.states: - st() - - -class transactional(object): - - """Adds transactional semantics to methods. Methods decorated with - @transactional will rollback to entry state upon exceptions. - """ - - def __init__(self, method): - self.method = method - - def __get__(self, obj, T): - def transaction(*args, **kwargs): - state = Memento(obj) - try: - return self.method(obj, *args, **kwargs) - except: - state() - raise - return transaction - - -class NumObj(object): - - def __init__(self, value): - self.value = value - - def __repr__(self): - return '<%s: %r>' % (self.__class__.__name__, self.value) - - def Increment(self): - self.value += 1 - - @transactional - def DoStuff(self): - self.value = '1111' # <- invalid value - self.Increment() # <- will fail and rollback - - -if __name__ == '__main__': - n = NumObj(-1) - print(n) - t = Transaction(n) - try: - for i in range(3): - n.Increment() - print(n) - t.Commit() - print('-- commited') - for i in range(3): - n.Increment() - print(n) - n.value += 'x' # will fail - print(n) - except: - t.Rollback() - print('-- rolled back') - print(n) - print('-- now doing stuff ...') - try: - n.DoStuff() - except: - print('-> doing stuff failed!') - import sys - import traceback - traceback.print_exc(file=sys.stdout) - pass - print(n) - -### OUTPUT ### -# -# -# -# -# -- commited -# -# -# -# -- rolled back -# -# -- now doing stuff ... -# -> doing stuff failed! -# Traceback (most recent call last): -# File "memento.py", line 91, in -# n.DoStuff() -# File "memento.py", line 47, in transaction -# return self.method(obj, *args, **kwargs) -# File "memento.py", line 67, in DoStuff -# self.Increment() # <- will fail and rollback -# File "memento.py", line 62, in Increment -# self.value += 1 -# TypeError: Can't convert 'int' object to str implicitly -# diff --git a/mvc.py b/mvc.py deleted file mode 100644 index 8087bdab..00000000 --- a/mvc.py +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - - -class Model(object): - - products = { - 'milk': {'price': 1.50, 'quantity': 10}, - 'eggs': {'price': 0.20, 'quantity': 100}, - 'cheese': {'price': 2.00, 'quantity': 10} - } - - -class View(object): - - def product_list(self, product_list): - print('PRODUCT LIST:') - for product in product_list: - print(product) - print('') - - def product_information(self, product, product_info): - print('PRODUCT INFORMATION:') - print('Name: %s, Price: %.2f, Quantity: %d\n' % - (product.title(), product_info.get('price', 0), - product_info.get('quantity', 0))) - - def product_not_found(self, product): - print('That product "%s" does not exist in the records' % product) - - -class Controller(object): - - def __init__(self): - self.model = Model() - self.view = View() - - def get_product_list(self): - product_list = self.model.products.keys() - self.view.product_list(product_list) - - def get_product_information(self, product): - product_info = self.model.products.get(product, None) - if product_info is not None: - self.view.product_information(product, product_info) - else: - self.view.product_not_found(product) - - -if __name__ == '__main__': - - controller = Controller() - controller.get_product_list() - controller.get_product_information('cheese') - controller.get_product_information('eggs') - controller.get_product_information('milk') - controller.get_product_information('arepas') - -### OUTPUT ### -# PRODUCT LIST: -# cheese -# eggs -# milk -# -# PRODUCT INFORMATION: -# Name: Cheese, Price: 2.00, Quantity: 10 -# -# PRODUCT INFORMATION: -# Name: Eggs, Price: 0.20, Quantity: 100 -# -# PRODUCT INFORMATION: -# Name: Milk, Price: 1.50, Quantity: 10 -# -# That product "arepas" does not exist in the records diff --git a/observer.py b/observer.py deleted file mode 100644 index 4ce1aed6..00000000 --- a/observer.py +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -"""http://code.activestate.com/recipes/131499-observer-pattern/""" - - -class Subject(object): - - def __init__(self): - self._observers = [] - - def attach(self, observer): - if observer not in self._observers: - self._observers.append(observer) - - def detach(self, observer): - try: - self._observers.remove(observer) - except ValueError: - pass - - def notify(self, modifier=None): - for observer in self._observers: - if modifier != observer: - observer.update(self) - - -# Example usage -class Data(Subject): - - def __init__(self, name=''): - Subject.__init__(self) - self.name = name - self._data = 0 - - @property - def data(self): - return self._data - - @data.setter - def data(self, value): - self._data = value - self.notify() - - -class HexViewer: - - def update(self, subject): - print('HexViewer: Subject %s has data 0x%x' % - (subject.name, subject.data)) - - -class DecimalViewer: - - def update(self, subject): - print('DecimalViewer: Subject %s has data %d' % - (subject.name, subject.data)) - - -# Example usage... -def main(): - data1 = Data('Data 1') - data2 = Data('Data 2') - view1 = DecimalViewer() - view2 = HexViewer() - data1.attach(view1) - data1.attach(view2) - data2.attach(view2) - data2.attach(view1) - - print("Setting Data 1 = 10") - data1.data = 10 - print("Setting Data 2 = 15") - data2.data = 15 - print("Setting Data 1 = 3") - data1.data = 3 - print("Setting Data 2 = 5") - data2.data = 5 - print("Detach HexViewer from data1 and data2.") - data1.detach(view2) - data2.detach(view2) - print("Setting Data 1 = 10") - data1.data = 10 - print("Setting Data 2 = 15") - data2.data = 15 - - -if __name__ == '__main__': - main() - -### OUTPUT ### -# Setting Data 1 = 10 -# DecimalViewer: Subject Data 1 has data 10 -# HexViewer: Subject Data 1 has data 0xa -# Setting Data 2 = 15 -# HexViewer: Subject Data 2 has data 0xf -# DecimalViewer: Subject Data 2 has data 15 -# Setting Data 1 = 3 -# DecimalViewer: Subject Data 1 has data 3 -# HexViewer: Subject Data 1 has data 0x3 -# Setting Data 2 = 5 -# HexViewer: Subject Data 2 has data 0x5 -# DecimalViewer: Subject Data 2 has data 5 -# Detach HexViewer from data1 and data2. -# Setting Data 1 = 10 -# DecimalViewer: Subject Data 1 has data 10 -# Setting Data 2 = 15 -# DecimalViewer: Subject Data 2 has data 15 diff --git a/patterns/__init__.py b/patterns/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/patterns/behavioral/__init__.py b/patterns/behavioral/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/patterns/behavioral/catalog.py b/patterns/behavioral/catalog.py new file mode 100644 index 00000000..ba85f500 --- /dev/null +++ b/patterns/behavioral/catalog.py @@ -0,0 +1,180 @@ +""" +A class that uses different static function depending of a parameter passed in +init. Note the use of a single dictionary instead of multiple conditions +""" + +__author__ = "Ibrahim Diop " + + +class Catalog: + """catalog of multiple static methods that are executed depending on an init + + parameter + """ + + def __init__(self, param: str) -> None: + + # dictionary that will be used to determine which static method is + # to be executed but that will be also used to store possible param + # value + self._static_method_choices = { + "param_value_1": self._static_method_1, + "param_value_2": self._static_method_2, + } + + # simple test to validate param value + if param in self._static_method_choices.keys(): + self.param = param + else: + raise ValueError(f"Invalid Value for Param: {param}") + + @staticmethod + def _static_method_1() -> None: + print("executed method 1!") + + @staticmethod + def _static_method_2() -> None: + print("executed method 2!") + + def main_method(self) -> None: + """will execute either _static_method_1 or _static_method_2 + + depending on self.param value + """ + self._static_method_choices[self.param]() + + +# Alternative implementation for different levels of methods +class CatalogInstance: + """catalog of multiple methods that are executed depending on an init + + parameter + """ + + def __init__(self, param: str) -> None: + self.x1 = "x1" + self.x2 = "x2" + # simple test to validate param value + if param in self._instance_method_choices: + self.param = param + else: + raise ValueError(f"Invalid Value for Param: {param}") + + def _instance_method_1(self) -> None: + print(f"Value {self.x1}") + + def _instance_method_2(self) -> None: + print(f"Value {self.x2}") + + _instance_method_choices = { + "param_value_1": _instance_method_1, + "param_value_2": _instance_method_2, + } + + def main_method(self) -> None: + """will execute either _instance_method_1 or _instance_method_2 + + depending on self.param value + """ + self._instance_method_choices[self.param].__get__(self)() # type: ignore + # type ignore reason: https://github.com/python/mypy/issues/10206 + + +class CatalogClass: + """catalog of multiple class methods that are executed depending on an init + + parameter + """ + + x1 = "x1" + x2 = "x2" + + def __init__(self, param: str) -> None: + # simple test to validate param value + if param in self._class_method_choices: + self.param = param + else: + raise ValueError(f"Invalid Value for Param: {param}") + + @classmethod + def _class_method_1(cls) -> None: + print(f"Value {cls.x1}") + + @classmethod + def _class_method_2(cls) -> None: + print(f"Value {cls.x2}") + + _class_method_choices = { + "param_value_1": _class_method_1, + "param_value_2": _class_method_2, + } + + def main_method(self): + """will execute either _class_method_1 or _class_method_2 + + depending on self.param value + """ + self._class_method_choices[self.param].__get__(None, self.__class__)() # type: ignore + # type ignore reason: https://github.com/python/mypy/issues/10206 + + +class CatalogStatic: + """catalog of multiple static methods that are executed depending on an init + + parameter + """ + + def __init__(self, param: str) -> None: + # simple test to validate param value + if param in self._static_method_choices: + self.param = param + else: + raise ValueError(f"Invalid Value for Param: {param}") + + @staticmethod + def _static_method_1() -> None: + print("executed method 1!") + + @staticmethod + def _static_method_2() -> None: + print("executed method 2!") + + _static_method_choices = { + "param_value_1": _static_method_1, + "param_value_2": _static_method_2, + } + + def main_method(self) -> None: + """will execute either _static_method_1 or _static_method_2 + + depending on self.param value + """ + + self._static_method_choices[self.param].__get__(None, self.__class__)() # type: ignore + # type ignore reason: https://github.com/python/mypy/issues/10206 + + +def main(): + """ + >>> test = Catalog('param_value_2') + >>> test.main_method() + executed method 2! + + >>> test = CatalogInstance('param_value_1') + >>> test.main_method() + Value x1 + + >>> test = CatalogClass('param_value_2') + >>> test.main_method() + Value x2 + + >>> test = CatalogStatic('param_value_1') + >>> test.main_method() + executed method 1! + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/behavioral/chain_of_responsibility.py b/patterns/behavioral/chain_of_responsibility.py new file mode 100644 index 00000000..9d46c4a8 --- /dev/null +++ b/patterns/behavioral/chain_of_responsibility.py @@ -0,0 +1,119 @@ +""" +*What is this pattern about? + +The Chain of responsibility is an object oriented version of the +`if ... elif ... elif ... else ...` idiom, with the +benefit that the condition–action blocks can be dynamically rearranged +and reconfigured at runtime. + +This pattern aims to decouple the senders of a request from its +receivers by allowing request to move through chained +receivers until it is handled. + +Request receiver in simple form keeps a reference to a single successor. +As a variation some receivers may be capable of sending requests out +in several directions, forming a `tree of responsibility`. + +*TL;DR +Allow a request to pass down a chain of receivers until it is handled. +""" + +from abc import ABC, abstractmethod +from typing import Optional, Tuple + + +class Handler(ABC): + def __init__(self, successor: Optional["Handler"] = None): + self.successor = successor + + def handle(self, request: int) -> None: + """ + Handle request and stop. + If can't - call next handler in chain. + + As an alternative you might even in case of success + call the next handler. + """ + res = self.check_range(request) + if not res and self.successor: + self.successor.handle(request) + + @abstractmethod + def check_range(self, request: int) -> Optional[bool]: + """Compare passed value to predefined interval""" + + +class ConcreteHandler0(Handler): + """Each handler can be different. + Be simple and static... + """ + + @staticmethod + def check_range(request: int) -> Optional[bool]: + if 0 <= request < 10: + print(f"request {request} handled in handler 0") + return True + return None + + +class ConcreteHandler1(Handler): + """... With it's own internal state""" + + start, end = 10, 20 + + def check_range(self, request: int) -> Optional[bool]: + if self.start <= request < self.end: + print(f"request {request} handled in handler 1") + return True + return None + + +class ConcreteHandler2(Handler): + """... With helper methods.""" + + def check_range(self, request: int) -> Optional[bool]: + start, end = self.get_interval_from_db() + if start <= request < end: + print(f"request {request} handled in handler 2") + return True + return None + + @staticmethod + def get_interval_from_db() -> Tuple[int, int]: + return (20, 30) + + +class FallbackHandler(Handler): + @staticmethod + def check_range(request: int) -> Optional[bool]: + print(f"end of chain, no handler for {request}") + return False + + +def main(): + """ + >>> h0 = ConcreteHandler0() + >>> h1 = ConcreteHandler1() + >>> h2 = ConcreteHandler2(FallbackHandler()) + >>> h0.successor = h1 + >>> h1.successor = h2 + + >>> requests = [2, 5, 14, 22, 18, 3, 35, 27, 20] + >>> for request in requests: + ... h0.handle(request) + request 2 handled in handler 0 + request 5 handled in handler 0 + request 14 handled in handler 1 + request 22 handled in handler 2 + request 18 handled in handler 1 + request 3 handled in handler 0 + end of chain, no handler for 35 + request 27 handled in handler 2 + request 20 handled in handler 2 + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod(optionflags=doctest.ELLIPSIS) diff --git a/patterns/behavioral/chaining_method.py b/patterns/behavioral/chaining_method.py new file mode 100644 index 00000000..26f11018 --- /dev/null +++ b/patterns/behavioral/chaining_method.py @@ -0,0 +1,37 @@ +from __future__ import annotations + + +class Person: + def __init__(self, name: str) -> None: + self.name = name + + def do_action(self, action: Action) -> Action: + print(self.name, action.name, end=" ") + return action + + +class Action: + def __init__(self, name: str) -> None: + self.name = name + + def amount(self, val: str) -> Action: + print(val, end=" ") + return self + + def stop(self) -> None: + print("then stop") + + +def main(): + """ + >>> move = Action('move') + >>> person = Person('Jack') + >>> person.do_action(move).amount('5m').stop() + Jack move 5m then stop + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/behavioral/command.py b/patterns/behavioral/command.py new file mode 100644 index 00000000..a88ea8be --- /dev/null +++ b/patterns/behavioral/command.py @@ -0,0 +1,107 @@ +""" +Command pattern decouples the object invoking a job from the one who knows +how to do it. As mentioned in the GoF book, a good example is in menu items. +You have a menu that has lots of items. Each item is responsible for doing a +special thing and you want your menu item just call the execute method when +it is pressed. To achieve this you implement a command object with the execute +method for each menu item and pass to it. + +*About the example +We have a menu containing two items. Each item accepts a file name, one hides the file +and the other deletes it. Both items have an undo option. +Each item is a MenuItem class that accepts the corresponding command as input and executes +it's execute method when it is pressed. + +*TL;DR +Object oriented implementation of callback functions. + +*Examples in Python ecosystem: +Django HttpRequest (without execute method): +https://docs.djangoproject.com/en/2.1/ref/request-response/#httprequest-objects +""" + +from typing import List, Union + + +class HideFileCommand: + """ + A command to hide a file given its name + """ + + def __init__(self) -> None: + # an array of files hidden, to undo them as needed + self._hidden_files: List[str] = [] + + def execute(self, filename: str) -> None: + print(f"hiding {filename}") + self._hidden_files.append(filename) + + def undo(self) -> None: + filename = self._hidden_files.pop() + print(f"un-hiding {filename}") + + +class DeleteFileCommand: + """ + A command to delete a file given its name + """ + + def __init__(self) -> None: + # an array of deleted files, to undo them as needed + self._deleted_files: List[str] = [] + + def execute(self, filename: str) -> None: + print(f"deleting {filename}") + self._deleted_files.append(filename) + + def undo(self) -> None: + filename = self._deleted_files.pop() + print(f"restoring {filename}") + + +class MenuItem: + """ + The invoker class. Here it is items in a menu. + """ + + def __init__(self, command: Union[HideFileCommand, DeleteFileCommand]) -> None: + self._command = command + + def on_do_press(self, filename: str) -> None: + self._command.execute(filename) + + def on_undo_press(self) -> None: + self._command.undo() + + +def main(): + """ + >>> item1 = MenuItem(DeleteFileCommand()) + + >>> item2 = MenuItem(HideFileCommand()) + + # create a file named `test-file` to work with + >>> test_file_name = 'test-file' + + # deleting `test-file` + >>> item1.on_do_press(test_file_name) + deleting test-file + + # restoring `test-file` + >>> item1.on_undo_press() + restoring test-file + + # hiding `test-file` + >>> item2.on_do_press(test_file_name) + hiding test-file + + # un-hiding `test-file` + >>> item2.on_undo_press() + un-hiding test-file + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/behavioral/iterator.py b/patterns/behavioral/iterator.py new file mode 100644 index 00000000..3ed4043b --- /dev/null +++ b/patterns/behavioral/iterator.py @@ -0,0 +1,47 @@ +""" +http://ginstrom.com/scribbles/2007/10/08/design-patterns-python-style/ +Implementation of the iterator pattern with a generator + +*TL;DR +Traverses a container and accesses the container's elements. +""" + + +def count_to(count: int): + """Counts by word numbers, up to a maximum of five""" + numbers = ["one", "two", "three", "four", "five"] + yield from numbers[:count] + + +# Test the generator +def count_to_two() -> None: + return count_to(2) + + +def count_to_five() -> None: + return count_to(5) + + +def main(): + """ + # Counting to two... + >>> for number in count_to_two(): + ... print(number) + one + two + + # Counting to five... + >>> for number in count_to_five(): + ... print(number) + one + two + three + four + five + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/behavioral/iterator_alt.py b/patterns/behavioral/iterator_alt.py new file mode 100644 index 00000000..a2a71d82 --- /dev/null +++ b/patterns/behavioral/iterator_alt.py @@ -0,0 +1,62 @@ +""" +Implementation of the iterator pattern using the iterator protocol from Python + +*TL;DR +Traverses a container and accesses the container's elements. +""" + +from __future__ import annotations + + +class NumberWords: + """Counts by word numbers, up to a maximum of five""" + + _WORD_MAP = ( + "one", + "two", + "three", + "four", + "five", + ) + + def __init__(self, start: int, stop: int) -> None: + self.start = start + self.stop = stop + + def __iter__(self) -> NumberWords: # this makes the class an Iterable + return self + + def __next__(self) -> str: # this makes the class an Iterator + if self.start > self.stop or self.start > len(self._WORD_MAP): + raise StopIteration + current = self.start + self.start += 1 + return self._WORD_MAP[current - 1] + + +# Test the iterator + + +def main(): + """ + # Counting to two... + >>> for number in NumberWords(start=1, stop=2): + ... print(number) + one + two + + # Counting to five... + >>> for number in NumberWords(start=1, stop=5): + ... print(number) + one + two + three + four + five + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/behavioral/mediator.py b/patterns/behavioral/mediator.py new file mode 100644 index 00000000..e4b3c34a --- /dev/null +++ b/patterns/behavioral/mediator.py @@ -0,0 +1,53 @@ +""" +https://www.djangospin.com/design-patterns-python/mediator/ + +Objects in a system communicate through a Mediator instead of directly with each other. +This reduces the dependencies between communicating objects, thereby reducing coupling. + +*TL;DR +Encapsulates how a set of objects interact. +""" + +from __future__ import annotations + + +class ChatRoom: + """Mediator class""" + + def display_message(self, user: User, message: str) -> None: + print(f"[{user} says]: {message}") + + +class User: + """A class whose instances want to interact with each other""" + + def __init__(self, name: str) -> None: + self.name = name + self.chat_room = ChatRoom() + + def say(self, message: str) -> None: + self.chat_room.display_message(self, message) + + def __str__(self) -> str: + return self.name + + +def main(): + """ + >>> molly = User('Molly') + >>> mark = User('Mark') + >>> ethan = User('Ethan') + + >>> molly.say("Hi Team! Meeting at 3 PM today.") + [Molly says]: Hi Team! Meeting at 3 PM today. + >>> mark.say("Roger that!") + [Mark says]: Roger that! + >>> ethan.say("Alright.") + [Ethan says]: Alright. + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/behavioral/memento.py b/patterns/behavioral/memento.py new file mode 100644 index 00000000..c1bc7f0b --- /dev/null +++ b/patterns/behavioral/memento.py @@ -0,0 +1,132 @@ +""" +http://code.activestate.com/recipes/413838-memento-closure/ + +*TL;DR +Provides the ability to restore an object to its previous state. +""" + +from copy import copy, deepcopy +from typing import Callable, List + + +def memento(obj, deep=False): + state = deepcopy(obj.__dict__) if deep else copy(obj.__dict__) + + def restore(): + obj.__dict__.clear() + obj.__dict__.update(state) + + return restore + + +class Transaction: + """A transaction guard. + + This is, in fact, just syntactic sugar around a memento closure. + """ + + deep = False + states: List[Callable[[], None]] = [] + + def __init__(self, deep, *targets): + self.deep = deep + self.targets = targets + self.commit() + + def commit(self): + self.states = [memento(target, self.deep) for target in self.targets] + + def rollback(self): + for a_state in self.states: + a_state() + + +def Transactional(method): + """Adds transactional semantics to methods. Methods decorated with + @Transactional will roll back to entry-state upon exceptions. + + :param method: The function to be decorated. + """ + def transaction(obj, *args, **kwargs): + state = memento(obj) + try: + return method(obj, *args, **kwargs) + except Exception as e: + state() + raise e + return transaction + +class NumObj: + def __init__(self, value): + self.value = value + + def __repr__(self): + return f"<{self.__class__.__name__}: {self.value!r}>" + + def increment(self): + self.value += 1 + + @Transactional + def do_stuff(self): + self.value = "1111" # <- invalid value + self.increment() # <- will fail and rollback + + +def main(): + """ + >>> num_obj = NumObj(-1) + >>> print(num_obj) + + + >>> a_transaction = Transaction(True, num_obj) + + >>> try: + ... for i in range(3): + ... num_obj.increment() + ... print(num_obj) + ... a_transaction.commit() + ... print('-- committed') + ... for i in range(3): + ... num_obj.increment() + ... print(num_obj) + ... num_obj.value += 'x' # will fail + ... print(num_obj) + ... except Exception: + ... a_transaction.rollback() + ... print('-- rolled back') + + + + -- committed + + + + -- rolled back + + >>> print(num_obj) + + + >>> print('-- now doing stuff ...') + -- now doing stuff ... + + >>> try: + ... num_obj.do_stuff() + ... except Exception: + ... print('-> doing stuff failed!') + ... import sys + ... import traceback + ... traceback.print_exc(file=sys.stdout) + -> doing stuff failed! + Traceback (most recent call last): + ... + TypeError: ...str...int... + + >>> print(num_obj) + + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod(optionflags=doctest.ELLIPSIS) diff --git a/patterns/behavioral/observer.py b/patterns/behavioral/observer.py new file mode 100644 index 00000000..03d970ad --- /dev/null +++ b/patterns/behavioral/observer.py @@ -0,0 +1,110 @@ +""" +http://code.activestate.com/recipes/131499-observer-pattern/ + +*TL;DR +Maintains a list of dependents and notifies them of any state changes. + +*Examples in Python ecosystem: +Django Signals: https://docs.djangoproject.com/en/3.1/topics/signals/ +Flask Signals: https://flask.palletsprojects.com/en/1.1.x/signals/ +""" + +from __future__ import annotations + +from contextlib import suppress +from typing import Protocol + + +# define a generic observer type +class Observer(Protocol): + def update(self, subject: Subject) -> None: + pass + + +class Subject: + def __init__(self) -> None: + self._observers: list[Observer] = [] + + def attach(self, observer: Observer) -> None: + if observer not in self._observers: + self._observers.append(observer) + + def detach(self, observer: Observer) -> None: + with suppress(ValueError): + self._observers.remove(observer) + + def notify(self, modifier: Observer | None = None) -> None: + for observer in self._observers: + if modifier != observer: + observer.update(self) + + +class Data(Subject): + def __init__(self, name: str = "") -> None: + super().__init__() + self.name = name + self._data = 0 + + @property + def data(self) -> int: + return self._data + + @data.setter + def data(self, value: int) -> None: + self._data = value + self.notify() + + +class HexViewer: + def update(self, subject: Data) -> None: + print(f"HexViewer: Subject {subject.name} has data 0x{subject.data:x}") + + +class DecimalViewer: + def update(self, subject: Data) -> None: + print(f"DecimalViewer: Subject {subject.name} has data {subject.data}") + + +def main(): + """ + >>> data1 = Data('Data 1') + >>> data2 = Data('Data 2') + >>> view1 = DecimalViewer() + >>> view2 = HexViewer() + >>> data1.attach(view1) + >>> data1.attach(view2) + >>> data2.attach(view2) + >>> data2.attach(view1) + + >>> data1.data = 10 + DecimalViewer: Subject Data 1 has data 10 + HexViewer: Subject Data 1 has data 0xa + + >>> data2.data = 15 + HexViewer: Subject Data 2 has data 0xf + DecimalViewer: Subject Data 2 has data 15 + + >>> data1.data = 3 + DecimalViewer: Subject Data 1 has data 3 + HexViewer: Subject Data 1 has data 0x3 + + >>> data2.data = 5 + HexViewer: Subject Data 2 has data 0x5 + DecimalViewer: Subject Data 2 has data 5 + + # Detach HexViewer from data1 and data2 + >>> data1.detach(view2) + >>> data2.detach(view2) + + >>> data1.data = 10 + DecimalViewer: Subject Data 1 has data 10 + + >>> data2.data = 15 + DecimalViewer: Subject Data 2 has data 15 + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/behavioral/publish_subscribe.py b/patterns/behavioral/publish_subscribe.py new file mode 100644 index 00000000..7e76955c --- /dev/null +++ b/patterns/behavioral/publish_subscribe.py @@ -0,0 +1,95 @@ +""" +Reference: +http://www.slideshare.net/ishraqabd/publish-subscribe-model-overview-13368808 +Author: https://github.com/HanWenfang +""" + +from __future__ import annotations + + +class Provider: + def __init__(self) -> None: + self.msg_queue = [] + self.subscribers = {} + + def notify(self, msg: str) -> None: + self.msg_queue.append(msg) + + def subscribe(self, msg: str, subscriber: Subscriber) -> None: + self.subscribers.setdefault(msg, []).append(subscriber) + + def unsubscribe(self, msg: str, subscriber: Subscriber) -> None: + self.subscribers[msg].remove(subscriber) + + def update(self) -> None: + for msg in self.msg_queue: + for sub in self.subscribers.get(msg, []): + sub.run(msg) + self.msg_queue = [] + + +class Publisher: + def __init__(self, msg_center: Provider) -> None: + self.provider = msg_center + + def publish(self, msg: str) -> None: + self.provider.notify(msg) + + +class Subscriber: + def __init__(self, name: str, msg_center: Provider) -> None: + self.name = name + self.provider = msg_center + + def subscribe(self, msg: str) -> None: + self.provider.subscribe(msg, self) + + def unsubscribe(self, msg: str) -> None: + self.provider.unsubscribe(msg, self) + + def run(self, msg: str) -> None: + print(f"{self.name} got {msg}") + + +def main(): + """ + >>> message_center = Provider() + + >>> fftv = Publisher(message_center) + + >>> jim = Subscriber("jim", message_center) + >>> jim.subscribe("cartoon") + >>> jack = Subscriber("jack", message_center) + >>> jack.subscribe("music") + >>> gee = Subscriber("gee", message_center) + >>> gee.subscribe("movie") + >>> vani = Subscriber("vani", message_center) + >>> vani.subscribe("movie") + >>> vani.unsubscribe("movie") + + # Note that no one subscribed to `ads` + # and that vani changed their mind + + >>> fftv.publish("cartoon") + >>> fftv.publish("music") + >>> fftv.publish("ads") + >>> fftv.publish("movie") + >>> fftv.publish("cartoon") + >>> fftv.publish("cartoon") + >>> fftv.publish("movie") + >>> fftv.publish("blank") + + >>> message_center.update() + jim got cartoon + jack got music + gee got movie + jim got cartoon + jim got cartoon + gee got movie + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/behavioral/registry.py b/patterns/behavioral/registry.py new file mode 100644 index 00000000..d44a992e --- /dev/null +++ b/patterns/behavioral/registry.py @@ -0,0 +1,49 @@ +from typing import Dict + + +class RegistryHolder(type): + + REGISTRY: Dict[str, "RegistryHolder"] = {} + + def __new__(cls, name, bases, attrs): + new_cls = type.__new__(cls, name, bases, attrs) + """ + Here the name of the class is used as key but it could be any class + parameter. + """ + cls.REGISTRY[new_cls.__name__] = new_cls + return new_cls + + @classmethod + def get_registry(cls): + return dict(cls.REGISTRY) + + +class BaseRegisteredClass(metaclass=RegistryHolder): + """ + Any class that will inherits from BaseRegisteredClass will be included + inside the dict RegistryHolder.REGISTRY, the key being the name of the + class and the associated value, the class itself. + """ + + +def main(): + """ + Before subclassing + >>> sorted(RegistryHolder.REGISTRY) + ['BaseRegisteredClass'] + + >>> class ClassRegistree(BaseRegisteredClass): + ... def __init__(self, *args, **kwargs): + ... pass + + After subclassing + >>> sorted(RegistryHolder.REGISTRY) + ['BaseRegisteredClass', 'ClassRegistree'] + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod(optionflags=doctest.ELLIPSIS) diff --git a/patterns/behavioral/specification.py b/patterns/behavioral/specification.py new file mode 100644 index 00000000..303ee513 --- /dev/null +++ b/patterns/behavioral/specification.py @@ -0,0 +1,109 @@ +""" +@author: Gordeev Andrey + +*TL;DR +Provides recombination business logic by chaining together using boolean logic. +""" + +from abc import abstractmethod + + +class Specification: + def and_specification(self, candidate): + raise NotImplementedError() + + def or_specification(self, candidate): + raise NotImplementedError() + + def not_specification(self): + raise NotImplementedError() + + @abstractmethod + def is_satisfied_by(self, candidate): + pass + + +class CompositeSpecification(Specification): + @abstractmethod + def is_satisfied_by(self, candidate): + pass + + def and_specification(self, candidate): + return AndSpecification(self, candidate) + + def or_specification(self, candidate): + return OrSpecification(self, candidate) + + def not_specification(self): + return NotSpecification(self) + + +class AndSpecification(CompositeSpecification): + def __init__(self, one, other): + self._one: Specification = one + self._other: Specification = other + + def is_satisfied_by(self, candidate): + return bool( + self._one.is_satisfied_by(candidate) + and self._other.is_satisfied_by(candidate) + ) + + +class OrSpecification(CompositeSpecification): + def __init__(self, one, other): + self._one: Specification = one + self._other: Specification = other + + def is_satisfied_by(self, candidate): + return bool( + self._one.is_satisfied_by(candidate) + or self._other.is_satisfied_by(candidate) + ) + + +class NotSpecification(CompositeSpecification): + def __init__(self, wrapped): + self._wrapped: Specification = wrapped + + def is_satisfied_by(self, candidate): + return bool(not self._wrapped.is_satisfied_by(candidate)) + + +class User: + def __init__(self, super_user=False): + self.super_user = super_user + + +class UserSpecification(CompositeSpecification): + def is_satisfied_by(self, candidate): + return isinstance(candidate, User) + + +class SuperUserSpecification(CompositeSpecification): + def is_satisfied_by(self, candidate): + return getattr(candidate, "super_user", False) + + +def main(): + """ + >>> andrey = User() + >>> ivan = User(super_user=True) + >>> vasiliy = 'not User instance' + + >>> root_specification = UserSpecification().and_specification(SuperUserSpecification()) + + # Is specification satisfied by + >>> root_specification.is_satisfied_by(andrey), 'andrey' + (False, 'andrey') + >>> root_specification.is_satisfied_by(ivan), 'ivan' + (True, 'ivan') + >>> root_specification.is_satisfied_by(vasiliy), 'vasiliy' + (False, 'vasiliy') + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/behavioral/state.py b/patterns/behavioral/state.py new file mode 100644 index 00000000..db4d9468 --- /dev/null +++ b/patterns/behavioral/state.py @@ -0,0 +1,89 @@ +""" +Implementation of the state pattern + +http://ginstrom.com/scribbles/2007/10/08/design-patterns-python-style/ + +*TL;DR +Implements state as a derived class of the state pattern interface. +Implements state transitions by invoking methods from the pattern's superclass. +""" + +from __future__ import annotations + + +class State: + """Base state. This is to share functionality""" + + def scan(self) -> None: + """Scan the dial to the next station""" + self.pos += 1 + if self.pos == len(self.stations): + self.pos = 0 + print(f"Scanning... Station is {self.stations[self.pos]} {self.name}") + + +class AmState(State): + def __init__(self, radio: Radio) -> None: + self.radio = radio + self.stations = ["1250", "1380", "1510"] + self.pos = 0 + self.name = "AM" + + def toggle_amfm(self) -> None: + print("Switching to FM") + self.radio.state = self.radio.fmstate + + +class FmState(State): + def __init__(self, radio: Radio) -> None: + self.radio = radio + self.stations = ["81.3", "89.1", "103.9"] + self.pos = 0 + self.name = "FM" + + def toggle_amfm(self) -> None: + print("Switching to AM") + self.radio.state = self.radio.amstate + + +class Radio: + """A radio. It has a scan button, and an AM/FM toggle switch.""" + + def __init__(self) -> None: + """We have an AM state and an FM state""" + self.amstate = AmState(self) + self.fmstate = FmState(self) + self.state = self.amstate + + def toggle_amfm(self) -> None: + self.state.toggle_amfm() + + def scan(self) -> None: + self.state.scan() + + +def main(): + """ + >>> radio = Radio() + >>> actions = [radio.scan] * 2 + [radio.toggle_amfm] + [radio.scan] * 2 + >>> actions *= 2 + + >>> for action in actions: + ... action() + Scanning... Station is 1380 AM + Scanning... Station is 1510 AM + Switching to FM + Scanning... Station is 89.1 FM + Scanning... Station is 103.9 FM + Scanning... Station is 81.3 FM + Scanning... Station is 89.1 FM + Switching to AM + Scanning... Station is 1250 AM + Scanning... Station is 1380 AM + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/behavioral/strategy.py b/patterns/behavioral/strategy.py new file mode 100644 index 00000000..000ff2ad --- /dev/null +++ b/patterns/behavioral/strategy.py @@ -0,0 +1,92 @@ +""" +*What is this pattern about? +Define a family of algorithms, encapsulate each one, and make them interchangeable. +Strategy lets the algorithm vary independently from clients that use it. + +*TL;DR +Enables selecting an algorithm at runtime. +""" + +from __future__ import annotations + +from typing import Callable + + +class DiscountStrategyValidator: # Descriptor class for check perform + @staticmethod + def validate(obj: Order, value: Callable) -> bool: + try: + if obj.price - value(obj) < 0: + raise ValueError( + f"Discount cannot be applied due to negative price resulting. {value.__name__}" + ) + except ValueError as ex: + print(str(ex)) + return False + else: + return True + + def __set_name__(self, owner, name: str) -> None: + self.private_name = f"_{name}" + + def __set__(self, obj: Order, value: Callable = None) -> None: + if value and self.validate(obj, value): + setattr(obj, self.private_name, value) + else: + setattr(obj, self.private_name, None) + + def __get__(self, obj: object, objtype: type = None): + return getattr(obj, self.private_name) + + +class Order: + discount_strategy = DiscountStrategyValidator() + + def __init__(self, price: float, discount_strategy: Callable = None) -> None: + self.price: float = price + self.discount_strategy = discount_strategy + + def apply_discount(self) -> float: + if self.discount_strategy: + discount = self.discount_strategy(self) + else: + discount = 0 + + return self.price - discount + + def __repr__(self) -> str: + strategy = getattr(self.discount_strategy, "__name__", None) + return f"" + + +def ten_percent_discount(order: Order) -> float: + return order.price * 0.10 + + +def on_sale_discount(order: Order) -> float: + return order.price * 0.25 + 20 + + +def main(): + """ + >>> order = Order(100, discount_strategy=ten_percent_discount) + >>> print(order) + + >>> print(order.apply_discount()) + 90.0 + >>> order = Order(100, discount_strategy=on_sale_discount) + >>> print(order) + + >>> print(order.apply_discount()) + 55.0 + >>> order = Order(10, discount_strategy=on_sale_discount) + Discount cannot be applied due to negative price resulting. on_sale_discount + >>> print(order) + + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/behavioral/template.py b/patterns/behavioral/template.py new file mode 100644 index 00000000..76fc136b --- /dev/null +++ b/patterns/behavioral/template.py @@ -0,0 +1,73 @@ +""" +An example of the Template pattern in Python + +*TL;DR +Defines the skeleton of a base algorithm, deferring definition of exact +steps to subclasses. + +*Examples in Python ecosystem: +Django class based views: https://docs.djangoproject.com/en/2.1/topics/class-based-views/ +""" + + +def get_text() -> str: + return "plain-text" + + +def get_pdf() -> str: + return "pdf" + + +def get_csv() -> str: + return "csv" + + +def convert_to_text(data: str) -> str: + print("[CONVERT]") + return f"{data} as text" + + +def saver() -> None: + print("[SAVE]") + + +def template_function(getter, converter=False, to_save=False) -> None: + data = getter() + print(f"Got `{data}`") + + if len(data) <= 3 and converter: + data = converter(data) + else: + print("Skip conversion") + + if to_save: + saver() + + print(f"`{data}` was processed") + + +def main(): + """ + >>> template_function(get_text, to_save=True) + Got `plain-text` + Skip conversion + [SAVE] + `plain-text` was processed + + >>> template_function(get_pdf, converter=convert_to_text) + Got `pdf` + [CONVERT] + `pdf as text` was processed + + >>> template_function(get_csv, to_save=True) + Got `csv` + Skip conversion + [SAVE] + `csv` was processed + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/behavioral/visitor.py b/patterns/behavioral/visitor.py new file mode 100644 index 00000000..00d95248 --- /dev/null +++ b/patterns/behavioral/visitor.py @@ -0,0 +1,74 @@ +""" +http://peter-hoffmann.com/2010/extrinsic-visitor-pattern-python-inheritance.html + +*TL;DR +Separates an algorithm from an object structure on which it operates. + +An interesting recipe could be found in +Brian Jones, David Beazley "Python Cookbook" (2013): +- "8.21. Implementing the Visitor Pattern" +- "8.22. Implementing the Visitor Pattern Without Recursion" + +*Examples in Python ecosystem: +- Python's ast.NodeVisitor: https://github.com/python/cpython/blob/master/Lib/ast.py#L250 +which is then being used e.g. in tools like `pyflakes`. +- `Black` formatter tool implements it's own: https://github.com/ambv/black/blob/master/black.py#L718 +""" + + +class Node: + pass + + +class A(Node): + pass + + +class B(Node): + pass + + +class C(A, B): + pass + + +class Visitor: + def visit(self, node, *args, **kwargs): + meth = None + for cls in node.__class__.__mro__: + meth_name = "visit_" + cls.__name__ + meth = getattr(self, meth_name, None) + if meth: + break + + if not meth: + meth = self.generic_visit + return meth(node, *args, **kwargs) + + def generic_visit(self, node, *args, **kwargs): + print("generic_visit " + node.__class__.__name__) + + def visit_B(self, node, *args, **kwargs): + print("visit_B " + node.__class__.__name__) + + +def main(): + """ + >>> a, b, c = A(), B(), C() + >>> visitor = Visitor() + + >>> visitor.visit(a) + generic_visit A + + >>> visitor.visit(b) + visit_B B + + >>> visitor.visit(c) + visit_B C + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/behavioral/viz/catalog.py.png b/patterns/behavioral/viz/catalog.py.png new file mode 100644 index 00000000..5942bed3 Binary files /dev/null and b/patterns/behavioral/viz/catalog.py.png differ diff --git a/patterns/behavioral/viz/chain.py.png b/patterns/behavioral/viz/chain.py.png new file mode 100644 index 00000000..878be087 Binary files /dev/null and b/patterns/behavioral/viz/chain.py.png differ diff --git a/patterns/behavioral/viz/chaining_method.py.png b/patterns/behavioral/viz/chaining_method.py.png new file mode 100644 index 00000000..9b3911da Binary files /dev/null and b/patterns/behavioral/viz/chaining_method.py.png differ diff --git a/patterns/behavioral/viz/command.py.png b/patterns/behavioral/viz/command.py.png new file mode 100644 index 00000000..049eee39 Binary files /dev/null and b/patterns/behavioral/viz/command.py.png differ diff --git a/patterns/behavioral/viz/iterator.py.png b/patterns/behavioral/viz/iterator.py.png new file mode 100644 index 00000000..6620dd56 Binary files /dev/null and b/patterns/behavioral/viz/iterator.py.png differ diff --git a/patterns/behavioral/viz/mediator.py.png b/patterns/behavioral/viz/mediator.py.png new file mode 100644 index 00000000..d3fb5492 Binary files /dev/null and b/patterns/behavioral/viz/mediator.py.png differ diff --git a/patterns/behavioral/viz/memento.py.png b/patterns/behavioral/viz/memento.py.png new file mode 100644 index 00000000..e24fa95c Binary files /dev/null and b/patterns/behavioral/viz/memento.py.png differ diff --git a/patterns/behavioral/viz/observer.py.png b/patterns/behavioral/viz/observer.py.png new file mode 100644 index 00000000..6b66bcf0 Binary files /dev/null and b/patterns/behavioral/viz/observer.py.png differ diff --git a/patterns/behavioral/viz/publish_subscribe.py.png b/patterns/behavioral/viz/publish_subscribe.py.png new file mode 100644 index 00000000..ffbe498e Binary files /dev/null and b/patterns/behavioral/viz/publish_subscribe.py.png differ diff --git a/patterns/behavioral/viz/registry.py.png b/patterns/behavioral/viz/registry.py.png new file mode 100644 index 00000000..cb0703f5 Binary files /dev/null and b/patterns/behavioral/viz/registry.py.png differ diff --git a/patterns/behavioral/viz/specification.py.png b/patterns/behavioral/viz/specification.py.png new file mode 100644 index 00000000..ee4f3378 Binary files /dev/null and b/patterns/behavioral/viz/specification.py.png differ diff --git a/patterns/behavioral/viz/state.py.png b/patterns/behavioral/viz/state.py.png new file mode 100644 index 00000000..0d2cb755 Binary files /dev/null and b/patterns/behavioral/viz/state.py.png differ diff --git a/patterns/behavioral/viz/strategy.py.png b/patterns/behavioral/viz/strategy.py.png new file mode 100644 index 00000000..f5bbf8d0 Binary files /dev/null and b/patterns/behavioral/viz/strategy.py.png differ diff --git a/patterns/behavioral/viz/template.py.png b/patterns/behavioral/viz/template.py.png new file mode 100644 index 00000000..4bd4d310 Binary files /dev/null and b/patterns/behavioral/viz/template.py.png differ diff --git a/patterns/behavioral/viz/visitor.py.png b/patterns/behavioral/viz/visitor.py.png new file mode 100644 index 00000000..c3e2e959 Binary files /dev/null and b/patterns/behavioral/viz/visitor.py.png differ diff --git a/patterns/creational/__init__.py b/patterns/creational/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/patterns/creational/abstract_factory.py b/patterns/creational/abstract_factory.py new file mode 100644 index 00000000..15e5d67f --- /dev/null +++ b/patterns/creational/abstract_factory.py @@ -0,0 +1,99 @@ +""" +*What is this pattern about? + +In Java and other languages, the Abstract Factory Pattern serves to provide an interface for +creating related/dependent objects without need to specify their +actual class. + +The idea is to abstract the creation of objects depending on business +logic, platform choice, etc. + +In Python, the interface we use is simply a callable, which is "builtin" interface +in Python, and in normal circumstances we can simply use the class itself as +that callable, because classes are first class objects in Python. + +*What does this example do? +This particular implementation abstracts the creation of a pet and +does so depending on the factory we chose (Dog or Cat, or random_animal) +This works because both Dog/Cat and random_animal respect a common +interface (callable for creation and .speak()). +Now my application can create pets abstractly and decide later, +based on my own criteria, dogs over cats. + +*Where is the pattern used practically? + +*References: +https://sourcemaking.com/design_patterns/abstract_factory +http://ginstrom.com/scribbles/2007/10/08/design-patterns-python-style/ + +*TL;DR +Provides a way to encapsulate a group of individual factories. +""" + +import random +from typing import Type + + +class Pet: + def __init__(self, name: str) -> None: + self.name = name + + def speak(self) -> None: + raise NotImplementedError + + def __str__(self) -> str: + raise NotImplementedError + + +class Dog(Pet): + def speak(self) -> None: + print("woof") + + def __str__(self) -> str: + return f"Dog<{self.name}>" + + +class Cat(Pet): + def speak(self) -> None: + print("meow") + + def __str__(self) -> str: + return f"Cat<{self.name}>" + + +class PetShop: + """A pet shop""" + + def __init__(self, animal_factory: Type[Pet]) -> None: + """pet_factory is our abstract factory. We can set it at will.""" + + self.pet_factory = animal_factory + + def buy_pet(self, name: str) -> Pet: + """Creates and shows a pet using the abstract factory""" + + pet = self.pet_factory(name) + print(f"Here is your lovely {pet}") + return pet + + +# Show pets with various factories +def main() -> None: + """ + # A Shop that sells only cats + >>> cat_shop = PetShop(Cat) + >>> pet = cat_shop.buy_pet("Lucy") + Here is your lovely Cat + >>> pet.speak() + meow + """ + + +if __name__ == "__main__": + animals = [Dog, Cat] + random_animal: Type[Pet] = random.choice(animals) + + shop = PetShop(random_animal) + import doctest + + doctest.testmod() diff --git a/patterns/creational/borg.py b/patterns/creational/borg.py new file mode 100644 index 00000000..edd0589d --- /dev/null +++ b/patterns/creational/borg.py @@ -0,0 +1,111 @@ +""" +*What is this pattern about? +The Borg pattern (also known as the Monostate pattern) is a way to +implement singleton behavior, but instead of having only one instance +of a class, there are multiple instances that share the same state. In +other words, the focus is on sharing state instead of sharing instance +identity. + +*What does this example do? +To understand the implementation of this pattern in Python, it is +important to know that, in Python, instance attributes are stored in a +attribute dictionary called __dict__. Usually, each instance will have +its own dictionary, but the Borg pattern modifies this so that all +instances have the same dictionary. +In this example, the __shared_state attribute will be the dictionary +shared between all instances, and this is ensured by assigning +__shared_state to the __dict__ variable when initializing a new +instance (i.e., in the __init__ method). Other attributes are usually +added to the instance's attribute dictionary, but, since the attribute +dictionary itself is shared (which is __shared_state), all other +attributes will also be shared. + +*Where is the pattern used practically? +Sharing state is useful in applications like managing database connections: +https://github.com/onetwopunch/pythonDbTemplate/blob/master/database.py + +*References: +- https://fkromer.github.io/python-pattern-references/design/#singleton +- https://learning.oreilly.com/library/view/python-cookbook/0596001673/ch05s23.html +- http://www.aleax.it/5ep.html + +*TL;DR +Provides singleton-like behavior sharing state between instances. +""" + +from typing import Dict + + +class Borg: + _shared_state: Dict[str, str] = {} + + def __init__(self) -> None: + self.__dict__ = self._shared_state + + +class YourBorg(Borg): + def __init__(self, state: str = None) -> None: + super().__init__() + if state: + self.state = state + else: + # initiate the first instance with default state + if not hasattr(self, "state"): + self.state = "Init" + + def __str__(self) -> str: + return self.state + + +def main(): + """ + >>> rm1 = YourBorg() + >>> rm2 = YourBorg() + + >>> rm1.state = 'Idle' + >>> rm2.state = 'Running' + + >>> print('rm1: {0}'.format(rm1)) + rm1: Running + >>> print('rm2: {0}'.format(rm2)) + rm2: Running + + # When the `state` attribute is modified from instance `rm2`, + # the value of `state` in instance `rm1` also changes + >>> rm2.state = 'Zombie' + + >>> print('rm1: {0}'.format(rm1)) + rm1: Zombie + >>> print('rm2: {0}'.format(rm2)) + rm2: Zombie + + # Even though `rm1` and `rm2` share attributes, the instances are not the same + >>> rm1 is rm2 + False + + # New instances also get the same shared state + >>> rm3 = YourBorg() + + >>> print('rm1: {0}'.format(rm1)) + rm1: Zombie + >>> print('rm2: {0}'.format(rm2)) + rm2: Zombie + >>> print('rm3: {0}'.format(rm3)) + rm3: Zombie + + # A new instance can explicitly change the state during creation + >>> rm4 = YourBorg('Running') + + >>> print('rm4: {0}'.format(rm4)) + rm4: Running + + # Existing instances reflect that change as well + >>> print('rm3: {0}'.format(rm3)) + rm3: Running + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/creational/builder.py b/patterns/creational/builder.py new file mode 100644 index 00000000..22383923 --- /dev/null +++ b/patterns/creational/builder.py @@ -0,0 +1,114 @@ +""" +*What is this pattern about? +It decouples the creation of a complex object and its representation, +so that the same process can be reused to build objects from the same +family. +This is useful when you must separate the specification of an object +from its actual representation (generally for abstraction). + +*What does this example do? + +The first example achieves this by using an abstract base +class for a building, where the initializer (__init__ method) specifies the +steps needed, and the concrete subclasses implement these steps. + +In other programming languages, a more complex arrangement is sometimes +necessary. In particular, you cannot have polymorphic behaviour in a constructor in C++ - +see https://stackoverflow.com/questions/1453131/how-can-i-get-polymorphic-behavior-in-a-c-constructor +- which means this Python technique will not work. The polymorphism +required has to be provided by an external, already constructed +instance of a different class. + +In general, in Python this won't be necessary, but a second example showing +this kind of arrangement is also included. + +*Where is the pattern used practically? + +*References: +https://sourcemaking.com/design_patterns/builder + +*TL;DR +Decouples the creation of a complex object and its representation. +""" + + +# Abstract Building +class Building: + def __init__(self) -> None: + self.build_floor() + self.build_size() + + def build_floor(self): + raise NotImplementedError + + def build_size(self): + raise NotImplementedError + + def __repr__(self) -> str: + return "Floor: {0.floor} | Size: {0.size}".format(self) + + +# Concrete Buildings +class House(Building): + def build_floor(self) -> None: + self.floor = "One" + + def build_size(self) -> None: + self.size = "Big" + + +class Flat(Building): + def build_floor(self) -> None: + self.floor = "More than One" + + def build_size(self) -> None: + self.size = "Small" + + +# In some very complex cases, it might be desirable to pull out the building +# logic into another function (or a method on another class), rather than being +# in the base class '__init__'. (This leaves you in the strange situation where +# a concrete class does not have a useful constructor) + + +class ComplexBuilding: + def __repr__(self) -> str: + return "Floor: {0.floor} | Size: {0.size}".format(self) + + +class ComplexHouse(ComplexBuilding): + def build_floor(self) -> None: + self.floor = "One" + + def build_size(self) -> None: + self.size = "Big and fancy" + + +def construct_building(cls) -> Building: + building = cls() + building.build_floor() + building.build_size() + return building + + +def main(): + """ + >>> house = House() + >>> house + Floor: One | Size: Big + + >>> flat = Flat() + >>> flat + Floor: More than One | Size: Small + + # Using an external constructor function: + >>> complex_house = construct_building(ComplexHouse) + >>> complex_house + Floor: One | Size: Big and fancy + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/creational/factory.py b/patterns/creational/factory.py new file mode 100644 index 00000000..3ef2d2a8 --- /dev/null +++ b/patterns/creational/factory.py @@ -0,0 +1,79 @@ +"""*What is this pattern about? +A Factory is an object for creating other objects. + +*What does this example do? +The code shows a way to localize words in two languages: English and +Greek. "get_localizer" is the factory function that constructs a +localizer depending on the language chosen. The localizer object will +be an instance from a different class according to the language +localized. However, the main code does not have to worry about which +localizer will be instantiated, since the method "localize" will be called +in the same way independently of the language. + +*Where can the pattern be used practically? +The Factory Method can be seen in the popular web framework Django: +https://docs.djangoproject.com/en/4.0/topics/forms/formsets/ +For example, different types of forms are created using a formset_factory + +*References: +http://ginstrom.com/scribbles/2007/10/08/design-patterns-python-style/ + +*TL;DR +Creates objects without having to specify the exact class. +""" + +from typing import Dict, Protocol, Type + + +class Localizer(Protocol): + def localize(self, msg: str) -> str: + pass + + +class GreekLocalizer: + """A simple localizer a la gettext""" + + def __init__(self) -> None: + self.translations = {"dog": "σκύλος", "cat": "γάτα"} + + def localize(self, msg: str) -> str: + """We'll punt if we don't have a translation""" + return self.translations.get(msg, msg) + + +class EnglishLocalizer: + """Simply echoes the message""" + + def localize(self, msg: str) -> str: + return msg + + +def get_localizer(language: str = "English") -> Localizer: + """Factory""" + localizers: Dict[str, Type[Localizer]] = { + "English": EnglishLocalizer, + "Greek": GreekLocalizer, + } + + return localizers[language]() + + +def main(): + """ + # Create our localizers + >>> e, g = get_localizer(language="English"), get_localizer(language="Greek") + + # Localize some text + >>> for msg in "dog parrot cat bear".split(): + ... print(e.localize(msg), g.localize(msg)) + dog σκύλος + parrot parrot + cat γάτα + bear bear + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/creational/lazy_evaluation.py b/patterns/creational/lazy_evaluation.py new file mode 100644 index 00000000..b56daf0c --- /dev/null +++ b/patterns/creational/lazy_evaluation.py @@ -0,0 +1,111 @@ +""" +Lazily-evaluated property pattern in Python. + +https://en.wikipedia.org/wiki/Lazy_evaluation + +*References: +bottle +https://github.com/bottlepy/bottle/blob/cafc15419cbb4a6cb748e6ecdccf92893bb25ce5/bottle.py#L270 +django +https://github.com/django/django/blob/ffd18732f3ee9e6f0374aff9ccf350d85187fac2/django/utils/functional.py#L19 +pip +https://github.com/pypa/pip/blob/cb75cca785629e15efb46c35903827b3eae13481/pip/utils/__init__.py#L821 +pyramid +https://github.com/Pylons/pyramid/blob/7909e9503cdfc6f6e84d2c7ace1d3c03ca1d8b73/pyramid/decorator.py#L4 +werkzeug +https://github.com/pallets/werkzeug/blob/5a2bf35441006d832ab1ed5a31963cbc366c99ac/werkzeug/utils.py#L35 + +*TL;DR +Delays the eval of an expr until its value is needed and avoids repeated evals. +""" + +import functools + + +class lazy_property: + def __init__(self, function): + self.function = function + functools.update_wrapper(self, function) + + def __get__(self, obj, type_): + if obj is None: + return self + val = self.function(obj) + obj.__dict__[self.function.__name__] = val + return val + + +def lazy_property2(fn): + """ + A lazy property decorator. + + The function decorated is called the first time to retrieve the result and + then that calculated result is used the next time you access the value. + """ + attr = "_lazy__" + fn.__name__ + + @property + def _lazy_property(self): + if not hasattr(self, attr): + setattr(self, attr, fn(self)) + return getattr(self, attr) + + return _lazy_property + + +class Person: + def __init__(self, name, occupation): + self.name = name + self.occupation = occupation + self.call_count2 = 0 + + @lazy_property + def relatives(self): + # Get all relatives, let's assume that it costs much time. + relatives = "Many relatives." + return relatives + + @lazy_property2 + def parents(self): + self.call_count2 += 1 + return "Father and mother" + + +def main(): + """ + >>> Jhon = Person('Jhon', 'Coder') + + >>> Jhon.name + 'Jhon' + >>> Jhon.occupation + 'Coder' + + # Before we access `relatives` + >>> sorted(Jhon.__dict__.items()) + [('call_count2', 0), ('name', 'Jhon'), ('occupation', 'Coder')] + + >>> Jhon.relatives + 'Many relatives.' + + # After we've accessed `relatives` + >>> sorted(Jhon.__dict__.items()) + [('call_count2', 0), ..., ('relatives', 'Many relatives.')] + + >>> Jhon.parents + 'Father and mother' + + >>> sorted(Jhon.__dict__.items()) + [('_lazy__parents', 'Father and mother'), ('call_count2', 1), ..., ('relatives', 'Many relatives.')] + + >>> Jhon.parents + 'Father and mother' + + >>> Jhon.call_count2 + 1 + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod(optionflags=doctest.ELLIPSIS) diff --git a/patterns/creational/pool.py b/patterns/creational/pool.py new file mode 100644 index 00000000..1d70ea69 --- /dev/null +++ b/patterns/creational/pool.py @@ -0,0 +1,86 @@ +""" +*What is this pattern about? +This pattern is used when creating an object is costly (and they are +created frequently) but only a few are used at a time. With a Pool we +can manage those instances we have as of now by caching them. Now it +is possible to skip the costly creation of an object if one is +available in the pool. +A pool allows to 'check out' an inactive object and then to return it. +If none are available the pool creates one to provide without wait. + +*What does this example do? +In this example queue.Queue is used to create the pool (wrapped in a +custom ObjectPool object to use with the with statement), and it is +populated with strings. +As we can see, the first string object put in "yam" is USED by the +with statement. But because it is released back into the pool +afterwards it is reused by the explicit call to sample_queue.get(). +Same thing happens with "sam", when the ObjectPool created inside the +function is deleted (by the GC) and the object is returned. + +*Where is the pattern used practically? + +*References: +http://stackoverflow.com/questions/1514120/python-implementation-of-the-object-pool-design-pattern +https://sourcemaking.com/design_patterns/object_pool + +*TL;DR +Stores a set of initialized objects kept ready to use. +""" + + +class ObjectPool: + def __init__(self, queue, auto_get=False): + self._queue = queue + self.item = self._queue.get() if auto_get else None + + def __enter__(self): + if self.item is None: + self.item = self._queue.get() + return self.item + + def __exit__(self, Type, value, traceback): + if self.item is not None: + self._queue.put(self.item) + self.item = None + + def __del__(self): + if self.item is not None: + self._queue.put(self.item) + self.item = None + + +def main(): + """ + >>> import queue + + >>> def test_object(queue): + ... pool = ObjectPool(queue, True) + ... print('Inside func: {}'.format(pool.item)) + + >>> sample_queue = queue.Queue() + + >>> sample_queue.put('yam') + >>> with ObjectPool(sample_queue) as obj: + ... print('Inside with: {}'.format(obj)) + Inside with: yam + + >>> print('Outside with: {}'.format(sample_queue.get())) + Outside with: yam + + >>> sample_queue.put('sam') + >>> test_object(sample_queue) + Inside func: sam + + >>> print('Outside func: {}'.format(sample_queue.get())) + Outside func: sam + + if not sample_queue.empty(): + print(sample_queue.get()) + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/creational/prototype.py b/patterns/creational/prototype.py new file mode 100644 index 00000000..4c2dd7ed --- /dev/null +++ b/patterns/creational/prototype.py @@ -0,0 +1,83 @@ +""" +*What is this pattern about? +This patterns aims to reduce the number of classes required by an +application. Instead of relying on subclasses it creates objects by +copying a prototypical instance at run-time. + +This is useful as it makes it easier to derive new kinds of objects, +when instances of the class have only a few different combinations of +state, and when instantiation is expensive. + +*What does this example do? +When the number of prototypes in an application can vary, it can be +useful to keep a Dispatcher (aka, Registry or Manager). This allows +clients to query the Dispatcher for a prototype before cloning a new +instance. + +Below provides an example of such Dispatcher, which contains three +copies of the prototype: 'default', 'objecta' and 'objectb'. + +*TL;DR +Creates new object instances by cloning prototype. +""" + +from __future__ import annotations + +from typing import Any + + +class Prototype: + def __init__(self, value: str = "default", **attrs: Any) -> None: + self.value = value + self.__dict__.update(attrs) + + def clone(self, **attrs: Any) -> Prototype: + """Clone a prototype and update inner attributes dictionary""" + # Python in Practice, Mark Summerfield + # copy.deepcopy can be used instead of next line. + obj = self.__class__(**self.__dict__) + obj.__dict__.update(attrs) + return obj + + +class PrototypeDispatcher: + def __init__(self): + self._objects = {} + + def get_objects(self) -> dict[str, Prototype]: + """Get all objects""" + return self._objects + + def register_object(self, name: str, obj: Prototype) -> None: + """Register an object""" + self._objects[name] = obj + + def unregister_object(self, name: str) -> None: + """Unregister an object""" + del self._objects[name] + + +def main() -> None: + """ + >>> dispatcher = PrototypeDispatcher() + >>> prototype = Prototype() + + >>> d = prototype.clone() + >>> a = prototype.clone(value='a-value', category='a') + >>> b = a.clone(value='b-value', is_checked=True) + >>> dispatcher.register_object('objecta', a) + >>> dispatcher.register_object('objectb', b) + >>> dispatcher.register_object('default', d) + + >>> [{n: p.value} for n, p in dispatcher.get_objects().items()] + [{'objecta': 'a-value'}, {'objectb': 'b-value'}, {'default': 'default'}] + + >>> print(b.category, b.is_checked) + a True + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/creational/viz/abstract_factory.py.png b/patterns/creational/viz/abstract_factory.py.png new file mode 100644 index 00000000..9f301bd8 Binary files /dev/null and b/patterns/creational/viz/abstract_factory.py.png differ diff --git a/patterns/creational/viz/borg.py.png b/patterns/creational/viz/borg.py.png new file mode 100644 index 00000000..6680e43b Binary files /dev/null and b/patterns/creational/viz/borg.py.png differ diff --git a/patterns/creational/viz/builder.py.png b/patterns/creational/viz/builder.py.png new file mode 100644 index 00000000..89e8e39f Binary files /dev/null and b/patterns/creational/viz/builder.py.png differ diff --git a/patterns/creational/viz/factory_method.py.png b/patterns/creational/viz/factory_method.py.png new file mode 100644 index 00000000..c990ea00 Binary files /dev/null and b/patterns/creational/viz/factory_method.py.png differ diff --git a/patterns/creational/viz/lazy_evaluation.py.png b/patterns/creational/viz/lazy_evaluation.py.png new file mode 100644 index 00000000..36fa237c Binary files /dev/null and b/patterns/creational/viz/lazy_evaluation.py.png differ diff --git a/patterns/creational/viz/pool.py.png b/patterns/creational/viz/pool.py.png new file mode 100644 index 00000000..f71465ae Binary files /dev/null and b/patterns/creational/viz/pool.py.png differ diff --git a/patterns/creational/viz/prototype.py.png b/patterns/creational/viz/prototype.py.png new file mode 100644 index 00000000..fd9ef84b Binary files /dev/null and b/patterns/creational/viz/prototype.py.png differ diff --git a/patterns/dependency_injection.py b/patterns/dependency_injection.py new file mode 100644 index 00000000..2979f763 --- /dev/null +++ b/patterns/dependency_injection.py @@ -0,0 +1,116 @@ +""" +Dependency Injection (DI) is a technique whereby one object supplies the dependencies (services) +to another object (client). +It allows to decouple objects: no need to change client code simply because an object it depends on +needs to be changed to a different one. (Open/Closed principle) + +Port of the Java example of Dependency Injection" in +"xUnit Test Patterns - Refactoring Test Code" by Gerard Meszaros +(ISBN-10: 0131495054, ISBN-13: 978-0131495050) + +In the following example `time_provider` (service) is embedded into TimeDisplay (client). +If such service performed an expensive operation you would like to substitute or mock it in tests. + +class TimeDisplay(object): + + def __init__(self): + self.time_provider = datetime.datetime.now + + def get_current_time_as_html_fragment(self): + current_time = self.time_provider() + current_time_as_html_fragment = "{}".format(current_time) + return current_time_as_html_fragment + +""" + +import datetime +from typing import Callable + + +class ConstructorInjection: + def __init__(self, time_provider: Callable) -> None: + self.time_provider = time_provider + + def get_current_time_as_html_fragment(self) -> str: + current_time = self.time_provider() + current_time_as_html_fragment = '{}'.format( + current_time + ) + return current_time_as_html_fragment + + +class ParameterInjection: + def __init__(self) -> None: + pass + + def get_current_time_as_html_fragment(self, time_provider: Callable) -> str: + current_time = time_provider() + current_time_as_html_fragment = '{}'.format( + current_time + ) + return current_time_as_html_fragment + + +class SetterInjection: + """Setter Injection""" + + def __init__(self): + pass + + def set_time_provider(self, time_provider: Callable): + self.time_provider = time_provider + + def get_current_time_as_html_fragment(self): + current_time = self.time_provider() + current_time_as_html_fragment = '{}'.format( + current_time + ) + return current_time_as_html_fragment + + +def production_code_time_provider() -> str: + """ + Production code version of the time provider (just a wrapper for formatting + datetime for this example). + """ + current_time = datetime.datetime.now() + current_time_formatted = f"{current_time.hour}:{current_time.minute}" + return current_time_formatted + + +def midnight_time_provider() -> str: + """Hard-coded stub""" + return "24:01" + + +def main(): + """ + >>> time_with_ci1 = ConstructorInjection(midnight_time_provider) + >>> time_with_ci1.get_current_time_as_html_fragment() + '24:01' + + >>> time_with_ci2 = ConstructorInjection(production_code_time_provider) + >>> time_with_ci2.get_current_time_as_html_fragment() + '...' + + >>> time_with_pi = ParameterInjection() + >>> time_with_pi.get_current_time_as_html_fragment(midnight_time_provider) + '24:01' + + >>> time_with_si = SetterInjection() + + >>> time_with_si.get_current_time_as_html_fragment() + Traceback (most recent call last): + ... + AttributeError: 'SetterInjection' object has no attribute 'time_provider' + + >>> time_with_si.set_time_provider(midnight_time_provider) + >>> time_with_si.get_current_time_as_html_fragment() + '24:01' + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod(optionflags=doctest.ELLIPSIS) diff --git a/patterns/fundamental/__init__.py b/patterns/fundamental/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/patterns/fundamental/delegation_pattern.py b/patterns/fundamental/delegation_pattern.py new file mode 100644 index 00000000..f7a7c2f5 --- /dev/null +++ b/patterns/fundamental/delegation_pattern.py @@ -0,0 +1,59 @@ +""" +Reference: https://en.wikipedia.org/wiki/Delegation_pattern +Author: https://github.com/IuryAlves + +*TL;DR +Allows object composition to achieve the same code reuse as inheritance. +""" + +from __future__ import annotations + +from typing import Any, Callable + + +class Delegator: + """ + >>> delegator = Delegator(Delegate()) + >>> delegator.p1 + 123 + >>> delegator.p2 + Traceback (most recent call last): + ... + AttributeError: 'Delegate' object has no attribute 'p2'. Did you mean: 'p1'? + >>> delegator.do_something("nothing") + 'Doing nothing' + >>> delegator.do_something("something", kw=", faif!") + 'Doing something, faif!' + >>> delegator.do_anything() + Traceback (most recent call last): + ... + AttributeError: 'Delegate' object has no attribute 'do_anything'. Did you mean: 'do_something'? + """ + + def __init__(self, delegate: Delegate) -> None: + self.delegate = delegate + + def __getattr__(self, name: str) -> Any | Callable: + attr = getattr(self.delegate, name) + + if not callable(attr): + return attr + + def wrapper(*args, **kwargs): + return attr(*args, **kwargs) + + return wrapper + + +class Delegate: + def __init__(self) -> None: + self.p1 = 123 + + def do_something(self, something: str, kw=None) -> str: + return f"Doing {something}{kw or ''}" + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/fundamental/viz/delegation_pattern.py.png b/patterns/fundamental/viz/delegation_pattern.py.png new file mode 100644 index 00000000..3e895f80 Binary files /dev/null and b/patterns/fundamental/viz/delegation_pattern.py.png differ diff --git a/patterns/other/__init__.py b/patterns/other/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/patterns/other/blackboard.py b/patterns/other/blackboard.py new file mode 100644 index 00000000..58fbdb98 --- /dev/null +++ b/patterns/other/blackboard.py @@ -0,0 +1,137 @@ +""" +@author: Eugene Duboviy | github.com/duboviy + +In Blackboard pattern several specialised sub-systems (knowledge sources) +assemble their knowledge to build a possibly partial or approximate solution. +In this way, the sub-systems work together to solve the problem, +where the solution is the sum of its parts. + +https://en.wikipedia.org/wiki/Blackboard_system +""" + +from abc import ABC, abstractmethod +import random + + +class AbstractExpert(ABC): + """Abstract class for experts in the blackboard system.""" + @abstractmethod + def __init__(self, blackboard) -> None: + self.blackboard = blackboard + + @property + @abstractmethod + def is_eager_to_contribute(self) -> int: + raise NotImplementedError("Must provide implementation in subclass.") + + @abstractmethod + def contribute(self) -> None: + raise NotImplementedError("Must provide implementation in subclass.") + + +class Blackboard: + """The blackboard system that holds the common state.""" + def __init__(self) -> None: + self.experts: list = [] + self.common_state = { + "problems": 0, + "suggestions": 0, + "contributions": [], + "progress": 0, # percentage, if 100 -> task is finished + } + + def add_expert(self, expert: AbstractExpert) -> None: + self.experts.append(expert) + + +class Controller: + """The controller that manages the blackboard system.""" + def __init__(self, blackboard: Blackboard) -> None: + self.blackboard = blackboard + + def run_loop(self): + """ + This function is a loop that runs until the progress reaches 100. + It checks if an expert is eager to contribute and then calls its contribute method. + """ + while self.blackboard.common_state["progress"] < 100: + for expert in self.blackboard.experts: + if expert.is_eager_to_contribute: + expert.contribute() + return self.blackboard.common_state["contributions"] + + +class Student(AbstractExpert): + """Concrete class for a student expert.""" + def __init__(self, blackboard) -> None: + super().__init__(blackboard) + + @property + def is_eager_to_contribute(self) -> bool: + return True + + def contribute(self) -> None: + self.blackboard.common_state["problems"] += random.randint(1, 10) + self.blackboard.common_state["suggestions"] += random.randint(1, 10) + self.blackboard.common_state["contributions"] += [self.__class__.__name__] + self.blackboard.common_state["progress"] += random.randint(1, 2) + + +class Scientist(AbstractExpert): + """Concrete class for a scientist expert.""" + def __init__(self, blackboard) -> None: + super().__init__(blackboard) + + @property + def is_eager_to_contribute(self) -> int: + return random.randint(0, 1) + + def contribute(self) -> None: + self.blackboard.common_state["problems"] += random.randint(10, 20) + self.blackboard.common_state["suggestions"] += random.randint(10, 20) + self.blackboard.common_state["contributions"] += [self.__class__.__name__] + self.blackboard.common_state["progress"] += random.randint(10, 30) + + +class Professor(AbstractExpert): + def __init__(self, blackboard) -> None: + super().__init__(blackboard) + + @property + def is_eager_to_contribute(self) -> bool: + return True if self.blackboard.common_state["problems"] > 100 else False + + def contribute(self) -> None: + self.blackboard.common_state["problems"] += random.randint(1, 2) + self.blackboard.common_state["suggestions"] += random.randint(10, 20) + self.blackboard.common_state["contributions"] += [self.__class__.__name__] + self.blackboard.common_state["progress"] += random.randint(10, 100) + + +def main(): + """ + >>> blackboard = Blackboard() + >>> blackboard.add_expert(Student(blackboard)) + >>> blackboard.add_expert(Scientist(blackboard)) + >>> blackboard.add_expert(Professor(blackboard)) + + >>> c = Controller(blackboard) + >>> contributions = c.run_loop() + + >>> from pprint import pprint + >>> pprint(contributions) + ['Student', + 'Scientist', + 'Student', + 'Scientist', + 'Student', + 'Scientist', + 'Professor'] + """ + + +if __name__ == "__main__": + random.seed(1234) # for deterministic doctest outputs + import doctest + + doctest.testmod() diff --git a/patterns/other/graph_search.py b/patterns/other/graph_search.py new file mode 100644 index 00000000..262a6f08 --- /dev/null +++ b/patterns/other/graph_search.py @@ -0,0 +1,150 @@ +class GraphSearch: + """Graph search emulation in python, from source + http://www.python.org/doc/essays/graphs/ + + dfs stands for Depth First Search + bfs stands for Breadth First Search""" + + def __init__(self, graph): + self.graph = graph + + def find_path_dfs(self, start, end, path=None): + path = path or [] + + path.append(start) + if start == end: + return path + for node in self.graph.get(start, []): + if node not in path: + newpath = self.find_path_dfs(node, end, path[:]) + if newpath: + return newpath + + def find_all_paths_dfs(self, start, end, path=None): + path = path or [] + path.append(start) + if start == end: + return [path] + paths = [] + for node in self.graph.get(start, []): + if node not in path: + newpaths = self.find_all_paths_dfs(node, end, path[:]) + paths.extend(newpaths) + return paths + + def find_shortest_path_dfs(self, start, end, path=None): + path = path or [] + path.append(start) + + if start == end: + return path + shortest = None + for node in self.graph.get(start, []): + if node not in path: + newpath = self.find_shortest_path_dfs(node, end, path[:]) + if newpath: + if not shortest or len(newpath) < len(shortest): + shortest = newpath + return shortest + + def find_shortest_path_bfs(self, start, end): + """ + Finds the shortest path between two nodes in a graph using breadth-first search. + + :param start: The node to start from. + :type start: str or int + :param end: The node to find the shortest path to. + :type end: str or int + + :returns queue_path_to_end, dist_to[end]: A list of nodes + representing the shortest path from `start` to `end`, and a dictionary + mapping each node in the graph (except for `start`) with its distance from it + (in terms of hops). If no such path exists, returns an empty list and an empty + dictionary instead. + """ + queue = [start] + dist_to = {start: 0} + edge_to = {} + + if start == end: + return queue + + while len(queue): + value = queue.pop(0) + for node in self.graph[value]: + if node not in dist_to.keys(): + edge_to[node] = value + dist_to[node] = dist_to[value] + 1 + queue.append(node) + if end in edge_to.keys(): + path = [] + node = end + while dist_to[node] != 0: + path.insert(0, node) + node = edge_to[node] + path.insert(0, start) + return path + + +def main(): + """ + # example of graph usage + >>> graph = { + ... 'A': ['B', 'C'], + ... 'B': ['C', 'D'], + ... 'C': ['D', 'G'], + ... 'D': ['C'], + ... 'E': ['F'], + ... 'F': ['C'], + ... 'G': ['E'], + ... 'H': ['C'] + ... } + + # initialization of new graph search object + >>> graph_search = GraphSearch(graph) + + >>> print(graph_search.find_path_dfs('A', 'D')) + ['A', 'B', 'C', 'D'] + + # start the search somewhere in the middle + >>> print(graph_search.find_path_dfs('G', 'F')) + ['G', 'E', 'F'] + + # unreachable node + >>> print(graph_search.find_path_dfs('C', 'H')) + None + + # non existing node + >>> print(graph_search.find_path_dfs('C', 'X')) + None + + >>> print(graph_search.find_all_paths_dfs('A', 'D')) + [['A', 'B', 'C', 'D'], ['A', 'B', 'D'], ['A', 'C', 'D']] + >>> print(graph_search.find_shortest_path_dfs('A', 'D')) + ['A', 'B', 'D'] + >>> print(graph_search.find_shortest_path_dfs('A', 'F')) + ['A', 'C', 'G', 'E', 'F'] + + >>> print(graph_search.find_shortest_path_bfs('A', 'D')) + ['A', 'B', 'D'] + >>> print(graph_search.find_shortest_path_bfs('A', 'F')) + ['A', 'C', 'G', 'E', 'F'] + + # start the search somewhere in the middle + >>> print(graph_search.find_shortest_path_bfs('G', 'F')) + ['G', 'E', 'F'] + + # unreachable node + >>> print(graph_search.find_shortest_path_bfs('A', 'H')) + None + + # non existing node + >>> print(graph_search.find_shortest_path_bfs('A', 'X')) + None + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/other/hsm/__init__.py b/patterns/other/hsm/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/patterns/other/hsm/classes_hsm.png b/patterns/other/hsm/classes_hsm.png new file mode 100644 index 00000000..1a649861 Binary files /dev/null and b/patterns/other/hsm/classes_hsm.png differ diff --git a/patterns/other/hsm/classes_test_hsm.png b/patterns/other/hsm/classes_test_hsm.png new file mode 100644 index 00000000..e7f78441 Binary files /dev/null and b/patterns/other/hsm/classes_test_hsm.png differ diff --git a/patterns/other/hsm/hsm.py b/patterns/other/hsm/hsm.py new file mode 100644 index 00000000..44498014 --- /dev/null +++ b/patterns/other/hsm/hsm.py @@ -0,0 +1,177 @@ +""" +Implementation of the HSM (hierarchical state machine) or +NFSM (nested finite state machine) C++ example from +http://www.eventhelix.com/RealtimeMantra/HierarchicalStateMachine.htm#.VwqLVEL950w +in Python + +- single source 'message type' for state transition changes +- message type considered, messages (comment) not considered to avoid complexity +""" + + +class UnsupportedMessageType(BaseException): + pass + + +class UnsupportedState(BaseException): + pass + + +class UnsupportedTransition(BaseException): + pass + + +class HierachicalStateMachine: + def __init__(self): + self._active_state = Active(self) # Unit.Inservice.Active() + self._standby_state = Standby(self) # Unit.Inservice.Standby() + self._suspect_state = Suspect(self) # Unit.OutOfService.Suspect() + self._failed_state = Failed(self) # Unit.OutOfService.Failed() + self._current_state = self._standby_state + self.states = { + "active": self._active_state, + "standby": self._standby_state, + "suspect": self._suspect_state, + "failed": self._failed_state, + } + self.message_types = { + "fault trigger": self._current_state.on_fault_trigger, + "switchover": self._current_state.on_switchover, + "diagnostics passed": self._current_state.on_diagnostics_passed, + "diagnostics failed": self._current_state.on_diagnostics_failed, + "operator inservice": self._current_state.on_operator_inservice, + } + + def _next_state(self, state): + try: + self._current_state = self.states[state] + except KeyError: + raise UnsupportedState + + def _send_diagnostics_request(self): + return "send diagnostic request" + + def _raise_alarm(self): + return "raise alarm" + + def _clear_alarm(self): + return "clear alarm" + + def _perform_switchover(self): + return "perform switchover" + + def _send_switchover_response(self): + return "send switchover response" + + def _send_operator_inservice_response(self): + return "send operator inservice response" + + def _send_diagnostics_failure_report(self): + return "send diagnostics failure report" + + def _send_diagnostics_pass_report(self): + return "send diagnostics pass report" + + def _abort_diagnostics(self): + return "abort diagnostics" + + def _check_mate_status(self): + return "check mate status" + + def on_message(self, message_type): # message ignored + if message_type in self.message_types.keys(): + self.message_types[message_type]() + else: + raise UnsupportedMessageType + + +class Unit: + def __init__(self, HierachicalStateMachine): + self.hsm = HierachicalStateMachine + + def on_switchover(self): + raise UnsupportedTransition + + def on_fault_trigger(self): + raise UnsupportedTransition + + def on_diagnostics_failed(self): + raise UnsupportedTransition + + def on_diagnostics_passed(self): + raise UnsupportedTransition + + def on_operator_inservice(self): + raise UnsupportedTransition + + +class Inservice(Unit): + def __init__(self, HierachicalStateMachine): + self._hsm = HierachicalStateMachine + + def on_fault_trigger(self): + self._hsm._next_state("suspect") + self._hsm._send_diagnostics_request() + self._hsm._raise_alarm() + + def on_switchover(self): + self._hsm._perform_switchover() + self._hsm._check_mate_status() + self._hsm._send_switchover_response() + + +class Active(Inservice): + def __init__(self, HierachicalStateMachine): + self._hsm = HierachicalStateMachine + + def on_fault_trigger(self): + super().perform_switchover() + super().on_fault_trigger() + + def on_switchover(self): + self._hsm.on_switchover() # message ignored + self._hsm.next_state("standby") + + +class Standby(Inservice): + def __init__(self, HierachicalStateMachine): + self._hsm = HierachicalStateMachine + + def on_switchover(self): + super().on_switchover() # message ignored + self._hsm._next_state("active") + + +class OutOfService(Unit): + def __init__(self, HierachicalStateMachine): + self._hsm = HierachicalStateMachine + + def on_operator_inservice(self): + self._hsm.on_switchover() # message ignored + self._hsm.send_operator_inservice_response() + self._hsm.next_state("suspect") + + +class Suspect(OutOfService): + def __init__(self, HierachicalStateMachine): + self._hsm = HierachicalStateMachine + + def on_diagnostics_failed(self): + super().send_diagnostics_failure_report() + super().next_state("failed") + + def on_diagnostics_passed(self): + super().send_diagnostics_pass_report() + super().clear_alarm() # loss of redundancy alarm + super().next_state("standby") + + def on_operator_inservice(self): + super().abort_diagnostics() + super().on_operator_inservice() # message ignored + + +class Failed(OutOfService): + """No need to override any method.""" + + def __init__(self, HierachicalStateMachine): + self._hsm = HierachicalStateMachine diff --git a/patterns/structural/3-tier.py b/patterns/structural/3-tier.py new file mode 100644 index 00000000..ecc04243 --- /dev/null +++ b/patterns/structural/3-tier.py @@ -0,0 +1,98 @@ +""" +*TL;DR +Separates presentation, application processing, and data management functions. +""" + +from typing import Dict, KeysView, Optional, Union + + +class Data: + """Data Store Class""" + + products = { + "milk": {"price": 1.50, "quantity": 10}, + "eggs": {"price": 0.20, "quantity": 100}, + "cheese": {"price": 2.00, "quantity": 10}, + } + + def __get__(self, obj, klas): + + print("(Fetching from Data Store)") + return {"products": self.products} + + +class BusinessLogic: + """Business logic holding data store instances""" + + data = Data() + + def product_list(self) -> KeysView[str]: + return self.data["products"].keys() + + def product_information( + self, product: str + ) -> Optional[Dict[str, Union[int, float]]]: + return self.data["products"].get(product, None) + + +class Ui: + """UI interaction class""" + + def __init__(self) -> None: + self.business_logic = BusinessLogic() + + def get_product_list(self) -> None: + print("PRODUCT LIST:") + for product in self.business_logic.product_list(): + print(product) + print("") + + def get_product_information(self, product: str) -> None: + product_info = self.business_logic.product_information(product) + if product_info: + print("PRODUCT INFORMATION:") + print( + f"Name: {product.title()}, " + + f"Price: {product_info.get('price', 0):.2f}, " + + f"Quantity: {product_info.get('quantity', 0):}" + ) + else: + print(f"That product '{product}' does not exist in the records") + + +def main(): + """ + >>> ui = Ui() + >>> ui.get_product_list() + PRODUCT LIST: + (Fetching from Data Store) + milk + eggs + cheese + + + >>> ui.get_product_information("cheese") + (Fetching from Data Store) + PRODUCT INFORMATION: + Name: Cheese, Price: 2.00, Quantity: 10 + + >>> ui.get_product_information("eggs") + (Fetching from Data Store) + PRODUCT INFORMATION: + Name: Eggs, Price: 0.20, Quantity: 100 + + >>> ui.get_product_information("milk") + (Fetching from Data Store) + PRODUCT INFORMATION: + Name: Milk, Price: 1.50, Quantity: 10 + + >>> ui.get_product_information("arepas") + (Fetching from Data Store) + That product 'arepas' does not exist in the records + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/structural/__init__.py b/patterns/structural/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/patterns/structural/adapter.py b/patterns/structural/adapter.py new file mode 100644 index 00000000..433369ee --- /dev/null +++ b/patterns/structural/adapter.py @@ -0,0 +1,125 @@ +""" +*What is this pattern about? +The Adapter pattern provides a different interface for a class. We can +think about it as a cable adapter that allows you to charge a phone +somewhere that has outlets in a different shape. Following this idea, +the Adapter pattern is useful to integrate classes that couldn't be +integrated due to their incompatible interfaces. + +*What does this example do? + +The example has classes that represent entities (Dog, Cat, Human, Car) +that make different noises. The Adapter class provides a different +interface to the original methods that make such noises. So the +original interfaces (e.g., bark and meow) are available under a +different name: make_noise. + +*Where is the pattern used practically? +The Grok framework uses adapters to make objects work with a +particular API without modifying the objects themselves: +http://grok.zope.org/doc/current/grok_overview.html#adapters + +*References: +http://ginstrom.com/scribbles/2008/11/06/generic-adapter-class-in-python/ +https://sourcemaking.com/design_patterns/adapter +http://python-3-patterns-idioms-test.readthedocs.io/en/latest/ChangeInterface.html#adapter + +*TL;DR +Allows the interface of an existing class to be used as another interface. +""" + +from typing import Callable, TypeVar + +T = TypeVar("T") + + +class Dog: + def __init__(self) -> None: + self.name = "Dog" + + def bark(self) -> str: + return "woof!" + + +class Cat: + def __init__(self) -> None: + self.name = "Cat" + + def meow(self) -> str: + return "meow!" + + +class Human: + def __init__(self) -> None: + self.name = "Human" + + def speak(self) -> str: + return "'hello'" + + +class Car: + def __init__(self) -> None: + self.name = "Car" + + def make_noise(self, octane_level: int) -> str: + return f"vroom{'!' * octane_level}" + + +class Adapter: + """Adapts an object by replacing methods. + + Usage + ------ + dog = Dog() + dog = Adapter(dog, make_noise=dog.bark) + """ + + def __init__(self, obj: T, **adapted_methods: Callable): + """We set the adapted methods in the object's dict.""" + self.obj = obj + self.__dict__.update(adapted_methods) + + def __getattr__(self, attr): + """All non-adapted calls are passed to the object.""" + return getattr(self.obj, attr) + + def original_dict(self): + """Print original object dict.""" + return self.obj.__dict__ + + +def main(): + """ + >>> objects = [] + >>> dog = Dog() + >>> print(dog.__dict__) + {'name': 'Dog'} + + >>> objects.append(Adapter(dog, make_noise=dog.bark)) + + >>> objects[0].__dict__['obj'], objects[0].__dict__['make_noise'] + (<...Dog object at 0x...>, >) + + >>> print(objects[0].original_dict()) + {'name': 'Dog'} + + >>> cat = Cat() + >>> objects.append(Adapter(cat, make_noise=cat.meow)) + >>> human = Human() + >>> objects.append(Adapter(human, make_noise=human.speak)) + >>> car = Car() + >>> objects.append(Adapter(car, make_noise=lambda: car.make_noise(3))) + + >>> for obj in objects: + ... print("A {0} goes {1}".format(obj.name, obj.make_noise())) + A Dog goes woof! + A Cat goes meow! + A Human goes 'hello' + A Car goes vroom!!! + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod(optionflags=doctest.ELLIPSIS) diff --git a/patterns/structural/bridge.py b/patterns/structural/bridge.py new file mode 100644 index 00000000..feddb675 --- /dev/null +++ b/patterns/structural/bridge.py @@ -0,0 +1,54 @@ +""" +*References: +http://en.wikibooks.org/wiki/Computer_Science_Design_Patterns/Bridge_Pattern#Python + +*TL;DR +Decouples an abstraction from its implementation. +""" + + +# ConcreteImplementor 1/2 +class DrawingAPI1: + def draw_circle(self, x, y, radius): + print(f"API1.circle at {x}:{y} radius {radius}") + + +# ConcreteImplementor 2/2 +class DrawingAPI2: + def draw_circle(self, x, y, radius): + print(f"API2.circle at {x}:{y} radius {radius}") + + +# Refined Abstraction +class CircleShape: + def __init__(self, x, y, radius, drawing_api): + self._x = x + self._y = y + self._radius = radius + self._drawing_api = drawing_api + + # low-level i.e. Implementation specific + def draw(self): + self._drawing_api.draw_circle(self._x, self._y, self._radius) + + # high-level i.e. Abstraction specific + def scale(self, pct): + self._radius *= pct + + +def main(): + """ + >>> shapes = (CircleShape(1, 2, 3, DrawingAPI1()), CircleShape(5, 7, 11, DrawingAPI2())) + + >>> for shape in shapes: + ... shape.scale(2.5) + ... shape.draw() + API1.circle at 1:2 radius 7.5 + API2.circle at 5:7 radius 27.5 + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/structural/composite.py b/patterns/structural/composite.py new file mode 100644 index 00000000..a4bedc1d --- /dev/null +++ b/patterns/structural/composite.py @@ -0,0 +1,93 @@ +""" +*What is this pattern about? +The composite pattern describes a group of objects that is treated the +same way as a single instance of the same type of object. The intent of +a composite is to "compose" objects into tree structures to represent +part-whole hierarchies. Implementing the composite pattern lets clients +treat individual objects and compositions uniformly. + +*What does this example do? +The example implements a graphic class,which can be either an ellipse +or a composition of several graphics. Every graphic can be printed. + +*Where is the pattern used practically? +In graphics editors a shape can be basic or complex. An example of a +simple shape is a line, where a complex shape is a rectangle which is +made of four line objects. Since shapes have many operations in common +such as rendering the shape to screen, and since shapes follow a +part-whole hierarchy, composite pattern can be used to enable the +program to deal with all shapes uniformly. + +*References: +https://en.wikipedia.org/wiki/Composite_pattern +https://infinitescript.com/2014/10/the-23-gang-of-three-design-patterns/ + +*TL;DR +Describes a group of objects that is treated as a single instance. +""" + +from abc import ABC, abstractmethod +from typing import List + + +class Graphic(ABC): + @abstractmethod + def render(self) -> None: + raise NotImplementedError("You should implement this!") + + +class CompositeGraphic(Graphic): + def __init__(self) -> None: + self.graphics: List[Graphic] = [] + + def render(self) -> None: + for graphic in self.graphics: + graphic.render() + + def add(self, graphic: Graphic) -> None: + self.graphics.append(graphic) + + def remove(self, graphic: Graphic) -> None: + self.graphics.remove(graphic) + + +class Ellipse(Graphic): + def __init__(self, name: str) -> None: + self.name = name + + def render(self) -> None: + print(f"Ellipse: {self.name}") + + +def main(): + """ + >>> ellipse1 = Ellipse("1") + >>> ellipse2 = Ellipse("2") + >>> ellipse3 = Ellipse("3") + >>> ellipse4 = Ellipse("4") + + >>> graphic1 = CompositeGraphic() + >>> graphic2 = CompositeGraphic() + + >>> graphic1.add(ellipse1) + >>> graphic1.add(ellipse2) + >>> graphic1.add(ellipse3) + >>> graphic2.add(ellipse4) + + >>> graphic = CompositeGraphic() + + >>> graphic.add(graphic1) + >>> graphic.add(graphic2) + + >>> graphic.render() + Ellipse: 1 + Ellipse: 2 + Ellipse: 3 + Ellipse: 4 + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/structural/decorator.py b/patterns/structural/decorator.py new file mode 100644 index 00000000..a32e2b06 --- /dev/null +++ b/patterns/structural/decorator.py @@ -0,0 +1,74 @@ +""" +*What is this pattern about? +The Decorator pattern is used to dynamically add a new feature to an +object without changing its implementation. It differs from +inheritance because the new feature is added only to that particular +object, not to the entire subclass. + +*What does this example do? +This example shows a way to add formatting options (boldface and +italic) to a text by appending the corresponding tags ( and +). Also, we can see that decorators can be applied one after the other, +since the original text is passed to the bold wrapper, which in turn +is passed to the italic wrapper. + +*Where is the pattern used practically? +The Grok framework uses decorators to add functionalities to methods, +like permissions or subscription to an event: +http://grok.zope.org/doc/current/reference/decorators.html + +*References: +https://sourcemaking.com/design_patterns/decorator + +*TL;DR +Adds behaviour to object without affecting its class. +""" + + +class TextTag: + """Represents a base text tag""" + + def __init__(self, text: str) -> None: + self._text = text + + def render(self) -> str: + return self._text + + +class BoldWrapper(TextTag): + """Wraps a tag in """ + + def __init__(self, wrapped: TextTag) -> None: + self._wrapped = wrapped + + def render(self) -> str: + return f"{self._wrapped.render()}" + + +class ItalicWrapper(TextTag): + """Wraps a tag in """ + + def __init__(self, wrapped: TextTag) -> None: + self._wrapped = wrapped + + def render(self) -> str: + return f"{self._wrapped.render()}" + + +def main(): + """ + >>> simple_hello = TextTag("hello, world!") + >>> special_hello = ItalicWrapper(BoldWrapper(simple_hello)) + + >>> print("before:", simple_hello.render()) + before: hello, world! + + >>> print("after:", special_hello.render()) + after: hello, world! + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/structural/facade.py b/patterns/structural/facade.py new file mode 100644 index 00000000..f7b00be3 --- /dev/null +++ b/patterns/structural/facade.py @@ -0,0 +1,97 @@ +""" +Example from https://en.wikipedia.org/wiki/Facade_pattern#Python + + +*What is this pattern about? +The Facade pattern is a way to provide a simpler unified interface to +a more complex system. It provides an easier way to access functions +of the underlying system by providing a single entry point. +This kind of abstraction is seen in many real life situations. For +example, we can turn on a computer by just pressing a button, but in +fact there are many procedures and operations done when that happens +(e.g., loading programs from disk to memory). In this case, the button +serves as an unified interface to all the underlying procedures to +turn on a computer. + +*Where is the pattern used practically? +This pattern can be seen in the Python standard library when we use +the isdir function. Although a user simply uses this function to know +whether a path refers to a directory, the system makes a few +operations and calls other modules (e.g., os.stat) to give the result. + +*References: +https://sourcemaking.com/design_patterns/facade +https://fkromer.github.io/python-pattern-references/design/#facade +http://python-3-patterns-idioms-test.readthedocs.io/en/latest/ChangeInterface.html#facade + +*TL;DR +Provides a simpler unified interface to a complex system. +""" + + +# Complex computer parts +class CPU: + """ + Simple CPU representation. + """ + + def freeze(self) -> None: + print("Freezing processor.") + + def jump(self, position: str) -> None: + print("Jumping to:", position) + + def execute(self) -> None: + print("Executing.") + + +class Memory: + """ + Simple memory representation. + """ + + def load(self, position: str, data: str) -> None: + print(f"Loading from {position} data: '{data}'.") + + +class SolidStateDrive: + """ + Simple solid state drive representation. + """ + + def read(self, lba: str, size: str) -> str: + return f"Some data from sector {lba} with size {size}" + + +class ComputerFacade: + """ + Represents a facade for various computer parts. + """ + + def __init__(self): + self.cpu = CPU() + self.memory = Memory() + self.ssd = SolidStateDrive() + + def start(self): + self.cpu.freeze() + self.memory.load("0x00", self.ssd.read("100", "1024")) + self.cpu.jump("0x00") + self.cpu.execute() + + +def main(): + """ + >>> computer_facade = ComputerFacade() + >>> computer_facade.start() + Freezing processor. + Loading from 0x00 data: 'Some data from sector 100 with size 1024'. + Jumping to: 0x00 + Executing. + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod(optionflags=doctest.ELLIPSIS) diff --git a/patterns/structural/flyweight.py b/patterns/structural/flyweight.py new file mode 100644 index 00000000..fad17a8b --- /dev/null +++ b/patterns/structural/flyweight.py @@ -0,0 +1,85 @@ +""" +*What is this pattern about? +This pattern aims to minimise the number of objects that are needed by +a program at run-time. A Flyweight is an object shared by multiple +contexts, and is indistinguishable from an object that is not shared. + +The state of a Flyweight should not be affected by it's context, this +is known as its intrinsic state. The decoupling of the objects state +from the object's context, allows the Flyweight to be shared. + +*What does this example do? +The example below sets-up an 'object pool' which stores initialised +objects. When a 'Card' is created it first checks to see if it already +exists instead of creating a new one. This aims to reduce the number of +objects initialised by the program. + +*References: +http://codesnipers.com/?q=python-flyweights +https://python-patterns.guide/gang-of-four/flyweight/ + +*Examples in Python ecosystem: +https://docs.python.org/3/library/sys.html#sys.intern + +*TL;DR +Minimizes memory usage by sharing data with other similar objects. +""" + +import weakref + + +class Card: + """The Flyweight""" + + # Could be a simple dict. + # With WeakValueDictionary garbage collection can reclaim the object + # when there are no other references to it. + _pool: weakref.WeakValueDictionary = weakref.WeakValueDictionary() + + def __new__(cls, value, suit): + # If the object exists in the pool - just return it + obj = cls._pool.get(value + suit) + # otherwise - create new one (and add it to the pool) + if obj is None: + obj = object.__new__(Card) + cls._pool[value + suit] = obj + # This row does the part we usually see in `__init__` + obj.value, obj.suit = value, suit + return obj + + # If you uncomment `__init__` and comment-out `__new__` - + # Card becomes normal (non-flyweight). + # def __init__(self, value, suit): + # self.value, self.suit = value, suit + + def __repr__(self): + return f"" + + +def main(): + """ + >>> c1 = Card('9', 'h') + >>> c2 = Card('9', 'h') + >>> c1, c2 + (, ) + >>> c1 == c2 + True + >>> c1 is c2 + True + + >>> c1.new_attr = 'temp' + >>> c3 = Card('9', 'h') + >>> hasattr(c3, 'new_attr') + True + + >>> Card._pool.clear() + >>> c4 = Card('9', 'h') + >>> hasattr(c4, 'new_attr') + False + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/structural/flyweight_with_metaclass.py b/patterns/structural/flyweight_with_metaclass.py new file mode 100644 index 00000000..ced8d915 --- /dev/null +++ b/patterns/structural/flyweight_with_metaclass.py @@ -0,0 +1,63 @@ +import weakref + + +class FlyweightMeta(type): + def __new__(mcs, name, parents, dct): + """ + Set up object pool + + :param name: class name + :param parents: class parents + :param dct: dict: includes class attributes, class methods, + static methods, etc + :return: new class + """ + dct["pool"] = weakref.WeakValueDictionary() + return super().__new__(mcs, name, parents, dct) + + @staticmethod + def _serialize_params(cls, *args, **kwargs): + """ + Serialize input parameters to a key. + Simple implementation is just to serialize it as a string + """ + args_list = list(map(str, args)) + args_list.extend([str(kwargs), cls.__name__]) + key = "".join(args_list) + return key + + def __call__(cls, *args, **kwargs): + key = FlyweightMeta._serialize_params(cls, *args, **kwargs) + pool = getattr(cls, "pool", {}) + + instance = pool.get(key) + if instance is None: + instance = super().__call__(*args, **kwargs) + pool[key] = instance + return instance + + +class Card2(metaclass=FlyweightMeta): + def __init__(self, *args, **kwargs): + # print('Init {}: {}'.format(self.__class__, (args, kwargs))) + pass + + +if __name__ == "__main__": + instances_pool = getattr(Card2, "pool") + cm1 = Card2("10", "h", a=1) + cm2 = Card2("10", "h", a=1) + cm3 = Card2("10", "h", a=2) + + assert (cm1 == cm2) and (cm1 != cm3) + assert (cm1 is cm2) and (cm1 is not cm3) + assert len(instances_pool) == 2 + + del cm1 + assert len(instances_pool) == 2 + + del cm2 + assert len(instances_pool) == 1 + + del cm3 + assert len(instances_pool) == 0 diff --git a/patterns/structural/front_controller.py b/patterns/structural/front_controller.py new file mode 100644 index 00000000..92f58b21 --- /dev/null +++ b/patterns/structural/front_controller.py @@ -0,0 +1,95 @@ +""" +@author: Gordeev Andrey + +*TL;DR +Provides a centralized entry point that controls and manages request handling. +""" + +from __future__ import annotations + +from typing import Any + + +class MobileView: + def show_index_page(self) -> None: + print("Displaying mobile index page") + + +class TabletView: + def show_index_page(self) -> None: + print("Displaying tablet index page") + + +class Dispatcher: + def __init__(self) -> None: + self.mobile_view = MobileView() + self.tablet_view = TabletView() + + def dispatch(self, request: Request) -> None: + """ + This function is used to dispatch the request based on the type of device. + If it is a mobile, then mobile view will be called and if it is a tablet, + then tablet view will be called. + Otherwise, an error message will be printed saying that cannot dispatch the request. + """ + if request.type == Request.mobile_type: + self.mobile_view.show_index_page() + elif request.type == Request.tablet_type: + self.tablet_view.show_index_page() + else: + print("Cannot dispatch the request") + + +class RequestController: + """front controller""" + + def __init__(self) -> None: + self.dispatcher = Dispatcher() + + def dispatch_request(self, request: Any) -> None: + """ + This function takes a request object and sends it to the dispatcher. + """ + if isinstance(request, Request): + self.dispatcher.dispatch(request) + else: + print("request must be a Request object") + + +class Request: + """request""" + + mobile_type = "mobile" + tablet_type = "tablet" + + def __init__(self, request): + self.type = None + request = request.lower() + if request == self.mobile_type: + self.type = self.mobile_type + elif request == self.tablet_type: + self.type = self.tablet_type + + +def main(): + """ + >>> front_controller = RequestController() + + >>> front_controller.dispatch_request(Request('mobile')) + Displaying mobile index page + + >>> front_controller.dispatch_request(Request('tablet')) + Displaying tablet index page + + >>> front_controller.dispatch_request(Request('desktop')) + Cannot dispatch the request + + >>> front_controller.dispatch_request('mobile') + request must be a Request object + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/structural/mvc.py b/patterns/structural/mvc.py new file mode 100644 index 00000000..24b0017a --- /dev/null +++ b/patterns/structural/mvc.py @@ -0,0 +1,204 @@ +""" +*TL;DR +Separates data in GUIs from the ways it is presented, and accepted. +""" + +from abc import ABC, abstractmethod +from inspect import signature +from sys import argv +from typing import Any + + +class Model(ABC): + """The Model is the data layer of the application.""" + @abstractmethod + def __iter__(self) -> Any: + pass + + @abstractmethod + def get(self, item: str) -> dict: + """Returns an object with a .items() call method + that iterates over key,value pairs of its information.""" + pass + + @property + @abstractmethod + def item_type(self) -> str: + pass + + +class ProductModel(Model): + """The Model is the data layer of the application.""" + class Price(float): + """A polymorphic way to pass a float with a particular + __str__ functionality.""" + + def __str__(self) -> str: + return f"{self:.2f}" + + products = { + "milk": {"price": Price(1.50), "quantity": 10}, + "eggs": {"price": Price(0.20), "quantity": 100}, + "cheese": {"price": Price(2.00), "quantity": 10}, + } + + item_type = "product" + + def __iter__(self) -> Any: + yield from self.products + + def get(self, product: str) -> dict: + try: + return self.products[product] + except KeyError as e: + raise KeyError(str(e) + " not in the model's item list.") + + +class View(ABC): + """The View is the presentation layer of the application.""" + @abstractmethod + def show_item_list(self, item_type: str, item_list: list) -> None: + pass + + @abstractmethod + def show_item_information(self, item_type: str, item_name: str, item_info: dict) -> None: + """Will look for item information by iterating over key,value pairs + yielded by item_info.items()""" + pass + + @abstractmethod + def item_not_found(self, item_type: str, item_name: str) -> None: + pass + + +class ConsoleView(View): + """The View is the presentation layer of the application.""" + def show_item_list(self, item_type: str, item_list: list) -> None: + print(item_type.upper() + " LIST:") + for item in item_list: + print(item) + print("") + + @staticmethod + def capitalizer(string: str) -> str: + """Capitalizes the first letter of a string and lowercases the rest.""" + return string[0].upper() + string[1:].lower() + + def show_item_information(self, item_type: str, item_name: str, item_info: dict) -> None: + """Will look for item information by iterating over key,value pairs""" + print(item_type.upper() + " INFORMATION:") + printout = "Name: %s" % item_name + for key, value in item_info.items(): + printout += ", " + self.capitalizer(str(key)) + ": " + str(value) + printout += "\n" + print(printout) + + def item_not_found(self, item_type: str, item_name: str) -> None: + print(f'That {item_type} "{item_name}" does not exist in the records') + + +class Controller: + """The Controller is the intermediary between the Model and the View.""" + def __init__(self, model_class: Model, view_class: View) -> None: + self.model: Model = model_class + self.view: View = view_class + + def show_items(self) -> None: + items = list(self.model) + item_type = self.model.item_type + self.view.show_item_list(item_type, items) + + def show_item_information(self, item_name: str) -> None: + """ + Show information about a {item_type} item. + :param str item_name: the name of the {item_type} item to show information about + """ + item_type: str = self.model.item_type + try: + item_info: dict = self.model.get(item_name) + except Exception: + self.view.item_not_found(item_type, item_name) + else: + self.view.show_item_information(item_type, item_name, item_info) + + +class Router: + """The Router is the entry point of the application.""" + def __init__(self): + self.routes = {} + + def register( + self, + path: str, + controller_class: type[Controller], + model_class: type[Model], + view_class: type[View]) -> None: + model_instance: Model = model_class() + view_instance: View = view_class() + self.routes[path] = controller_class(model_instance, view_instance) + + def resolve(self, path: str) -> Controller: + if self.routes.get(path): + controller: Controller = self.routes[path] + return controller + else: + raise KeyError(f"No controller registered for path '{path}'") + + +def main(): + """ + >>> model = ProductModel() + >>> view = ConsoleView() + >>> controller = Controller(model, view) + + >>> controller.show_items() + PRODUCT LIST: + milk + eggs + cheese + + + >>> controller.show_item_information("cheese") + PRODUCT INFORMATION: + Name: cheese, Price: 2.00, Quantity: 10 + + + >>> controller.show_item_information("eggs") + PRODUCT INFORMATION: + Name: eggs, Price: 0.20, Quantity: 100 + + + >>> controller.show_item_information("milk") + PRODUCT INFORMATION: + Name: milk, Price: 1.50, Quantity: 10 + + + >>> controller.show_item_information("arepas") + That product "arepas" does not exist in the records + """ + + +if __name__ == "__main__": + router = Router() + router.register("products", Controller, ProductModel, ConsoleView) + controller: Controller = router.resolve(argv[1]) + + action: str = str(argv[2]) if len(argv) > 2 else "" + args: str = ' '.join(map(str, argv[3:])) if len(argv) > 3 else "" + + if hasattr(controller, action): + command = getattr(controller, action) + sig = signature(command) + + if len(sig.parameters) > 0: + if args: + command(args) + else: + print("Command requires arguments.") + else: + command() + else: + print(f"Command {action} not found in the controller.") + + import doctest + doctest.testmod() diff --git a/patterns/structural/proxy.py b/patterns/structural/proxy.py new file mode 100644 index 00000000..3ef74ec0 --- /dev/null +++ b/patterns/structural/proxy.py @@ -0,0 +1,91 @@ +""" +*What is this pattern about? +Proxy is used in places where you want to add functionality to a class without +changing its interface. The main class is called `Real Subject`. A client should +use the proxy or the real subject without any code change, so both must have the +same interface. Logging and controlling access to the real subject are some of +the proxy pattern usages. + +*References: +https://refactoring.guru/design-patterns/proxy/python/example +https://python-3-patterns-idioms-test.readthedocs.io/en/latest/Fronting.html + +*TL;DR +Add functionality or logic (e.g. logging, caching, authorization) to a resource +without changing its interface. +""" + +from typing import Union + + +class Subject: + """ + As mentioned in the document, interfaces of both RealSubject and Proxy should + be the same, because the client should be able to use RealSubject or Proxy with + no code change. + + Not all times this interface is necessary. The point is the client should be + able to use RealSubject or Proxy interchangeably with no change in code. + """ + + def do_the_job(self, user: str) -> None: + raise NotImplementedError() + + +class RealSubject(Subject): + """ + This is the main job doer. External services like payment gateways can be a + good example. + """ + + def do_the_job(self, user: str) -> None: + print(f"I am doing the job for {user}") + + +class Proxy(Subject): + def __init__(self) -> None: + self._real_subject = RealSubject() + + def do_the_job(self, user: str) -> None: + """ + logging and controlling access are some examples of proxy usages. + """ + + print(f"[log] Doing the job for {user} is requested.") + + if user == "admin": + self._real_subject.do_the_job(user) + else: + print("[log] I can do the job just for `admins`.") + + +def client(job_doer: Union[RealSubject, Proxy], user: str) -> None: + job_doer.do_the_job(user) + + +def main(): + """ + >>> proxy = Proxy() + + >>> real_subject = RealSubject() + + >>> client(proxy, 'admin') + [log] Doing the job for admin is requested. + I am doing the job for admin + + >>> client(proxy, 'anonymous') + [log] Doing the job for anonymous is requested. + [log] I can do the job just for `admins`. + + >>> client(real_subject, 'admin') + I am doing the job for admin + + >>> client(real_subject, 'anonymous') + I am doing the job for anonymous + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/structural/viz/3-tier.py.png b/patterns/structural/viz/3-tier.py.png new file mode 100644 index 00000000..6a53f8ab Binary files /dev/null and b/patterns/structural/viz/3-tier.py.png differ diff --git a/patterns/structural/viz/adapter.py.png b/patterns/structural/viz/adapter.py.png new file mode 100644 index 00000000..56bf8e93 Binary files /dev/null and b/patterns/structural/viz/adapter.py.png differ diff --git a/patterns/structural/viz/bridge.py.png b/patterns/structural/viz/bridge.py.png new file mode 100644 index 00000000..ebaf85ff Binary files /dev/null and b/patterns/structural/viz/bridge.py.png differ diff --git a/patterns/structural/viz/composite.py.png b/patterns/structural/viz/composite.py.png new file mode 100644 index 00000000..eb7da401 Binary files /dev/null and b/patterns/structural/viz/composite.py.png differ diff --git a/patterns/structural/viz/decorator.py.png b/patterns/structural/viz/decorator.py.png new file mode 100644 index 00000000..401eea61 Binary files /dev/null and b/patterns/structural/viz/decorator.py.png differ diff --git a/patterns/structural/viz/facade.py.png b/patterns/structural/viz/facade.py.png new file mode 100644 index 00000000..aca00440 Binary files /dev/null and b/patterns/structural/viz/facade.py.png differ diff --git a/patterns/structural/viz/flyweight.py.png b/patterns/structural/viz/flyweight.py.png new file mode 100644 index 00000000..204b90e9 Binary files /dev/null and b/patterns/structural/viz/flyweight.py.png differ diff --git a/patterns/structural/viz/front_controller.py.png b/patterns/structural/viz/front_controller.py.png new file mode 100644 index 00000000..41943db7 Binary files /dev/null and b/patterns/structural/viz/front_controller.py.png differ diff --git a/patterns/structural/viz/mvc.py.png b/patterns/structural/viz/mvc.py.png new file mode 100644 index 00000000..12ed31cb Binary files /dev/null and b/patterns/structural/viz/mvc.py.png differ diff --git a/patterns/structural/viz/proxy.py.png b/patterns/structural/viz/proxy.py.png new file mode 100644 index 00000000..815f89af Binary files /dev/null and b/patterns/structural/viz/proxy.py.png differ diff --git a/pool.py b/pool.py deleted file mode 100644 index ec1107ba..00000000 --- a/pool.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -"""http://stackoverflow.com/questions/1514120/python-implementation-of-the-object-pool-design-pattern""" - - -class QueueObject(): - - def __init__(self, queue, auto_get=False): - self._queue = queue - self.object = self._queue.get() if auto_get else None - - def __enter__(self): - if self.object is None: - self.object = self._queue.get() - return self.object - - def __exit__(self, Type, value, traceback): - if self.object is not None: - self._queue.put(self.object) - self.object = None - - def __del__(self): - if self.object is not None: - self._queue.put(self.object) - self.object = None - - -def main(): - try: - import queue - except ImportError: # python 2.x compatibility - import Queue as queue - - def test_object(queue): - queue_object = QueueObject(queue, True) - print('Inside func: {}'.format(queue_object.object)) - - sample_queue = queue.Queue() - - sample_queue.put('yam') - with QueueObject(sample_queue) as obj: - print('Inside with: {}'.format(obj)) - print('Outside with: {}'.format(sample_queue.get())) - - sample_queue.put('sam') - test_object(sample_queue) - print('Outside func: {}'.format(sample_queue.get())) - - if not sample_queue.empty(): - print(sample_queue.get()) - - -if __name__ == '__main__': - main() - -### OUTPUT ### -# Inside with: yam -# Outside with: yam -# Inside func: sam -# Outside func: sam diff --git a/prototype.py b/prototype.py deleted file mode 100644 index 2f2a14a8..00000000 --- a/prototype.py +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -import copy - - -class Prototype: - - def __init__(self): - self._objects = {} - - def register_object(self, name, obj): - """Register an object""" - self._objects[name] = obj - - def unregister_object(self, name): - """Unregister an object""" - del self._objects[name] - - def clone(self, name, **attr): - """Clone a registered object and update inner attributes dictionary""" - obj = copy.deepcopy(self._objects.get(name)) - obj.__dict__.update(attr) - return obj - - -class A: - def __init__(self): - self.x = 3 - self.y = 8 - self.z = 15 - self.garbage = [38, 11, 19] - - def __str__(self): - return '{} {} {} {}'.format(self.x, self.y, self.z, self.garbage) - - -def main(): - a = A() - prototype = Prototype() - prototype.register_object('objecta', a) - b = prototype.clone('objecta') - c = prototype.clone('objecta', x=1, y=2, garbage=[88, 1]) - print([str(i) for i in (a, b, c)]) - -if __name__ == '__main__': - main() - -### OUTPUT ### -# ['3 8 15 [38, 11, 19]', '3 8 15 [38, 11, 19]', '1 2 15 [88, 1]'] diff --git a/proxy.py b/proxy.py deleted file mode 100644 index a60b1f53..00000000 --- a/proxy.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -import time - - -class SalesManager: - def work(self): - print("Sales Manager working...") - - def talk(self): - print("Sales Manager ready to talk") - - -class Proxy: - def __init__(self): - self.busy = 'No' - self.sales = None - - def work(self): - print("Proxy checking for Sales Manager availability") - if self.busy == 'No': - self.sales = SalesManager() - time.sleep(2) - self.sales.talk() - else: - time.sleep(2) - print("Sales Manager is busy") - - -class NoTalkProxy(Proxy): - def __init__(self): - Proxy.__init__(self) - - def work(self): - print("Proxy checking for Sales Manager availability") - time.sleep(2) - print("This Sales Manager will not talk to you whether he/she is busy or not") - - -if __name__ == '__main__': - p = Proxy() - p.work() - p.busy = 'Yes' - p.work() - p = NoTalkProxy() - p.work() - p.busy = 'Yes' - p.work() - -### OUTPUT ### -# Proxy checking for Sales Manager availability -# Sales Manager ready to talk -# Proxy checking for Sales Manager availability -# Sales Manager is busy -# Proxy checking for Sales Manager availability -# This Sales Manager will not talk to you whether he/she is busy or not -# Proxy checking for Sales Manager availability -# This Sales Manager will not talk to you whether he/she is busy or not diff --git a/publish_subscribe.py b/publish_subscribe.py deleted file mode 100644 index 4d899987..00000000 --- a/publish_subscribe.py +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -Reference: http://www.slideshare.net/ishraqabd/publish-subscribe-model-overview-13368808 -Author: https://github.com/HanWenfang -""" - - -class Provider: - - def __init__(self): - self.msg_queue = [] - self.subscribers = {} - - def notify(self, msg): - self.msg_queue.append(msg) - - def subscribe(self, msg, subscriber): - self.subscribers.setdefault(msg, []).append(subscriber) - - def unsubscribe(self, msg, subscriber): - self.subscribers[msg].remove(subscriber) - - def update(self): - for msg in self.msg_queue: - if msg in self.subscribers: - for sub in self.subscribers[msg]: - sub.run(msg) - self.msg_queue = [] - - -class Publisher: - - def __init__(self, msg_center): - self.provider = msg_center - - def publish(self, msg): - self.provider.notify(msg) - - -class Subscriber: - - def __init__(self, name, msg_center): - self.name = name - self.provider = msg_center - - def subscribe(self, msg): - self.provider.subscribe(msg, self) - - def run(self, msg): - print("{} got {}".format(self.name, msg)) - - -def main(): - message_center = Provider() - - fftv = Publisher(message_center) - - jim = Subscriber("jim", message_center) - jim.subscribe("cartoon") - jack = Subscriber("jack", message_center) - jack.subscribe("music") - gee = Subscriber("gee", message_center) - gee.subscribe("movie") - - fftv.publish("cartoon") - fftv.publish("music") - fftv.publish("ads") - fftv.publish("movie") - fftv.publish("cartoon") - fftv.publish("cartoon") - fftv.publish("movie") - fftv.publish("blank") - - message_center.update() - - -if __name__ == "__main__": - main() - -### OUTPUT ### -# jim got cartoon -# jack got music -# gee got movie -# jim got cartoon -# jim got cartoon -# gee got movie diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..57f6fbe7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,98 @@ +[build-system] +requires = ["setuptools>=42", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "patterns" +description = "A collection of design patterns and idioms in Python." +version = "0.1.0" +readme = "README.md" +requires-python = ">=3.9" +license = {text = "MIT"} +classifiers = [ + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] + +[project.optional-dependencies] +dev = ["pytest", "pytest-cov", "pytest-randomly", "flake8", "mypy", "coverage"] + +[tool.setuptools] +packages = ["patterns"] + +[tool.pytest.ini_options] +filterwarnings = [ + "ignore::Warning:.*test class 'TestRunner'.*" +] +# Adding settings from tox.ini for pytest +testpaths = ["tests"] +#testpaths = ["tests", "patterns"] +python_files = ["test_*.py", "*_test.py"] +# Enable doctest discovery in patterns directory +addopts = "--doctest-modules --randomly-seed=1234 --cov=patterns --cov-report=term-missing" +doctest_optionflags = ["ELLIPSIS", "NORMALIZE_WHITESPACE"] +log_level = "INFO" + +[tool.coverage.run] +branch = true +source = ["./"] +#source = ["patterns"] +# Ensure coverage data is collected properly +relative_files = true +parallel = true +dynamic_context = "test_function" +data_file = ".coverage" + +[tool.coverage.report] +# Regexes for lines to exclude from consideration +exclude_lines = [ + "def __repr__", + "if self\\.debug", + "raise AssertionError", + "raise NotImplementedError", + "if 0:", + "if __name__ == .__main__.:", + "@(abc\\.)?abstractmethod" +] +ignore_errors = true + +[tool.coverage.html] +directory = "coverage_html_report" + +[tool.mypy] +python_version = "3.12" +ignore_missing_imports = true + +[tool.flake8] +max-line-length = 120 +ignore = ["E266", "E731", "W503"] +exclude = ["venv*"] + +[tool.tox] +legacy_tox_ini = """ +[tox] +envlist = py312,cov-report +skip_missing_interpreters = true +usedevelop = true + +#[testenv] +#setenv = +# COVERAGE_FILE = .coverage.{envname} +#deps = +# -r requirements-dev.txt +#commands = +# flake8 --exclude="venv/,.tox/" patterns/ +# coverage run -m pytest --randomly-seed=1234 --doctest-modules patterns/ +# coverage run -m pytest -s -vv --cov=patterns/ --log-level=INFO tests/ + +#[testenv:cov-report] +#setenv = +# COVERAGE_FILE = .coverage +#deps = coverage +#commands = +# coverage combine +# coverage report +#""" \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 00000000..4aaa81f2 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,9 @@ +mypy +pyupgrade +pytest>=6.2.0 +pytest-cov>=2.11.0 +pytest-randomly>=3.1.0 +black>=25.1.0 +isort>=5.7.0 +flake8>=7.1.0 +tox>=4.25.0 \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..72bc2b46 --- /dev/null +++ b/setup.py @@ -0,0 +1,14 @@ +from setuptools import find_packages, setup + +setup( + name="patterns", + packages=find_packages(), + description="A collection of design patterns and idioms in Python.", + classifiers=[ + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + ], +) diff --git a/state.py b/state.py deleted file mode 100644 index 45cc30a3..00000000 --- a/state.py +++ /dev/null @@ -1,81 +0,0 @@ -"""Implementation of the state pattern""" - -# http://ginstrom.com/scribbles/2007/10/08/design-patterns-python-style/ -from __future__ import print_function - - -class State(object): - - """Base state. This is to share functionality""" - - def scan(self): - """Scan the dial to the next station""" - self.pos += 1 - if self.pos == len(self.stations): - self.pos = 0 - print("Scanning... Station is", self.stations[self.pos], self.name) - - -class AmState(State): - - def __init__(self, radio): - self.radio = radio - self.stations = ["1250", "1380", "1510"] - self.pos = 0 - self.name = "AM" - - def toggle_amfm(self): - print("Switching to FM") - self.radio.state = self.radio.fmstate - - -class FmState(State): - - def __init__(self, radio): - self.radio = radio - self.stations = ["81.3", "89.1", "103.9"] - self.pos = 0 - self.name = "FM" - - def toggle_amfm(self): - print("Switching to AM") - self.radio.state = self.radio.amstate - - -class Radio(object): - - """A radio. It has a scan button, and an AM/FM toggle switch.""" - - def __init__(self): - """We have an AM state and an FM state""" - self.amstate = AmState(self) - self.fmstate = FmState(self) - self.state = self.amstate - - def toggle_amfm(self): - self.state.toggle_amfm() - - def scan(self): - self.state.scan() - - -# Test our radio out -if __name__ == '__main__': - radio = Radio() - actions = [radio.scan] * 2 + [radio.toggle_amfm] + [radio.scan] * 2 - actions *= 2 - - for action in actions: - action() - -### OUTPUT ### -# Scanning... Station is 1380 AM -# Scanning... Station is 1510 AM -# Switching to FM -# Scanning... Station is 89.1 FM -# Scanning... Station is 103.9 FM -# Scanning... Station is 81.3 FM -# Scanning... Station is 89.1 FM -# Switching to AM -# Scanning... Station is 1250 AM -# Scanning... Station is 1380 AM diff --git a/strategy.py b/strategy.py deleted file mode 100644 index 8c7a9b5a..00000000 --- a/strategy.py +++ /dev/null @@ -1,49 +0,0 @@ -# http://stackoverflow.com/questions/963965/how-is-this-strategy-pattern -# -written-in-python-the-sample-in-wikipedia -""" -In most of other languages Strategy pattern is implemented via creating some -base strategy interface/abstract class and subclassing it with a number of -concrete strategies (as we can see at -http://en.wikipedia.org/wiki/Strategy_pattern), however Python supports -higher-order functions and allows us to have only one class and inject -functions into it's instances, as shown in this example. -""" -import types - - -class StrategyExample: - - def __init__(self, func=None): - self.name = 'Strategy Example 0' - if func is not None: - self.execute = types.MethodType(func, self) - - def execute(self): - print(self.name) - - -def execute_replacement1(self): - print(self.name + ' from execute 1') - - -def execute_replacement2(self): - print(self.name + ' from execute 2') - - -if __name__ == '__main__': - strat0 = StrategyExample() - - strat1 = StrategyExample(execute_replacement1) - strat1.name = 'Strategy Example 1' - - strat2 = StrategyExample(execute_replacement2) - strat2.name = 'Strategy Example 2' - - strat0.execute() - strat1.execute() - strat2.execute() - -### OUTPUT ### -# Strategy Example 0 -# Strategy Example 1 from execute 1 -# Strategy Example 2 from execute 2 diff --git a/template.py b/template.py deleted file mode 100644 index 42e7377e..00000000 --- a/template.py +++ /dev/null @@ -1,107 +0,0 @@ -"""http://ginstrom.com/scribbles/2007/10/08/design-patterns-python-style/ - -An example of the Template pattern in Python""" - -ingredients = "spam eggs apple" -line = '-' * 10 - - -# Skeletons -def iter_elements(getter, action): - """Template skeleton that iterates items""" - for element in getter(): - action(element) - print(line) - - -def rev_elements(getter, action): - """Template skeleton that iterates items in reverse order""" - for element in getter()[::-1]: - action(element) - print(line) - - -# Getters -def get_list(): - return ingredients.split() - - -def get_lists(): - return [list(x) for x in ingredients.split()] - - -# Actions -def print_item(item): - print(item) - - -def reverse_item(item): - print(item[::-1]) - - -# Makes templates -def make_template(skeleton, getter, action): - """Instantiate a template method with getter and action""" - def template(): - skeleton(getter, action) - return template - -# Create our template functions -templates = [make_template(s, g, a) - for g in (get_list, get_lists) - for a in (print_item, reverse_item) - for s in (iter_elements, rev_elements)] - -# Execute them -for template in templates: - template() - -### OUTPUT ### -# spam -# ---------- -# eggs -# ---------- -# apple -# ---------- -# apple -# ---------- -# eggs -# ---------- -# spam -# ---------- -# maps -# ---------- -# sgge -# ---------- -# elppa -# ---------- -# elppa -# ---------- -# sgge -# ---------- -# maps -# ---------- -# ['s', 'p', 'a', 'm'] -# ---------- -# ['e', 'g', 'g', 's'] -# ---------- -# ['a', 'p', 'p', 'l', 'e'] -# ---------- -# ['a', 'p', 'p', 'l', 'e'] -# ---------- -# ['e', 'g', 'g', 's'] -# ---------- -# ['s', 'p', 'a', 'm'] -# ---------- -# ['m', 'a', 'p', 's'] -# ---------- -# ['s', 'g', 'g', 'e'] -# ---------- -# ['e', 'l', 'p', 'p', 'a'] -# ---------- -# ['e', 'l', 'p', 'p', 'a'] -# ---------- -# ['s', 'g', 'g', 'e'] -# ---------- -# ['m', 'a', 'p', 's'] -# ---------- diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/behavioral/test_observer.py b/tests/behavioral/test_observer.py new file mode 100644 index 00000000..821f97a6 --- /dev/null +++ b/tests/behavioral/test_observer.py @@ -0,0 +1,33 @@ +from unittest.mock import Mock, patch + +import pytest + +from patterns.behavioral.observer import Data, DecimalViewer, HexViewer + + +@pytest.fixture +def observable(): + return Data("some data") + + +def test_attach_detach(observable): + decimal_viewer = DecimalViewer() + assert len(observable._observers) == 0 + + observable.attach(decimal_viewer) + assert decimal_viewer in observable._observers + + observable.detach(decimal_viewer) + assert decimal_viewer not in observable._observers + + +def test_one_data_change_notifies_each_observer_once(observable): + observable.attach(DecimalViewer()) + observable.attach(HexViewer()) + + with patch( + "patterns.behavioral.observer.DecimalViewer.update", new_callable=Mock() + ) as mocked_update: + assert mocked_update.call_count == 0 + observable.data = 10 + assert mocked_update.call_count == 1 diff --git a/tests/behavioral/test_publish_subscribe.py b/tests/behavioral/test_publish_subscribe.py new file mode 100644 index 00000000..c153da5b --- /dev/null +++ b/tests/behavioral/test_publish_subscribe.py @@ -0,0 +1,68 @@ +import unittest +from unittest.mock import call, patch + +from patterns.behavioral.publish_subscribe import Provider, Publisher, Subscriber + + +class TestProvider(unittest.TestCase): + """ + Integration tests ~ provider class with as little mocking as possible. + """ + + def test_subscriber_shall_be_attachable_to_subscriptions(cls): + subscription = "sub msg" + pro = Provider() + cls.assertEqual(len(pro.subscribers), 0) + sub = Subscriber("sub name", pro) + sub.subscribe(subscription) + cls.assertEqual(len(pro.subscribers[subscription]), 1) + + def test_subscriber_shall_be_detachable_from_subscriptions(cls): + subscription = "sub msg" + pro = Provider() + sub = Subscriber("sub name", pro) + sub.subscribe(subscription) + cls.assertEqual(len(pro.subscribers[subscription]), 1) + sub.unsubscribe(subscription) + cls.assertEqual(len(pro.subscribers[subscription]), 0) + + def test_publisher_shall_append_subscription_message_to_queue(cls): + """msg_queue ~ Provider.notify(msg) ~ Publisher.publish(msg)""" + expected_msg = "expected msg" + pro = Provider() + pub = Publisher(pro) + Subscriber("sub name", pro) + cls.assertEqual(len(pro.msg_queue), 0) + pub.publish(expected_msg) + cls.assertEqual(len(pro.msg_queue), 1) + cls.assertEqual(pro.msg_queue[0], expected_msg) + + def test_provider_shall_update_affected_subscribers_with_published_subscription( + cls, + ): + pro = Provider() + pub = Publisher(pro) + sub1 = Subscriber("sub 1 name", pro) + sub1.subscribe("sub 1 msg 1") + sub1.subscribe("sub 1 msg 2") + sub2 = Subscriber("sub 2 name", pro) + sub2.subscribe("sub 2 msg 1") + sub2.subscribe("sub 2 msg 2") + with patch.object(sub1, "run") as mock_subscriber1_run, patch.object( + sub2, "run" + ) as mock_subscriber2_run: + pro.update() + cls.assertEqual(mock_subscriber1_run.call_count, 0) + cls.assertEqual(mock_subscriber2_run.call_count, 0) + pub.publish("sub 1 msg 1") + pub.publish("sub 1 msg 2") + pub.publish("sub 2 msg 1") + pub.publish("sub 2 msg 2") + with patch.object(sub1, "run") as mock_subscriber1_run, patch.object( + sub2, "run" + ) as mock_subscriber2_run: + pro.update() + expected_sub1_calls = [call("sub 1 msg 1"), call("sub 1 msg 2")] + mock_subscriber1_run.assert_has_calls(expected_sub1_calls) + expected_sub2_calls = [call("sub 2 msg 1"), call("sub 2 msg 2")] + mock_subscriber2_run.assert_has_calls(expected_sub2_calls) diff --git a/tests/behavioral/test_state.py b/tests/behavioral/test_state.py new file mode 100644 index 00000000..77473f51 --- /dev/null +++ b/tests/behavioral/test_state.py @@ -0,0 +1,27 @@ +import pytest + +from patterns.behavioral.state import Radio + + +@pytest.fixture +def radio(): + return Radio() + + +def test_initial_state(radio): + assert radio.state.name == "AM" + + +def test_initial_am_station(radio): + initial_pos = radio.state.pos + assert radio.state.stations[initial_pos] == "1250" + + +def test_toggle_amfm(radio): + assert radio.state.name == "AM" + + radio.toggle_amfm() + assert radio.state.name == "FM" + + radio.toggle_amfm() + assert radio.state.name == "AM" diff --git a/tests/behavioral/test_strategy.py b/tests/behavioral/test_strategy.py new file mode 100644 index 00000000..53976f38 --- /dev/null +++ b/tests/behavioral/test_strategy.py @@ -0,0 +1,41 @@ +import pytest + +from patterns.behavioral.strategy import Order, on_sale_discount, ten_percent_discount + + +@pytest.fixture +def order(): + return Order(100) + + +@pytest.mark.parametrize( + "func, discount", [(ten_percent_discount, 10.0), (on_sale_discount, 45.0)] +) +def test_discount_function_return(func, order, discount): + assert func(order) == discount + + +@pytest.mark.parametrize( + "func, price", [(ten_percent_discount, 100), (on_sale_discount, 100)] +) +def test_order_discount_strategy_validate_success(func, price): + order = Order(price, func) + + assert order.price == price + assert order.discount_strategy == func + + +def test_order_discount_strategy_validate_error(): + order = Order(10, discount_strategy=on_sale_discount) + + assert order.discount_strategy is None + + +@pytest.mark.parametrize( + "func, price, discount", + [(ten_percent_discount, 100, 90.0), (on_sale_discount, 100, 55.0)], +) +def test_discount_apply_success(func, price, discount): + order = Order(price, func) + + assert order.apply_discount() == discount diff --git a/tests/creational/test_abstract_factory.py b/tests/creational/test_abstract_factory.py new file mode 100644 index 00000000..1676e59d --- /dev/null +++ b/tests/creational/test_abstract_factory.py @@ -0,0 +1,13 @@ +import unittest +from unittest.mock import patch + +from patterns.creational.abstract_factory import Dog, PetShop + + +class TestPetShop(unittest.TestCase): + def test_dog_pet_shop_shall_show_dog_instance(self): + dog_pet_shop = PetShop(Dog) + with patch.object(Dog, "speak") as mock_Dog_speak: + pet = dog_pet_shop.buy_pet("") + pet.speak() + self.assertEqual(mock_Dog_speak.call_count, 1) diff --git a/tests/creational/test_borg.py b/tests/creational/test_borg.py new file mode 100644 index 00000000..182611c3 --- /dev/null +++ b/tests/creational/test_borg.py @@ -0,0 +1,28 @@ +import unittest + +from patterns.creational.borg import Borg, YourBorg + + +class BorgTest(unittest.TestCase): + def setUp(self): + self.b1 = Borg() + self.b2 = Borg() + # creating YourBorg instance implicitly sets the state attribute + # for all borg instances. + self.ib1 = YourBorg() + + def tearDown(self): + self.ib1.state = "Init" + + def test_initial_borg_state_shall_be_init(self): + b = Borg() + self.assertEqual(b.state, "Init") + + def test_changing_instance_attribute_shall_change_borg_state(self): + self.b1.state = "Running" + self.assertEqual(self.b1.state, "Running") + self.assertEqual(self.b2.state, "Running") + self.assertEqual(self.ib1.state, "Running") + + def test_instances_shall_have_own_ids(self): + self.assertNotEqual(id(self.b1), id(self.b2), id(self.ib1)) diff --git a/tests/creational/test_builder.py b/tests/creational/test_builder.py new file mode 100644 index 00000000..923bc4a5 --- /dev/null +++ b/tests/creational/test_builder.py @@ -0,0 +1,22 @@ +import unittest + +from patterns.creational.builder import ComplexHouse, Flat, House, construct_building + + +class TestSimple(unittest.TestCase): + def test_house(self): + house = House() + self.assertEqual(house.size, "Big") + self.assertEqual(house.floor, "One") + + def test_flat(self): + flat = Flat() + self.assertEqual(flat.size, "Small") + self.assertEqual(flat.floor, "More than One") + + +class TestComplex(unittest.TestCase): + def test_house(self): + house = construct_building(ComplexHouse) + self.assertEqual(house.size, "Big and fancy") + self.assertEqual(house.floor, "One") diff --git a/tests/creational/test_lazy.py b/tests/creational/test_lazy.py new file mode 100644 index 00000000..1b815b60 --- /dev/null +++ b/tests/creational/test_lazy.py @@ -0,0 +1,38 @@ +import unittest + +from patterns.creational.lazy_evaluation import Person + + +class TestDynamicExpanding(unittest.TestCase): + def setUp(self): + self.John = Person("John", "Coder") + + def test_innate_properties(self): + self.assertDictEqual( + {"name": "John", "occupation": "Coder", "call_count2": 0}, + self.John.__dict__, + ) + + def test_relatives_not_in_properties(self): + self.assertNotIn("relatives", self.John.__dict__) + + def test_extended_properties(self): + print(f"John's relatives: {self.John.relatives}") + self.assertDictEqual( + { + "name": "John", + "occupation": "Coder", + "relatives": "Many relatives.", + "call_count2": 0, + }, + self.John.__dict__, + ) + + def test_relatives_after_access(self): + print(f"John's relatives: {self.John.relatives}") + self.assertIn("relatives", self.John.__dict__) + + def test_parents(self): + for _ in range(2): + self.assertEqual(self.John.parents, "Father and mother") + self.assertEqual(self.John.call_count2, 1) diff --git a/tests/creational/test_pool.py b/tests/creational/test_pool.py new file mode 100644 index 00000000..cd501db3 --- /dev/null +++ b/tests/creational/test_pool.py @@ -0,0 +1,50 @@ +import queue +import unittest + +from patterns.creational.pool import ObjectPool + + +class TestPool(unittest.TestCase): + def setUp(self): + self.sample_queue = queue.Queue() + self.sample_queue.put("first") + self.sample_queue.put("second") + + def test_items_recoil(self): + with ObjectPool(self.sample_queue, True) as pool: + self.assertEqual(pool, "first") + self.assertTrue(self.sample_queue.get() == "second") + self.assertFalse(self.sample_queue.empty()) + self.assertTrue(self.sample_queue.get() == "first") + self.assertTrue(self.sample_queue.empty()) + + def test_frozen_pool(self): + with ObjectPool(self.sample_queue) as pool: + self.assertEqual(pool, "first") + self.assertEqual(pool, "first") + self.assertTrue(self.sample_queue.get() == "second") + self.assertFalse(self.sample_queue.empty()) + self.assertTrue(self.sample_queue.get() == "first") + self.assertTrue(self.sample_queue.empty()) + + +class TestNaitivePool(unittest.TestCase): + """def test_object(queue): + queue_object = QueueObject(queue, True) + print('Inside func: {}'.format(queue_object.object))""" + + def test_pool_behavior_with_single_object_inside(self): + sample_queue = queue.Queue() + sample_queue.put("yam") + with ObjectPool(sample_queue) as obj: + # print('Inside with: {}'.format(obj)) + self.assertEqual(obj, "yam") + self.assertFalse(sample_queue.empty()) + self.assertTrue(sample_queue.get() == "yam") + self.assertTrue(sample_queue.empty()) + + # sample_queue.put('sam') + # test_object(sample_queue) + # print('Outside func: {}'.format(sample_queue.get())) + + # if not sample_queue.empty(): diff --git a/tests/creational/test_prototype.py b/tests/creational/test_prototype.py new file mode 100644 index 00000000..758ac872 --- /dev/null +++ b/tests/creational/test_prototype.py @@ -0,0 +1,48 @@ +import unittest + +from patterns.creational.prototype import Prototype, PrototypeDispatcher + + +class TestPrototypeFeatures(unittest.TestCase): + def setUp(self): + self.prototype = Prototype() + + def test_cloning_propperty_innate_values(self): + sample_object_1 = self.prototype.clone() + sample_object_2 = self.prototype.clone() + self.assertEqual(sample_object_1.value, sample_object_2.value) + + def test_extended_property_values_cloning(self): + sample_object_1 = self.prototype.clone() + sample_object_1.some_value = "test string" + sample_object_2 = self.prototype.clone() + self.assertRaises(AttributeError, lambda: sample_object_2.some_value) + + def test_cloning_propperty_assigned_values(self): + sample_object_1 = self.prototype.clone() + sample_object_2 = self.prototype.clone(value="re-assigned") + self.assertNotEqual(sample_object_1.value, sample_object_2.value) + + +class TestDispatcherFeatures(unittest.TestCase): + def setUp(self): + self.dispatcher = PrototypeDispatcher() + self.prototype = Prototype() + c = self.prototype.clone() + a = self.prototype.clone(value="a-value", ext_value="E") + b = self.prototype.clone(value="b-value", diff=True) + self.dispatcher.register_object("A", a) + self.dispatcher.register_object("B", b) + self.dispatcher.register_object("C", c) + + def test_batch_retrieving(self): + self.assertEqual(len(self.dispatcher.get_objects()), 3) + + def test_particular_properties_retrieving(self): + self.assertEqual(self.dispatcher.get_objects()["A"].value, "a-value") + self.assertEqual(self.dispatcher.get_objects()["B"].value, "b-value") + self.assertEqual(self.dispatcher.get_objects()["C"].value, "default") + + def test_extended_properties_retrieving(self): + self.assertEqual(self.dispatcher.get_objects()["A"].ext_value, "E") + self.assertTrue(self.dispatcher.get_objects()["B"].diff) diff --git a/tests/structural/test_adapter.py b/tests/structural/test_adapter.py new file mode 100644 index 00000000..01323075 --- /dev/null +++ b/tests/structural/test_adapter.py @@ -0,0 +1,74 @@ +import unittest + +from patterns.structural.adapter import Adapter, Car, Cat, Dog, Human + + +class ClassTest(unittest.TestCase): + def setUp(self): + self.dog = Dog() + self.cat = Cat() + self.human = Human() + self.car = Car() + + def test_dog_shall_bark(self): + noise = self.dog.bark() + expected_noise = "woof!" + self.assertEqual(noise, expected_noise) + + def test_cat_shall_meow(self): + noise = self.cat.meow() + expected_noise = "meow!" + self.assertEqual(noise, expected_noise) + + def test_human_shall_speak(self): + noise = self.human.speak() + expected_noise = "'hello'" + self.assertEqual(noise, expected_noise) + + def test_car_shall_make_loud_noise(self): + noise = self.car.make_noise(1) + expected_noise = "vroom!" + self.assertEqual(noise, expected_noise) + + def test_car_shall_make_very_loud_noise(self): + noise = self.car.make_noise(10) + expected_noise = "vroom!!!!!!!!!!" + self.assertEqual(noise, expected_noise) + + +class AdapterTest(unittest.TestCase): + def test_dog_adapter_shall_make_noise(self): + dog = Dog() + dog_adapter = Adapter(dog, make_noise=dog.bark) + noise = dog_adapter.make_noise() + expected_noise = "woof!" + self.assertEqual(noise, expected_noise) + + def test_cat_adapter_shall_make_noise(self): + cat = Cat() + cat_adapter = Adapter(cat, make_noise=cat.meow) + noise = cat_adapter.make_noise() + expected_noise = "meow!" + self.assertEqual(noise, expected_noise) + + def test_human_adapter_shall_make_noise(self): + human = Human() + human_adapter = Adapter(human, make_noise=human.speak) + noise = human_adapter.make_noise() + expected_noise = "'hello'" + self.assertEqual(noise, expected_noise) + + def test_car_adapter_shall_make_loud_noise(self): + car = Car() + car_adapter = Adapter(car, make_noise=car.make_noise) + noise = car_adapter.make_noise(1) + expected_noise = "vroom!" + self.assertEqual(noise, expected_noise) + + def test_car_adapter_shall_make_very_loud_noise(self): + car = Car() + car_adapter = Adapter(car, make_noise=car.make_noise) + noise = car_adapter.make_noise(10) + expected_noise = "vroom!!!!!!!!!!" + + self.assertEqual(noise, expected_noise) diff --git a/tests/structural/test_bridge.py b/tests/structural/test_bridge.py new file mode 100644 index 00000000..7fa8a278 --- /dev/null +++ b/tests/structural/test_bridge.py @@ -0,0 +1,42 @@ +import unittest +from unittest.mock import patch + +from patterns.structural.bridge import CircleShape, DrawingAPI1, DrawingAPI2 + + +class BridgeTest(unittest.TestCase): + def test_bridge_shall_draw_with_concrete_api_implementation(cls): + ci1 = DrawingAPI1() + ci2 = DrawingAPI2() + with patch.object(ci1, "draw_circle") as mock_ci1_draw_circle, patch.object( + ci2, "draw_circle" + ) as mock_ci2_draw_circle: + sh1 = CircleShape(1, 2, 3, ci1) + sh1.draw() + cls.assertEqual(mock_ci1_draw_circle.call_count, 1) + sh2 = CircleShape(1, 2, 3, ci2) + sh2.draw() + cls.assertEqual(mock_ci2_draw_circle.call_count, 1) + + def test_bridge_shall_scale_both_api_circles_with_own_implementation(cls): + SCALE_FACTOR = 2 + CIRCLE1_RADIUS = 3 + EXPECTED_CIRCLE1_RADIUS = 6 + CIRCLE2_RADIUS = CIRCLE1_RADIUS * CIRCLE1_RADIUS + EXPECTED_CIRCLE2_RADIUS = CIRCLE2_RADIUS * SCALE_FACTOR + + ci1 = DrawingAPI1() + ci2 = DrawingAPI2() + sh1 = CircleShape(1, 2, CIRCLE1_RADIUS, ci1) + sh2 = CircleShape(1, 2, CIRCLE2_RADIUS, ci2) + sh1.scale(SCALE_FACTOR) + sh2.scale(SCALE_FACTOR) + cls.assertEqual(sh1._radius, EXPECTED_CIRCLE1_RADIUS) + cls.assertEqual(sh2._radius, EXPECTED_CIRCLE2_RADIUS) + with patch.object(sh1, "scale") as mock_sh1_scale_circle, patch.object( + sh2, "scale" + ) as mock_sh2_scale_circle: + sh1.scale(2) + sh2.scale(2) + cls.assertEqual(mock_sh1_scale_circle.call_count, 1) + cls.assertEqual(mock_sh2_scale_circle.call_count, 1) diff --git a/tests/structural/test_decorator.py b/tests/structural/test_decorator.py new file mode 100644 index 00000000..8a4154a9 --- /dev/null +++ b/tests/structural/test_decorator.py @@ -0,0 +1,24 @@ +import unittest + +from patterns.structural.decorator import BoldWrapper, ItalicWrapper, TextTag + + +class TestTextWrapping(unittest.TestCase): + def setUp(self): + self.raw_string = TextTag("raw but not cruel") + + def test_italic(self): + self.assertEqual( + ItalicWrapper(self.raw_string).render(), "raw but not cruel" + ) + + def test_bold(self): + self.assertEqual( + BoldWrapper(self.raw_string).render(), "raw but not cruel" + ) + + def test_mixed_bold_and_italic(self): + self.assertEqual( + BoldWrapper(ItalicWrapper(self.raw_string)).render(), + "raw but not cruel", + ) diff --git a/tests/structural/test_proxy.py b/tests/structural/test_proxy.py new file mode 100644 index 00000000..3409bf0b --- /dev/null +++ b/tests/structural/test_proxy.py @@ -0,0 +1,37 @@ +import sys +import unittest +from io import StringIO + +from patterns.structural.proxy import Proxy, client + + +class ProxyTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + """Class scope setup.""" + cls.proxy = Proxy() + + def setUp(cls): + """Function/test case scope setup.""" + cls.output = StringIO() + cls.saved_stdout = sys.stdout + sys.stdout = cls.output + + def tearDown(cls): + """Function/test case scope teardown.""" + cls.output.close() + sys.stdout = cls.saved_stdout + + def test_do_the_job_for_admin_shall_pass(self): + client(self.proxy, "admin") + assert self.output.getvalue() == ( + "[log] Doing the job for admin is requested.\n" + "I am doing the job for admin\n" + ) + + def test_do_the_job_for_anonymous_shall_reject(self): + client(self.proxy, "anonymous") + assert self.output.getvalue() == ( + "[log] Doing the job for anonymous is requested.\n" + "[log] I can do the job just for `admins`.\n" + ) diff --git a/tests/test_hsm.py b/tests/test_hsm.py new file mode 100644 index 00000000..f42323a9 --- /dev/null +++ b/tests/test_hsm.py @@ -0,0 +1,99 @@ +import unittest +from unittest.mock import patch + +from patterns.other.hsm.hsm import ( + Active, + HierachicalStateMachine, + Standby, + Suspect, + UnsupportedMessageType, + UnsupportedState, + UnsupportedTransition, +) + + +class HsmMethodTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.hsm = HierachicalStateMachine() + + def test_initial_state_shall_be_standby(cls): + cls.assertEqual(isinstance(cls.hsm._current_state, Standby), True) + + def test_unsupported_state_shall_raise_exception(cls): + with cls.assertRaises(UnsupportedState): + cls.hsm._next_state("missing") + + def test_unsupported_message_type_shall_raise_exception(cls): + with cls.assertRaises(UnsupportedMessageType): + cls.hsm.on_message("trigger") + + def test_calling_next_state_shall_change_current_state(cls): + cls.hsm._current_state = Standby # initial state + cls.hsm._next_state("active") + cls.assertEqual(isinstance(cls.hsm._current_state, Active), True) + cls.hsm._current_state = Standby(cls.hsm) # initial state + + def test_method_perform_switchover_shall_return_specifically(cls): + """Exemplary HierachicalStateMachine method test. + (here: _perform_switchover()). Add additional test cases...""" + return_value = cls.hsm._perform_switchover() + expected_return_value = "perform switchover" + cls.assertEqual(return_value, expected_return_value) + + +class StandbyStateTest(unittest.TestCase): + """Exemplary 2nd level state test class (here: Standby state). Add missing + state test classes...""" + + @classmethod + def setUpClass(cls): + cls.hsm = HierachicalStateMachine() + + def setUp(cls): + cls.hsm._current_state = Standby(cls.hsm) + + def test_given_standby_on_message_switchover_shall_set_active(cls): + cls.hsm.on_message("switchover") + cls.assertEqual(isinstance(cls.hsm._current_state, Active), True) + + def test_given_standby_on_message_switchover_shall_call_hsm_methods(cls): + with patch.object( + cls.hsm, "_perform_switchover" + ) as mock_perform_switchover, patch.object( + cls.hsm, "_check_mate_status" + ) as mock_check_mate_status, patch.object( + cls.hsm, "_send_switchover_response" + ) as mock_send_switchover_response, patch.object( + cls.hsm, "_next_state" + ) as mock_next_state: + cls.hsm.on_message("switchover") + cls.assertEqual(mock_perform_switchover.call_count, 1) + cls.assertEqual(mock_check_mate_status.call_count, 1) + cls.assertEqual(mock_send_switchover_response.call_count, 1) + cls.assertEqual(mock_next_state.call_count, 1) + + def test_given_standby_on_message_fault_trigger_shall_set_suspect(cls): + cls.hsm.on_message("fault trigger") + cls.assertEqual(isinstance(cls.hsm._current_state, Suspect), True) + + def test_given_standby_on_message_diagnostics_failed_shall_raise_exception_and_keep_in_state( + cls, + ): + with cls.assertRaises(UnsupportedTransition): + cls.hsm.on_message("diagnostics failed") + cls.assertEqual(isinstance(cls.hsm._current_state, Standby), True) + + def test_given_standby_on_message_diagnostics_passed_shall_raise_exception_and_keep_in_state( + cls, + ): + with cls.assertRaises(UnsupportedTransition): + cls.hsm.on_message("diagnostics passed") + cls.assertEqual(isinstance(cls.hsm._current_state, Standby), True) + + def test_given_standby_on_message_operator_inservice_shall_raise_exception_and_keep_in_state( + cls, + ): + with cls.assertRaises(UnsupportedTransition): + cls.hsm.on_message("operator inservice") + cls.assertEqual(isinstance(cls.hsm._current_state, Standby), True) diff --git a/visitor.py b/visitor.py deleted file mode 100644 index ca45102e..00000000 --- a/visitor.py +++ /dev/null @@ -1,52 +0,0 @@ -"""http://peter-hoffmann.com/2010/extrinsic-visitor-pattern-python-inheritance.html""" - - -class Node(object): - pass - - -class A(Node): - pass - - -class B(Node): - pass - - -class C(A, B): - pass - - -class Visitor(object): - - def visit(self, node, *args, **kwargs): - meth = None - for cls in node.__class__.__mro__: - meth_name = 'visit_' + cls.__name__ - meth = getattr(self, meth_name, None) - if meth: - break - - if not meth: - meth = self.generic_visit - return meth(node, *args, **kwargs) - - def generic_visit(self, node, *args, **kwargs): - print('generic_visit ' + node.__class__.__name__) - - def visit_B(self, node, *args, **kwargs): - print('visit_B ' + node.__class__.__name__) - - -a = A() -b = B() -c = C() -visitor = Visitor() -visitor.visit(a) -visitor.visit(b) -visitor.visit(c) - -### OUTPUT ### -# generic_visit A -# visit_B B -# visit_B C