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/.coveragerc b/.coveragerc deleted file mode 100644 index 3778bf3d..00000000 --- a/.coveragerc +++ /dev/null @@ -1,6 +0,0 @@ -[report] -exclude_lines = - pragma: no cover - # Don't complain if tests don't hit defensive assertion code: - # See: https://stackoverflow.com/a/9212387/3407256 - raise NotImplementedError diff --git a/.github/workflows/lint_pr.yml b/.github/workflows/lint_pr.yml new file mode 100644 index 00000000..f18e5c2e --- /dev/null +++ b/.github/workflows/lint_pr.yml @@ -0,0 +1,286 @@ +name: lint_pull_request +on: [pull_request, push] +jobs: + check_changes: + runs-on: ubuntu-24.04 + outputs: + has_python_changes: ${{ steps.changed-files.outputs.has_python_changes }} + files: ${{ steps.changed-files.outputs.files }} + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # To get all history for git diff commands + + - name: Get changed Python files + id: changed-files + run: | + if [ "${{ github.event_name }}" == "pull_request" ]; then + # For PRs, compare against base branch + CHANGED_FILES=$(git diff --name-only --diff-filter=ACMRT origin/${{ github.base_ref }} HEAD | grep '\.py$' | grep -v "^setup\.py$" || echo "") + # Check if setup.py specifically changed + SETUP_PY_CHANGED=$(git diff --name-only --diff-filter=ACMRT origin/${{ github.base_ref }} HEAD | grep "^setup\.py$" || echo "") + if [ ! -z "$SETUP_PY_CHANGED" ]; then + CHANGED_FILES="$CHANGED_FILES $SETUP_PY_CHANGED" + fi + else + # For pushes, use the before/after SHAs + CHANGED_FILES=$(git diff --name-only --diff-filter=ACMRT ${{ github.event.before }} ${{ github.event.after }} | grep '\.py$' | grep -v "^setup\.py$" || echo "") + # Check if setup.py specifically changed + SETUP_PY_CHANGED=$(git diff --name-only --diff-filter=ACMRT ${{ github.event.before }} ${{ github.event.after }} | grep "^setup\.py$" || echo "") + if [ ! -z "$SETUP_PY_CHANGED" ]; then + CHANGED_FILES="$CHANGED_FILES $SETUP_PY_CHANGED" + fi + fi + + # Check if any Python files were changed and set the output accordingly + if [ -z "$CHANGED_FILES" ]; then + echo "No Python files changed" + echo "has_python_changes=false" >> $GITHUB_OUTPUT + echo "files=" >> $GITHUB_OUTPUT + else + echo "Changed Python files: $CHANGED_FILES" + echo "has_python_changes=true" >> $GITHUB_OUTPUT + echo "files=$CHANGED_FILES" >> $GITHUB_OUTPUT + fi + + - name: PR information + if: ${{ github.event_name == 'pull_request' }} + run: | + if [[ "${{ steps.changed-files.outputs.has_python_changes }}" == "true" ]]; then + echo "This PR contains Python changes that will be linted." + else + echo "This PR contains no Python changes, but still requires manual approval." + fi + + lint: + needs: check_changes + if: ${{ needs.check_changes.outputs.has_python_changes == 'true' }} + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + tool: [flake8, format, mypy, pytest, pyupgrade, tox] + steps: + # Additional check to ensure we have Python files before proceeding + - name: Verify Python changes + run: | + if [[ "${{ needs.check_changes.outputs.has_python_changes }}" != "true" ]]; then + echo "No Python files were changed. Skipping linting." + exit 0 + fi + + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - uses: actions/setup-python@v4 + with: + python-version: 3.12 + + - uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('requirements-dev.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-dev.txt + + # Flake8 linting + - name: Lint with flake8 + if: ${{ matrix.tool == 'flake8' }} + id: flake8 + run: | + echo "Linting files: ${{ needs.check_changes.outputs.files }}" + flake8 ${{ needs.check_changes.outputs.files }} --count --show-source --statistics + + # Format checking with isort and black + - name: Format check + if: ${{ matrix.tool == 'format' }} + id: format + run: | + echo "Checking format with isort for: ${{ needs.check_changes.outputs.files }}" + isort --profile black --check ${{ needs.check_changes.outputs.files }} + echo "Checking format with black for: ${{ needs.check_changes.outputs.files }}" + black --check ${{ needs.check_changes.outputs.files }} + + # Type checking with mypy + - name: Type check with mypy + if: ${{ matrix.tool == 'mypy' }} + id: mypy + run: | + echo "Type checking: ${{ needs.check_changes.outputs.files }}" + mypy --ignore-missing-imports ${{ needs.check_changes.outputs.files }} + + # Run tests with pytest + - name: Run tests with pytest + if: ${{ matrix.tool == 'pytest' }} + id: pytest + run: | + echo "Running pytest discovery..." + python -m pytest --collect-only -v + + # First run any test files that correspond to changed files + echo "Running tests for changed files..." + changed_files="${{ needs.check_changes.outputs.files }}" + + # Extract module paths from changed files + modules=() + for file in $changed_files; do + # Convert file path to module path (remove .py and replace / with .) + if [[ $file == patterns/* ]]; then + module_path=${file%.py} + module_path=${module_path//\//.} + modules+=("$module_path") + fi + done + + # Run tests for each module + for module in "${modules[@]}"; do + echo "Testing module: $module" + python -m pytest -xvs tests/ -k "$module" || true + done + + # Then run doctests on the changed files + echo "Running doctests for changed files..." + for file in $changed_files; do + if [[ $file == *.py ]]; then + echo "Running doctest for $file" + python -m pytest --doctest-modules -v $file || true + fi + done + + # Check Python version compatibility + - name: Check Python version compatibility + if: ${{ matrix.tool == 'pyupgrade' }} + id: pyupgrade + run: pyupgrade --py312-plus ${{ needs.check_changes.outputs.files }} + + # Run tox + - name: Run tox + if: ${{ matrix.tool == 'tox' }} + id: tox + run: | + echo "Running tox integration for changed files..." + changed_files="${{ needs.check_changes.outputs.files }}" + + # Create a temporary tox configuration that extends the original one + echo "[tox]" > tox_pr.ini + echo "envlist = py312" >> tox_pr.ini + echo "skip_missing_interpreters = true" >> tox_pr.ini + + echo "[testenv]" >> tox_pr.ini + echo "setenv =" >> tox_pr.ini + echo " COVERAGE_FILE = .coverage.{envname}" >> tox_pr.ini + echo "deps =" >> tox_pr.ini + echo " -r requirements-dev.txt" >> tox_pr.ini + echo "allowlist_externals =" >> tox_pr.ini + echo " pytest" >> tox_pr.ini + echo " coverage" >> tox_pr.ini + echo " python" >> tox_pr.ini + echo "commands =" >> tox_pr.ini + + # Check if we have any implementation files that changed + pattern_files=0 + test_files=0 + + for file in $changed_files; do + if [[ $file == patterns/* ]]; then + pattern_files=1 + elif [[ $file == tests/* ]]; then + test_files=1 + fi + done + + # Only run targeted tests, no baseline + echo " # Run specific tests for changed files" >> tox_pr.ini + + has_tests=false + + # Add coverage-focused test commands + for file in $changed_files; do + if [[ $file == *.py ]]; then + # Run coverage tests for implementation files + if [[ $file == patterns/* ]]; then + module_name=$(basename $file .py) + + # Get the pattern type (behavioral, structural, etc.) + if [[ $file == patterns/behavioral/* ]]; then + pattern_dir="behavioral" + elif [[ $file == patterns/creational/* ]]; then + pattern_dir="creational" + elif [[ $file == patterns/structural/* ]]; then + pattern_dir="structural" + elif [[ $file == patterns/fundamental/* ]]; then + pattern_dir="fundamental" + elif [[ $file == patterns/other/* ]]; then + pattern_dir="other" + else + pattern_dir="" + fi + + echo " # Testing $file" >> tox_pr.ini + + # Check if specific test exists + if [ -n "$pattern_dir" ]; then + test_path="tests/${pattern_dir}/test_${module_name}.py" + echo " if [ -f \"${test_path}\" ]; then echo \"Test file ${test_path} exists: true\" && coverage run -m pytest -xvs --cov=patterns --cov-append ${test_path}; else echo \"Test file ${test_path} exists: false\"; fi" >> tox_pr.ini + + # Also try to find any test that might include this module + echo " coverage run -m pytest -xvs --cov=patterns --cov-append tests/${pattern_dir}/ -k \"${module_name}\" --no-header" >> tox_pr.ini + fi + + # Run doctests for the file + echo " coverage run -m pytest --doctest-modules -v --cov=patterns --cov-append $file" >> tox_pr.ini + + has_tests=true + fi + + # Run test files directly if modified + if [[ $file == tests/* ]]; then + echo " coverage run -m pytest -xvs --cov=patterns --cov-append $file" >> tox_pr.ini + has_tests=true + fi + fi + done + + # If we didn't find any specific tests to run, mention it + if [ "$has_tests" = false ]; then + echo " python -c \"print('No specific tests found for changed files. Consider adding tests.')\"" >> tox_pr.ini + # Add a minimal test to avoid failure, but ensure it generates coverage data + echo " coverage run -m pytest -xvs --cov=patterns --cov-append -k \"not integration\" --no-header" >> tox_pr.ini + fi + + # Add coverage report command + echo " coverage combine" >> tox_pr.ini + echo " coverage report -m" >> tox_pr.ini + + # Run tox with the custom configuration + echo "Running tox with custom PR configuration..." + echo "======================== TOX CONFIG ========================" + cat tox_pr.ini + echo "===========================================================" + tox -c tox_pr.ini + + summary: + needs: [check_changes, lint] + # Run summary in all cases, regardless of whether lint job ran + if: ${{ always() }} + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v3 + + - name: Summarize results + run: | + echo "## Pull Request Lint Results" >> $GITHUB_STEP_SUMMARY + if [[ "${{ needs.check_changes.outputs.has_python_changes }}" == "true" ]]; then + echo "Linting has completed for all Python files changed in this PR." >> $GITHUB_STEP_SUMMARY + echo "See individual job logs for detailed results." >> $GITHUB_STEP_SUMMARY + else + echo "No Python files were changed in this PR. Linting was skipped." >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + echo "⚠️ **Note:** This PR still requires manual approval regardless of linting results." >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/lint_python.yml b/.github/workflows/lint_python.yml new file mode 100644 index 00000000..19d6c078 --- /dev/null +++ b/.github/workflows/lint_python.yml @@ -0,0 +1,36 @@ +name: lint_python +on: [pull_request, push] +jobs: + lint_python: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: 3.12 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-dev.txt + - name: Lint with flake8 + run: flake8 ./patterns --count --show-source --statistics + continue-on-error: true + - name: Format check with isort and black + run: | + isort --profile black --check ./patterns + black --check ./patterns + continue-on-error: true + - name: Type check with mypy + run: mypy --ignore-missing-imports ./patterns || true + continue-on-error: true + - name: Run tests with pytest + run: | + pytest ./patterns + pytest --doctest-modules ./patterns || true + continue-on-error: true + - name: Check Python version compatibility + run: shopt -s globstar && pyupgrade --py312-plus ./patterns/**/*.py + continue-on-error: true + - name: Run tox + run: tox + continue-on-error: true diff --git a/.gitignore b/.gitignore index 4285f0cf..d272a2e1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,12 @@ __pycache__ .idea *.egg-info/ .tox/ -venv +env/ +venv/ +.env +.venv +.vscode/ +.python-version +.coverage +build/ +dist/ \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index ba68fde3..dfeece70 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,21 +1,17 @@ -dist: xenial +os: linux +dist: noble language: python -sudo: false - -matrix: +jobs: include: - - python: "3.7" - env: TOXENV=ci37 - - python: "3.8" - env: TOXENV=ci38 + - python: "3.12" + env: TOXENV=py312 cache: - pip install: - - pip install tox - - pip install codecov + - pip install codecov tox script: - tox 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 49ad4d4a..05222bc9 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ python-patterns 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. + Current Patterns ---------------- @@ -90,12 +92,6 @@ Contributing ------------ When an implementation is added or modified, please review the following guidelines: -##### Output -All files with example patterns have `### OUTPUT ###` section at the bottom -(migration to OUTPUT = """...""" is in progress). - -Run `append_output.sh` (e.g. `./append_output.sh borg.py`) to generate/update it. - ##### Docstrings Add module level description in form of a docstring with links to corresponding references or other useful information. @@ -104,19 +100,19 @@ Add "Examples in Python ecosystem" section if you know some. It shows how patter [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. -In some cases class-level docstring with doctest would also help (see [adapter.py](patterns/structural/adapter.py)) -but readable OUTPUT section is much better. - - ##### 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 `tox` or `tox -e ci37` before submitting a patch to be sure your changes will pass CI. +Please run the following before submitting a patch +- `black .` This lints your code. + +Then either: +- `tox` or `tox -e ci37` This runs unit tests. see tox.ini for further details. +- If you have a bash compatible shell use `./lint.sh` This script will lint and test your code. This script mirrors the CI pipeline actions. You can also run `flake8` or `pytest` commands manually. Examples can be found in `tox.ini`. diff --git a/append_output.sh b/append_output.sh deleted file mode 100755 index 3bb9202c..00000000 --- a/append_output.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -# This script (given path to a python script as an argument) -# appends python outputs to given file. - -set -e - -output_marker='OUTPUT = """' - -# get everything (excluding part between `output_marker` and the end of the file) -# into `src` var -src=$(sed -n -e "/$output_marker/,\$!p" "$1") -output=$(python3 "$1") - -echo "$src" > $1 -echo -e "\n" >> $1 -echo "$output_marker" >> $1 -echo "$output" >> $1 -echo '""" # noqa' >> $1 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/setup.cfg b/config_backup/setup.cfg similarity index 61% rename from setup.cfg rename to config_backup/setup.cfg index 536a52d7..e109555b 100644 --- a/setup.cfg +++ b/config_backup/setup.cfg @@ -1,9 +1,13 @@ [flake8] max-line-length = 120 -ignore = E266 E731 -exclude = .venv* +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/tox.ini b/config_backup/tox.ini similarity index 64% rename from tox.ini rename to config_backup/tox.ini index d86eeec9..36e2577e 100644 --- a/tox.ini +++ b/config_backup/tox.ini @@ -1,17 +1,22 @@ [tox] -envlist = ci37,ci38,cov-report - +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 patterns/ + 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={envsitepackagesdir}/patterns --log-level=INFO tests/ + pytest -s -vv --cov=patterns/ --log-level=INFO tests/ [testenv:cov-report] 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/patterns/behavioral/catalog.py b/patterns/behavioral/catalog.py index 0570f7d3..ba85f500 100644 --- a/patterns/behavioral/catalog.py +++ b/patterns/behavioral/catalog.py @@ -12,28 +12,31 @@ class Catalog: parameter """ - def __init__(self, param): + 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} + self._static_method_choices = { + "param_value_1": self._static_method_1, + "param_value_2": self._static_method_2, + } # simple test to validate param value if param in self._static_method_choices.keys(): self.param = param else: - raise ValueError("Invalid Value for Param: {0}".format(param)) + raise ValueError(f"Invalid Value for Param: {param}") @staticmethod - def _static_method_1(): + def _static_method_1() -> None: print("executed method 1!") @staticmethod - def _static_method_2(): + def _static_method_2() -> None: print("executed method 2!") - def main_method(self): + def main_method(self) -> None: """will execute either _static_method_1 or _static_method_2 depending on self.param value @@ -43,102 +46,112 @@ def main_method(self): # 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): - self.x1 = 'x1' - self.x2 = 'x2' + 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("Invalid Value for Param: {0}".format(param)) + raise ValueError(f"Invalid Value for Param: {param}") - def _instance_method_1(self): - print("Value {}".format(self.x1)) + def _instance_method_1(self) -> None: + print(f"Value {self.x1}") - def _instance_method_2(self): - print("Value {}".format(self.x2)) + def _instance_method_2(self) -> None: + print(f"Value {self.x2}") - _instance_method_choices = {'param_value_1': _instance_method_1, 'param_value_2': _instance_method_2} + _instance_method_choices = { + "param_value_1": _instance_method_1, + "param_value_2": _instance_method_2, + } - def main_method(self): + def main_method(self) -> None: """will execute either _instance_method_1 or _instance_method_2 depending on self.param value """ - self._instance_method_choices[self.param].__get__(self)() + 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' + x1 = "x1" + x2 = "x2" - def __init__(self, param): + 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("Invalid Value for Param: {0}".format(param)) + raise ValueError(f"Invalid Value for Param: {param}") @classmethod - def _class_method_1(cls): - print("Value {}".format(cls.x1)) + def _class_method_1(cls) -> None: + print(f"Value {cls.x1}") @classmethod - def _class_method_2(cls): - print("Value {}".format(cls.x2)) + def _class_method_2(cls) -> None: + print(f"Value {cls.x2}") - _class_method_choices = {'param_value_1': _class_method_1, 'param_value_2': _class_method_2} + _class_method_choices = { + "param_value_1": _class_method_1, + "param_value_2": _class_method_2, + } def main_method(self): """will execute either _class_method_1 or _class_method_2 depending on self.param value """ - self._class_method_choices[self.param].__get__(None, self.__class__)() + 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): + 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("Invalid Value for Param: {0}".format(param)) + raise ValueError(f"Invalid Value for Param: {param}") @staticmethod - def _static_method_1(): + def _static_method_1() -> None: print("executed method 1!") @staticmethod - def _static_method_2(): + def _static_method_2() -> None: print("executed method 2!") - _static_method_choices = {'param_value_1': _static_method_1, 'param_value_2': _static_method_2} + _static_method_choices = { + "param_value_1": _static_method_1, + "param_value_2": _static_method_2, + } - def main_method(self): + def main_method(self) -> None: """will execute either _static_method_1 or _static_method_2 depending on self.param value """ - self._static_method_choices[self.param].__get__(None, self.__class__)() + + self._static_method_choices[self.param].__get__(None, self.__class__)() # type: ignore + # type ignore reason: https://github.com/python/mypy/issues/10206 def main(): @@ -163,4 +176,5 @@ def main(): if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/patterns/behavioral/chain_of_responsibility.py b/patterns/behavioral/chain_of_responsibility.py index ecc00f77..9d46c4a8 100644 --- a/patterns/behavioral/chain_of_responsibility.py +++ b/patterns/behavioral/chain_of_responsibility.py @@ -18,15 +18,15 @@ Allow a request to pass down a chain of receivers until it is handled. """ -import abc +from abc import ABC, abstractmethod +from typing import Optional, Tuple -class Handler(metaclass=abc.ABCMeta): - - def __init__(self, successor=None): +class Handler(ABC): + def __init__(self, successor: Optional["Handler"] = None): self.successor = successor - def handle(self, request): + def handle(self, request: int) -> None: """ Handle request and stop. If can't - call next handler in chain. @@ -38,8 +38,8 @@ def handle(self, request): if not res and self.successor: self.successor.handle(request) - @abc.abstractmethod - def check_range(self, request): + @abstractmethod + def check_range(self, request: int) -> Optional[bool]: """Compare passed value to predefined interval""" @@ -49,10 +49,11 @@ class ConcreteHandler0(Handler): """ @staticmethod - def check_range(request): + def check_range(request: int) -> Optional[bool]: if 0 <= request < 10: - print("request {} handled in handler 0".format(request)) + print(f"request {request} handled in handler 0") return True + return None class ConcreteHandler1(Handler): @@ -60,30 +61,32 @@ class ConcreteHandler1(Handler): start, end = 10, 20 - def check_range(self, request): + def check_range(self, request: int) -> Optional[bool]: if self.start <= request < self.end: - print("request {} handled in handler 1".format(request)) + print(f"request {request} handled in handler 1") return True + return None class ConcreteHandler2(Handler): """... With helper methods.""" - def check_range(self, request): + def check_range(self, request: int) -> Optional[bool]: start, end = self.get_interval_from_db() if start <= request < end: - print("request {} handled in handler 2".format(request)) + print(f"request {request} handled in handler 2") return True + return None @staticmethod - def get_interval_from_db(): + def get_interval_from_db() -> Tuple[int, int]: return (20, 30) class FallbackHandler(Handler): @staticmethod - def check_range(request): - print("end of chain, no handler for {}".format(request)) + def check_range(request: int) -> Optional[bool]: + print(f"end of chain, no handler for {request}") return False @@ -112,4 +115,5 @@ def main(): if __name__ == "__main__": import doctest + doctest.testmod(optionflags=doctest.ELLIPSIS) diff --git a/patterns/behavioral/chaining_method.py b/patterns/behavioral/chaining_method.py index 13c8032c..26f11018 100644 --- a/patterns/behavioral/chaining_method.py +++ b/patterns/behavioral/chaining_method.py @@ -1,34 +1,37 @@ +from __future__ import annotations + + class Person: - def __init__(self, name, action): + def __init__(self, name: str) -> None: self.name = name - self.action = action - def do_action(self): - print(self.name, self.action.name, end=' ') - return self.action + def do_action(self, action: Action) -> Action: + print(self.name, action.name, end=" ") + return action class Action: - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name - def amount(self, val): - print(val, end=' ') + def amount(self, val: str) -> Action: + print(val, end=" ") return self - def stop(self): - print('then stop') + def stop(self) -> None: + print("then stop") def main(): """ >>> move = Action('move') - >>> person = Person('Jack', move) - >>> person.do_action().amount('5m').stop() + >>> person = Person('Jack') + >>> person.do_action(move).amount('5m').stop() Jack move 5m then stop """ -if __name__ == '__main__': +if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/patterns/behavioral/command.py b/patterns/behavioral/command.py index c989d2a9..a88ea8be 100644 --- a/patterns/behavioral/command.py +++ b/patterns/behavioral/command.py @@ -20,23 +20,25 @@ 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): + def __init__(self) -> None: # an array of files hidden, to undo them as needed - self._hidden_files = [] + self._hidden_files: List[str] = [] - def execute(self, filename): - print(f'hiding {filename}') + def execute(self, filename: str) -> None: + print(f"hiding {filename}") self._hidden_files.append(filename) - def undo(self): + def undo(self) -> None: filename = self._hidden_files.pop() - print(f'un-hiding {filename}') + print(f"un-hiding {filename}") class DeleteFileCommand: @@ -44,17 +46,17 @@ class DeleteFileCommand: A command to delete a file given its name """ - def __init__(self): + def __init__(self) -> None: # an array of deleted files, to undo them as needed - self._deleted_files = [] + self._deleted_files: List[str] = [] - def execute(self, filename): - print(f'deleting {filename}') + def execute(self, filename: str) -> None: + print(f"deleting {filename}") self._deleted_files.append(filename) - def undo(self): + def undo(self) -> None: filename = self._deleted_files.pop() - print(f'restoring {filename}') + print(f"restoring {filename}") class MenuItem: @@ -62,13 +64,13 @@ class MenuItem: The invoker class. Here it is items in a menu. """ - def __init__(self, command): + def __init__(self, command: Union[HideFileCommand, DeleteFileCommand]) -> None: self._command = command - def on_do_press(self, filename): + def on_do_press(self, filename: str) -> None: self._command.execute(filename) - def on_undo_press(self): + def on_undo_press(self) -> None: self._command.undo() @@ -101,4 +103,5 @@ def main(): if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/patterns/behavioral/iterator.py b/patterns/behavioral/iterator.py index c329e64d..3ed4043b 100644 --- a/patterns/behavioral/iterator.py +++ b/patterns/behavioral/iterator.py @@ -7,16 +7,19 @@ """ -def count_to(count): +def count_to(count: int): """Counts by word numbers, up to a maximum of five""" numbers = ["one", "two", "three", "four", "five"] - for number in numbers[:count]: - yield number + yield from numbers[:count] # Test the generator -count_to_two = lambda: count_to(2) -count_to_five = lambda: count_to(5) +def count_to_two() -> None: + return count_to(2) + + +def count_to_five() -> None: + return count_to(5) def main(): @@ -40,4 +43,5 @@ def main(): if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/patterns/behavioral/iterator_alt.py b/patterns/behavioral/iterator_alt.py index afc23a03..a2a71d82 100644 --- a/patterns/behavioral/iterator_alt.py +++ b/patterns/behavioral/iterator_alt.py @@ -5,25 +5,28 @@ 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', + "one", + "two", + "three", + "four", + "five", ) - def __init__(self, start, stop): + def __init__(self, start: int, stop: int) -> None: self.start = start self.stop = stop - def __iter__(self): # this makes the class an Iterable + def __iter__(self) -> NumberWords: # this makes the class an Iterable return self - def __next__(self): # this makes the class an Iterator + 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 @@ -33,6 +36,7 @@ def __next__(self): # this makes the class an Iterator # Test the iterator + def main(): """ # Counting to two... diff --git a/patterns/behavioral/mediator.py b/patterns/behavioral/mediator.py index 0410d2c3..e4b3c34a 100644 --- a/patterns/behavioral/mediator.py +++ b/patterns/behavioral/mediator.py @@ -8,25 +8,27 @@ Encapsulates how a set of objects interact. """ +from __future__ import annotations + class ChatRoom: """Mediator class""" - def display_message(self, user, message): - print("[{} says]: {}".format(user, message)) + def display_message(self, user: User, message: str) -> None: + print(f"[{user} says]: {message}") class User: """A class whose instances want to interact with each other""" - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name self.chat_room = ChatRoom() - def say(self, message): + def say(self, message: str) -> None: self.chat_room.display_message(self, message) - def __str__(self): + def __str__(self) -> str: return self.name @@ -45,6 +47,7 @@ def main(): """ -if __name__ == '__main__': +if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/patterns/behavioral/memento.py b/patterns/behavioral/memento.py index 7e4c62fe..c1bc7f0b 100644 --- a/patterns/behavioral/memento.py +++ b/patterns/behavioral/memento.py @@ -5,8 +5,8 @@ Provides the ability to restore an object to its previous state. """ -from copy import copy -from copy import deepcopy +from copy import copy, deepcopy +from typing import Callable, List def memento(obj, deep=False): @@ -26,7 +26,7 @@ class Transaction: """ deep = False - states = [] + states: List[Callable[[], None]] = [] def __init__(self, deep, *targets): self.deep = deep @@ -41,40 +41,34 @@ def rollback(self): a_state() -class Transactional: +def Transactional(method): """Adds transactional semantics to methods. Methods decorated with + @Transactional will roll back to entry-state upon exceptions. - @Transactional will rollback to entry-state upon exceptions. + :param method: The function to be decorated. """ - - 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 Exception as e: - state() - raise e - - return transaction - + def transaction(obj, *args, **kwargs): + state = memento(obj) + try: + return method(obj, *args, **kwargs) + except Exception as e: + state() + raise e + return transaction class NumObj: def __init__(self, value): self.value = value def __repr__(self): - return '<%s: %r>' % (self.__class__.__name__, self.value) + return f"<{self.__class__.__name__}: {self.value!r}>" def increment(self): self.value += 1 @Transactional def do_stuff(self): - self.value = '1111' # <- invalid value + self.value = "1111" # <- invalid value self.increment() # <- will fail and rollback @@ -134,4 +128,5 @@ def main(): if __name__ == "__main__": import doctest + doctest.testmod(optionflags=doctest.ELLIPSIS) diff --git a/patterns/behavioral/observer.py b/patterns/behavioral/observer.py index a3077558..03d970ad 100644 --- a/patterns/behavioral/observer.py +++ b/patterns/behavioral/observer.py @@ -5,55 +5,64 @@ Maintains a list of dependents and notifies them of any state changes. *Examples in Python ecosystem: -Django Signals: https://docs.djangoproject.com/en/2.1/topics/signals/ -Flask Signals: http://flask.pocoo.org/docs/1.0/signals/ +Django Signals: https://docs.djangoproject.com/en/3.1/topics/signals/ +Flask Signals: https://flask.palletsprojects.com/en/1.1.x/signals/ """ +from __future__ import annotations + +from contextlib import suppress +from typing import Protocol + + +# define a generic observer type +class Observer(Protocol): + def update(self, subject: Subject) -> None: + pass + class Subject: - def __init__(self): - self._observers = [] + def __init__(self) -> None: + self._observers: list[Observer] = [] - def attach(self, observer): + def attach(self, observer: Observer) -> None: if observer not in self._observers: self._observers.append(observer) - def detach(self, observer): - try: + def detach(self, observer: Observer) -> None: + with suppress(ValueError): self._observers.remove(observer) - except ValueError: - pass - def notify(self, modifier=None): + def notify(self, modifier: Observer | None = None) -> None: for observer in self._observers: if modifier != observer: observer.update(self) class Data(Subject): - def __init__(self, name=''): - Subject.__init__(self) + def __init__(self, name: str = "") -> None: + super().__init__() self.name = name self._data = 0 @property - def data(self): + def data(self) -> int: return self._data @data.setter - def data(self, value): + def data(self, value: int) -> None: self._data = value self.notify() class HexViewer: - def update(self, subject): - print('HexViewer: Subject {} has data 0x{:x}'.format(subject.name, subject.data)) + def update(self, subject: Data) -> None: + print(f"HexViewer: Subject {subject.name} has data 0x{subject.data:x}") class DecimalViewer: - def update(self, subject): - print('DecimalViewer: Subject %s has data %d' % (subject.name, subject.data)) + def update(self, subject: Data) -> None: + print(f"DecimalViewer: Subject {subject.name} has data {subject.data}") def main(): @@ -97,4 +106,5 @@ def main(): if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/patterns/behavioral/publish_subscribe.py b/patterns/behavioral/publish_subscribe.py index abd8fac1..7e76955c 100644 --- a/patterns/behavioral/publish_subscribe.py +++ b/patterns/behavioral/publish_subscribe.py @@ -4,22 +4,24 @@ Author: https://github.com/HanWenfang """ +from __future__ import annotations + class Provider: - def __init__(self): + def __init__(self) -> None: self.msg_queue = [] self.subscribers = {} - def notify(self, msg): + def notify(self, msg: str) -> None: self.msg_queue.append(msg) - def subscribe(self, msg, subscriber): + def subscribe(self, msg: str, subscriber: Subscriber) -> None: self.subscribers.setdefault(msg, []).append(subscriber) - def unsubscribe(self, msg, subscriber): + def unsubscribe(self, msg: str, subscriber: Subscriber) -> None: self.subscribers[msg].remove(subscriber) - def update(self): + def update(self) -> None: for msg in self.msg_queue: for sub in self.subscribers.get(msg, []): sub.run(msg) @@ -27,26 +29,26 @@ def update(self): class Publisher: - def __init__(self, msg_center): + def __init__(self, msg_center: Provider) -> None: self.provider = msg_center - def publish(self, msg): + def publish(self, msg: str) -> None: self.provider.notify(msg) class Subscriber: - def __init__(self, name, msg_center): + def __init__(self, name: str, msg_center: Provider) -> None: self.name = name self.provider = msg_center - def subscribe(self, msg): + def subscribe(self, msg: str) -> None: self.provider.subscribe(msg, self) - def unsubscribe(self, msg): + def unsubscribe(self, msg: str) -> None: self.provider.unsubscribe(msg, self) - def run(self, msg): - print("{} got {}".format(self.name, msg)) + def run(self, msg: str) -> None: + print(f"{self.name} got {msg}") def main(): @@ -65,7 +67,7 @@ def main(): >>> vani.subscribe("movie") >>> vani.unsubscribe("movie") - # Note that no one subscirbed to `ads` + # Note that no one subscribed to `ads` # and that vani changed their mind >>> fftv.publish("cartoon") @@ -89,4 +91,5 @@ def main(): if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/patterns/behavioral/registry.py b/patterns/behavioral/registry.py index 5ed18f46..d44a992e 100644 --- a/patterns/behavioral/registry.py +++ b/patterns/behavioral/registry.py @@ -1,6 +1,9 @@ +from typing import Dict + + class RegistryHolder(type): - REGISTRY = {} + REGISTRY: Dict[str, "RegistryHolder"] = {} def __new__(cls, name, bases, attrs): new_cls = type.__new__(cls, name, bases, attrs) @@ -42,4 +45,5 @@ def main(): if __name__ == "__main__": import doctest + doctest.testmod(optionflags=doctest.ELLIPSIS) diff --git a/patterns/behavioral/specification.py b/patterns/behavioral/specification.py index f125859a..303ee513 100644 --- a/patterns/behavioral/specification.py +++ b/patterns/behavioral/specification.py @@ -39,34 +39,32 @@ def not_specification(self): class AndSpecification(CompositeSpecification): - _one = Specification() - _other = Specification() - def __init__(self, one, other): - self._one = one - self._other = other + self._one: Specification = one + self._other: Specification = other def is_satisfied_by(self, candidate): - return bool(self._one.is_satisfied_by(candidate) and self._other.is_satisfied_by(candidate)) + return bool( + self._one.is_satisfied_by(candidate) + and self._other.is_satisfied_by(candidate) + ) class OrSpecification(CompositeSpecification): - _one = Specification() - _other = Specification() - def __init__(self, one, other): - self._one = one - self._other = other + self._one: Specification = one + self._other: Specification = other def is_satisfied_by(self, candidate): - return bool(self._one.is_satisfied_by(candidate) or self._other.is_satisfied_by(candidate)) + return bool( + self._one.is_satisfied_by(candidate) + or self._other.is_satisfied_by(candidate) + ) class NotSpecification(CompositeSpecification): - _wrapped = Specification() - def __init__(self, wrapped): - self._wrapped = wrapped + self._wrapped: Specification = wrapped def is_satisfied_by(self, candidate): return bool(not self._wrapped.is_satisfied_by(candidate)) @@ -84,7 +82,7 @@ def is_satisfied_by(self, candidate): class SuperUserSpecification(CompositeSpecification): def is_satisfied_by(self, candidate): - return getattr(candidate, 'super_user', False) + return getattr(candidate, "super_user", False) def main(): @@ -105,6 +103,7 @@ def main(): """ -if __name__ == '__main__': +if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/patterns/behavioral/state.py b/patterns/behavioral/state.py index 971da428..db4d9468 100644 --- a/patterns/behavioral/state.py +++ b/patterns/behavioral/state.py @@ -8,57 +8,57 @@ Implements state transitions by invoking methods from the pattern's superclass. """ +from __future__ import annotations -class State: +class State: """Base state. This is to share functionality""" - def scan(self): + def scan(self) -> None: """Scan the dial to the next station""" self.pos += 1 if self.pos == len(self.stations): self.pos = 0 - print("Scanning... Station is {} {}".format(self.stations[self.pos], self.name)) + print(f"Scanning... Station is {self.stations[self.pos]} {self.name}") class AmState(State): - def __init__(self, radio): + def __init__(self, radio: Radio) -> None: self.radio = radio self.stations = ["1250", "1380", "1510"] self.pos = 0 self.name = "AM" - def toggle_amfm(self): + def toggle_amfm(self) -> None: print("Switching to FM") self.radio.state = self.radio.fmstate class FmState(State): - def __init__(self, radio): + 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): + 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): + 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): + def toggle_amfm(self) -> None: self.state.toggle_amfm() - def scan(self): + def scan(self) -> None: self.state.scan() @@ -83,6 +83,7 @@ def main(): """ -if __name__ == '__main__': +if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/patterns/behavioral/strategy.py b/patterns/behavioral/strategy.py index e6f0aab3..000ff2ad 100644 --- a/patterns/behavioral/strategy.py +++ b/patterns/behavioral/strategy.py @@ -7,45 +7,86 @@ 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: - def __init__(self, price, discount_strategy=None): - self.price = price + discount_strategy = DiscountStrategyValidator() + + def __init__(self, price: float, discount_strategy: Callable = None) -> None: + self.price: float = price self.discount_strategy = discount_strategy - def price_after_discount(self): + def apply_discount(self) -> float: if self.discount_strategy: discount = self.discount_strategy(self) else: discount = 0 + return self.price - discount - def __repr__(self): - fmt = "" - return fmt.format(self.price, self.price_after_discount()) + def __repr__(self) -> str: + strategy = getattr(self.discount_strategy, "__name__", None) + return f"" -def ten_percent_discount(order): +def ten_percent_discount(order: Order) -> float: return order.price * 0.10 -def on_sale_discount(order): +def on_sale_discount(order: Order) -> float: return order.price * 0.25 + 20 def main(): """ - >>> Order(100) - - - >>> Order(100, discount_strategy=ten_percent_discount) - - - >>> Order(1000, discount_strategy=on_sale_discount) - + >>> 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 index 98c0cf2d..76fc136b 100644 --- a/patterns/behavioral/template.py +++ b/patterns/behavioral/template.py @@ -10,30 +10,30 @@ """ -def get_text(): +def get_text() -> str: return "plain-text" -def get_pdf(): +def get_pdf() -> str: return "pdf" -def get_csv(): +def get_csv() -> str: return "csv" -def convert_to_text(data): +def convert_to_text(data: str) -> str: print("[CONVERT]") - return "{} as text".format(data) + return f"{data} as text" -def saver(): +def saver() -> None: print("[SAVE]") -def template_function(getter, converter=False, to_save=False): +def template_function(getter, converter=False, to_save=False) -> None: data = getter() - print("Got `{}`".format(data)) + print(f"Got `{data}`") if len(data) <= 3 and converter: data = converter(data) @@ -43,7 +43,7 @@ def template_function(getter, converter=False, to_save=False): if to_save: saver() - print("`{}` was processed".format(data)) + print(f"`{data}` was processed") def main(): @@ -69,4 +69,5 @@ def main(): if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/patterns/behavioral/visitor.py b/patterns/behavioral/visitor.py index a9bbd7e8..00d95248 100644 --- a/patterns/behavioral/visitor.py +++ b/patterns/behavioral/visitor.py @@ -36,7 +36,7 @@ class Visitor: def visit(self, node, *args, **kwargs): meth = None for cls in node.__class__.__mro__: - meth_name = 'visit_' + cls.__name__ + meth_name = "visit_" + cls.__name__ meth = getattr(self, meth_name, None) if meth: break @@ -46,10 +46,10 @@ def visit(self, node, *args, **kwargs): return meth(node, *args, **kwargs) def generic_visit(self, node, *args, **kwargs): - print('generic_visit ' + node.__class__.__name__) + print("generic_visit " + node.__class__.__name__) def visit_B(self, node, *args, **kwargs): - print('visit_B ' + node.__class__.__name__) + print("visit_B " + node.__class__.__name__) def main(): @@ -70,4 +70,5 @@ def main(): if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/patterns/creational/abstract_factory.py b/patterns/creational/abstract_factory.py index a9ed4bf5..15e5d67f 100644 --- a/patterns/creational/abstract_factory.py +++ b/patterns/creational/abstract_factory.py @@ -31,73 +31,69 @@ """ import random +from typing import Type -class PetShop: +class Pet: + def __init__(self, name: str) -> None: + self.name = name - """A pet shop""" + def speak(self) -> None: + raise NotImplementedError - def __init__(self, animal_factory=None): - """pet_factory is our abstract factory. We can set it at will.""" + def __str__(self) -> str: + raise NotImplementedError - self.pet_factory = animal_factory - def show_pet(self): - """Creates and shows a pet using the abstract factory""" +class Dog(Pet): + def speak(self) -> None: + print("woof") - pet = self.pet_factory() - print("We have a lovely {}".format(pet)) - print("It says {}".format(pet.speak())) + def __str__(self) -> str: + return f"Dog<{self.name}>" -class Dog: - def speak(self): - return "woof" +class Cat(Pet): + def speak(self) -> None: + print("meow") - def __str__(self): - return "Dog" + def __str__(self) -> str: + return f"Cat<{self.name}>" -class Cat: - def speak(self): - return "meow" +class PetShop: + """A pet shop""" - def __str__(self): - return "Cat" + 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 -# Additional factories: + def buy_pet(self, name: str) -> Pet: + """Creates and shows a pet using the abstract factory""" -# Create a random animal -def random_animal(): - """Let's be dynamic!""" - return random.choice([Dog, Cat])() + pet = self.pet_factory(name) + print(f"Here is your lovely {pet}") + return pet # Show pets with various factories -if __name__ == "__main__": - +def main() -> None: + """ # A Shop that sells only cats - cat_shop = PetShop(Cat) - cat_shop.show_pet() - print("") + >>> 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) - # A shop that sells random animals shop = PetShop(random_animal) - for i in range(3): - shop.show_pet() - print("=" * 20) - -### OUTPUT ### -# We have a lovely Cat -# It says meow -# -# We have a lovely Dog -# It says woof -# ==================== -# We have a lovely Cat -# It says meow -# ==================== -# We have a lovely Cat -# It says meow -# ==================== + import doctest + + doctest.testmod() diff --git a/patterns/creational/borg.py b/patterns/creational/borg.py index 83b42142..edd0589d 100644 --- a/patterns/creational/borg.py +++ b/patterns/creational/borg.py @@ -13,7 +13,7 @@ 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 assigining +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 @@ -25,32 +25,42 @@ https://github.com/onetwopunch/pythonDbTemplate/blob/master/database.py *References: -https://fkromer.github.io/python-pattern-references/design/#singleton +- 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 = {} - def __init__(self): - self.__dict__ = self.__shared_state - self.state = 'Init' +class Borg: + _shared_state: Dict[str, str] = {} - def __str__(self): - return self.state + def __init__(self) -> None: + self.__dict__ = self._shared_state class YourBorg(Borg): - pass + 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 = Borg() - >>> rm2 = Borg() + >>> rm1 = YourBorg() + >>> rm2 = YourBorg() >>> rm1.state = 'Idle' >>> rm2.state = 'Running' @@ -73,18 +83,29 @@ def main(): >>> rm1 is rm2 False - # Shared state is also modified from a subclass instance `rm3` + # New instances also get the same shared state >>> rm3 = YourBorg() >>> print('rm1: {0}'.format(rm1)) - rm1: Init + rm1: Zombie >>> print('rm2: {0}'.format(rm2)) - rm2: Init + rm2: Zombie >>> print('rm3: {0}'.format(rm3)) - rm3: Init + 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 index 73f7d2fc..22383923 100644 --- a/patterns/creational/builder.py +++ b/patterns/creational/builder.py @@ -34,7 +34,7 @@ class for a building, where the initializer (__init__ method) specifies the # Abstract Building class Building: - def __init__(self): + def __init__(self) -> None: self.build_floor() self.build_size() @@ -44,25 +44,25 @@ def build_floor(self): def build_size(self): raise NotImplementedError - def __repr__(self): - return 'Floor: {0.floor} | Size: {0.size}'.format(self) + def __repr__(self) -> str: + return "Floor: {0.floor} | Size: {0.size}".format(self) # Concrete Buildings class House(Building): - def build_floor(self): - self.floor = 'One' + def build_floor(self) -> None: + self.floor = "One" - def build_size(self): - self.size = 'Big' + def build_size(self) -> None: + self.size = "Big" class Flat(Building): - def build_floor(self): - self.floor = 'More than One' + def build_floor(self) -> None: + self.floor = "More than One" - def build_size(self): - self.size = 'Small' + def build_size(self) -> None: + self.size = "Small" # In some very complex cases, it might be desirable to pull out the building @@ -72,19 +72,19 @@ def build_size(self): class ComplexBuilding: - def __repr__(self): - return 'Floor: {0.floor} | Size: {0.size}'.format(self) + def __repr__(self) -> str: + return "Floor: {0.floor} | Size: {0.size}".format(self) class ComplexHouse(ComplexBuilding): - def build_floor(self): - self.floor = 'One' + def build_floor(self) -> None: + self.floor = "One" - def build_size(self): - self.size = 'Big and fancy' + def build_size(self) -> None: + self.size = "Big and fancy" -def construct_building(cls): +def construct_building(cls) -> Building: building = cls() building.build_floor() building.build_size() @@ -110,4 +110,5 @@ def main(): if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/patterns/creational/factory.py b/patterns/creational/factory.py index e70e0f15..3ef2d2a8 100644 --- a/patterns/creational/factory.py +++ b/patterns/creational/factory.py @@ -12,11 +12,8 @@ *Where can the pattern be used practically? The Factory Method can be seen in the popular web framework Django: -http://django.wikispaces.asu.edu/*NEW*+Django+Design+Patterns For -example, in a contact form of a web page, the subject and the message -fields are created using the same form factory (CharField()), even -though they have different implementations according to their -purposes. +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/ @@ -25,6 +22,13 @@ Creates objects without having to specify the exact class. """ +from typing import Dict, Protocol, Type + + +class Localizer(Protocol): + def localize(self, msg: str) -> str: + pass + class GreekLocalizer: """A simple localizer a la gettext""" @@ -44,10 +48,9 @@ def localize(self, msg: str) -> str: return msg -def get_localizer(language: str = "English") -> object: - +def get_localizer(language: str = "English") -> Localizer: """Factory""" - localizers = { + localizers: Dict[str, Type[Localizer]] = { "English": EnglishLocalizer, "Greek": GreekLocalizer, } diff --git a/patterns/creational/lazy_evaluation.py b/patterns/creational/lazy_evaluation.py index cc752e2e..b56daf0c 100644 --- a/patterns/creational/lazy_evaluation.py +++ b/patterns/creational/lazy_evaluation.py @@ -10,7 +10,7 @@ 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 -pyramimd +pyramid https://github.com/Pylons/pyramid/blob/7909e9503cdfc6f6e84d2c7ace1d3c03ca1d8b73/pyramid/decorator.py#L4 werkzeug https://github.com/pallets/werkzeug/blob/5a2bf35441006d832ab1ed5a31963cbc366c99ac/werkzeug/utils.py#L35 @@ -36,7 +36,13 @@ def __get__(self, obj, type_): def lazy_property2(fn): - attr = '_lazy__' + fn.__name__ + """ + 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): @@ -101,4 +107,5 @@ def main(): if __name__ == "__main__": import doctest + doctest.testmod(optionflags=doctest.ELLIPSIS) diff --git a/patterns/creational/pool.py b/patterns/creational/pool.py index a58ff8d8..1d70ea69 100644 --- a/patterns/creational/pool.py +++ b/patterns/creational/pool.py @@ -51,32 +51,36 @@ def __del__(self): def main(): - import queue + """ + >>> import queue - def test_object(queue): - pool = ObjectPool(queue, True) - print('Inside func: {}'.format(pool.item)) + >>> def test_object(queue): + ... pool = ObjectPool(queue, True) + ... print('Inside func: {}'.format(pool.item)) - sample_queue = queue.Queue() + >>> sample_queue = queue.Queue() - sample_queue.put('yam') - with ObjectPool(sample_queue) as obj: - print('Inside with: {}'.format(obj)) - print('Outside with: {}'.format(sample_queue.get())) + >>> sample_queue.put('yam') + >>> with ObjectPool(sample_queue) as obj: + ... print('Inside with: {}'.format(obj)) + Inside with: yam - sample_queue.put('sam') - test_object(sample_queue) - print('Outside func: {}'.format(sample_queue.get())) + >>> 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__': - main() +if __name__ == "__main__": + import doctest -### OUTPUT ### -# Inside with: yam -# Outside with: yam -# Inside func: sam -# Outside func: sam + doctest.testmod() diff --git a/patterns/creational/prototype.py b/patterns/creational/prototype.py index 55af0dec..4c2dd7ed 100644 --- a/patterns/creational/prototype.py +++ b/patterns/creational/prototype.py @@ -21,15 +21,21 @@ Creates new object instances by cloning prototype. """ +from __future__ import annotations + +from typing import Any -class Prototype: - value = 'default' +class Prototype: + def __init__(self, value: str = "default", **attrs: Any) -> None: + self.value = value + self.__dict__.update(attrs) - def clone(self, **attrs): + def clone(self, **attrs: Any) -> Prototype: """Clone a prototype and update inner attributes dictionary""" # Python in Practice, Mark Summerfield - obj = self.__class__() + # copy.deepcopy can be used instead of next line. + obj = self.__class__(**self.__dict__) obj.__dict__.update(attrs) return obj @@ -38,36 +44,40 @@ class PrototypeDispatcher: def __init__(self): self._objects = {} - def get_objects(self): + def get_objects(self) -> dict[str, Prototype]: """Get all objects""" return self._objects - def register_object(self, name, obj): + def register_object(self, name: str, obj: Prototype) -> None: """Register an object""" self._objects[name] = obj - def unregister_object(self, name): + def unregister_object(self, name: str) -> None: """Unregister an object""" del self._objects[name] -def main(): +def main() -> None: """ >>> dispatcher = PrototypeDispatcher() >>> prototype = Prototype() >>> d = prototype.clone() >>> a = prototype.clone(value='a-value', category='a') - >>> b = prototype.clone(value='b-value', is_checked=True) + >>> 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__': +if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/patterns/dependency_injection.py b/patterns/dependency_injection.py index e5ddfbac..2979f763 100644 --- a/patterns/dependency_injection.py +++ b/patterns/dependency_injection.py @@ -74,7 +74,7 @@ def production_code_time_provider() -> str: datetime for this example). """ current_time = datetime.datetime.now() - current_time_formatted = "{}:{}".format(current_time.hour, current_time.minute) + current_time_formatted = f"{current_time.hour}:{current_time.minute}" return current_time_formatted diff --git a/patterns/fundamental/delegation_pattern.py b/patterns/fundamental/delegation_pattern.py index bc6a8366..f7a7c2f5 100644 --- a/patterns/fundamental/delegation_pattern.py +++ b/patterns/fundamental/delegation_pattern.py @@ -7,7 +7,8 @@ """ from __future__ import annotations -from typing import Any, Callable, Union + +from typing import Any, Callable class Delegator: @@ -18,19 +19,21 @@ class Delegator: >>> delegator.p2 Traceback (most recent call last): ... - AttributeError: 'Delegate' object has no attribute 'p2' + 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' + AttributeError: 'Delegate' object has no attribute 'do_anything'. Did you mean: 'do_something'? """ - def __init__(self, delegate: Delegate): + def __init__(self, delegate: Delegate) -> None: self.delegate = delegate - def __getattr__(self, name: str) -> Union[Any, Callable]: + def __getattr__(self, name: str) -> Any | Callable: attr = getattr(self.delegate, name) if not callable(attr): @@ -43,11 +46,11 @@ def wrapper(*args, **kwargs): class Delegate: - def __init__(self): + def __init__(self) -> None: self.p1 = 123 - def do_something(self, something: str) -> str: - return f"Doing {something}" + def do_something(self, something: str, kw=None) -> str: + return f"Doing {something}{kw or ''}" if __name__ == "__main__": diff --git a/patterns/other/blackboard.py b/patterns/other/blackboard.py index 551411c5..58fbdb98 100644 --- a/patterns/other/blackboard.py +++ b/patterns/other/blackboard.py @@ -9,85 +9,103 @@ https://en.wikipedia.org/wiki/Blackboard_system """ -import abc +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: - def __init__(self): - self.experts = [] + """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 + "problems": 0, + "suggestions": 0, + "contributions": [], + "progress": 0, # percentage, if 100 -> task is finished } - def add_expert(self, expert): + def add_expert(self, expert: AbstractExpert) -> None: self.experts.append(expert) class Controller: - def __init__(self, blackboard): + """The controller that manages the blackboard system.""" + def __init__(self, blackboard: Blackboard) -> None: self.blackboard = blackboard def run_loop(self): - while self.blackboard.common_state['progress'] < 100: + """ + 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 AbstractExpert(metaclass=abc.ABCMeta): - - def __init__(self, blackboard): - self.blackboard = blackboard - - @property - @abc.abstractmethod - def is_eager_to_contribute(self): - raise NotImplementedError('Must provide implementation in subclass.') - - @abc.abstractmethod - def contribute(self): - raise NotImplementedError('Must provide implementation in subclass.') + 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): + def is_eager_to_contribute(self) -> bool: return True - def contribute(self): - 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) + 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): + def is_eager_to_contribute(self) -> int: return random.randint(0, 1) - def contribute(self): - 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) + 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): - return True if self.blackboard.common_state['problems'] > 100 else False + def is_eager_to_contribute(self) -> bool: + return True if self.blackboard.common_state["problems"] > 100 else False - def contribute(self): - 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 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(): @@ -102,25 +120,18 @@ def main(): >>> from pprint import pprint >>> pprint(contributions) - ['Student', - 'Student', - 'Student', - 'Student', - 'Scientist', - 'Student', - 'Student', - 'Student', - 'Scientist', - 'Student', - 'Scientist', - 'Student', - 'Student', - 'Scientist', - 'Professor'] + ['Student', + 'Scientist', + 'Student', + 'Scientist', + 'Student', + 'Scientist', + 'Professor'] """ -if __name__ == '__main__': +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 index 968e4342..262a6f08 100644 --- a/patterns/other/graph_search.py +++ b/patterns/other/graph_search.py @@ -1,12 +1,14 @@ class GraphSearch: - """Graph search emulation in python, from source - http://www.python.org/doc/essays/graphs/""" + http://www.python.org/doc/essays/graphs/ + + dfs stands for Depth First Search + bfs stands for Breadth First Search""" def __init__(self, graph): self.graph = graph - def find_path(self, start, end, path=None): + def find_path_dfs(self, start, end, path=None): path = path or [] path.append(start) @@ -14,11 +16,11 @@ def find_path(self, start, end, path=None): return path for node in self.graph.get(start, []): if node not in path: - newpath = self.find_path(node, end, path[:]) + newpath = self.find_path_dfs(node, end, path[:]) if newpath: return newpath - def find_all_path(self, start, end, path=None): + def find_all_paths_dfs(self, start, end, path=None): path = path or [] path.append(start) if start == end: @@ -26,11 +28,11 @@ def find_all_path(self, start, end, path=None): paths = [] for node in self.graph.get(start, []): if node not in path: - newpaths = self.find_all_path(node, end, path[:]) + newpaths = self.find_all_paths_dfs(node, end, path[:]) paths.extend(newpaths) return paths - def find_shortest_path(self, start, end, path=None): + def find_shortest_path_dfs(self, start, end, path=None): path = path or [] path.append(start) @@ -39,30 +41,110 @@ def find_shortest_path(self, start, end, path=None): shortest = None for node in self.graph.get(start, []): if node not in path: - newpath = self.find_shortest_path(node, end, path[:]) + newpath = self.find_shortest_path_dfs(node, end, path[:]) if newpath: if not shortest or len(newpath) < len(shortest): shortest = newpath return shortest + def find_shortest_path_bfs(self, start, end): + """ + Finds the shortest path between two nodes in a graph using breadth-first search. + + :param start: The node to start from. + :type start: str or int + :param end: The node to find the shortest path to. + :type end: str or int + + :returns queue_path_to_end, dist_to[end]: A list of nodes + representing the shortest path from `start` to `end`, and a dictionary + mapping each node in the graph (except for `start`) with its distance from it + (in terms of hops). If no such path exists, returns an empty list and an empty + dictionary instead. + """ + queue = [start] + dist_to = {start: 0} + edge_to = {} + + if start == end: + return queue + + while len(queue): + value = queue.pop(0) + for node in self.graph[value]: + if node not in dist_to.keys(): + edge_to[node] = value + dist_to[node] = dist_to[value] + 1 + queue.append(node) + if end in edge_to.keys(): + path = [] + node = end + while dist_to[node] != 0: + path.insert(0, node) + node = edge_to[node] + path.insert(0, start) + return path + def main(): """ # example of graph usage - >>> graph = {'A': ['B', 'C'], 'B': ['C', 'D'], 'C': ['D'], 'D': ['C'], 'E': ['F'], 'F': ['C']} + >>> 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 - >>> graph1 = GraphSearch(graph) + >>> graph_search = GraphSearch(graph) - >>> print(graph1.find_path('A', 'D')) + >>> print(graph_search.find_path_dfs('A', 'D')) ['A', 'B', 'C', 'D'] - >>> print(graph1.find_all_path('A', '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(graph1.find_shortest_path('A', '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/hsm.py b/patterns/other/hsm/hsm.py index 26bb2da8..44498014 100644 --- a/patterns/other/hsm/hsm.py +++ b/patterns/other/hsm/hsm.py @@ -29,17 +29,17 @@ def __init__(self): 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, + "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, + "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): @@ -49,34 +49,34 @@ def _next_state(self, state): raise UnsupportedState def _send_diagnostics_request(self): - return 'send diagnostic request' + return "send diagnostic request" def _raise_alarm(self): - return 'raise alarm' + return "raise alarm" def _clear_alarm(self): - return 'clear alarm' + return "clear alarm" def _perform_switchover(self): - return 'perform switchover' + return "perform switchover" def _send_switchover_response(self): - return 'send switchover response' + return "send switchover response" def _send_operator_inservice_response(self): - return 'send operator inservice response' + return "send operator inservice response" def _send_diagnostics_failure_report(self): - return 'send diagnostics failure report' + return "send diagnostics failure report" def _send_diagnostics_pass_report(self): - return 'send diagnostics pass report' + return "send diagnostics pass report" def _abort_diagnostics(self): - return 'abort diagnostics' + return "abort diagnostics" def _check_mate_status(self): - return 'check mate status' + return "check mate status" def on_message(self, message_type): # message ignored if message_type in self.message_types.keys(): @@ -110,7 +110,7 @@ def __init__(self, HierachicalStateMachine): self._hsm = HierachicalStateMachine def on_fault_trigger(self): - self._hsm._next_state('suspect') + self._hsm._next_state("suspect") self._hsm._send_diagnostics_request() self._hsm._raise_alarm() @@ -130,7 +130,7 @@ def on_fault_trigger(self): def on_switchover(self): self._hsm.on_switchover() # message ignored - self._hsm.next_state('standby') + self._hsm.next_state("standby") class Standby(Inservice): @@ -139,7 +139,7 @@ def __init__(self, HierachicalStateMachine): def on_switchover(self): super().on_switchover() # message ignored - self._hsm._next_state('active') + self._hsm._next_state("active") class OutOfService(Unit): @@ -149,7 +149,7 @@ def __init__(self, HierachicalStateMachine): def on_operator_inservice(self): self._hsm.on_switchover() # message ignored self._hsm.send_operator_inservice_response() - self._hsm.next_state('suspect') + self._hsm.next_state("suspect") class Suspect(OutOfService): @@ -158,12 +158,12 @@ def __init__(self, HierachicalStateMachine): def on_diagnostics_failed(self): super().send_diagnostics_failure_report() - super().next_state('failed') + 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') + super().next_state("standby") def on_operator_inservice(self): super().abort_diagnostics() diff --git a/patterns/structural/3-tier.py b/patterns/structural/3-tier.py index 5497730c..ecc04243 100644 --- a/patterns/structural/3-tier.py +++ b/patterns/structural/3-tier.py @@ -3,11 +3,11 @@ Separates presentation, application processing, and data management functions. """ -from typing import Dict, KeysView, Optional, Type, TypeVar, Union +from typing import Dict, KeysView, Optional, Union class Data: - """ Data Store Class """ + """Data Store Class""" products = { "milk": {"price": 1.50, "quantity": 10}, @@ -22,7 +22,7 @@ def __get__(self, obj, klas): class BusinessLogic: - """ Business logic holding data store instances """ + """Business logic holding data store instances""" data = Data() @@ -36,7 +36,7 @@ def product_information( class Ui: - """ UI interaction class """ + """UI interaction class""" def __init__(self) -> None: self.business_logic = BusinessLogic() @@ -61,32 +61,38 @@ def get_product_information(self, product: str) -> None: def main(): - ui = Ui() - ui.get_product_list() - ui.get_product_information("cheese") - ui.get_product_information("eggs") - ui.get_product_information("milk") - ui.get_product_information("arepas") + """ + >>> 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__": - main() - -### OUTPUT ### -# PRODUCT LIST: -# (Fetching from Data Store) -# cheese -# eggs -# milk -# -# (Fetching from Data Store) -# PRODUCT INFORMATION: -# Name: Cheese, Price: 2.00, Quantity: 10 -# (Fetching from Data Store) -# PRODUCT INFORMATION: -# Name: Eggs, Price: 0.20, Quantity: 100 -# (Fetching from Data Store) -# PRODUCT INFORMATION: -# Name: Milk, Price: 1.50, Quantity: 10 -# (Fetching from Data Store) -# That product "arepas" does not exist in the records + import doctest + + doctest.testmod() diff --git a/patterns/structural/adapter.py b/patterns/structural/adapter.py index 99314a2a..433369ee 100644 --- a/patterns/structural/adapter.py +++ b/patterns/structural/adapter.py @@ -28,58 +28,63 @@ Allows the interface of an existing class to be used as another interface. """ +from typing import Callable, TypeVar + +T = TypeVar("T") + class Dog: - def __init__(self): + def __init__(self) -> None: self.name = "Dog" - def bark(self): + def bark(self) -> str: return "woof!" class Cat: - def __init__(self): + def __init__(self) -> None: self.name = "Cat" - def meow(self): + def meow(self) -> str: return "meow!" class Human: - def __init__(self): + def __init__(self) -> None: self.name = "Human" - def speak(self): + def speak(self) -> str: return "'hello'" class Car: - def __init__(self): + def __init__(self) -> None: self.name = "Car" - def make_noise(self, octane_level): - return "vroom{0}".format("!" * octane_level) + def make_noise(self, octane_level: int) -> str: + return f"vroom{'!' * octane_level}" class Adapter: - """ - Adapts an object by replacing methods. - Usage: + """Adapts an object by replacing methods. + + Usage + ------ dog = Dog() dog = Adapter(dog, make_noise=dog.bark) """ - def __init__(self, obj, **adapted_methods): - """We set the adapted methods in the object's dict""" + def __init__(self, obj: T, **adapted_methods: Callable): + """We set the adapted methods in the object's dict.""" self.obj = obj self.__dict__.update(adapted_methods) def __getattr__(self, attr): - """All non-adapted calls are passed to the object""" + """All non-adapted calls are passed to the object.""" return getattr(self.obj, attr) def original_dict(self): - """Print original object dict""" + """Print original object dict.""" return self.obj.__dict__ @@ -116,4 +121,5 @@ def main(): if __name__ == "__main__": import doctest + doctest.testmod(optionflags=doctest.ELLIPSIS) diff --git a/patterns/structural/bridge.py b/patterns/structural/bridge.py index 64b4b422..feddb675 100644 --- a/patterns/structural/bridge.py +++ b/patterns/structural/bridge.py @@ -10,13 +10,13 @@ # ConcreteImplementor 1/2 class DrawingAPI1: def draw_circle(self, x, y, radius): - print('API1.circle at {}:{} radius {}'.format(x, y, radius)) + print(f"API1.circle at {x}:{y} radius {radius}") # ConcreteImplementor 2/2 class DrawingAPI2: def draw_circle(self, x, y, radius): - print('API2.circle at {}:{} radius {}'.format(x, y, radius)) + print(f"API2.circle at {x}:{y} radius {radius}") # Refined Abstraction @@ -48,6 +48,7 @@ def main(): """ -if __name__ == '__main__': +if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/patterns/structural/composite.py b/patterns/structural/composite.py index 2f2e6da9..a4bedc1d 100644 --- a/patterns/structural/composite.py +++ b/patterns/structural/composite.py @@ -26,58 +26,68 @@ Describes a group of objects that is treated as a single instance. """ +from abc import ABC, abstractmethod +from typing import List -class Graphic: - def render(self): - raise NotImplementedError("You should implement this.") + +class Graphic(ABC): + @abstractmethod + def render(self) -> None: + raise NotImplementedError("You should implement this!") class CompositeGraphic(Graphic): - def __init__(self): - self.graphics = [] + def __init__(self) -> None: + self.graphics: List[Graphic] = [] - def render(self): + def render(self) -> None: for graphic in self.graphics: graphic.render() - def add(self, graphic): + def add(self, graphic: Graphic) -> None: self.graphics.append(graphic) - def remove(self, graphic): + def remove(self, graphic: Graphic) -> None: self.graphics.remove(graphic) class Ellipse(Graphic): - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name - def render(self): - print("Ellipse: {}".format(self.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() -if __name__ == '__main__': - ellipse1 = Ellipse("1") - ellipse2 = Ellipse("2") - ellipse3 = Ellipse("3") - ellipse4 = Ellipse("4") + >>> graphic1.add(ellipse1) + >>> graphic1.add(ellipse2) + >>> graphic1.add(ellipse3) + >>> graphic2.add(ellipse4) - graphic1 = CompositeGraphic() - graphic2 = CompositeGraphic() + >>> graphic = CompositeGraphic() - graphic1.add(ellipse1) - graphic1.add(ellipse2) - graphic1.add(ellipse3) - graphic2.add(ellipse4) + >>> graphic.add(graphic1) + >>> graphic.add(graphic2) - graphic = CompositeGraphic() + >>> graphic.render() + Ellipse: 1 + Ellipse: 2 + Ellipse: 3 + Ellipse: 4 + """ - graphic.add(graphic1) - graphic.add(graphic2) - graphic.render() +if __name__ == "__main__": + import doctest -### OUTPUT ### -# Ellipse: 1 -# Ellipse: 2 -# Ellipse: 3 -# Ellipse: 4 + doctest.testmod() diff --git a/patterns/structural/decorator.py b/patterns/structural/decorator.py index b94c0527..a32e2b06 100644 --- a/patterns/structural/decorator.py +++ b/patterns/structural/decorator.py @@ -28,39 +28,47 @@ class TextTag: """Represents a base text tag""" - def __init__(self, text): + def __init__(self, text: str) -> None: self._text = text - def render(self): + def render(self) -> str: return self._text class BoldWrapper(TextTag): """Wraps a tag in """ - def __init__(self, wrapped): + def __init__(self, wrapped: TextTag) -> None: self._wrapped = wrapped - def render(self): - return "{}".format(self._wrapped.render()) + def render(self) -> str: + return f"{self._wrapped.render()}" class ItalicWrapper(TextTag): """Wraps a tag in """ - def __init__(self, wrapped): + def __init__(self, wrapped: TextTag) -> None: self._wrapped = wrapped - def render(self): - return "{}".format(self._wrapped.render()) + def render(self) -> str: + return f"{self._wrapped.render()}" -if __name__ == '__main__': - simple_hello = TextTag("hello, world!") - special_hello = ItalicWrapper(BoldWrapper(simple_hello)) - print("before:", simple_hello.render()) - print("after:", special_hello.render()) +def main(): + """ + >>> simple_hello = TextTag("hello, world!") + >>> special_hello = ItalicWrapper(BoldWrapper(simple_hello)) -### OUTPUT ### -# before: hello, world! -# after: hello, world! + >>> 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 index 6c04c472..f7b00be3 100644 --- a/patterns/structural/facade.py +++ b/patterns/structural/facade.py @@ -34,13 +34,14 @@ class CPU: """ Simple CPU representation. """ - def freeze(self): + + def freeze(self) -> None: print("Freezing processor.") - def jump(self, position): + def jump(self, position: str) -> None: print("Jumping to:", position) - def execute(self): + def execute(self) -> None: print("Executing.") @@ -48,22 +49,25 @@ class Memory: """ Simple memory representation. """ - def load(self, position, data): - print("Loading from {0} data: '{1}'.".format(position, data)) + + 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, size): - return "Some data from sector {0} with size {1}".format(lba, size) + + 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() @@ -89,4 +93,5 @@ def main(): if __name__ == "__main__": import doctest + doctest.testmod(optionflags=doctest.ELLIPSIS) diff --git a/patterns/structural/flyweight.py b/patterns/structural/flyweight.py index b5911370..fad17a8b 100644 --- a/patterns/structural/flyweight.py +++ b/patterns/structural/flyweight.py @@ -34,7 +34,7 @@ class Card: # Could be a simple dict. # With WeakValueDictionary garbage collection can reclaim the object # when there are no other references to it. - _pool = weakref.WeakValueDictionary() + _pool: weakref.WeakValueDictionary = weakref.WeakValueDictionary() def __new__(cls, value, suit): # If the object exists in the pool - just return it @@ -53,7 +53,7 @@ def __new__(cls, value, suit): # self.value, self.suit = value, suit def __repr__(self): - return "".format(self.value, self.suit) + return f"" def main(): @@ -81,4 +81,5 @@ def main(): if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/patterns/structural/flyweight_with_metaclass.py b/patterns/structural/flyweight_with_metaclass.py index bba21360..ced8d915 100644 --- a/patterns/structural/flyweight_with_metaclass.py +++ b/patterns/structural/flyweight_with_metaclass.py @@ -12,7 +12,7 @@ def __new__(mcs, name, parents, dct): static methods, etc :return: new class """ - dct['pool'] = weakref.WeakValueDictionary() + dct["pool"] = weakref.WeakValueDictionary() return super().__new__(mcs, name, parents, dct) @staticmethod @@ -23,12 +23,12 @@ def _serialize_params(cls, *args, **kwargs): """ args_list = list(map(str, args)) args_list.extend([str(kwargs), cls.__name__]) - key = ''.join(args_list) + key = "".join(args_list) return key def __call__(cls, *args, **kwargs): key = FlyweightMeta._serialize_params(cls, *args, **kwargs) - pool = getattr(cls, 'pool', {}) + pool = getattr(cls, "pool", {}) instance = pool.get(key) if instance is None: @@ -43,11 +43,11 @@ def __init__(self, *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) +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) diff --git a/patterns/structural/front_controller.py b/patterns/structural/front_controller.py index 346392e4..92f58b21 100644 --- a/patterns/structural/front_controller.py +++ b/patterns/structural/front_controller.py @@ -5,49 +5,62 @@ 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): - print('Displaying mobile index page') + def show_index_page(self) -> None: + print("Displaying mobile index page") class TabletView: - def show_index_page(self): - print('Displaying tablet index page') + def show_index_page(self) -> None: + print("Displaying tablet index page") class Dispatcher: - def __init__(self): + def __init__(self) -> None: self.mobile_view = MobileView() self.tablet_view = TabletView() - def dispatch(self, request): + 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('cant dispatch the request') + print("Cannot dispatch the request") class RequestController: - """ front controller """ + """front controller""" - def __init__(self): + def __init__(self) -> None: self.dispatcher = Dispatcher() - def dispatch_request(self, request): + 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') + print("request must be a Request object") class Request: - """ request """ + """request""" - mobile_type = 'mobile' - tablet_type = 'tablet' + mobile_type = "mobile" + tablet_type = "tablet" def __init__(self, request): self.type = None @@ -58,17 +71,25 @@ def __init__(self, request): self.type = self.tablet_type -if __name__ == '__main__': - front_controller = RequestController() - front_controller.dispatch_request(Request('mobile')) - front_controller.dispatch_request(Request('tablet')) +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 + """ - front_controller.dispatch_request(Request('desktop')) - front_controller.dispatch_request('mobile') +if __name__ == "__main__": + import doctest -### OUTPUT ### -# Displaying mobile index page -# Displaying tablet index page -# cant dispatch the request -# request must be a Request object + doctest.testmod() diff --git a/patterns/structural/mvc.py b/patterns/structural/mvc.py index ff22ea59..24b0017a 100644 --- a/patterns/structural/mvc.py +++ b/patterns/structural/mvc.py @@ -3,130 +3,202 @@ Separates data in GUIs from the ways it is presented, and accepted. """ +from abc import ABC, abstractmethod +from inspect import signature +from sys import argv +from typing import Any -class Model: - def __iter__(self): - raise NotImplementedError - def get(self, item): +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.""" - raise NotImplementedError + pass @property - def item_type(self): - raise NotImplementedError + @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): - return "{:.2f}".format(self) + 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}, + "milk": {"price": Price(1.50), "quantity": 10}, + "eggs": {"price": Price(0.20), "quantity": 100}, + "cheese": {"price": Price(2.00), "quantity": 10}, } - item_type = 'product' + item_type = "product" - def __iter__(self): - for item in self.products: - yield item + def __iter__(self) -> Any: + yield from self.products - def get(self, product): + 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: - def show_item_list(self, item_type, item_list): - raise NotImplementedError +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 - def show_item_information(self, item_type, item_name, item_info): + @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()""" - raise NotImplementedError + pass - def item_not_found(self, item_type, item_name): - raise NotImplementedError + @abstractmethod + def item_not_found(self, item_type: str, item_name: str) -> None: + pass class ConsoleView(View): - def show_item_list(self, item_type, item_list): - print(item_type.upper() + ' LIST:') + """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('') + print("") @staticmethod - def capitalizer(string): + 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, item_name, item_info): - print(item_type.upper() + ' INFORMATION:') - printout = 'Name: %s' % item_name + 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' + printout += ", " + self.capitalizer(str(key)) + ": " + str(value) + printout += "\n" print(printout) - def item_not_found(self, item_type, item_name): - print('That {} "{}" does not exist in the records'.format(item_type, item_name)) + 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: - def __init__(self, model, view): - self.model = model - self.view = view + """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): + 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): + 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 = self.model.get(item_name) + item_info: dict = self.model.get(item_name) except Exception: - item_type = self.model.item_type self.view.item_not_found(item_type, item_name) else: - item_type = self.model.item_type self.view.show_item_information(item_type, item_name, item_info) -if __name__ == '__main__': - - model = ProductModel() - view = ConsoleView() - controller = Controller(model, view) - controller.show_items() - controller.show_item_information('cheese') - controller.show_item_information('eggs') - controller.show_item_information('milk') - controller.show_item_information('arepas') - - -### OUTPUT ### -# PRODUCT LIST: -# cheese -# eggs -# milk -# -# PRODUCT INFORMATION: -# Name: Cheese, Price: 2.00, Quantity: 10 -# -# PRODUCT INFORMATION: -# Name: Eggs, Price: 0.20, Quantity: 100 -# -# PRODUCT INFORMATION: -# Name: Milk, Price: 1.50, Quantity: 10 -# -# That product "arepas" does not exist in the records +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 index 51edb4a7..3ef74ec0 100644 --- a/patterns/structural/proxy.py +++ b/patterns/structural/proxy.py @@ -56,7 +56,7 @@ def do_the_job(self, user: str) -> None: if user == "admin": self._real_subject.do_the_job(user) else: - print(f"[log] I can do the job just for `admins`.") + print("[log] I can do the job just for `admins`.") def client(job_doer: Union[RealSubject, Proxy], user: str) -> None: diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..57f6fbe7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,98 @@ +[build-system] +requires = ["setuptools>=42", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "patterns" +description = "A collection of design patterns and idioms in Python." +version = "0.1.0" +readme = "README.md" +requires-python = ">=3.9" +license = {text = "MIT"} +classifiers = [ + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] + +[project.optional-dependencies] +dev = ["pytest", "pytest-cov", "pytest-randomly", "flake8", "mypy", "coverage"] + +[tool.setuptools] +packages = ["patterns"] + +[tool.pytest.ini_options] +filterwarnings = [ + "ignore::Warning:.*test class 'TestRunner'.*" +] +# Adding settings from tox.ini for pytest +testpaths = ["tests"] +#testpaths = ["tests", "patterns"] +python_files = ["test_*.py", "*_test.py"] +# Enable doctest discovery in patterns directory +addopts = "--doctest-modules --randomly-seed=1234 --cov=patterns --cov-report=term-missing" +doctest_optionflags = ["ELLIPSIS", "NORMALIZE_WHITESPACE"] +log_level = "INFO" + +[tool.coverage.run] +branch = true +source = ["./"] +#source = ["patterns"] +# Ensure coverage data is collected properly +relative_files = true +parallel = true +dynamic_context = "test_function" +data_file = ".coverage" + +[tool.coverage.report] +# Regexes for lines to exclude from consideration +exclude_lines = [ + "def __repr__", + "if self\\.debug", + "raise AssertionError", + "raise NotImplementedError", + "if 0:", + "if __name__ == .__main__.:", + "@(abc\\.)?abstractmethod" +] +ignore_errors = true + +[tool.coverage.html] +directory = "coverage_html_report" + +[tool.mypy] +python_version = "3.12" +ignore_missing_imports = true + +[tool.flake8] +max-line-length = 120 +ignore = ["E266", "E731", "W503"] +exclude = ["venv*"] + +[tool.tox] +legacy_tox_ini = """ +[tox] +envlist = py312,cov-report +skip_missing_interpreters = true +usedevelop = true + +#[testenv] +#setenv = +# COVERAGE_FILE = .coverage.{envname} +#deps = +# -r requirements-dev.txt +#commands = +# flake8 --exclude="venv/,.tox/" patterns/ +# coverage run -m pytest --randomly-seed=1234 --doctest-modules patterns/ +# coverage run -m pytest -s -vv --cov=patterns/ --log-level=INFO tests/ + +#[testenv:cov-report] +#setenv = +# COVERAGE_FILE = .coverage +#deps = coverage +#commands = +# coverage combine +# coverage report +#""" \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt index 451dad45..4aaa81f2 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,9 @@ --e . - -pytest~=4.3.0 -pytest-cov~=2.6.0 -flake8~=3.7.0 -pytest-randomly~=3.1.0 +mypy +pyupgrade +pytest>=6.2.0 +pytest-cov>=2.11.0 +pytest-randomly>=3.1.0 +black>=25.1.0 +isort>=5.7.0 +flake8>=7.1.0 +tox>=4.25.0 \ No newline at end of file diff --git a/setup.py b/setup.py index 80930a8b..72bc2b46 100644 --- a/setup.py +++ b/setup.py @@ -1,14 +1,14 @@ -from setuptools import setup, find_packages +from setuptools import find_packages, setup setup( name="patterns", packages=find_packages(), description="A collection of design patterns and idioms in Python.", classifiers=[ - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ], ) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/behavioral/test_observer.py b/tests/behavioral/test_observer.py index e24efe44..821f97a6 100644 --- a/tests/behavioral/test_observer.py +++ b/tests/behavioral/test_observer.py @@ -1,4 +1,4 @@ -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch import pytest @@ -7,7 +7,8 @@ @pytest.fixture def observable(): - return Data('some data') + return Data("some data") + def test_attach_detach(observable): decimal_viewer = DecimalViewer() @@ -19,11 +20,14 @@ def test_attach_detach(observable): 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: + 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 index 84015080..c153da5b 100644 --- a/tests/behavioral/test_publish_subscribe.py +++ b/tests/behavioral/test_publish_subscribe.py @@ -1,5 +1,6 @@ import unittest -from unittest.mock import patch, call +from unittest.mock import call, patch + from patterns.behavioral.publish_subscribe import Provider, Publisher, Subscriber @@ -9,53 +10,59 @@ class TestProvider(unittest.TestCase): """ def test_subscriber_shall_be_attachable_to_subscriptions(cls): - subscription = 'sub msg' + subscription = "sub msg" pro = Provider() cls.assertEqual(len(pro.subscribers), 0) - sub = Subscriber('sub name', pro) + 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' + subscription = "sub msg" pro = Provider() - sub = Subscriber('sub name', pro) + 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' + """msg_queue ~ Provider.notify(msg) ~ Publisher.publish(msg)""" + expected_msg = "expected msg" pro = Provider() pub = Publisher(pro) - Subscriber('sub name', 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): + 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: + 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: + 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')] + 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')] + expected_sub2_calls = [call("sub 2 msg 1"), call("sub 2 msg 2")] mock_subscriber2_run.assert_has_calls(expected_sub2_calls) diff --git a/tests/behavioral/test_state.py b/tests/behavioral/test_state.py index adaae509..77473f51 100644 --- a/tests/behavioral/test_state.py +++ b/tests/behavioral/test_state.py @@ -7,18 +7,21 @@ def radio(): return Radio() + def test_initial_state(radio): - assert radio.state.name == 'AM' + assert radio.state.name == "AM" + def test_initial_am_station(radio): initial_pos = radio.state.pos - assert radio.state.stations[initial_pos] == '1250' + assert radio.state.stations[initial_pos] == "1250" + def test_toggle_amfm(radio): - assert radio.state.name == 'AM' + assert radio.state.name == "AM" radio.toggle_amfm() - assert radio.state.name == 'FM' + assert radio.state.name == "FM" radio.toggle_amfm() - assert radio.state.name == 'AM' + assert radio.state.name == "AM" diff --git a/tests/behavioral/test_strategy.py b/tests/behavioral/test_strategy.py new file mode 100644 index 00000000..53976f38 --- /dev/null +++ b/tests/behavioral/test_strategy.py @@ -0,0 +1,41 @@ +import pytest + +from patterns.behavioral.strategy import Order, on_sale_discount, ten_percent_discount + + +@pytest.fixture +def order(): + return Order(100) + + +@pytest.mark.parametrize( + "func, discount", [(ten_percent_discount, 10.0), (on_sale_discount, 45.0)] +) +def test_discount_function_return(func, order, discount): + assert func(order) == discount + + +@pytest.mark.parametrize( + "func, price", [(ten_percent_discount, 100), (on_sale_discount, 100)] +) +def test_order_discount_strategy_validate_success(func, price): + order = Order(price, func) + + assert order.price == price + assert order.discount_strategy == func + + +def test_order_discount_strategy_validate_error(): + order = Order(10, discount_strategy=on_sale_discount) + + assert order.discount_strategy is None + + +@pytest.mark.parametrize( + "func, price, discount", + [(ten_percent_discount, 100, 90.0), (on_sale_discount, 100, 55.0)], +) +def test_discount_apply_success(func, price, discount): + order = Order(price, func) + + assert order.apply_discount() == discount diff --git a/tests/creational/test_abstract_factory.py b/tests/creational/test_abstract_factory.py index ad7a7fcf..1676e59d 100644 --- a/tests/creational/test_abstract_factory.py +++ b/tests/creational/test_abstract_factory.py @@ -1,12 +1,13 @@ import unittest from unittest.mock import patch -from patterns.creational.abstract_factory import PetShop, Dog +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: - dog_pet_shop.show_pet() + 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 index 82d9efaf..182611c3 100644 --- a/tests/creational/test_borg.py +++ b/tests/creational/test_borg.py @@ -1,4 +1,5 @@ import unittest + from patterns.creational.borg import Borg, YourBorg @@ -6,17 +7,22 @@ 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') + 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') + 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 index 7f0d8e72..923bc4a5 100644 --- a/tests/creational/test_builder.py +++ b/tests/creational/test_builder.py @@ -1,21 +1,22 @@ import unittest -from patterns.creational.builder import construct_building, House, Flat, ComplexHouse + +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') + 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') + 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') + self.assertEqual(house.size, "Big and fancy") + self.assertEqual(house.floor, "One") diff --git a/tests/creational/test_lazy.py b/tests/creational/test_lazy.py index 8da429ec..1b815b60 100644 --- a/tests/creational/test_lazy.py +++ b/tests/creational/test_lazy.py @@ -1,28 +1,36 @@ -from __future__ import print_function import unittest + from patterns.creational.lazy_evaluation import Person class TestDynamicExpanding(unittest.TestCase): def setUp(self): - self.John = Person('John', 'Coder') + self.John = Person("John", "Coder") def test_innate_properties(self): - self.assertDictEqual({'name': 'John', 'occupation': 'Coder', 'call_count2': 0}, self.John.__dict__) + 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__) + self.assertNotIn("relatives", self.John.__dict__) def test_extended_properties(self): - print(u"John's relatives: {0}".format(self.John.relatives)) + print(f"John's relatives: {self.John.relatives}") self.assertDictEqual( - {'name': 'John', 'occupation': 'Coder', 'relatives': 'Many relatives.', 'call_count2': 0}, + { + "name": "John", + "occupation": "Coder", + "relatives": "Many relatives.", + "call_count2": 0, + }, self.John.__dict__, ) def test_relatives_after_access(self): - print(u"John's relatives: {0}".format(self.John.relatives)) - self.assertIn('relatives', self.John.__dict__) + print(f"John's relatives: {self.John.relatives}") + self.assertIn("relatives", self.John.__dict__) def test_parents(self): for _ in range(2): diff --git a/tests/creational/test_pool.py b/tests/creational/test_pool.py index b63f58c7..cd501db3 100644 --- a/tests/creational/test_pool.py +++ b/tests/creational/test_pool.py @@ -1,5 +1,5 @@ -import unittest import queue +import unittest from patterns.creational.pool import ObjectPool @@ -7,41 +7,40 @@ class TestPool(unittest.TestCase): def setUp(self): self.sample_queue = queue.Queue() - self.sample_queue.put('first') - self.sample_queue.put('second') + 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.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.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.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.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))""" + 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') + sample_queue.put("yam") with ObjectPool(sample_queue) as obj: # print('Inside with: {}'.format(obj)) - self.assertEqual(obj, 'yam') + self.assertEqual(obj, "yam") self.assertFalse(sample_queue.empty()) - self.assertTrue(sample_queue.get() == 'yam') + self.assertTrue(sample_queue.get() == "yam") self.assertTrue(sample_queue.empty()) # sample_queue.put('sam') diff --git a/tests/creational/test_prototype.py b/tests/creational/test_prototype.py index 74eb12cf..758ac872 100644 --- a/tests/creational/test_prototype.py +++ b/tests/creational/test_prototype.py @@ -1,4 +1,5 @@ import unittest + from patterns.creational.prototype import Prototype, PrototypeDispatcher @@ -13,13 +14,13 @@ def test_cloning_propperty_innate_values(self): def test_extended_property_values_cloning(self): sample_object_1 = self.prototype.clone() - sample_object_1.some_value = 'test string' + 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') + sample_object_2 = self.prototype.clone(value="re-assigned") self.assertNotEqual(sample_object_1.value, sample_object_2.value) @@ -28,20 +29,20 @@ 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) + 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') + 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) + self.assertEqual(self.dispatcher.get_objects()["A"].ext_value, "E") + self.assertTrue(self.dispatcher.get_objects()["B"].diff) diff --git a/tests/structural/test_adapter.py b/tests/structural/test_adapter.py index 76f80425..01323075 100644 --- a/tests/structural/test_adapter.py +++ b/tests/structural/test_adapter.py @@ -1,5 +1,6 @@ import unittest -from patterns.structural.adapter import Dog, Cat, Human, Car, Adapter + +from patterns.structural.adapter import Adapter, Car, Cat, Dog, Human class ClassTest(unittest.TestCase): diff --git a/tests/structural/test_bridge.py b/tests/structural/test_bridge.py index 0bb704f3..7fa8a278 100644 --- a/tests/structural/test_bridge.py +++ b/tests/structural/test_bridge.py @@ -1,15 +1,15 @@ import unittest from unittest.mock import patch -from patterns.structural.bridge import DrawingAPI1, DrawingAPI2, CircleShape +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' + 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() @@ -33,7 +33,9 @@ def test_bridge_shall_scale_both_api_circles_with_own_implementation(cls): 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: + 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) diff --git a/tests/structural/test_decorator.py b/tests/structural/test_decorator.py index df8e9b21..8a4154a9 100644 --- a/tests/structural/test_decorator.py +++ b/tests/structural/test_decorator.py @@ -1,16 +1,24 @@ import unittest -from patterns.structural.decorator import TextTag, BoldWrapper, ItalicWrapper + +from patterns.structural.decorator import BoldWrapper, ItalicWrapper, TextTag class TestTextWrapping(unittest.TestCase): def setUp(self): - self.raw_string = TextTag('raw but not cruel') + self.raw_string = TextTag("raw but not cruel") def test_italic(self): - self.assertEqual(ItalicWrapper(self.raw_string).render(), 'raw but not cruel') + 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') + 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') + self.assertEqual( + BoldWrapper(ItalicWrapper(self.raw_string)).render(), + "raw but not cruel", + ) diff --git a/tests/structural/test_proxy.py b/tests/structural/test_proxy.py index e0dcaac0..3409bf0b 100644 --- a/tests/structural/test_proxy.py +++ b/tests/structural/test_proxy.py @@ -1,94 +1,37 @@ import sys -from time import time import unittest from io import StringIO -from patterns.structural.proxy import Proxy, NoTalkProxy +from patterns.structural.proxy import Proxy, client class ProxyTest(unittest.TestCase): @classmethod def setUpClass(cls): - """ Class scope setup. """ - cls.p = Proxy() + """Class scope setup.""" + cls.proxy = Proxy() def setUp(cls): - """ Function/test case scope setup. """ + """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. """ + """Function/test case scope teardown.""" cls.output.close() sys.stdout = cls.saved_stdout - def test_sales_manager_shall_talk_through_proxy_with_delay(cls): - cls.p.busy = 'No' - start_time = time() - cls.p.talk() - end_time = time() - execution_time = end_time - start_time - print_output = cls.output.getvalue() - expected_print_output = 'Proxy checking for Sales Manager availability\n\ -Sales Manager ready to talk\n' - cls.assertEqual(print_output, expected_print_output) - expected_execution_time = 1 - cls.assertEqual(int(execution_time * 10), expected_execution_time) - - def test_sales_manager_shall_respond_through_proxy_with_delay(cls): - cls.p.busy = 'Yes' - start_time = time() - cls.p.talk() - end_time = time() - execution_time = end_time - start_time - print_output = cls.output.getvalue() - expected_print_output = 'Proxy checking for Sales Manager availability\n\ -Sales Manager is busy\n' - cls.assertEqual(print_output, expected_print_output) - expected_execution_time = 1 - cls.assertEqual(int(execution_time * 10), expected_execution_time) - - -class NoTalkProxyTest(unittest.TestCase): - @classmethod - def setUpClass(cls): - """ Class scope setup. """ - cls.ntp = NoTalkProxy() - - 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_sales_manager_shall_not_talk_through_proxy_with_delay(cls): - cls.ntp.busy = 'No' - start_time = time() - cls.ntp.talk() - end_time = time() - execution_time = end_time - start_time - print_output = cls.output.getvalue() - expected_print_output = 'Proxy checking for Sales Manager availability\n\ -This Sales Manager will not talk to you whether he/she is busy or not\n' - cls.assertEqual(print_output, expected_print_output) - expected_execution_time = 1 - cls.assertEqual(int(execution_time * 10), expected_execution_time) - - def test_sales_manager_shall_not_respond_through_proxy_with_delay(cls): - cls.ntp.busy = 'Yes' - start_time = time() - cls.ntp.talk() - end_time = time() - execution_time = end_time - start_time - print_output = cls.output.getvalue() - expected_print_output = 'Proxy checking for Sales Manager availability\n\ -This Sales Manager will not talk to you whether he/she is busy or not\n' - cls.assertEqual(print_output, expected_print_output) - expected_execution_time = 1 - cls.assertEqual(int(execution_time * 10), expected_execution_time) + 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 index 7b759e79..f42323a9 100644 --- a/tests/test_hsm.py +++ b/tests/test_hsm.py @@ -2,13 +2,13 @@ from unittest.mock import patch from patterns.other.hsm.hsm import ( + Active, HierachicalStateMachine, + Standby, + Suspect, UnsupportedMessageType, UnsupportedState, UnsupportedTransition, - Active, - Standby, - Suspect, ) @@ -22,29 +22,29 @@ def test_initial_state_shall_be_standby(cls): def test_unsupported_state_shall_raise_exception(cls): with cls.assertRaises(UnsupportedState): - cls.hsm._next_state('missing') + cls.hsm._next_state("missing") def test_unsupported_message_type_shall_raise_exception(cls): with cls.assertRaises(UnsupportedMessageType): - cls.hsm.on_message('trigger') + 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.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... """ + """Exemplary HierachicalStateMachine method test. + (here: _perform_switchover()). Add additional test cases...""" return_value = cls.hsm._perform_switchover() - expected_return_value = '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... """ + """Exemplary 2nd level state test class (here: Standby state). Add missing + state test classes...""" @classmethod def setUpClass(cls): @@ -54,38 +54,46 @@ 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.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' + 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' + cls.hsm, "_send_switchover_response" ) as mock_send_switchover_response, patch.object( - cls.hsm, '_next_state' + cls.hsm, "_next_state" ) as mock_next_state: - cls.hsm.on_message('switchover') + 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.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): + 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.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): + 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.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): + 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.hsm.on_message("operator inservice") cls.assertEqual(isinstance(cls.hsm._current_state, Standby), True)