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..3942ffce --- /dev/null +++ b/.github/workflows/lint_pr.yml @@ -0,0 +1,288 @@ +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@v6 + 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 + # Use proper delimiter formatting for GitHub Actions + FILES_SINGLE_LINE=$(echo "$CHANGED_FILES" | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g' | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//') + echo "files=$FILES_SINGLE_LINE" >> $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@v6 + with: + fetch-depth: 0 + + - uses: actions/setup-python@v6 + with: + python-version: 3.12 + + - uses: actions/cache@v5 + 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@v6 + + - 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..4e2f16be --- /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@v6 + - uses: actions/setup-python@v6 + with: + python-version: 3.12 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install .[dev] + - 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 new file mode 100644 index 00000000..4521242b --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +__pycache__ +*.pyc +.idea +*.egg-info/ +.tox/ +env/ +venv/ +.env +.venv +.vscode/ +.python-version +.coverage +.project +.pydevproject +/.pytest_cache/ +build/ +dist/ 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 c982d226..00000000 --- a/3-tier.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - - -class Data(object): - - products = { - 'milk': {'price': 1.50, 'quantity': 10}, - 'eggs': {'price': 0.20, 'quantity': 100}, - 'cheese': {'price': 2.00, 'quantity': 10} - } - - -class BusinessLogic(object): - - def __init__(self): - self.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): - - 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 is not None: - print('PRODUCT INFORMATION:') - print('Name: %s, Price: %.2f, Quantity: %d\n' % - (product.title(), product_info.get('price', 0), - product_info.get('quantity', 0))) - else: - print('That product "%s" does not exist in the records' % product) - - -if __name__ == '__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') 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 ff1ebb26..c5796895 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,181 @@ -python-patterns -=============== +# python-patterns -A collection of design patterns implemented (by other people) in python \ No newline at end of file +A collection of design patterns and idioms in Python. + +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. + +## Creational Patterns + +> Patterns that deal with **object creation** — abstracting and controlling how instances are made. + +```mermaid +graph LR + Client -->|requests object| AbstractFactory + AbstractFactory -->|delegates to| ConcreteFactory + ConcreteFactory -->|produces| Product + + Builder -->|step-by-step| Director + Director -->|returns| BuiltObject + + FactoryMethod -->|subclass decides| ConcreteProduct + Pool -->|reuses| PooledInstance +``` + +| 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 + +> Patterns that define **how classes and objects are composed** to form larger, flexible structures. + +```mermaid +graph TD + Client --> Facade + Facade --> SubsystemA + Facade --> SubsystemB + Facade --> SubsystemC + + Client2 --> Adapter + Adapter --> LegacyService + + Client3 --> Proxy + Proxy -->|controls access to| RealSubject + + Component --> Composite + Composite --> Leaf1 + Composite --> Leaf2 +``` + +| 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 + +> Patterns concerned with **communication and responsibility** between objects. + +```mermaid +graph LR + Sender -->|sends event| Observer1 + Sender -->|sends event| Observer2 + + Request --> Handler1 + Handler1 -->|passes if unhandled| Handler2 + Handler2 -->|passes if unhandled| Handler3 + + Context -->|delegates to| Strategy + Strategy -->|executes| Algorithm + + Originator -->|saves state to| Memento + Caretaker -->|holds| Memento +``` + +| 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 | +| [interpreter](patterns/behavioral/interpreter.py) | define a grammar for a language and use it to interpret statements | +| [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 | +| [servant](patterns/behavioral/servant.py) | provide common functionality to a group of classes without using inheritance | +| [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 | +|:-------:| ----------- | +| [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 | + +## 🚫 Anti-Patterns + +This section lists some common design patterns that are **not recommended** in Python and explains why. + +### 🧱 Singleton +**Why not:** +- Python modules are already singletons — every module is imported only once. +- Explicit singleton classes add unnecessary complexity. +- Better alternatives: use module-level variables or dependency injection. + +### 🌀 God Object +**Why not:** +- Centralizes too much logic in a single class. +- Makes code harder to test and maintain. +- Better alternative: split functionality into smaller, cohesive classes. + +### 🔁 Inheritance overuse +**Why not:** +- Deep inheritance trees make code brittle. +- Prefer composition and delegation. +- “Favor composition over inheritance.” + +## 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. +- Either `tox` or `tox -e ci37` for unit tests. +- If you have a bash compatible shell, use `./lint.sh`. + +## 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 on [CodeTriage](https://www.codetriage.com/faif/python-patterns). diff --git a/__pycache__/facade.cpython-33.pyc b/__pycache__/facade.cpython-33.pyc deleted file mode 100644 index 803a0c9b..00000000 Binary files a/__pycache__/facade.cpython-33.pyc and /dev/null differ diff --git a/__pycache__/mediator.cpython-33.pyc b/__pycache__/mediator.cpython-33.pyc deleted file mode 100644 index c002de4b..00000000 Binary files a/__pycache__/mediator.cpython-33.pyc and /dev/null differ diff --git a/__pycache__/null.cpython-33.pyc b/__pycache__/null.cpython-33.pyc deleted file mode 100644 index 37dd75c3..00000000 Binary files a/__pycache__/null.cpython-33.pyc and /dev/null differ diff --git a/abstract_factory.py b/abstract_factory.py deleted file mode 100644 index 27aab752..00000000 --- a/abstract_factory.py +++ /dev/null @@ -1,75 +0,0 @@ -# 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("This is a lovely", pet) - print("It says", pet.speak()) - print("It eats", 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__": - shop = PetShop() - for i in range(3): - shop.pet_factory = get_factory() - shop.show_pet() - print("=" * 20) diff --git a/adapter.py b/adapter.py deleted file mode 100644 index 42c339dd..00000000 --- a/adapter.py +++ /dev/null @@ -1,72 +0,0 @@ -# http://ginstrom.com/scribbles/2008/11/06/generic-adapter-class-in-python/ - -import os - - -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%s" % ("!" * octane_level) - - -class Adapter(object): - """ - Adapts an object by replacing methods. - Usage: - dog = Dog - dog = Adapter(dog, dict(make_noise=dog.bark)) - """ - 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, dict(make_noise=dog.bark))) - cat = Cat() - objects.append(Adapter(cat, dict(make_noise=cat.meow))) - human = Human() - objects.append(Adapter(human, dict(make_noise=human.speak))) - car = Car() - car_noise = lambda: car.make_noise(3) - objects.append(Adapter(car, dict(make_noise=car_noise))) - - for obj in objects: - print("A", obj.name, "goes", obj.make_noise()) - - -if __name__ == "__main__": - main() diff --git a/borg.py b/borg.py deleted file mode 100644 index 48ac26c9..00000000 --- a/borg.py +++ /dev/null @@ -1,36 +0,0 @@ -class Borg: - __shared_state = {} - - def __init__(self): - self.__dict__ = self.__shared_state - - 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:', rm1) - print('rm2:', rm2) - - rm2.state = 'Zombie' - - print('rm1:', rm1) - print('rm2:', rm2) - - print('rm1 id:', id(rm1)) - print('rm2 id:', id(rm2)) - - rm3 = YourBorg() - - print('rm1:', rm1) - print('rm2:', rm2) - print('rm3:', rm3) diff --git a/bridge.py b/bridge.py deleted file mode 100644 index aecb43b5..00000000 --- a/bridge.py +++ /dev/null @@ -1,45 +0,0 @@ -# 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() diff --git a/builder.py b/builder.py deleted file mode 100644 index 35b88070..00000000 --- a/builder.py +++ /dev/null @@ -1,70 +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: %s | Size: %s' % (self.floor, self.size) - - -# 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) diff --git a/chain.py b/chain.py deleted file mode 100644 index c08f7f66..00000000 --- a/chain.py +++ /dev/null @@ -1,48 +0,0 @@ -# http://www.testingperspective.com/wiki/doku.php/collaboration/chetan/designpatternsinpython/chain-of-responsibilitypattern - - -class Handler: - def successor(self, successor): - self.successor = successor - - -class ConcreteHandler1(Handler): - def handle(self, request): - if 0 < request <= 10: - print("in handler1") - else: - self.successor.handle(request) - - -class ConcreteHandler2(Handler): - def handle(self, request): - if 10 < request <= 20: - print("in handler2") - else: - self.successor.handle(request) - - -class ConcreteHandler3(Handler): - def handle(self, request): - if 20 < request <= 30: - print("in handler3") - else: - print('end of chain, no handler for {}'.format(request)) - - -class Client: - def __init__(self): - h1 = ConcreteHandler1() - h2 = ConcreteHandler2() - h3 = ConcreteHandler3() - - h1.successor(h2) - h2.successor(h3) - - requests = [2, 5, 14, 22, 18, 3, 35, 27, 20] - for request in requests: - h1.handle(request) - - -if __name__ == "__main__": - client = Client() diff --git a/command.py b/command.py deleted file mode 100644 index d161d0e4..00000000 --- a/command.py +++ /dev/null @@ -1,34 +0,0 @@ -import os - - -class MoveFileCommand(object): - def __init__(self, src, dest): - self.src = src - self.dest = dest - - def execute(self): - self() - - def __call__(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) - - -if __name__ == "__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() diff --git a/composite.py b/composite.py deleted file mode 100644 index 3a1fe42a..00000000 --- a/composite.py +++ /dev/null @@ -1,331 +0,0 @@ -""" -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) 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 4be17267..00000000 --- a/decorator.py +++ /dev/null @@ -1,26 +0,0 @@ -# http://stackoverflow.com/questions/3118929/implementing-the-decorator-pattern-in-python - - -class foo(object): - def f1(self): - print("original f1") - - def f2(self): - print("original f2") - - -class foo_decorator(object): - def __init__(self, decoratee): - self._decoratee = decoratee - - def f1(self): - print("decorated f1") - self._decoratee.f1() - - def __getattr__(self, name): - return getattr(self._decoratee, name) - -u = foo() -v = foo_decorator(u) -v.f1() -v.f2() diff --git a/facade.py b/facade.py deleted file mode 100644 index 00605cf1..00000000 --- a/facade.py +++ /dev/null @@ -1,63 +0,0 @@ -"""http://dpip.testingperspective.com/?p=26""" - -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() diff --git a/factory_method.py b/factory_method.py deleted file mode 100644 index cbcc09b1..00000000 --- a/factory_method.py +++ /dev/null @@ -1,33 +0,0 @@ -#encoding=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("English"), get_localizer("Greek") -# Localize some text -for msgid in "dog parrot cat bear".split(): - print(e.get(msgid), g.get(msgid)) diff --git a/flyweight.py b/flyweight.py deleted file mode 100644 index 8166a53b..00000000 --- a/flyweight.py +++ /dev/null @@ -1,33 +0,0 @@ -"""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)) diff --git a/foo.txt b/foo.txt deleted file mode 100644 index 5aecc849..00000000 --- a/foo.txt +++ /dev/null @@ -1 +0,0 @@ -All krakens crush undead, evil sails. diff --git a/graph_search.py b/graph_search.py deleted file mode 100644 index 798c6e24..00000000 --- a/graph_search.py +++ /dev/null @@ -1,76 +0,0 @@ -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=[]): - self.start = start - self.end = end - self.path = path - - self.path += [self.start] - if self.start == self.end: - return self.path - if not self.graph.has_key(self.start): - 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=[]): - self.start = start - self.end = end - self.path = path - self.path += [self.start] - if self.start == self.end: - return [self.path] - if not self.graph.has_key(self.start): - return [] - paths = [] - for node in self.graph[self.start]: - if node not in self.path: - newpaths = self.find_all_path(node, self.end, self.path) - for newpath in newpaths: - paths.append(newpath) - return paths - - def find_shortest_path(self, start, end, path=[]): - self.start = start - self.end = end - self.path = path - - self.path += [self.start] - if self.start == self.end: - return self.path - if not self.graph.has_key(self.start): - return None - shortest = None - for node in self.graph[self.start]: - if node not in self.path: - newpath = self.find_shortest_path(node, self.end, self.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'] - } - -#inistialization 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') diff --git a/iterator.py b/iterator.py deleted file mode 100644 index f8922bbe..00000000 --- a/iterator.py +++ /dev/null @@ -1,28 +0,0 @@ -"""http://ginstrom.com/scribbles/2007/10/08/design-patterns-python-style/ - -Implementation of the iterator pattern with a generator""" - - -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() 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 aa794142..00000000 --- a/mediator.py +++ /dev/null @@ -1,117 +0,0 @@ -"""http://dpip.testingperspective.com/?p=28""" - -import time - - -class TC: - def __init__(self): - self._tm = tm - self._bProblem = 0 - - def setup(self): - print("Setting up the Test") - time.sleep(1) - self._tm.prepareReporting() - - def execute(self): - if not self._bProblem: - print("Executing the test") - time.sleep(1) - else: - print("Problem in setup. Test not executed.") - - def tearDown(self): - if not self._bProblem: - print("Tearing down") - time.sleep(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(1) - - def report(self): - print("Reporting the results of Test") - time.sleep(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(1) - #Following code is to simulate a communication from DB to TC - import random - if random.randrange(1, 4) == 3: - return -1 - - def update(self): - print("Updating the test results in Database") - time.sleep(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() - rvalue = 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 - while True: - tc = TC() - tc.setTM(tm) - tm.setTC(tc) - tc.setup() - tc.execute() - tc.tearDown() diff --git a/memento.py b/memento.py deleted file mode 100644 index bc55d283..00000000 --- a/memento.py +++ /dev/null @@ -1,94 +0,0 @@ -"""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 traceback - traceback.print_exc(0) - pass - print(n) diff --git a/mvc.py b/mvc.py deleted file mode 100644 index 69d5e8ce..00000000 --- a/mvc.py +++ /dev/null @@ -1,57 +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') diff --git a/null.py b/null.py deleted file mode 100644 index 90295777..00000000 --- a/null.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/user/bin/env python - -"""http://code.activestate.com/recipes/68205-null-object-design-pattern/""" - - -class Null: - def __init__(self, *args, **kwargs): - """Ignore parameters.""" - return None - - def __call__(self, *args, **kwargs): - """Ignore method calls.""" - return self - - def __getattr__(self, mname): - """Ignore attribute requests.""" - return self - - def __setattr__(self, name, value): - """Ignore attribute setting.""" - return self - - def __delattr__(self, name): - """Ignore deleting attributes.""" - return self - - def __repr__(self): - """Return a string representation.""" - return "" - - def __str__(self): - """Convert to a string and return it.""" - return "Null" - - -def test(): - """Perform some decent tests, or rather: demos.""" - - # constructing and calling - - n = Null() - print(n) - - n = Null('value') - print(n) - - n = Null('value', param='value') - print(n) - - n() - n('value') - n('value', param='value') - print(n) - - # attribute handling - - n.attr1 - print('attr1', n.attr1) - n.attr1.attr2 - n.method1() - n.method1().method2() - n.method('value') - n.method(param='value') - n.method('value', param='value') - n.attr1.method1() - n.method1().attr1 - - n.attr1 = 'value' - n.attr1.attr2 = 'value' - - del n.attr1 - del n.attr1.attr2.attr3 - - # representation and conversion to a string - - assert repr(n) == '' - assert str(n) == 'Null' - - -if __name__ == '__main__': - test() diff --git a/observer.py b/observer.py deleted file mode 100644 index 23141e87..00000000 --- a/observer.py +++ /dev/null @@ -1,82 +0,0 @@ -"""http://code.activestate.com/recipes/131499-observer-pattern/""" - - -class Subject(object): - def __init__(self): - self._observers = [] - - def attach(self, observer): - if not observer 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() 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..11a730c3 --- /dev/null +++ b/patterns/behavioral/catalog.py @@ -0,0 +1,175 @@ +""" +A class that uses different static functions depending on a parameter passed +during initialization. Uses 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() -> str: + return "executed method 1!" + + @staticmethod + def _static_method_2() -> str: + return "executed method 2!" + + def main_method(self) -> str: + """will execute either _static_method_1 or _static_method_2 + + depending on self.param value + """ + return 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) -> str: + return f"Value {self.x1}" + + def _instance_method_2(self) -> str: + return f"Value {self.x2}" + + _instance_method_choices = { + "param_value_1": _instance_method_1, + "param_value_2": _instance_method_2, + } + + def main_method(self) -> str: + """will execute either _instance_method_1 or _instance_method_2 + + depending on self.param value + """ + return 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) -> str: + return f"Value {cls.x1}" + + @classmethod + def _class_method_2(cls) -> str: + return f"Value {cls.x2}" + + _class_method_choices = { + "param_value_1": _class_method_1, + "param_value_2": _class_method_2, + } + + def main_method(self) -> str: + """will execute either _class_method_1 or _class_method_2 + + depending on self.param value + """ + return 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() -> str: + return "executed method 1!" + + @staticmethod + def _static_method_2() -> str: + return "executed method 2!" + + _static_method_choices = { + "param_value_1": _static_method_1, + "param_value_2": _static_method_2, + } + + def main_method(self) -> str: + """will execute either _static_method_1 or _static_method_2 + + depending on self.param value + """ + + return 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..46c3a419 --- /dev/null +++ b/patterns/behavioral/chain_of_responsibility.py @@ -0,0 +1,123 @@ +""" +*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`. + +*Examples in Python ecosystem: +Django Middleware: https://docs.djangoproject.com/en/stable/topics/http/middleware/ +The middleware components act as a chain where each processes the request/response. + +*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..6a59bbb6 --- /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: + return 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: + return 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..c0d63e9e --- /dev/null +++ b/patterns/behavioral/memento.py @@ -0,0 +1,145 @@ +""" +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 Any, Callable, List, Type + + +def memento(obj: Any, deep: bool = False) -> Callable: + state = deepcopy(obj.__dict__) if deep else copy(obj.__dict__) + + def restore() -> None: + 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: bool, *targets: Any) -> None: + self.deep = deep + self.targets = targets + self.commit() + + def commit(self) -> None: + self.states = [memento(target, self.deep) for target in self.targets] + + def rollback(self) -> None: + 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 __init__(self, method: Callable) -> None: + self.method = method + + def __get__(self, obj: Any, T: Type) -> Callable: + """ + A decorator that makes a function transactional. + + :param method: The function to be decorated. + """ + + def transaction(*args, **kwargs): + state = memento(obj) + try: + return self.method(obj, *args, **kwargs) + except Exception as e: + state() + raise e + + return transaction + + +class NumObj: + def __init__(self, value: int) -> None: + self.value = value + + def __repr__(self) -> str: + return f"<{self.__class__.__name__}: {self.value!r}>" + + def increment(self) -> None: + self.value += 1 + + @Transactional + def do_stuff(self) -> None: + 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..c9184be1 --- /dev/null +++ b/patterns/behavioral/observer.py @@ -0,0 +1,135 @@ +""" +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/ +""" + +# observer.py + +from __future__ import annotations +from typing import List + +class Observer: + def update(self, subject: Subject) -> None: + """ + Receive update from the subject. + + Args: + subject (Subject): The subject instance sending the update. + """ + pass + + +class Subject: + _observers: List[Observer] + + def __init__(self) -> None: + """ + Initialize the subject with an empty observer list. + """ + self._observers = [] + + def attach(self, observer: Observer) -> None: + """ + Attach an observer to the subject. + + Args: + observer (Observer): The observer instance to attach. + """ + if observer not in self._observers: + self._observers.append(observer) + + def detach(self, observer: Observer) -> None: + """ + Detach an observer from the subject. + + Args: + observer (Observer): The observer instance to detach. + """ + try: + self._observers.remove(observer) + except ValueError: + pass + + def notify(self) -> None: + """ + Notify all attached observers by calling their update method. + """ + for observer in self._observers: + 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..60cae019 --- /dev/null +++ b/patterns/behavioral/registry.py @@ -0,0 +1,48 @@ +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/servant.py b/patterns/behavioral/servant.py new file mode 100644 index 00000000..776c4126 --- /dev/null +++ b/patterns/behavioral/servant.py @@ -0,0 +1,131 @@ +""" +Implementation of the Servant design pattern. + +The Servant design pattern is a behavioral pattern used to offer functionality +to a group of classes without requiring them to inherit from a base class. + +This pattern involves creating a Servant class that provides certain services +or functionalities. These services are used by other classes which do not need +to be related through a common parent class. It is particularly useful in +scenarios where adding the desired functionality through inheritance is impractical +or would lead to a rigid class hierarchy. + +This pattern is characterized by the following: + +- A Servant class that provides specific services or actions. +- Client classes that need these services, but do not derive from the Servant class. +- The use of the Servant class by the client classes to perform actions on their behalf. + +References: +- https://en.wikipedia.org/wiki/Servant_(design_pattern) +""" + +import math + + +class Position: + """Representation of a 2D position with x and y coordinates.""" + + def __init__(self, x, y): + self.x = x + self.y = y + + +class Circle: + """Representation of a circle defined by a radius and a position.""" + + def __init__(self, radius, position: Position): + self.radius = radius + self.position = position + + +class Rectangle: + """Representation of a rectangle defined by width, height, and a position.""" + + def __init__(self, width, height, position: Position): + self.width = width + self.height = height + self.position = position + + +class GeometryTools: + """ + Servant class providing geometry-related services, including area and + perimeter calculations and position updates. + """ + + @staticmethod + def calculate_area(shape): + """ + Calculate the area of a given shape. + + Args: + shape: The geometric shape whose area is to be calculated. + + Returns: + The area of the shape. + + Raises: + ValueError: If the shape type is unsupported. + """ + if isinstance(shape, Circle): + return math.pi * shape.radius**2 + elif isinstance(shape, Rectangle): + return shape.width * shape.height + else: + raise ValueError("Unsupported shape type") + + @staticmethod + def calculate_perimeter(shape): + """ + Calculate the perimeter of a given shape. + + Args: + shape: The geometric shape whose perimeter is to be calculated. + + Returns: + The perimeter of the shape. + + Raises: + ValueError: If the shape type is unsupported. + """ + if isinstance(shape, Circle): + return 2 * math.pi * shape.radius + elif isinstance(shape, Rectangle): + return 2 * (shape.width + shape.height) + else: + raise ValueError("Unsupported shape type") + + @staticmethod + def move_to(shape, new_position: Position): + """ + Move a given shape to a new position. + + Args: + shape: The geometric shape to be moved. + new_position: The new position to move the shape to. + """ + shape.position = new_position + print(f"Moved to ({shape.position.x}, {shape.position.y})") + + +def main(): + """ + >>> servant = GeometryTools() + >>> circle = Circle(5, Position(0, 0)) + >>> rectangle = Rectangle(3, 4, Position(0, 0)) + >>> servant.calculate_area(circle) + 78.53981633974483 + >>> servant.calculate_perimeter(rectangle) + 14 + >>> servant.move_to(circle, Position(3, 4)) + Moved to (3, 4) + >>> servant.move_to(rectangle, Position(5, 6)) + Moved to (5, 6) + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/behavioral/specification.py b/patterns/behavioral/specification.py new file mode 100644 index 00000000..10d22689 --- /dev/null +++ b/patterns/behavioral/specification.py @@ -0,0 +1,110 @@ +""" +@author: Gordeev Andrey + +*TL;DR +Provides recombination business logic by chaining together using boolean logic. +""" + +from abc import abstractmethod +from typing import Union + + +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: "Specification") -> "AndSpecification": + return AndSpecification(self, candidate) + + def or_specification(self, candidate: "Specification") -> "OrSpecification": + return OrSpecification(self, candidate) + + def not_specification(self) -> "NotSpecification": + return NotSpecification(self) + + +class AndSpecification(CompositeSpecification): + def __init__(self, one: "Specification", other: "Specification") -> None: + self._one: Specification = one + self._other: Specification = other + + def is_satisfied_by(self, candidate: Union["User", str]) -> bool: + return bool( + self._one.is_satisfied_by(candidate) + and self._other.is_satisfied_by(candidate) + ) + + +class OrSpecification(CompositeSpecification): + def __init__(self, one: "Specification", other: "Specification") -> None: + self._one: Specification = one + self._other: Specification = other + + def is_satisfied_by(self, candidate: Union["User", str]): + return bool( + self._one.is_satisfied_by(candidate) + or self._other.is_satisfied_by(candidate) + ) + + +class NotSpecification(CompositeSpecification): + def __init__(self, wrapped: "Specification"): + self._wrapped: Specification = wrapped + + def is_satisfied_by(self, candidate: Union["User", str]): + return bool(not self._wrapped.is_satisfied_by(candidate)) + + +class User: + def __init__(self, super_user: bool = False) -> None: + self.super_user = super_user + + +class UserSpecification(CompositeSpecification): + def is_satisfied_by(self, candidate: Union["User", str]) -> bool: + return isinstance(candidate, User) + + +class SuperUserSpecification(CompositeSpecification): + def is_satisfied_by(self, candidate: "User") -> bool: + 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..aa10b58c --- /dev/null +++ b/patterns/behavioral/visitor.py @@ -0,0 +1,75 @@ +""" +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 +""" +from typing import Union + + +class Node: + pass + + +class A(Node): + pass + + +class B(Node): + pass + + +class C(A, B): + pass + + +class Visitor: + def visit(self, node: Union[A, C, B], *args, **kwargs) -> None: + 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: A, *args, **kwargs) -> None: + print("generic_visit " + node.__class__.__name__) + + def visit_B(self, node: Union[C, B], *args, **kwargs) -> None: + 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..16af2295 --- /dev/null +++ b/patterns/creational/builder.py @@ -0,0 +1,112 @@ +""" +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? +See: 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..c8fea112 --- /dev/null +++ b/patterns/creational/factory.py @@ -0,0 +1,78 @@ +"""*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: ... + + +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.get(language, EnglishLocalizer)() + + +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..1f8db6bd --- /dev/null +++ b/patterns/creational/lazy_evaluation.py @@ -0,0 +1,112 @@ +""" +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 +from typing import Callable, Type + + +class lazy_property: + def __init__(self, function: Callable) -> None: + self.function = function + functools.update_wrapper(self, function) + + def __get__(self, obj: "Person", type_: Type["Person"]) -> str: + if obj is None: + return self + val = self.function(obj) + obj.__dict__[self.function.__name__] = val + return val + + +def lazy_property2(fn: Callable) -> property: + """ + 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: str, occupation: str) -> None: + self.name = name + self.occupation = occupation + self.call_count2 = 0 + + @lazy_property + def relatives(self) -> str: + # Get all relatives, let's assume that it costs much time. + relatives = "Many relatives." + return relatives + + @lazy_property2 + def parents(self) -> str: + 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..02f61791 --- /dev/null +++ b/patterns/creational/pool.py @@ -0,0 +1,94 @@ +""" +*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. +""" +from queue import Queue +from types import TracebackType +from typing import Union + + +class ObjectPool: + def __init__(self, queue: Queue, auto_get: bool = False) -> None: + self._queue = queue + self.item = self._queue.get() if auto_get else None + + def __enter__(self) -> str: + if self.item is None: + self.item = self._queue.get() + return self.item + + def __exit__( + self, + Type: Union[type[BaseException], None], + value: Union[BaseException, None], + traceback: Union[TracebackType, None], + ) -> None: + if self.item is not None: + self._queue.put(self.item) + self.item = None + + def __del__(self) -> None: + 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..0269a3e7 --- /dev/null +++ b/patterns/other/blackboard.py @@ -0,0 +1,142 @@ +""" +@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..6e3cdffb --- /dev/null +++ b/patterns/other/graph_search.py @@ -0,0 +1,159 @@ +from typing import Any, Dict, List, Optional, Union + + +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: Dict[str, List[str]]) -> None: + self.graph = graph + + def find_path_dfs( + self, start: str, end: str, path: Optional[List[str]] = None + ) -> Optional[List[str]]: + 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: str, end: str, path: Optional[List[str]] = None + ) -> List[Union[List[str], Any]]: + 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: str, end: str, path: Optional[List[str]] = None + ) -> Optional[List[str]]: + 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: str, end: str) -> Optional[List[str]]: + """ + 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..287badaf --- /dev/null +++ b/patterns/structural/3-tier.py @@ -0,0 +1,97 @@ +""" +*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..22adca88 --- /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, Any, Dict + +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[..., Any]) -> None: + """We set the adapted methods in the object's dict.""" + self.obj = obj + self.__dict__.update(adapted_methods) + + def __getattr__(self, attr: str) -> Any: + """All non-adapted calls are passed to the object.""" + return getattr(self.obj, attr) + + def original_dict(self) -> Dict[str, Any]: + """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..1575cb53 --- /dev/null +++ b/patterns/structural/bridge.py @@ -0,0 +1,57 @@ +""" +*References: +http://en.wikibooks.org/wiki/Computer_Science_Design_Patterns/Bridge_Pattern#Python + +*TL;DR +Decouples an abstraction from its implementation. +""" +from typing import Union + + +# ConcreteImplementor 1/2 +class DrawingAPI1: + def draw_circle(self, x: int, y: int, radius: float) -> None: + print(f"API1.circle at {x}:{y} radius {radius}") + + +# ConcreteImplementor 2/2 +class DrawingAPI2: + def draw_circle(self, x: int, y: int, radius: float) -> None: + print(f"API2.circle at {x}:{y} radius {radius}") + + +# Refined Abstraction +class CircleShape: + def __init__( + self, x: int, y: int, radius: int, drawing_api: Union[DrawingAPI2, DrawingAPI1] + ) -> None: + self._x = x + self._y = y + self._radius = radius + self._drawing_api = drawing_api + + # low-level i.e. Implementation specific + def draw(self) -> None: + self._drawing_api.draw_circle(self._x, self._y, self._radius) + + # high-level i.e. Abstraction specific + def scale(self, pct: float) -> None: + 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..68b6f43c --- /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: str, suit: str): + # 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) -> str: + 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..0a7c4034 --- /dev/null +++ b/patterns/structural/mvc.py @@ -0,0 +1,216 @@ +""" +*TL;DR +Separates data in GUIs from the ways it is presented, and accepted. +""" + +from abc import ABC, abstractmethod +from typing import Dict, List, Union, Any +from inspect import signature +from sys import argv + + +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/poetry.lock b/poetry.lock new file mode 100644 index 00000000..a2992404 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1029 @@ +# This file is automatically @generated by Poetry 2.3.1 and should not be changed by hand. + +[[package]] +name = "argcomplete" +version = "3.6.3" +description = "Bash tab completion for argparse" +optional = true +python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce"}, + {file = "argcomplete-3.6.3.tar.gz", hash = "sha256:62e8ed4fd6a45864acc8235409461b72c9a28ee785a2011cc5eb78318786c89c"}, +] + +[package.extras] +test = ["coverage", "mypy", "pexpect", "ruff", "wheel"] + +[[package]] +name = "black" +version = "26.1.0" +description = "The uncompromising code formatter." +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "black-26.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ca699710dece84e3ebf6e92ee15f5b8f72870ef984bf944a57a777a48357c168"}, + {file = "black-26.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5e8e75dabb6eb83d064b0db46392b25cabb6e784ea624219736e8985a6b3675d"}, + {file = "black-26.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb07665d9a907a1a645ee41a0df8a25ffac8ad9c26cdb557b7b88eeeeec934e0"}, + {file = "black-26.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:7ed300200918147c963c87700ccf9966dceaefbbb7277450a8d646fc5646bf24"}, + {file = "black-26.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:c5b7713daea9bf943f79f8c3b46f361cc5229e0e604dcef6a8bb6d1c37d9df89"}, + {file = "black-26.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3cee1487a9e4c640dc7467aaa543d6c0097c391dc8ac74eb313f2fbf9d7a7cb5"}, + {file = "black-26.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d62d14ca31c92adf561ebb2e5f2741bf8dea28aef6deb400d49cca011d186c68"}, + {file = "black-26.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb1dafbbaa3b1ee8b4550a84425aac8874e5f390200f5502cf3aee4a2acb2f14"}, + {file = "black-26.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:101540cb2a77c680f4f80e628ae98bd2bd8812fb9d72ade4f8995c5ff019e82c"}, + {file = "black-26.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:6f3977a16e347f1b115662be07daa93137259c711e526402aa444d7a88fdc9d4"}, + {file = "black-26.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6eeca41e70b5f5c84f2f913af857cf2ce17410847e1d54642e658e078da6544f"}, + {file = "black-26.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dd39eef053e58e60204f2cdf059e2442e2eb08f15989eefe259870f89614c8b6"}, + {file = "black-26.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9459ad0d6cd483eacad4c6566b0f8e42af5e8b583cee917d90ffaa3778420a0a"}, + {file = "black-26.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a19915ec61f3a8746e8b10adbac4a577c6ba9851fa4a9e9fbfbcf319887a5791"}, + {file = "black-26.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:643d27fb5facc167c0b1b59d0315f2674a6e950341aed0fc05cf307d22bf4954"}, + {file = "black-26.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ba1d768fbfb6930fc93b0ecc32a43d8861ded16f47a40f14afa9bb04ab93d304"}, + {file = "black-26.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b807c240b64609cb0e80d2200a35b23c7df82259f80bef1b2c96eb422b4aac9"}, + {file = "black-26.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1de0f7d01cc894066a1153b738145b194414cc6eeaad8ef4397ac9abacf40f6b"}, + {file = "black-26.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:91a68ae46bf07868963671e4d05611b179c2313301bd756a89ad4e3b3db2325b"}, + {file = "black-26.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:be5e2fe860b9bd9edbf676d5b60a9282994c03fbbd40fe8f5e75d194f96064ca"}, + {file = "black-26.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9dc8c71656a79ca49b8d3e2ce8103210c9481c57798b48deeb3a8bb02db5f115"}, + {file = "black-26.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b22b3810451abe359a964cc88121d57f7bce482b53a066de0f1584988ca36e79"}, + {file = "black-26.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53c62883b3f999f14e5d30b5a79bd437236658ad45b2f853906c7cbe79de00af"}, + {file = "black-26.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:f016baaadc423dc960cdddf9acae679e71ee02c4c341f78f3179d7e4819c095f"}, + {file = "black-26.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:66912475200b67ef5a0ab665011964bf924745103f51977a78b4fb92a9fc1bf0"}, + {file = "black-26.1.0-py3-none-any.whl", hash = "sha256:1054e8e47ebd686e078c0bb0eaf31e6ce69c966058d122f2c0c950311f9f3ede"}, + {file = "black-26.1.0.tar.gz", hash = "sha256:d294ac3340eef9c9eb5d29288e96dc719ff269a88e27b396340459dd85da4c58"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=1.0.0" +platformdirs = ">=2" +pytokens = ">=0.3.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.10)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "build" +version = "1.4.0" +description = "A simple, correct Python build frontend" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "build-1.4.0-py3-none-any.whl", hash = "sha256:6a07c1b8eb6f2b311b96fcbdbce5dab5fe637ffda0fd83c9cac622e927501596"}, + {file = "build-1.4.0.tar.gz", hash = "sha256:f1b91b925aa322be454f8330c6fb48b465da993d1e7e7e6fa35027ec49f3c936"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "os_name == \"nt\""} +importlib-metadata = {version = ">=4.6", markers = "python_full_version < \"3.10.2\""} +packaging = ">=24.0" +pyproject_hooks = "*" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} + +[package.extras] +uv = ["uv (>=0.1.18)"] +virtualenv = ["virtualenv (>=20.11) ; python_version < \"3.10\"", "virtualenv (>=20.17) ; python_version >= \"3.10\" and python_version < \"3.14\"", "virtualenv (>=20.31) ; python_version >= \"3.14\""] + +[[package]] +name = "cachetools" +version = "6.2.4" +description = "Extensible memoizing collections and decorators" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "cachetools-6.2.4-py3-none-any.whl", hash = "sha256:69a7a52634fed8b8bf6e24a050fb60bff1c9bd8f6d24572b99c32d4e71e62a51"}, + {file = "cachetools-6.2.4.tar.gz", hash = "sha256:82c5c05585e70b6ba2d3ae09ea60b79548872185d2f24ae1f2709d37299fd607"}, +] + +[[package]] +name = "chardet" +version = "5.2.0" +description = "Universal encoding detector for Python 3" +optional = true +python-versions = ">=3.7" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, + {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, +] + +[[package]] +name = "click" +version = "8.3.1" +description = "Composable command line interface toolkit" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"}, + {file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = true +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.13.1" +description = "Code coverage measurement for Python" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "coverage-7.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1fa280b3ad78eea5be86f94f461c04943d942697e0dac889fa18fff8f5f9147"}, + {file = "coverage-7.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c3d8c679607220979434f494b139dfb00131ebf70bb406553d69c1ff01a5c33d"}, + {file = "coverage-7.13.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:339dc63b3eba969067b00f41f15ad161bf2946613156fb131266d8debc8e44d0"}, + {file = "coverage-7.13.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db622b999ffe49cb891f2fff3b340cdc2f9797d01a0a202a0973ba2562501d90"}, + {file = "coverage-7.13.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1443ba9acbb593fa7c1c29e011d7c9761545fe35e7652e85ce7f51a16f7e08d"}, + {file = "coverage-7.13.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c832ec92c4499ac463186af72f9ed4d8daec15499b16f0a879b0d1c8e5cf4a3b"}, + {file = "coverage-7.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:562ec27dfa3f311e0db1ba243ec6e5f6ab96b1edfcfc6cf86f28038bc4961ce6"}, + {file = "coverage-7.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4de84e71173d4dada2897e5a0e1b7877e5eefbfe0d6a44edee6ce31d9b8ec09e"}, + {file = "coverage-7.13.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a5a68357f686f8c4d527a2dc04f52e669c2fc1cbde38f6f7eb6a0e58cbd17cae"}, + {file = "coverage-7.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:77cc258aeb29a3417062758975521eae60af6f79e930d6993555eeac6a8eac29"}, + {file = "coverage-7.13.1-cp310-cp310-win32.whl", hash = "sha256:bb4f8c3c9a9f34423dba193f241f617b08ffc63e27f67159f60ae6baf2dcfe0f"}, + {file = "coverage-7.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:c8e2706ceb622bc63bac98ebb10ef5da80ed70fbd8a7999a5076de3afaef0fb1"}, + {file = "coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88"}, + {file = "coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3"}, + {file = "coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9"}, + {file = "coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee"}, + {file = "coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf"}, + {file = "coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3"}, + {file = "coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef"}, + {file = "coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851"}, + {file = "coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb"}, + {file = "coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba"}, + {file = "coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19"}, + {file = "coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a"}, + {file = "coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c"}, + {file = "coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3"}, + {file = "coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e"}, + {file = "coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c"}, + {file = "coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62"}, + {file = "coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968"}, + {file = "coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e"}, + {file = "coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f"}, + {file = "coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee"}, + {file = "coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf"}, + {file = "coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c"}, + {file = "coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7"}, + {file = "coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6"}, + {file = "coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c"}, + {file = "coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78"}, + {file = "coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b"}, + {file = "coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd"}, + {file = "coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992"}, + {file = "coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4"}, + {file = "coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a"}, + {file = "coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766"}, + {file = "coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4"}, + {file = "coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398"}, + {file = "coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784"}, + {file = "coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461"}, + {file = "coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500"}, + {file = "coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9"}, + {file = "coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc"}, + {file = "coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a"}, + {file = "coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4"}, + {file = "coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6"}, + {file = "coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1"}, + {file = "coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd"}, + {file = "coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c"}, + {file = "coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0"}, + {file = "coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e"}, + {file = "coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53"}, + {file = "coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842"}, + {file = "coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2"}, + {file = "coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09"}, + {file = "coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894"}, + {file = "coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a"}, + {file = "coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f"}, + {file = "coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909"}, + {file = "coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4"}, + {file = "coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75"}, + {file = "coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9"}, + {file = "coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465"}, + {file = "coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864"}, + {file = "coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9"}, + {file = "coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5"}, + {file = "coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a"}, + {file = "coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0"}, + {file = "coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a"}, + {file = "coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6"}, + {file = "coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673"}, + {file = "coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5"}, + {file = "coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d"}, + {file = "coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8"}, + {file = "coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486"}, + {file = "coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564"}, + {file = "coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7"}, + {file = "coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416"}, + {file = "coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f"}, + {file = "coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79"}, + {file = "coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4"}, + {file = "coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573"}, + {file = "coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] + +[[package]] +name = "distlib" +version = "0.4.0" +description = "Distribution utilities" +optional = true +python-versions = "*" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16"}, + {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +description = "Backport of PEP 654 (exception groups)" +optional = true +python-versions = ">=3.7" +groups = ["main"] +markers = "extra == \"dev\" and python_version == \"3.10\"" +files = [ + {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, + {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "filelock" +version = "3.20.3" +description = "A platform independent file lock." +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1"}, + {file = "filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1"}, +] + +[[package]] +name = "flake8" +version = "7.3.0" +description = "the modular source code checker: pep8 pyflakes and co" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e"}, + {file = "flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.14.0,<2.15.0" +pyflakes = ">=3.4.0,<3.5.0" + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +description = "Read metadata from Python packages" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"dev\" and python_full_version < \"3.10.2\"" +files = [ + {file = "importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151"}, + {file = "importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb"}, +] + +[package.dependencies] +zipp = ">=3.20" + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +perf = ["ipython"] +test = ["flufl.flake8", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["mypy (<1.19) ; platform_python_implementation == \"PyPy\"", "pytest-mypy (>=1.0.1)"] + +[[package]] +name = "iniconfig" +version = "2.3.0" +description = "brain-dead simple config-ini parsing" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, + {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, +] + +[[package]] +name = "isort" +version = "7.0.0" +description = "A Python utility / library to sort Python imports." +optional = true +python-versions = ">=3.10.0" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "isort-7.0.0-py3-none-any.whl", hash = "sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1"}, + {file = "isort-7.0.0.tar.gz", hash = "sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187"}, +] + +[package.extras] +colors = ["colorama"] +plugins = ["setuptools"] + +[[package]] +name = "librt" +version = "0.7.8" +description = "Mypyc runtime library" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"dev\" and platform_python_implementation != \"PyPy\"" +files = [ + {file = "librt-0.7.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b45306a1fc5f53c9330fbee134d8b3227fe5da2ab09813b892790400aa49352d"}, + {file = "librt-0.7.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:864c4b7083eeee250ed55135d2127b260d7eb4b5e953a9e5df09c852e327961b"}, + {file = "librt-0.7.8-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6938cc2de153bc927ed8d71c7d2f2ae01b4e96359126c602721340eb7ce1a92d"}, + {file = "librt-0.7.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66daa6ac5de4288a5bbfbe55b4caa7bf0cd26b3269c7a476ffe8ce45f837f87d"}, + {file = "librt-0.7.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4864045f49dc9c974dadb942ac56a74cd0479a2aafa51ce272c490a82322ea3c"}, + {file = "librt-0.7.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a36515b1328dc5b3ffce79fe204985ca8572525452eacabee2166f44bb387b2c"}, + {file = "librt-0.7.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b7e7f140c5169798f90b80d6e607ed2ba5059784968a004107c88ad61fb3641d"}, + {file = "librt-0.7.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff71447cb778a4f772ddc4ce360e6ba9c95527ed84a52096bd1bbf9fee2ec7c0"}, + {file = "librt-0.7.8-cp310-cp310-win32.whl", hash = "sha256:047164e5f68b7a8ebdf9fae91a3c2161d3192418aadd61ddd3a86a56cbe3dc85"}, + {file = "librt-0.7.8-cp310-cp310-win_amd64.whl", hash = "sha256:d6f254d096d84156a46a84861183c183d30734e52383602443292644d895047c"}, + {file = "librt-0.7.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3e9c11aa260c31493d4b3197d1e28dd07768594a4f92bec4506849d736248f"}, + {file = "librt-0.7.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddb52499d0b3ed4aa88746aaf6f36a08314677d5c346234c3987ddc506404eac"}, + {file = "librt-0.7.8-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e9c0afebbe6ce177ae8edba0c7c4d626f2a0fc12c33bb993d163817c41a7a05c"}, + {file = "librt-0.7.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:631599598e2c76ded400c0a8722dec09217c89ff64dc54b060f598ed68e7d2a8"}, + {file = "librt-0.7.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c1ba843ae20db09b9d5c80475376168feb2640ce91cd9906414f23cc267a1ff"}, + {file = "librt-0.7.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b5b007bb22ea4b255d3ee39dfd06d12534de2fcc3438567d9f48cdaf67ae1ae3"}, + {file = "librt-0.7.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbd79caaf77a3f590cbe32dc2447f718772d6eea59656a7dcb9311161b10fa75"}, + {file = "librt-0.7.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:87808a8d1e0bd62a01cafc41f0fd6818b5a5d0ca0d8a55326a81643cdda8f873"}, + {file = "librt-0.7.8-cp311-cp311-win32.whl", hash = "sha256:31724b93baa91512bd0a376e7cf0b59d8b631ee17923b1218a65456fa9bda2e7"}, + {file = "librt-0.7.8-cp311-cp311-win_amd64.whl", hash = "sha256:978e8b5f13e52cf23a9e80f3286d7546baa70bc4ef35b51d97a709d0b28e537c"}, + {file = "librt-0.7.8-cp311-cp311-win_arm64.whl", hash = "sha256:20e3946863d872f7cabf7f77c6c9d370b8b3d74333d3a32471c50d3a86c0a232"}, + {file = "librt-0.7.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b6943885b2d49c48d0cff23b16be830ba46b0152d98f62de49e735c6e655a63"}, + {file = "librt-0.7.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46ef1f4b9b6cc364b11eea0ecc0897314447a66029ee1e55859acb3dd8757c93"}, + {file = "librt-0.7.8-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:907ad09cfab21e3c86e8f1f87858f7049d1097f77196959c033612f532b4e592"}, + {file = "librt-0.7.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2991b6c3775383752b3ca0204842743256f3ad3deeb1d0adc227d56b78a9a850"}, + {file = "librt-0.7.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03679b9856932b8c8f674e87aa3c55ea11c9274301f76ae8dc4d281bda55cf62"}, + {file = "librt-0.7.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3968762fec1b2ad34ce57458b6de25dbb4142713e9ca6279a0d352fa4e9f452b"}, + {file = "librt-0.7.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bb7a7807523a31f03061288cc4ffc065d684c39db7644c676b47d89553c0d714"}, + {file = "librt-0.7.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad64a14b1e56e702e19b24aae108f18ad1bf7777f3af5fcd39f87d0c5a814449"}, + {file = "librt-0.7.8-cp312-cp312-win32.whl", hash = "sha256:0241a6ed65e6666236ea78203a73d800dbed896cf12ae25d026d75dc1fcd1dac"}, + {file = "librt-0.7.8-cp312-cp312-win_amd64.whl", hash = "sha256:6db5faf064b5bab9675c32a873436b31e01d66ca6984c6f7f92621656033a708"}, + {file = "librt-0.7.8-cp312-cp312-win_arm64.whl", hash = "sha256:57175aa93f804d2c08d2edb7213e09276bd49097611aefc37e3fa38d1fb99ad0"}, + {file = "librt-0.7.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4c3995abbbb60b3c129490fa985dfe6cac11d88fc3c36eeb4fb1449efbbb04fc"}, + {file = "librt-0.7.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44e0c2cbc9bebd074cf2cdbe472ca185e824be4e74b1c63a8e934cea674bebf2"}, + {file = "librt-0.7.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d2f1e492cae964b3463a03dc77a7fe8742f7855d7258c7643f0ee32b6651dd3"}, + {file = "librt-0.7.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451e7ffcef8f785831fdb791bd69211f47e95dc4c6ddff68e589058806f044c6"}, + {file = "librt-0.7.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3469e1af9f1380e093ae06bedcbdd11e407ac0b303a56bbe9afb1d6824d4982d"}, + {file = "librt-0.7.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f11b300027ce19a34f6d24ebb0a25fd0e24a9d53353225a5c1e6cadbf2916b2e"}, + {file = "librt-0.7.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4adc73614f0d3c97874f02f2c7fd2a27854e7e24ad532ea6b965459c5b757eca"}, + {file = "librt-0.7.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60c299e555f87e4c01b2eca085dfccda1dde87f5a604bb45c2906b8305819a93"}, + {file = "librt-0.7.8-cp313-cp313-win32.whl", hash = "sha256:b09c52ed43a461994716082ee7d87618096851319bf695d57ec123f2ab708951"}, + {file = "librt-0.7.8-cp313-cp313-win_amd64.whl", hash = "sha256:f8f4a901a3fa28969d6e4519deceab56c55a09d691ea7b12ca830e2fa3461e34"}, + {file = "librt-0.7.8-cp313-cp313-win_arm64.whl", hash = "sha256:43d4e71b50763fcdcf64725ac680d8cfa1706c928b844794a7aa0fa9ac8e5f09"}, + {file = "librt-0.7.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:be927c3c94c74b05128089a955fba86501c3b544d1d300282cc1b4bd370cb418"}, + {file = "librt-0.7.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7b0803e9008c62a7ef79058233db7ff6f37a9933b8f2573c05b07ddafa226611"}, + {file = "librt-0.7.8-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:79feb4d00b2a4e0e05c9c56df707934f41fcb5fe53fd9efb7549068d0495b758"}, + {file = "librt-0.7.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9122094e3f24aa759c38f46bd8863433820654927370250f460ae75488b66ea"}, + {file = "librt-0.7.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e03bea66af33c95ce3addf87a9bf1fcad8d33e757bc479957ddbc0e4f7207ac"}, + {file = "librt-0.7.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1ade7f31675db00b514b98f9ab9a7698c7282dad4be7492589109471852d398"}, + {file = "librt-0.7.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a14229ac62adcf1b90a15992f1ab9c69ae8b99ffb23cb64a90878a6e8a2f5b81"}, + {file = "librt-0.7.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bcaaf624fd24e6a0cb14beac37677f90793a96864c67c064a91458611446e83"}, + {file = "librt-0.7.8-cp314-cp314-win32.whl", hash = "sha256:7aa7d5457b6c542ecaed79cec4ad98534373c9757383973e638ccced0f11f46d"}, + {file = "librt-0.7.8-cp314-cp314-win_amd64.whl", hash = "sha256:3d1322800771bee4a91f3b4bd4e49abc7d35e65166821086e5afd1e6c0d9be44"}, + {file = "librt-0.7.8-cp314-cp314-win_arm64.whl", hash = "sha256:5363427bc6a8c3b1719f8f3845ea53553d301382928a86e8fab7984426949bce"}, + {file = "librt-0.7.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ca916919793a77e4a98d4a1701e345d337ce53be4a16620f063191f7322ac80f"}, + {file = "librt-0.7.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:54feb7b4f2f6706bb82325e836a01be805770443e2400f706e824e91f6441dde"}, + {file = "librt-0.7.8-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39a4c76fee41007070f872b648cc2f711f9abf9a13d0c7162478043377b52c8e"}, + {file = "librt-0.7.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9c8a458245c7de80bc1b9765b177055efff5803f08e548dd4bb9ab9a8d789b"}, + {file = "librt-0.7.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b67aa7eff150f075fda09d11f6bfb26edffd300f6ab1666759547581e8f666"}, + {file = "librt-0.7.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:535929b6eff670c593c34ff435d5440c3096f20fa72d63444608a5aef64dd581"}, + {file = "librt-0.7.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:63937bd0f4d1cb56653dc7ae900d6c52c41f0015e25aaf9902481ee79943b33a"}, + {file = "librt-0.7.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf243da9e42d914036fd362ac3fa77d80a41cadcd11ad789b1b5eec4daaf67ca"}, + {file = "librt-0.7.8-cp314-cp314t-win32.whl", hash = "sha256:171ca3a0a06c643bd0a2f62a8944e1902c94aa8e5da4db1ea9a8daf872685365"}, + {file = "librt-0.7.8-cp314-cp314t-win_amd64.whl", hash = "sha256:445b7304145e24c60288a2f172b5ce2ca35c0f81605f5299f3fa567e189d2e32"}, + {file = "librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06"}, + {file = "librt-0.7.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c7e8f88f79308d86d8f39c491773cbb533d6cb7fa6476f35d711076ee04fceb6"}, + {file = "librt-0.7.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:389bd25a0db916e1d6bcb014f11aa9676cedaa485e9ec3752dfe19f196fd377b"}, + {file = "librt-0.7.8-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73fd300f501a052f2ba52ede721232212f3b06503fa12665408ecfc9d8fd149c"}, + {file = "librt-0.7.8-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d772edc6a5f7835635c7562f6688e031f0b97e31d538412a852c49c9a6c92d5"}, + {file = "librt-0.7.8-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde8a130bd0f239e45503ab39fab239ace094d63ee1d6b67c25a63d741c0f71"}, + {file = "librt-0.7.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fdec6e2368ae4f796fc72fad7fd4bd1753715187e6d870932b0904609e7c878e"}, + {file = "librt-0.7.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:00105e7d541a8f2ee5be52caacea98a005e0478cfe78c8080fbb7b5d2b340c63"}, + {file = "librt-0.7.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c6f8947d3dfd7f91066c5b4385812c18be26c9d5a99ca56667547f2c39149d94"}, + {file = "librt-0.7.8-cp39-cp39-win32.whl", hash = "sha256:41d7bb1e07916aeb12ae4a44e3025db3691c4149ab788d0315781b4d29b86afb"}, + {file = "librt-0.7.8-cp39-cp39-win_amd64.whl", hash = "sha256:e90a8e237753c83b8e484d478d9a996dc5e39fd5bd4c6ce32563bc8123f132be"}, + {file = "librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862"}, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = true +python-versions = ">=3.6" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mypy" +version = "1.19.1" +description = "Optional static typing for Python" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec"}, + {file = "mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b"}, + {file = "mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6"}, + {file = "mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74"}, + {file = "mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1"}, + {file = "mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac"}, + {file = "mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288"}, + {file = "mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab"}, + {file = "mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6"}, + {file = "mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331"}, + {file = "mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925"}, + {file = "mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042"}, + {file = "mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1"}, + {file = "mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e"}, + {file = "mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2"}, + {file = "mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8"}, + {file = "mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a"}, + {file = "mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13"}, + {file = "mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250"}, + {file = "mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b"}, + {file = "mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e"}, + {file = "mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef"}, + {file = "mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75"}, + {file = "mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd"}, + {file = "mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1"}, + {file = "mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718"}, + {file = "mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b"}, + {file = "mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045"}, + {file = "mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957"}, + {file = "mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f"}, + {file = "mypy-1.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3"}, + {file = "mypy-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a"}, + {file = "mypy-1.19.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67"}, + {file = "mypy-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e"}, + {file = "mypy-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376"}, + {file = "mypy-1.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24"}, + {file = "mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247"}, + {file = "mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba"}, +] + +[package.dependencies] +librt = {version = ">=0.6.2", markers = "platform_python_implementation != \"PyPy\""} +mypy_extensions = ">=1.0.0" +pathspec = ">=0.9.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing_extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = true +python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, +] + +[[package]] +name = "packaging" +version = "25.0" +description = "Core utilities for Python packages" +optional = true +python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, +] + +[[package]] +name = "pathspec" +version = "1.0.3" +description = "Utility library for gitignore style pattern matching of file paths." +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c"}, + {file = "pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d"}, +] + +[package.extras] +hyperscan = ["hyperscan (>=0.7)"] +optional = ["typing-extensions (>=4)"] +re2 = ["google-re2 (>=1.1)"] +tests = ["pytest (>=9)", "typing-extensions (>=4.15)"] + +[[package]] +name = "pipx" +version = "1.8.0" +description = "Install and Run Python Applications in Isolated Environments" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "pipx-1.8.0-py3-none-any.whl", hash = "sha256:b9535d59108d31675e7e14a837273e7816be2b8f08a96b3cc48daf09c066e696"}, + {file = "pipx-1.8.0.tar.gz", hash = "sha256:61a653ef2046de67c3201306b9d07428e93c80e6bebdcbbcb8177ecf3328b403"}, +] + +[package.dependencies] +argcomplete = ">=1.9.4" +colorama = {version = ">=0.4.4", markers = "sys_platform == \"win32\""} +packaging = ">=20" +platformdirs = ">=2.1" +tomli = {version = "*", markers = "python_version < \"3.11\""} +userpath = ">=1.6,<1.9 || >1.9" + +[[package]] +name = "platformdirs" +version = "4.5.1" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31"}, + {file = "platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda"}, +] + +[package.extras] +docs = ["furo (>=2025.9.25)", "proselint (>=0.14)", "sphinx (>=8.2.3)", "sphinx-autodoc-typehints (>=3.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.4.2)", "pytest-cov (>=7)", "pytest-mock (>=3.15.1)"] +type = ["mypy (>=1.18.2)"] + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "pycodestyle" +version = "2.14.0" +description = "Python style guide checker" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d"}, + {file = "pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783"}, +] + +[[package]] +name = "pyflakes" +version = "3.4.0" +description = "passive checker of Python programs" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f"}, + {file = "pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58"}, +] + +[[package]] +name = "pygments" +version = "2.19.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = true +python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pyproject-api" +version = "1.10.0" +description = "API to interact with the python pyproject.toml based projects" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "pyproject_api-1.10.0-py3-none-any.whl", hash = "sha256:8757c41a79c0f4ab71b99abed52b97ecf66bd20b04fa59da43b5840bac105a09"}, + {file = "pyproject_api-1.10.0.tar.gz", hash = "sha256:40c6f2d82eebdc4afee61c773ed208c04c19db4c4a60d97f8d7be3ebc0bbb330"}, +] + +[package.dependencies] +packaging = ">=25" +tomli = {version = ">=2.3", markers = "python_version < \"3.11\""} + +[package.extras] +docs = ["furo (>=2025.9.25)", "sphinx-autodoc-typehints (>=3.5.1)"] +testing = ["covdefaults (>=2.3)", "pytest (>=8.4.2)", "pytest-cov (>=7)", "pytest-mock (>=3.15.1)", "setuptools (>=80.9)"] + +[[package]] +name = "pyproject-hooks" +version = "1.2.0" +description = "Wrappers to call pyproject.toml-based build backend hooks." +optional = true +python-versions = ">=3.7" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913"}, + {file = "pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8"}, +] + +[[package]] +name = "pytest" +version = "9.0.2" +description = "pytest: simple powerful testing with Python" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b"}, + {file = "pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11"}, +] + +[package.dependencies] +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} +iniconfig = ">=1.0.1" +packaging = ">=22" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +description = "Pytest plugin for measuring coverage." +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861"}, + {file = "pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1"}, +] + +[package.dependencies] +coverage = {version = ">=7.10.6", extras = ["toml"]} +pluggy = ">=1.2" +pytest = ">=7" + +[package.extras] +testing = ["process-tests", "pytest-xdist", "virtualenv"] + +[[package]] +name = "pytest-randomly" +version = "4.0.1" +description = "Pytest plugin to randomly order tests and control random.seed." +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "pytest_randomly-4.0.1-py3-none-any.whl", hash = "sha256:e0dfad2fd4f35e07beff1e47c17fbafcf98f9bf4531fd369d9260e2f858bfcb7"}, + {file = "pytest_randomly-4.0.1.tar.gz", hash = "sha256:174e57bb12ac2c26f3578188490bd333f0e80620c3f47340158a86eca0593cd8"}, +] + +[package.dependencies] +pytest = "*" + +[[package]] +name = "pytokens" +version = "0.4.0" +description = "A Fast, spec compliant Python 3.14+ tokenizer that runs on older Pythons." +optional = true +python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "pytokens-0.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:af0c3166aea367a9e755a283171befb92dd3043858b94ae9b3b7efbe9def26a3"}, + {file = "pytokens-0.4.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daae524ed14ca459932cbf51d74325bea643701ba8a8b0cc2d10f7cd4b3e2b63"}, + {file = "pytokens-0.4.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e95cb158c44d642ed62f555bf8136bbe780dbd64d2fb0b9169e11ffb944664c3"}, + {file = "pytokens-0.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:df58d44630eaf25f587540e94bdf1fc50b4e6d5f212c786de0fb024bfcb8753a"}, + {file = "pytokens-0.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55efcc36f9a2e0e930cfba0ce7f83445306b02f8326745585ed5551864eba73a"}, + {file = "pytokens-0.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:92eb3ef88f27c22dc9dbab966ace4d61f6826e02ba04dac8e2d65ea31df56c8e"}, + {file = "pytokens-0.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4b77858a680635ee9904306f54b0ee4781effb89e211ba0a773d76539537165"}, + {file = "pytokens-0.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25cacc20c2ad90acb56f3739d87905473c54ca1fa5967ffcd675463fe965865e"}, + {file = "pytokens-0.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:628fab535ebc9079e4db35cd63cb401901c7ce8720a9834f9ad44b9eb4e0f1d4"}, + {file = "pytokens-0.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:4d0f568d7e82b7e96be56d03b5081de40e43c904eb6492bf09aaca47cd55f35b"}, + {file = "pytokens-0.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cd8da894e5a29ba6b6da8be06a4f7589d7220c099b5e363cb0643234b9b38c2a"}, + {file = "pytokens-0.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:237ba7cfb677dbd3b01b09860810aceb448871150566b93cd24501d5734a04b1"}, + {file = "pytokens-0.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01d1a61e36812e4e971cfe2c0e4c1f2d66d8311031dac8bf168af8a249fa04dd"}, + {file = "pytokens-0.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e47e2ef3ec6ee86909e520d79f965f9b23389fda47460303cf715d510a6fe544"}, + {file = "pytokens-0.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:3d36954aba4557fd5a418a03cf595ecbb1cdcce119f91a49b19ef09d691a22ae"}, + {file = "pytokens-0.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73eff3bdd8ad08da679867992782568db0529b887bed4c85694f84cdf35eafc6"}, + {file = "pytokens-0.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d97cc1f91b1a8e8ebccf31c367f28225699bea26592df27141deade771ed0afb"}, + {file = "pytokens-0.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c8952c537cb73a1a74369501a83b7f9d208c3cf92c41dd88a17814e68d48ce"}, + {file = "pytokens-0.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5dbf56f3c748aed9310b310d5b8b14e2c96d3ad682ad5a943f381bdbbdddf753"}, + {file = "pytokens-0.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:e131804513597f2dff2b18f9911d9b6276e21ef3699abeffc1c087c65a3d975e"}, + {file = "pytokens-0.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0d7374c917197106d3c4761374718bc55ea2e9ac0fb94171588ef5840ee1f016"}, + {file = "pytokens-0.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cd3fa1caf9e47a72ee134a29ca6b5bea84712724bba165d6628baa190c6ea5b"}, + {file = "pytokens-0.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c6986576b7b07fe9791854caa5347923005a80b079d45b63b0be70d50cce5f1"}, + {file = "pytokens-0.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9940f7c2e2f54fb1cb5fe17d0803c54da7a2bf62222704eb4217433664a186a7"}, + {file = "pytokens-0.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:54691cf8f299e7efabcc25adb4ce715d3cef1491e1c930eaf555182f898ef66a"}, + {file = "pytokens-0.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:94ff5db97a0d3cd7248a5b07ba2167bd3edc1db92f76c6db00137bbaf068ddf8"}, + {file = "pytokens-0.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0dd6261cd9cc95fae1227b1b6ebee023a5fd4a4b6330b071c73a516f5f59b63"}, + {file = "pytokens-0.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cdca8159df407dbd669145af4171a0d967006e0be25f3b520896bc7068f02c4"}, + {file = "pytokens-0.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4b5770abeb2a24347380a1164a558f0ebe06e98aedbd54c45f7929527a5fb26e"}, + {file = "pytokens-0.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:74500d72c561dad14c037a9e86a657afd63e277dd5a3bb7570932ab7a3b12551"}, + {file = "pytokens-0.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e368e0749e4e9d86a6e08763310dc92bc69ad73d9b6db5243b30174c71a8a534"}, + {file = "pytokens-0.4.0-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:865cc65c75c8f2e9e0d8330338f649b12bfd9442561900ebaf58c596a72107d2"}, + {file = "pytokens-0.4.0-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dbb9338663b3538f31c4ca7afe4f38d9b9b3a16a8be18a273a5704a1bc7a2367"}, + {file = "pytokens-0.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:658f870523ac1a5f4733d7db61ce9af61a0c23b2aeea3d03d1800c93f760e15f"}, + {file = "pytokens-0.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:d69a2491190a74e4b6f87f3b9dfce7a6873de3f3bf330d20083d374380becac0"}, + {file = "pytokens-0.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8cd795191c4127fcb3d7b76d84006a07748c390226f47657869235092eedbc05"}, + {file = "pytokens-0.4.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef2bcbddb73ac18599a86c8c549d5145130f2cd9d83dc2b5482fd8322b7806cd"}, + {file = "pytokens-0.4.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:06ac081c1187389762b58823d90d6339e6880ce0df912f71fb9022d81d7fd429"}, + {file = "pytokens-0.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:278129d54573efdc79e75c6082e73ebd19858e22a2e848359f93629323186ca6"}, + {file = "pytokens-0.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:9380fb6d96fa5ab83ed606ebad27b6171930cc14a8a8d215f6adb187ba428690"}, + {file = "pytokens-0.4.0-py3-none-any.whl", hash = "sha256:0508d11b4de157ee12063901603be87fb0253e8f4cb9305eb168b1202ab92068"}, + {file = "pytokens-0.4.0.tar.gz", hash = "sha256:6b0b03e6ea7c9f9d47c5c61164b69ad30f4f0d70a5d9fe7eac4d19f24f77af2d"}, +] + +[package.extras] +dev = ["black", "build", "mypy", "pytest", "pytest-cov", "setuptools", "tox", "twine", "wheel"] + +[[package]] +name = "pyupgrade" +version = "3.21.2" +description = "A tool to automatically upgrade syntax for newer versions." +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "pyupgrade-3.21.2-py2.py3-none-any.whl", hash = "sha256:2ac7b95cbd176475041e4dfe8ef81298bd4654a244f957167bd68af37d52be9f"}, + {file = "pyupgrade-3.21.2.tar.gz", hash = "sha256:1a361bea39deda78d1460f65d9dd548d3a36ff8171d2482298539b9dc11c9c06"}, +] + +[package.dependencies] +tokenize-rt = ">=6.1.0" + +[[package]] +name = "tokenize-rt" +version = "6.2.0" +description = "A wrapper around the stdlib `tokenize` which roundtrips." +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "tokenize_rt-6.2.0-py2.py3-none-any.whl", hash = "sha256:a152bf4f249c847a66497a4a95f63376ed68ac6abf092a2f7cfb29d044ecff44"}, + {file = "tokenize_rt-6.2.0.tar.gz", hash = "sha256:8439c042b330c553fdbe1758e4a05c0ed460dbbbb24a606f11f0dee75da4cad6"}, +] + +[[package]] +name = "tomli" +version = "2.4.0" +description = "A lil' TOML parser" +optional = true +python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"dev\" and python_full_version <= \"3.11.0a6\"" +files = [ + {file = "tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867"}, + {file = "tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9"}, + {file = "tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95"}, + {file = "tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76"}, + {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d"}, + {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576"}, + {file = "tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a"}, + {file = "tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa"}, + {file = "tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614"}, + {file = "tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1"}, + {file = "tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8"}, + {file = "tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a"}, + {file = "tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1"}, + {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b"}, + {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51"}, + {file = "tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729"}, + {file = "tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da"}, + {file = "tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3"}, + {file = "tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0"}, + {file = "tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e"}, + {file = "tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4"}, + {file = "tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e"}, + {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c"}, + {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f"}, + {file = "tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86"}, + {file = "tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87"}, + {file = "tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132"}, + {file = "tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6"}, + {file = "tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc"}, + {file = "tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66"}, + {file = "tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d"}, + {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702"}, + {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8"}, + {file = "tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776"}, + {file = "tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475"}, + {file = "tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2"}, + {file = "tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9"}, + {file = "tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0"}, + {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df"}, + {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d"}, + {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f"}, + {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b"}, + {file = "tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087"}, + {file = "tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd"}, + {file = "tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4"}, + {file = "tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a"}, + {file = "tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c"}, +] + +[[package]] +name = "tox" +version = "4.34.1" +description = "tox is a generic virtualenv management and test command line tool" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "tox-4.34.1-py3-none-any.whl", hash = "sha256:5610d69708bab578d618959b023f8d7d5d3386ed14a2392aeebf9c583615af60"}, + {file = "tox-4.34.1.tar.gz", hash = "sha256:ef1e82974c2f5ea02954d590ee0b967fad500c3879b264ea19efb9a554f3cc60"}, +] + +[package.dependencies] +cachetools = ">=6.2.4" +chardet = ">=5.2" +colorama = ">=0.4.6" +filelock = ">=3.20.2" +packaging = ">=25" +platformdirs = ">=4.5.1" +pluggy = ">=1.6" +pyproject-api = ">=1.10" +tomli = {version = ">=2.3", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.15", markers = "python_version < \"3.11\""} +virtualenv = ">=20.35.4" + +[[package]] +name = "typing-extensions" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] + +[[package]] +name = "userpath" +version = "1.9.2" +description = "Cross-platform tool for adding locations to the user PATH" +optional = true +python-versions = ">=3.7" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "userpath-1.9.2-py3-none-any.whl", hash = "sha256:2cbf01a23d655a1ff8fc166dfb78da1b641d1ceabf0fe5f970767d380b14e89d"}, + {file = "userpath-1.9.2.tar.gz", hash = "sha256:6c52288dab069257cc831846d15d48133522455d4677ee69a9781f11dbefd815"}, +] + +[package.dependencies] +click = "*" + +[[package]] +name = "virtualenv" +version = "20.36.1" +description = "Virtual Python Environment builder" +optional = true +python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f"}, + {file = "virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = {version = ">=3.20.1,<4", markers = "python_version >= \"3.10\""} +platformdirs = ">=3.9.1,<5" +typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""} + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] + +[[package]] +name = "zipp" +version = "3.23.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"dev\" and python_full_version < \"3.10.2\"" +files = [ + {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, + {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] + +[extras] +dev = ["black", "build", "flake8", "isort", "mypy", "pipx", "pytest", "pytest-cov", "pytest-randomly", "pyupgrade", "tox"] + +[metadata] +lock-version = "2.1" +python-versions = ">=3.10" +content-hash = "069cb13b2d57cd6c10ec2639902adf8b15dd8c594a1aaeab1a64b52df188a8af" diff --git a/pool.py b/pool.py deleted file mode 100644 index e6bbd361..00000000 --- a/pool.py +++ /dev/null @@ -1,51 +0,0 @@ -"""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() diff --git a/prototype.py b/prototype.py deleted file mode 100644 index 7b9d9552..00000000 --- a/prototype.py +++ /dev/null @@ -1,37 +0,0 @@ -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 - - -def main(): - class A: - pass - - a = A() - prototype = Prototype() - prototype.register_object('a', a) - b = prototype.clone('a', a=1, b=2, c=3) - - print(a) - print(b.a, b.b, b.c) - - -if __name__ == '__main__': - main() diff --git a/proxy.py b/proxy.py deleted file mode 100644 index bbba6106..00000000 --- a/proxy.py +++ /dev/null @@ -1,32 +0,0 @@ -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") - - -if __name__ == '__main__': - p = Proxy() - p.work() - p.busy = 'Yes' - p.work() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..dfac5da9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,120 @@ +[build-system] +requires = ["setuptools >= 77.0.3"] +build-backend = "setuptools.build_meta" + +[project] +name = "python-patterns" +description = "A collection of design patterns and idioms in Python." +version = "0.1.0" +readme = "README.md" +requires-python = ">=3.10" +classifiers = [ + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] +dependencies= [ +] + +maintainers=[ + { name="faif" } +] + +[project.urls] +Homepage = "https://github.com/faif/python-patterns" +Repository = "https://github.com/faif/python-patterns" +"Bug Tracker" = "https://github.com/faif/python-patterns/issues" +Contributors = "https://github.com/faif/python-patterns/graphs/contributors" + +[project.optional-dependencies] +dev = [ + "mypy", + "pipx>=1.7.1", + "pyupgrade", + "pytest>=6.2.0", + "pytest-cov>=2.11.0", + "pytest-randomly>=3.1.0", + "black>=25.1.0", + "build>=1.2.2", + "isort>=5.7.0", + "flake8>=7.1.0", + "tox>=4.25.0" +] + +[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/pytest_local.ini b/pytest_local.ini new file mode 100644 index 00000000..154db6e6 --- /dev/null +++ b/pytest_local.ini @@ -0,0 +1,3 @@ +[pytest] +addopts = -q +testpaths = tests diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 00000000..1194272a --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,8 @@ +flake8 +black +isort +pytest +pytest-randomly +mypy +pyupgrade +tox diff --git a/state.py b/state.py deleted file mode 100644 index 33ed9959..00000000 --- a/state.py +++ /dev/null @@ -1,63 +0,0 @@ -"""Implementation of the state pattern""" - -# http://ginstrom.com/scribbles/2007/10/08/design-patterns-python-style/ - - -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() diff --git a/strategy.py b/strategy.py deleted file mode 100644 index 6d187e1b..00000000 --- a/strategy.py +++ /dev/null @@ -1,43 +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() diff --git a/template.py b/template.py deleted file mode 100644 index 8ffe4914..00000000 --- a/template.py +++ /dev/null @@ -1,57 +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() diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/behavioral/test_catalog.py b/tests/behavioral/test_catalog.py new file mode 100644 index 00000000..60933816 --- /dev/null +++ b/tests/behavioral/test_catalog.py @@ -0,0 +1,23 @@ +import pytest + +from patterns.behavioral.catalog import Catalog, CatalogClass, CatalogInstance, CatalogStatic + +def test_catalog_multiple_methods(): + test = Catalog('param_value_2') + token = test.main_method() + assert token == 'executed method 2!' + +def test_catalog_multiple_instance_methods(): + test = CatalogInstance('param_value_1') + token = test.main_method() + assert token == 'Value x1' + +def test_catalog_multiple_class_methods(): + test = CatalogClass('param_value_2') + token = test.main_method() + assert token == 'Value x2' + +def test_catalog_multiple_static_methods(): + test = CatalogStatic('param_value_1') + token = test.main_method() + assert token == 'executed method 1!' diff --git a/tests/behavioral/test_mediator.py b/tests/behavioral/test_mediator.py new file mode 100644 index 00000000..1af60e67 --- /dev/null +++ b/tests/behavioral/test_mediator.py @@ -0,0 +1,16 @@ +import pytest + +from patterns.behavioral.mediator import User + +def test_mediated_comments(): + molly = User('Molly') + mediated_comment = molly.say("Hi Team! Meeting at 3 PM today.") + assert mediated_comment == "[Molly says]: Hi Team! Meeting at 3 PM today." + + mark = User('Mark') + mediated_comment = mark.say("Roger that!") + assert mediated_comment == "[Mark says]: Roger that!" + + ethan = User('Ethan') + mediated_comment = ethan.say("Alright.") + assert mediated_comment == "[Ethan says]: Alright." diff --git a/tests/behavioral/test_memento.py b/tests/behavioral/test_memento.py new file mode 100644 index 00000000..bd307b76 --- /dev/null +++ b/tests/behavioral/test_memento.py @@ -0,0 +1,29 @@ +import pytest + +from patterns.behavioral.memento import NumObj, Transaction + +def test_object_creation(): + num_obj = NumObj(-1) + assert repr(num_obj) == '', "Object representation not as expected" + +def test_rollback_on_transaction(): + num_obj = NumObj(-1) + a_transaction = Transaction(True, num_obj) + for _i in range(3): + num_obj.increment() + a_transaction.commit() + assert num_obj.value == 2 + + for _i in range(3): + num_obj.increment() + try: + num_obj.value += 'x' # will fail + except TypeError: + a_transaction.rollback() + assert num_obj.value == 2, "Transaction did not rollback as expected" + +def test_rollback_with_transactional_annotation(): + num_obj = NumObj(2) + with pytest.raises(TypeError): + num_obj.do_stuff() + assert num_obj.value == 2 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..8bb7130c --- /dev/null +++ b/tests/behavioral/test_publish_subscribe.py @@ -0,0 +1,70 @@ +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_servant.py b/tests/behavioral/test_servant.py new file mode 100644 index 00000000..dd487171 --- /dev/null +++ b/tests/behavioral/test_servant.py @@ -0,0 +1,39 @@ +from patterns.behavioral.servant import GeometryTools, Circle, Rectangle, Position +import pytest +import math + + +@pytest.fixture +def circle(): + return Circle(3, Position(0, 0)) + + +@pytest.fixture +def rectangle(): + return Rectangle(4, 5, Position(0, 0)) + + +def test_calculate_area(circle, rectangle): + assert GeometryTools.calculate_area(circle) == math.pi * 3**2 + assert GeometryTools.calculate_area(rectangle) == 4 * 5 + + with pytest.raises(ValueError): + GeometryTools.calculate_area("invalid shape") + + +def test_calculate_perimeter(circle, rectangle): + assert GeometryTools.calculate_perimeter(circle) == 2 * math.pi * 3 + assert GeometryTools.calculate_perimeter(rectangle) == 2 * (4 + 5) + + with pytest.raises(ValueError): + GeometryTools.calculate_perimeter("invalid shape") + + +def test_move_to(circle, rectangle): + new_position = Position(1, 1) + GeometryTools.move_to(circle, new_position) + assert circle.position == new_position + + new_position = Position(1, 1) + GeometryTools.move_to(rectangle, new_position) + assert rectangle.position == new_position 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/behavioral/test_visitor.py b/tests/behavioral/test_visitor.py new file mode 100644 index 00000000..31d230de --- /dev/null +++ b/tests/behavioral/test_visitor.py @@ -0,0 +1,22 @@ +import pytest + +from patterns.behavioral.visitor import A, B, C, Visitor + +@pytest.fixture +def visitor(): + return Visitor() + +def test_visiting_generic_node(visitor): + a = A() + token = visitor.visit(a) + assert token == 'generic_visit A', "The expected generic object was not called" + +def test_visiting_specific_nodes(visitor): + b = B() + token = visitor.visit(b) + assert token == 'visit_B B', "The expected specific object was not called" + +def test_visiting_inherited_nodes(visitor): + c = C() + token = visitor.visit(c) + assert token == 'visit_B C', "The expected inherited object was not called" 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_factory.py b/tests/creational/test_factory.py new file mode 100644 index 00000000..4bcfd4c5 --- /dev/null +++ b/tests/creational/test_factory.py @@ -0,0 +1,30 @@ +import unittest +from patterns.creational.factory import get_localizer, GreekLocalizer, EnglishLocalizer + +class TestFactory(unittest.TestCase): + def test_get_localizer_greek(self): + localizer = get_localizer("Greek") + self.assertIsInstance(localizer, GreekLocalizer) + self.assertEqual(localizer.localize("dog"), "σκύλος") + self.assertEqual(localizer.localize("cat"), "γάτα") + # Test unknown word returns the word itself + self.assertEqual(localizer.localize("monkey"), "monkey") + + def test_get_localizer_english(self): + localizer = get_localizer("English") + self.assertIsInstance(localizer, EnglishLocalizer) + self.assertEqual(localizer.localize("dog"), "dog") + self.assertEqual(localizer.localize("cat"), "cat") + + def test_get_localizer_default(self): + # Test default argument + localizer = get_localizer() + self.assertIsInstance(localizer, EnglishLocalizer) + + def test_get_localizer_unknown_language(self): + # Test fallback for unknown language if applicable, + # or just verify what happens. + # Based on implementation: localizers.get(language, EnglishLocalizer)() + # It defaults to EnglishLocalizer for unknown keys. + localizer = get_localizer("Spanish") + self.assertIsInstance(localizer, EnglishLocalizer) 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/fundamental/test_delegation.py b/tests/fundamental/test_delegation.py new file mode 100644 index 00000000..3bfd0496 --- /dev/null +++ b/tests/fundamental/test_delegation.py @@ -0,0 +1,16 @@ +import pytest + +from patterns.fundamental.delegation_pattern import Delegator, Delegate + + +def test_delegator_delegates_attribute_and_call(): + d = Delegator(Delegate()) + assert d.p1 == 123 + assert d.do_something("something") == "Doing something" + assert d.do_something("something", kw=", hi") == "Doing something, hi" + + +def test_delegator_missing_attribute_raises(): + d = Delegator(Delegate()) + with pytest.raises(AttributeError): + _ = d.p2 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..6665f327 --- /dev/null +++ b/tests/structural/test_bridge.py @@ -0,0 +1,44 @@ +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_facade.py b/tests/structural/test_facade.py new file mode 100644 index 00000000..2ff24ca3 --- /dev/null +++ b/tests/structural/test_facade.py @@ -0,0 +1,11 @@ +from patterns.structural.facade import ComputerFacade + + +def test_computer_facade_start(capsys): + cf = ComputerFacade() + cf.start() + out = capsys.readouterr().out + assert "Freezing processor." in out + assert "Loading from 0x00 data:" in out + assert "Jumping to: 0x00" in out + assert "Executing." in out diff --git a/tests/structural/test_flyweight.py b/tests/structural/test_flyweight.py new file mode 100644 index 00000000..a200203f --- /dev/null +++ b/tests/structural/test_flyweight.py @@ -0,0 +1,20 @@ +from patterns.structural.flyweight import Card + + +def test_card_flyweight_identity_and_repr(): + c1 = Card("9", "h") + c2 = Card("9", "h") + assert c1 is c2 + assert repr(c1) == "" + + +def test_card_attribute_persistence_and_pool_clear(): + Card._pool.clear() + c1 = Card("A", "s") + c1.temp = "t" + c2 = Card("A", "s") + assert hasattr(c2, "temp") + + Card._pool.clear() + c3 = Card("A", "s") + assert not hasattr(c3, "temp") diff --git a/tests/structural/test_mvc.py b/tests/structural/test_mvc.py new file mode 100644 index 00000000..5991c511 --- /dev/null +++ b/tests/structural/test_mvc.py @@ -0,0 +1,67 @@ +import pytest + +from patterns.structural.mvc import ( + ProductModel, + ConsoleView, + Controller, + Router, +) + + +def test_productmodel_iteration_and_price_str(): + pm = ProductModel() + items = list(pm) + assert set(items) == {"milk", "eggs", "cheese"} + + info = pm.get("cheese") + assert info["quantity"] == 10 + assert str(info["price"]) == "2.00" + + +def test_productmodel_get_raises_keyerror(): + pm = ProductModel() + with pytest.raises(KeyError) as exc: + pm.get("unknown_item") + assert "not in the model's item list." in str(exc.value) + + +def test_consoleview_capitalizer_and_list_and_info(capsys): + view = ConsoleView() + # capitalizer + assert view.capitalizer("heLLo") == "Hello" + + # show item list + view.show_item_list("product", ["x", "y"]) + out = capsys.readouterr().out + assert "PRODUCT LIST:" in out + assert "x" in out and "y" in out + + # show item information formatting + pm = ProductModel() + controller = Controller(pm, view) + controller.show_item_information("milk") + out = capsys.readouterr().out + assert "PRODUCT INFORMATION:" in out + assert "Name: milk" in out + assert "Price: 1.50" in out + assert "Quantity: 10" in out + + +def test_show_item_information_missing_calls_item_not_found(capsys): + view = ConsoleView() + pm = ProductModel() + controller = Controller(pm, view) + + controller.show_item_information("arepas") + out = capsys.readouterr().out + assert 'That product "arepas" does not exist in the records' in out + + +def test_router_register_resolve_and_unknown(): + router = Router() + router.register("products", Controller, ProductModel, ConsoleView) + controller = router.resolve("products") + assert isinstance(controller, Controller) + + with pytest.raises(KeyError): + router.resolve("no-such-path") 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..5b49fb97 --- /dev/null +++ b/tests/test_hsm.py @@ -0,0 +1,98 @@ +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 cdaa101e..00000000 --- a/visitor.py +++ /dev/null @@ -1,46 +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)