diff --git a/.codespellignore b/.codespellignore new file mode 100644 index 000000000..d272a2e1e --- /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 3778bf3d2..000000000 --- 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/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..b565ddcb8 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: + - package-ecosystem: pip + directory: / + schedule: + interval: weekly + versioning-strategy: increase-if-necessary + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly diff --git a/.github/workflows/lint_python.yml b/.github/workflows/lint_python.yml new file mode 100644 index 000000000..e5ef51b51 --- /dev/null +++ b/.github/workflows/lint_python.yml @@ -0,0 +1,62 @@ +name: Python CI + +on: + push: + pull_request: + +permissions: + contents: read + +concurrency: + group: python-ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + quality: + name: Lint, type-check, and test + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-python@v6 + with: + python-version: "3.12" + cache: pip + cache-dependency-path: Pipfile.lock + + - name: Run quality checks + run: ./lint.sh + + tests: + name: Test on Python ${{ matrix.python-version }} + runs-on: ubuntu-24.04 + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + python-version: + - "3.10" + - "3.11" + - "3.12" + - "3.13" + - "3.14" + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + cache: pip + cache-dependency-path: Pipfile.lock + + - name: Install locked dependencies + run: | + python -m pip install --upgrade pip + python -m pip install pipenv + pipenv requirements --dev > "${RUNNER_TEMP}/requirements-dev.txt" + python -m pip install -r "${RUNNER_TEMP}/requirements-dev.txt" + python -m pip install --no-build-isolation --no-deps -e . + + - name: Run tests + run: python -m pytest tests patterns diff --git a/.gitignore b/.gitignore index ac1ef8afb..4521242bf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,17 @@ __pycache__ *.pyc .idea +*.egg-info/ +.tox/ +env/ +venv/ +.env +.venv +.vscode/ +.python-version +.coverage +.project +.pydevproject +/.pytest_cache/ +build/ +dist/ diff --git a/.travis.yml b/.travis.yml index bff54dd22..ecd1fb556 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,26 +1,24 @@ -# vim ft=yaml -dist: xenial +os: linux +dist: noble language: python -sudo: false - python: - - "2.7" - - "3.6" - - "3.7" + - "3.10" + - "3.11" + - "3.12" + - "3.13" + - "3.14" cache: - pip install: - - pip install -r requirements-dev.txt + - pip install pipenv + - pipenv sync --dev --system + - pip install --no-build-isolation --no-deps -e . script: - - pytest --doctest-modules patterns/ - - pytest -s -vv --cov=. --log-level=INFO tests/ - # Actually run all the scripts, contributing to coverage - - PYTHONPATH=. ./run_all.sh - - flake8 patterns/ + - tox after_success: - codecov diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..5b48aaffb --- /dev/null +++ b/Makefile @@ -0,0 +1,76 @@ +# 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 lock + +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: lock +lock: checkvenv + @command -v pipenv >/dev/null || python -m pip install pipenv + @pipenv lock --dev + @pipenv sync --dev + + +.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/Pipfile b/Pipfile new file mode 100644 index 000000000..4896442e0 --- /dev/null +++ b/Pipfile @@ -0,0 +1,21 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[packages] + +[dev-packages] +black = ">=25.1.0" +build = ">=1.2.2" +codecov = "*" +codespell = "*" +flake8 = ">=7.1.0" +isort = ">=5.7.0" +mypy = "*" +pipx = ">=1.7.1" +pyupgrade = "*" +pytest = ">=6.2.0" +pytest-cov = ">=2.11.0" +pytest-randomly = ">=3.1.0" +tox = ">=4.25.0" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 000000000..7f02e674c --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,813 @@ +{ + "_meta": { + "hash": { + "sha256": "ad3364694cc5b9ce231a66480b4034c72a47f3761f604c9f16d8e9ae83f9977f" + }, + "pipfile-spec": 6, + "requires": {}, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": {}, + "develop": { + "argcomplete": { + "hashes": [ + "sha256:afde224f753f874807b1dc1414e883ab8fe0cda9c04807b6047dcb8e1ac23913", + "sha256:d8f0f22d2a8a7caa383be1e22b6caf1ecaf0ebd10d8f83cc125e36540c95830c" + ], + "markers": "python_version >= '3.8'", + "version": "==3.7.0" + }, + "ast-serialize": { + "hashes": [ + "sha256:0067b25fce104eaae5b88383de9ab803faeb671831e14ca698b771b356e2600f", + "sha256:085de7f62dc9cc247eb01e965a362707d1d90b1d89a82c5bf78301a60a3c417b", + "sha256:093cb8bb91b720d8523580498d031791bb1bbaa048599c3d21085d380e11a596", + "sha256:113b58346f9ceb664352032770caca817d4a3c86f611c6088e6ef65ddaa70f0e", + "sha256:13b7769970a39983b0adf2f38917b1cd3b8946f76df045756c3d741bc689f089", + "sha256:2d947e45cafc4b09bd7528917fa84c517654a43de173c79785574b7b3068ac24", + "sha256:305802f2ce2a7c4e87835078ea85c58b586ddda8095b92fe2ead9364ae19c80a", + "sha256:32ef62ec34cf6be20ad77d4799556638fbdf187f3ae10698dfb20ef9f2c89516", + "sha256:3ae22a366b752ab4496191525b78b097b5b72d531752e3c1dd7e383a8f2c8a1a", + "sha256:4d6ef91590258ada18909b9caea344dac4de2013906b035473cd674a43f4b790", + "sha256:4ed29121da8b3fdc291002801a1de0f76248fa07dce89157a5f277842cf6126e", + "sha256:5337cb256dcea3df9288205213d1601581536526b8f4da44b6974f1180f3252a", + "sha256:577a6c189068686869f5f1ddc38363f3ae1808a4753b577266f9202071a7bb66", + "sha256:6e15ec740436e1a0d62de848641abe5f3a2f89a7f94907d534795ac91bbacf14", + "sha256:6f7a408601bb3edaefb3bc67a4c01f5235e3253653b6a5729a2ee2382b35341c", + "sha256:82c312a7844d2fdeb4d5c48bd3d215bf940dafd4704e1a9bcf252a99010a99b1", + "sha256:8670bfa51208a2c0c8d138928e40e998fab158f9200d53bb80c088b5b8eda7b8", + "sha256:897ac47b5637be41c0c07061c8a912fafa967ef1dc73fa115e4bfa70882a093b", + "sha256:9f8a8b78b13173de6a9ec22111d9be674874cd5bdccda04f14ae5ebc2bef403a", + "sha256:a14191beec7e0c078d2fc1f6edc0aee88bcd4db9f18e1bc9f8052b559c22dddc", + "sha256:a4826809eb8597a8cd59fd924b6d7c285b8969a1e0007e2cb652cab62376270f", + "sha256:a7520b672827885bafeae7501f684d14d47d17e5f45256f9df547686cca52264", + "sha256:aadd3ffcf4858c9726bf3515f7b199c7eadbe504f96028e4a87172c0da65a8fe", + "sha256:b1dac4e09d341c1300ba69cdcbe62867b32a8c75d90db9bf4d083bec3b039f0b", + "sha256:c4af9a1386166e40ed01464991806f89038a2d89782576c7774876fa77034e32", + "sha256:c617417f9cbb0cb144f6283c3cbe0d2e0f01beaf9f608f662b21191058a626ec", + "sha256:c7b8b8f0c42f752ea00b2b7d7c090b3f80d9c1c5c75cadf16423790a0cc74081", + "sha256:c901adbd750029b9ac4ad3d6aa56853e0ad4875119fbf52b7b8298afc223828b", + "sha256:ccd132fe8db56f61fe743b1f644d01b8d65b83248a8da506f3132bda86d6ed5e", + "sha256:cd5b91b9e6f2356ace3a556963b0cd783b395fbbb0bb17b4defc283415466e77", + "sha256:cdc4e6f930b9090c2f92c9036ad12ffb8e6e44d4a5ba06f1458a05d60f203f7b", + "sha256:dcbed41e9386059fc0261d602445ede0976c2ecec2939688bcbcb9ed0b6f28b7", + "sha256:e61580a69faf47e3689795367ed211f2a10fd741478cc0f36a0f128793360aad", + "sha256:f2ff3baffc3a29c1f15bc9098aa0c09763410262d5e6cef42116f7356c184554" + ], + "markers": "python_version >= '3.7'", + "version": "==0.6.0" + }, + "black": { + "hashes": [ + "sha256:0e48b87e03bf109288e55cfceadcfa15ff5470aca2851a851950ed2926f450d7", + "sha256:1037d5ac7b7b310b2632ad867ec8d0e4c4819dcdb0b820f63135da746a24e418", + "sha256:1ef92b76f7733f282fd096ea406200b5a286c42947412b0eaff3a74e3616cefe", + "sha256:1f7ea64ebfa01b50f693508fc39f875e264446d3b097088f84f203b9d09618a0", + "sha256:22f2cd76d069cc54c71f10360744ba8983fbb616903b4304a85b734915c8e1b4", + "sha256:2b36cf2ddf5566e205f6535f782a62194a184d33e175b64ae8c40b1737522be3", + "sha256:30d3c14661f2792e9142cce3eeeb1cbc175b3eb5f733be0c8eeb99651e52b0c3", + "sha256:32d5ea7f6c8bdfa6e648326ebca1f02b0764e2a029edc6f8dce2627e19d468c3", + "sha256:3915f256e75a2d7cf88d8953d37f780455dc586cc72dee059c528fe77f581217", + "sha256:4ad6fa01f941920f54f2bbb35f3df7673428a0ef98a0b0840c2eaef3b110efa8", + "sha256:4ed7f7da04046d2e488437170797d3b4a4ad83906683bcb7dfc68b673bbce5e2", + "sha256:5119fa92ae61f786e8c3662fd60aece1d0a2dd5cca5d0c79417a95e7a4272a59", + "sha256:577f21094ea469ef92ec1adaf2c9441a226d2144d01a5be2fa823cecf6543e50", + "sha256:58b4bd92cf88aacf83d88479c8f9caee044b1ec55f2451a337354a7ea2590a22", + "sha256:5c34b25da232ead53a6f335b76dbea124f4d152ad568b9080d6f944bc2b34b52", + "sha256:87ed5c6f450580a2f6790bc7cbfb016dfc73bc750249762268a3695361315eef", + "sha256:89c93167a74d3a75dfaa38a5c7cca015537d5820dd7f17d63267d674a61cae90", + "sha256:96ae2c733b2aabdd9986e2c5df628ff3473676cd1c5faded1ff496cf6d74083c", + "sha256:9942db8888e06943c5dde66ca0037dcff82a2a4ec1ad0ada9e0d2ee9d9823893", + "sha256:9d98d4137277c75dfb898ec8d846c4fd68ba1e9cf77f95e2865c203dc18f4c3d", + "sha256:a1dca32d9f1784af512a13410ec204c6f7f0aa9797a111c42e1c03449821c264", + "sha256:dd321f668053961824bcc1be1cc1df748b2d7e4fa28086b08331e577b0100a73", + "sha256:e1a26503279b6b310669fb0b219c39e4820b77e8189fe80f522bb511f247db0a", + "sha256:e88976690a64b0af98312ca958415849cb42423423c5f2ee74af4b49a97a2168", + "sha256:ea8d16dc41655aa113cd64665e7219446cd7e4ff2248d7178eaa905190c86b18", + "sha256:ecb3e624844c798144e9bd986954e0adc81d8911a1f30f375e1252fe26e8c294", + "sha256:ed1a20af114c301a0269bf01163d51dbef72737fd65f850001e7cbe7f3c7abae" + ], + "index": "pypi", + "markers": "python_version >= '3.10'", + "version": "==26.5.1" + }, + "build": { + "hashes": [ + "sha256:13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f", + "sha256:302c22c3ba2a0fd5f3911918651341ebb3896176cbdec15bd421f80b1afc7647" + ], + "index": "pypi", + "markers": "python_version >= '3.10'", + "version": "==1.5.0" + }, + "cachetools": { + "hashes": [ + "sha256:2c12e255780330af28b91bb7fb96cce4c766f04e38396b9a24510190a5827096", + "sha256:c7a79e7f30ba9943c1cefd08cc36f006aaae086e017af9166f1d59d6170c47e1" + ], + "markers": "python_version >= '3.10'", + "version": "==7.1.6" + }, + "certifi": { + "hashes": [ + "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", + "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55" + ], + "markers": "python_version >= '3.7'", + "version": "==2026.7.22" + }, + "charset-normalizer": { + "hashes": [ + "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", + "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", + "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c", + "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", + "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5", + "sha256:0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833", + "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", + "sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99", + "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", + "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec", + "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", + "sha256:231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4", + "sha256:253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a", + "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3", + "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2", + "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a", + "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", + "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", + "sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419", + "sha256:3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84", + "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da", + "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", + "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", + "sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381", + "sha256:43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29", + "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", + "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", + "sha256:476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe", + "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29", + "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", + "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", + "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", + "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", + "sha256:58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94", + "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", + "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", + "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", + "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", + "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", + "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1", + "sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15", + "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", + "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993", + "sha256:68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4", + "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", + "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", + "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", + "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", + "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", + "sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d", + "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", + "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", + "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", + "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db", + "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", + "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", + "sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee", + "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", + "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", + "sha256:920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f", + "sha256:93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9", + "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", + "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", + "sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8", + "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", + "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", + "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", + "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", + "sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616", + "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d", + "sha256:ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba", + "sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2", + "sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b", + "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", + "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b", + "sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209", + "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", + "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", + "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", + "sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a", + "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", + "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", + "sha256:ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b", + "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", + "sha256:df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5", + "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", + "sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9", + "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", + "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", + "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", + "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", + "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", + "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115" + ], + "markers": "python_version >= '3.7'", + "version": "==3.4.9" + }, + "click": { + "hashes": [ + "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", + "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76" + ], + "markers": "python_version >= '3.10'", + "version": "==8.4.2" + }, + "codecov": { + "hashes": [ + "sha256:2362b685633caeaf45b9951a9b76ce359cd3581dd515b430c6c3f5dfb4d92a8c", + "sha256:7d2b16c1153d01579a89a94ff14f9dbeb63634ee79e18c11036f34e7de66cbc9", + "sha256:c2ca5e51bba9ebb43644c43d0690148a55086f7f5e6fd36170858fa4206744d5" + ], + "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.1.13" + }, + "codespell": { + "hashes": [ + "sha256:af2505b335e8573dbd2d384d1c4ef498f4006f4ba2d6fceca01e55b91f52628a", + "sha256:cbe085e331227b37bb86ef8bddd08dc768c704ee9a07ca869852c093fa2793e2" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==2.4.3" + }, + "colorama": { + "hashes": [ + "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", + "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", + "version": "==0.4.6" + }, + "coverage": { + "extras": [ + "toml" + ], + "hashes": [ + "sha256:075560438765b7a2ef43bf7aa7758661b53d889df47f062a31bda6c1ade553a2", + "sha256:0901cfe6c13bcd2302da4f83e884555d2a22bda6e4c476f09ef204ba20ca536e", + "sha256:094dd37f3ef7b2da8b068b583d1f4c40f91c65197e16c52a71962d5d537fc5db", + "sha256:09f5c6ec5901f667bd97dd140b5b9a2586b10efec66f46fb1e6d8135f8b95bdf", + "sha256:0e55510bc98ae943cece9e667a6c0fe94c6a92913720dea34243657a17993d0c", + "sha256:1121caa19159a38b5463eaae4b1e1fde81e525b15ecc5e000cd5b1a108f743a8", + "sha256:1268ac8fb9ddcd783d3948dbabaf80a5d53bfdaa0575e873e2139a692f797443", + "sha256:1473b3ba8e7ee0f076117b1a72c23f579a2b9e2bb742f48a8d86ea27ca93f91a", + "sha256:17c432b5f73ad52ef46fb06019f6fa7c66ce381961cf0f7dfd1d3a4bd3a98145", + "sha256:1adac78e5abc7c5438f7a209c9ca69d06542f0bf481d728b6989ea80b813fdf9", + "sha256:1cd7a5beb7af3e864a13b1f0fb26efd3695da43ef0daf71e586adfffaf34d5b2", + "sha256:1d16e3a7104ea84f03e614611b3edbf6fb6892554b3ab0fe7fbb3f2b2ef04376", + "sha256:25fd15dd40a0a2c51a500d664ca29053c09c3259d998407bf982b6e114696138", + "sha256:2617f8799d268fabdeef42a7e89ac3a23e1deee9025427db2df970f99a89a578", + "sha256:26c3b04a6377fd7c09800921fa934e3a17c0020439cd59df73e73ae1d4b6a78c", + "sha256:29c052f7c83ccfcc5c577eaae025d2e4a9bb80daf03c0ac31c996e83b000ce88", + "sha256:2f1ec6f304b156669cfde653b4e9a953f5de87e247ea02ac599bce0ab2744036", + "sha256:2fbeeeecea279727f8ac16c8e1133ddfeee793e985c86ae343d6a5ce744eef8c", + "sha256:2ff08701be2d1556fc78b326c80a3e8042da09352ecb3819105f8e386c8a3071", + "sha256:38c9518b7103826c403a461544e3c2e77151e8676d06eaed85911a97e962584a", + "sha256:3df60dc267f0a2ca23cb7a9ab1109c62b9335ffbf519fcfe167157c28c09b81d", + "sha256:3ed010aa1b69cda8e827aabfca9866216c980e2dca82ab9a78c5f83689964c8b", + "sha256:40f633c5c5fc783732f6312280122e859538fa24461235597c13d803ea9a108a", + "sha256:42ec3d989421b174a2ab607c1539f24127ad362757b7f1c0c0d7a2993f7eb37b", + "sha256:434e68d531858205895eb0d74b73d20b84260de426387d53c422a5acda2cf050", + "sha256:44826758cfe73fcd0e6af5deb4ba6d5417cc1d13df3acb35c93484a11160f846", + "sha256:4510fb9cdf6bb02dfa6af0be4a534b8102d086e22e4a33f8836df663da3d660d", + "sha256:48ccc6395958eda89093ecdc35644c86f23a8b23a7f4d44958812b721aad67c1", + "sha256:4d3361879d736f469f45723c11ea1a5bbdaf1f6928f0e632c940378b5aa9b660", + "sha256:582edc45c2040543fef83341be23c43024a3ab3ae0c2d8bc498a06282905ad40", + "sha256:63022c4c8dec1d0342f05c3ede99842fe3d007689acc45e86f123a1746e4a026", + "sha256:67d7602480a47bdf5b675635403625553ebaa70d5a62a657c035149fd401cea0", + "sha256:68af907f595ab01a78f794932ff3bdf929c316d3000810d38dbc247129e26f8b", + "sha256:6aa28cfb6488e5453b5b762d65f73aa586380f6693a04d58078ce228a29b06c0", + "sha256:6c0be82b4d4aa5b2704e08518e2252f3e3d110164bcca826816801052e48a7aa", + "sha256:6f6966fc30e6f06ca8f98fb0ce51eda6b111b3ee8d066a8b1ec9e77fa06ab55d", + "sha256:6fc448c377d6eeb00a47c673494bd9bae29280ca53987e1869e67ebedfe20658", + "sha256:728a33676d4c3f0db977990a4bd421dcaa3be3e53b5b6273036fff6666008e89", + "sha256:7466cc7ab6dc0db871d264bf99e8779f0917ee63d40730af0552f71535a6e072", + "sha256:77f091ea3a9cc611cd29f433565476bc1936c084ac8eee00ea0e7e70c27e4199", + "sha256:77f0ef5011df53a4bd1b35211ab122287f8d9b8d7aa1c4553e5c2deb24b1d446", + "sha256:7c63387e21ab21f512c69c9756a8c7dadd322c7275edb064064433c9a09c3743", + "sha256:7d29ca7bd67af6e12e74632d65f026eabc1364da5c254494cd914446a28a3ef7", + "sha256:7dc2950a2992cd676d35c20ae63522836deeb034f08874699d14068710af3dc1", + "sha256:7e8f27131dc7cd53de2c137dd207b3720919320b3c20d499dc30aa9ee6173287", + "sha256:81f382c5a94b434ec1f6da607edb904c76d7212e618cd4d1bc9f97bed4120ef5", + "sha256:835ec4e20b45f0a7f63ed78f94065aca00de033403df8377bfe8b9c6abc0a7be", + "sha256:8bb9f4b4279187560796a4cdaca3b0a93dd97e48ee667df005f4ed9a97403688", + "sha256:8c726b232659cbd2ae57ade46509eb068c9bd7a06df9fcbff6fe484870006934", + "sha256:913b6c56e110da40e035bbd168353bf7aaa2544a5eaccea5d98a4629aac156c7", + "sha256:97a5c5457a9fb1d6c4e06cfb5dc835871fbfb6a6a51addc9e925bdeff5ef7440", + "sha256:9854ca62c152874b2060772503535be2e8f53f70b8aaa7686b094888d872f984", + "sha256:9911f31aad8906abe337c271343485cf20df5e70df5d2f57f9f136e7b55f26bc", + "sha256:9b5bd92ff1ec22e535eab0de75fa6db021992791f461a2aceb7822c625a1187d", + "sha256:9deddf09eecb717b7f980414b43d90a5b22ff3967d2949ab29cb0aa83d9e9098", + "sha256:9e36686f7a442185db2400b3df171aac520869faf9deb59df687d28659eda2a6", + "sha256:9f4432898c4bf2fba0435bbe35dd4437d7264565e5a88a21f5b49d8662a6b629", + "sha256:a0f47002c6eeb7c280228467a4cb0cc15ca2103a8421b986b2d3ec04a0f9bd8b", + "sha256:a164b50081fc7357331c4024ef4d17b78ba325f8380d05f5a69599a7e05257ee", + "sha256:a29ec5305a7335aacee2d799e3422e91e1c8a12474986e2b3b07e315c91be82f", + "sha256:a300c6934e0989c327b9e8a1e110329da4641149f872bbe9f70168be66da76c1", + "sha256:a4c46b247b5d4b78f613bd89fea926d32b25c6cc61a50bd1e99ba310348f3dad", + "sha256:a638db90c61cd219aeee65e83a24fdaa57269a741ae0cf773309208ac862cee3", + "sha256:a63b9e190711134d581c4d703df5df09851b1acf99792c7aacbbe9f41f0283c9", + "sha256:aaccad4129d735a8a4d526f26929894c9a4e8ef7034566f210b176749d6906e3", + "sha256:ae901f7e55ba405c84ee1cab3d3e962e4e871e4a2bcb9c90911adbd69b42ac5a", + "sha256:afa29e2eff3d5729267e2cb2fd4ce9d61c952932fb2694e34ccb5d9540c6a296", + "sha256:affd532502d34c0472d0cdb181325c89f1d2c44992fef0c17e88e7b1576259a1", + "sha256:b171bdd71cb7ff792bf32e376173b0ace7e7963e7e57c58dfc42063a6a7174cd", + "sha256:b868acc62aa5de3be7a9d05c2333bf8359ca987e43f9cb30ff8fbda6a024ab73", + "sha256:b9a6367e4aff723e8ee8190836836124284e8fcd4265e307c844010cfa074f3f", + "sha256:bbc808daf4f5cd567af8075ecc72d21c6dfef9a254709a621a84c217c935ebc0", + "sha256:bbf44513ceb1589e31948e20eafbde9deaface90e1a1afa5f5f77b4423d17ce6", + "sha256:bcc0aae933921d03096f53b0b03eeb702129fd406dee59f08d2efacc68681fa5", + "sha256:bfd341ccf78128e72c094bc70cc25b3ef309c33c7c2c66ba3ed4309549e02de1", + "sha256:c6a98d698f9e2c8008d0370ec7fc452ebfcc530002ae2d0061170d768b992589", + "sha256:cb0fddaa6884be6aae36ced9544b5e90f7d5f03845a2853bf47a14953a4e8688", + "sha256:cee0f89f4767a6057c8fbf168f8135f18be651300496086bd873e3189fed0487", + "sha256:d17d7512151fedfcc64c1821a8977fc9be0dbf495754669afcab7b57abc98ae9", + "sha256:d46e62cb35d91e6e2589fda6d28074426b0e276422b5d2ebef2c6b11dc60dbfd", + "sha256:d50dd325e18ec25bfcc10cd7f99b04df1ab9ec76b0918c260e60817ad0643dee", + "sha256:db9c8438057e5b0f6a22a0af99c0c1d26b57fbbdbd1be5861ddb8f897fcc3a2d", + "sha256:dee88b1ed88587abd8c0269a1fc1f4cc77f7750d1dfde2869e2a123af420e67d", + "sha256:dfd3db045e95960ae3683059571e597fda7cc610106a8916f77c5839048c1deb", + "sha256:e26ff680768b8095e8874aabe0e9d3a47a2a9f176a8340d05f8604c56457c23a", + "sha256:e370c12133095ff18432de8c044962be85a5a96d90c6fcbce8e17e76236d2328", + "sha256:e38def96ad59853824c97953fdcd2c320a84ba3ce99b417db78af8bb6c3db635", + "sha256:e8f91bce78e32343af184c3b7fa28fcf5a9e2641f4b6623d392038f804939188", + "sha256:eb6bcae8d1a9d305351ecb108232441d11c5cfe9de840a04388ba5d2db8d735c", + "sha256:f653e5d7248c1191ec988a85c72edeab46c3ff44f90639a4ed4874ec0be90243", + "sha256:fe41909c9515c3bfdb5f02c4d1f857dba322d9a9a1178069b91eea77889df63a" + ], + "markers": "python_version >= '3.10'", + "version": "==7.15.2" + }, + "distlib": { + "hashes": [ + "sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b", + "sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed" + ], + "version": "==0.4.3" + }, + "filelock": { + "hashes": [ + "sha256:7be2ad23a14607ccc71808e68fe30848aeace7058ace17852f68e2a68e310402", + "sha256:d396bea984af47333ef05e50eae7eff88c84256de6112aea0ec48a233c064fe3" + ], + "markers": "python_version >= '3.10'", + "version": "==3.32.0" + }, + "flake8": { + "hashes": [ + "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", + "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==7.3.0" + }, + "idna": { + "hashes": [ + "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", + "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848" + ], + "markers": "python_version >= '3.9'", + "version": "==3.18" + }, + "iniconfig": { + "hashes": [ + "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", + "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12" + ], + "markers": "python_version >= '3.10'", + "version": "==2.3.0" + }, + "isort": { + "hashes": [ + "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d", + "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75" + ], + "index": "pypi", + "markers": "python_full_version >= '3.10.0'", + "version": "==8.0.1" + }, + "librt": { + "hashes": [ + "sha256:05d96b80b95d3a2721b619f8982b8558848b04875bb4772fd54842b59f61dd97", + "sha256:0763ca2ab66058174f9dee426dc64f5e0a89c24a7df8d3fe3f1836c04e25de4b", + "sha256:091b60a4d2174fc1ec5c34cdc0b72efb6224753d76b7da61ebeab7a191aec8bd", + "sha256:0b795f5fc70fbbb787ceaf79bb3a0d627bcc33c53de51741755263ec406b775a", + "sha256:0d38604854e8d22faadf683ec6c02bb0f886e2ba56ef981a1c36ee275f21ea22", + "sha256:109b84a9edf69ad89dc1f66358659e14a031baca95e3e5b0060bd903ede8efd6", + "sha256:1304368a3e7ffc3e9db986796cc5326fdb5943a3567ecc137cff318e4240c0e7", + "sha256:17221a7569f8f292aa0014226e48aa25b8c2b08da18088cd230953d0ea0f9cd1", + "sha256:1b5a7bbff495baedbd9b916c367d66854008f8f3b575908ded477c499dc60082", + "sha256:1ce61b3746545029d4f5c17d6bd74b676254ad98433086c846ffb5e8fa73f007", + "sha256:1d2a610c14ac0d0750ee0a3ab8548e83155258387891caaca04def4bf7289781", + "sha256:21b7ac084f701a9cdff6139745a6620579d65a9379ac2d9d50a86368b109e63c", + "sha256:22034924f5b42d5a56371cf271771bfeaabf235a7a8b6264bef2d20013f786c6", + "sha256:25218d94b1d2cbc0ba1d8a3f9dc9af578d9646e5ed16443a70cde1dfdcce6d71", + "sha256:2608d3b39f9e0b4a66a130d9150c615cba40a5090d25eeeaa225e0e46de8c0ac", + "sha256:260c33e92263fa629b4f6d3c51967a1c2158fe6c33237aaa3ebeac586b085259", + "sha256:2e56ea4ee4df77585a6b5c138f6538680886024fa559f5b55bd14b12e98e67b2", + "sha256:2f281549a4c52ac7bb97997f14353f8bd0e53a34ca0dad1c905cfd0b4a58ae99", + "sha256:301067672387902c55f94b51d5022304b36c966ea9fe1f21caab99a9bef487c9", + "sha256:30536798f4504c0fad0885b1d371b0539abb081e4570c9d7c641cb51141b49f0", + "sha256:32c26893cd085c1efe83219e78d866da23fb20a066101b8f68210004361d224c", + "sha256:34bc7938b9fdf14fe32a406c19c71faf894c5cee7e7474bd0be2f17200b82d14", + "sha256:34e47058fcc69a313293d6dee94216a4f30c929ae6f2476e58c5ba635aa639d5", + "sha256:3657346f867469e962549435aa05fd15330b1d6a92829f8e27988e194382d005", + "sha256:36b306a623aaad96fe4b378692b54f9c0789fccd833b9851753d5fbf6138cfde", + "sha256:371f7ce73026815dafd51c50ce38416e91428b28c4b2ec97cd39271164b0045c", + "sha256:375f5af8f99cbaa99dd293af986e3d57caabc9ba81a5d3f021603764854197a1", + "sha256:387e2f1d27e89bffe0d3f520f0da0662c973fd607ca16c1808f8a5085419485e", + "sha256:3aaedf52171bee90860704c560bc798fe83b76247df47568e0197e9b13c735a0", + "sha256:3dbb2a31882456cadc7053378e81ad7ed7693db4ac9f98ab5f81ef034aa8ec9f", + "sha256:4000d961ff9598ac6ea603c6c836a5ed49bc205ade5fc378b998dfe1e2c36628", + "sha256:40ccd13c252d3fe473ffc8a57be7565abc8b64cf1b108344c859d5164f7f3e0c", + "sha256:46c330e82565962c761dbce7941be2cff7db674ee807455a8d0cadc5f9b759b0", + "sha256:4f6db193d2e5e0ed60359b9a5a682cd67205d0d3b1e459a867dd4b5c4e7eaa7a", + "sha256:531b2df3e9fe96b1fcf73a6d165921e4656be5f58d631d384ebce344298368db", + "sha256:54dab44a847d5ad1acd05c8a83fe518ae685516ecf4d3f7cc6e3df2a66767650", + "sha256:5929da1981a46bcf4b28b1b9499905f0ff58e2419da402a048234e9783acbc4b", + "sha256:5f31b0aa13c9b04370d4da6be1ab7779776b3a075cceb6747a39a4be85fe1e40", + "sha256:5fdcf34f86de8fb66d7dc7589f96ba91c4aa46671200d400e6fd6f109a483f18", + "sha256:66c0e7e6b02a155576df2c77ec933a70b72da726e248c494abf690923e624348", + "sha256:66cb1138f384a191a6d75f986064841fcfdc0cea98f7bd9c9ab9b38049917588", + "sha256:68a5faee4bba381cb93b5961f684a514cf0053cb92308ff9c792c2fea0b174c6", + "sha256:6bf6a559ffe4a93bbea6cf31ddf01a7fd9ba342ef51f27beb178e318b74acd61", + "sha256:70d9c62a4cffd9f23396cd5ef93fc5d11b31596b9b7d6306074abe3d5fcf09bd", + "sha256:791aa18a373b90da8ac3c44fc77544f33fdf53ae403acdce9b39f1c26b4a3b94", + "sha256:79e44cff71750d299d61a678e49995b0d5935a9cda238c2574daeca3ba536927", + "sha256:7db9a3ff32ef5f7d1703d93831a3316cdf0b537de6a1cc03cc8fdd09b9194e89", + "sha256:860bd1d8ba48456ce08feaf8d343a8aaeb2fa086f2bcaa2a923fa3f7a3ff9aa3", + "sha256:9320d34c3376ae204b2cd176e8d4883a013934e0aef822f1aed9c536490c275d", + "sha256:93d24ebb82aa4420b1409c389e7857bc35bd0b668007ac8172427d5c73cc8cc5", + "sha256:94b85d664d777bab6c0d709416cb42938251fda9e221b79e3a2215d85df5f4f9", + "sha256:96bad8725a4f196a798366c25ce075d1f7543a4ec045ffc13e6a7ec095cdab04", + "sha256:9af313c66157a69dc69ea0059a66961692250e0dc95af9c385a48ffb770a0d16", + "sha256:9c5d02b89de5acd0379a51ec44a89476fb03df6145442e1c8ecd6bee2f91b176", + "sha256:9e786428f291dd2d2f1cbfc0e0caa45a2e395fab0ad3e2c9314daa8873414390", + "sha256:9f836c37478f167a81200d8c8b2c920a22224564bed2c23d7aeec760965c367a", + "sha256:9fd35e95ab5e45c3901d37110263c7db85a961110f5460588fe37f8c131f88a7", + "sha256:a001519c315d5db40710f2665d32c4791f1d4779fc96a9423fd18d92c8b9ac7b", + "sha256:a3762e75fcac8c9e4dacaaf438bffd9003e2ca2c531b756f3c0035deefa674c8", + "sha256:a38fb81d8376dfa2f8963b265fec07637802b0d01e2a127c19c66cb070fb24f5", + "sha256:a3dfe4edf10e8ed7e55b026a8bfc2c2a8704218b659cd4bffdf604fab966dc39", + "sha256:a4517d47b2b8af26975a406fba7d314de9696d864252e0257c6ea90238cfe27f", + "sha256:a468951af16155824e88bdd8326ebe5bdb371f3ec0ac04642994b98201d914f3", + "sha256:a6e556d6aba31c93dd97ce661d66614d2429c0a3923f9dc8f0af7e8df10223a4", + "sha256:ac04bcd3328eb91d99dfedf6a60d9c1f15d3434e6f6daf922f0420f7d90b85c7", + "sha256:ae01d8512cc17079e53425635327dbf3f7ff57a42c00dec348bf79791c56444c", + "sha256:b15e26cc0fe622d0c67e98bee6ef6bc8f792e20ee3006aa12627a00463d9399f", + "sha256:b222493da6e7b6199db9bd79502436cf5a27da3c1f7fa83c7e285444fc93fd03", + "sha256:c3cd253cf32fe4f4662960d6bf7d55cb8be0c31a5d644a4d48aeafebaff3409a", + "sha256:c6014e3c80f9c1fe268ef8b0e0ef113bac672cc032f2f93866e7ddad4f3e663d", + "sha256:c718e99a0992127af84385378460db624103b559ab260435abcfe77a4e4ed1c1", + "sha256:c7897db4e95e22468bdda33d8e012ceacd0182abf001e6389d763f0def6286b9", + "sha256:cb8a1adce42d8b75485a5d56a9623a50bcab995b6079f1dac59fc44034dd93d9", + "sha256:cc99dfb62b23c9207c33d0be8a2e2af7a42e21e6ea388b380a0c948c7b88953b", + "sha256:d4c8d9bd5abce34b2e75edb3bf37ab0f34e49b1f915a40ae8468eb7c85bc5b46", + "sha256:d4cb6fbfdf874340ab5e51450753c0f817b6958a3621125ee695bbc3de866566", + "sha256:d63bae12a8aeb51380be3438e4dc4bd27354d0f8e19166b2f44e3e94d6f552dc", + "sha256:d6fb0eaa108814581c4d3bfbd068c3fb6757812a81415008d1bae08267cca360", + "sha256:d9188caac26e47671b52836a5e2a49873a7fc11c673b0c122d22515f98bc14e1", + "sha256:db327e7271e653c32040b85ae6188059c924b57d7e1e29f935523fa017cd4e82", + "sha256:dbdd5b6509d0c2a8fe72cf494c299a61dbd58142a90a4190664ae159e4a7b547", + "sha256:e4f9b472e7d308d94b62c801982065661158c6ed02790d6c7ddb4337cea0f9c1", + "sha256:e54a315caf843c8d77e388cadc56ea9ded569935ee2d2347d7ea94992e5aa6fa", + "sha256:f125f5d46b20f89dc5587a55cc416b4ba2a5b2ffda36d048ee120e17598a653a", + "sha256:f19e181de5b3a1148bb3420b8c4b0b0ea0fce6950099724ad151d6cea5acc180", + "sha256:f1f9cc4d09a46d9cb3c2063ae100629d3f52a6517c3c08c2f4c9828261883929", + "sha256:f26629539d4893c2957a16c41bb058e1e135c1f150f6a2e25ed047f64cf3f5c6", + "sha256:f2a7253458e34f33543551394ae4fe104b497ec2a65ac266074de64c1df82e37", + "sha256:f40e56b61b41be5f7dec938cfeffd660668cf4b5e72c78e7bd671d66b7bc2c79", + "sha256:f442e3954b1addc759faae22a7c9a3f1e16d7d1db3f484279dc27d62e06968fa", + "sha256:fadc63331f4388c3dc90090448f682a7e9feafc11481391c1e94f2f907a3976e", + "sha256:fc67741da44c6eaa90e01eafb586bbba9b51eb5b6ed381ee6f5ae72eb3316d21" + ], + "markers": "python_version >= '3.9'", + "version": "==0.13.0" + }, + "mccabe": { + "hashes": [ + "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", + "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" + ], + "markers": "python_version >= '3.6'", + "version": "==0.7.0" + }, + "mypy": { + "hashes": [ + "sha256:04e617030eca5221909c8b7d8d7fd1c637948199aa2100b2ad9813feb07e1491", + "sha256:094af99f92638aa92852326188b85a89e50f4a472f44827c03362228482f0762", + "sha256:09abd66d8685e73f8f7d17b847c3e104d9a7b164a8706ea87d6c96a3d45816d5", + "sha256:0b025a93cffb9781d231f232be07a17912f35f10a313c24f301c81e842870654", + "sha256:13b1b16e2fa39f3b2e33fb1c468abc7a69369fa2e886b4b87b5afc81472325cd", + "sha256:1c6c6bf687b17f90dbfcad95b960d32eaa0154c00da45f03ab50bf8952e047fe", + "sha256:1fa8d916ac3b705af733c4c1e6c9ebe38fd0d52beb15b105c3e8355b55e6ecdc", + "sha256:28e1e2af8cd8fff551fd30f2fe4b03fb76764ac8b1ba6c6a1bd00ad32b412db3", + "sha256:2d53fc67b9d28a43c6199077f49fea0f05839e36cf6158500331c9549225e5a5", + "sha256:3419d00717afbc5265b50dd14b1278f29ea4884dd398ab67873489ac093fd329", + "sha256:37fa4de896a84e2dc9200d91e614c22563b43d1a266789d4bbac7b22ebe6192b", + "sha256:3961a4a34b05f7c74b0f05aa51fbfe99a2d1e126038df40318d15c8f558b7ef3", + "sha256:3dd0bed92c4bdec57c42505b96416fb9e6a5aa7be84d2809bcd5f2ecec2860d7", + "sha256:3e77244df3843048c3f927182916730e40c124cbaa43905c1fb86cb382aa0805", + "sha256:4359424140d985192c778c1ce2c114a10c1ca58a381ed79cfa70d37df94b299f", + "sha256:465965d41cd9a2726694e983e8ce7113259327bec798115d1e1dfa2a52fb666e", + "sha256:56c184d2c20ca6b6378d58d1960270a767f41f5e44acbbd27f05effef4f4e1d7", + "sha256:5e91adad1ca81742ac7ef9893959911df867752206b37135185e88dfb3c89494", + "sha256:691fdc37132b1ae628d834f672e74de83462d9fb4aff621835767fb43a8dd373", + "sha256:6b1cdb579446b60432432b2b2403a6201b4b475a004d7f488511c9ba177c9e88", + "sha256:6f99ec626e3c3a2f7c0b22c5b90ddb5dabb1c18729c971e9bdaca1f1766d2cee", + "sha256:7247eb2824f996722a949530183394921ca71deb9680052a338cf53cff7925c2", + "sha256:75b0984bb3cbd76bb5c9291a8671f7ae66ca3b51c7584c358fc2e923259f0757", + "sha256:75cbb4b9ef04a0c84a957f07abc4504fbf64b8dcc145675101f2d3a78a4b1d6a", + "sha256:7da939dd335cfd2ad788bdfd081c9f4e47634ab995e5a45eb15fd1e5bc052f8b", + "sha256:85c5385b93012ffa3b31479ab579aef5415f4f3a32c6cf1ae07a984d2a0ff461", + "sha256:91ad22a52ae2c7e621c2f67c94d5a17f66b3209a4cff5cf8a573579835c69e97", + "sha256:944c665d984157cb96a679dfb7a4a81dd1d36b24b9c284b699514e6e626b82d4", + "sha256:9559ab18a9c9957dfa3004ab57cd4bac5f26a724329a9584e583367f0c2e1117", + "sha256:982e3d53dd23d0a4cef67dd66791fdbede0cf38f9eb617bf47663554c51e1e36", + "sha256:99ac767cc5d3b64c8d0ae226ead10c96694f94e4e7da1668642225dcd4e75aac", + "sha256:adebc76aab4f3495a88b41d48aa4aff0c03f2822501da76625afcca5975f19e5", + "sha256:aec15d465d477558fd842757b487849007311cf3897849cdda0e3162ac0ac556", + "sha256:b1942b9314d4c784b8ea1dbab4972603290e5dd5630f06675f13aec97526bc4c", + "sha256:b352b7e49f5e6576009e8df730e1ff4f915cb565b851b396d2ffe2f5a6f5da88", + "sha256:b5cd2f027a972a4a5f2278a11fac9747f5f81a53a30b714d74950b6807e55568", + "sha256:be51653d7669d7d7955d613b8d0bb57d5b652eaf71a873ddf65ac87254dd2595", + "sha256:cfca8ee88544090f86b6dcce05ec55d66eb48a762412ac2507810ba4bd793b6f", + "sha256:d78fcf900b59cb7e82cb7e3a235e31b462d9333d92285bd1e4952d355b8ffba1", + "sha256:de121747278144fc9ae7caa2e978cf5df12aebc82933182f5b3b86081a30baef", + "sha256:de6d2c484742a4d7b0ed6d07b143375624d3b899c5749c7b3c947f56261f48a6", + "sha256:ea317b060ce83e26050f8f9e4d7d6bf44ed7597c8ff9990bccffbb9d1d8522db", + "sha256:f1b3a98dfd21058bc759bb3337d5d1f61d0fdf9f3cf9c00f4291790fb5427bff", + "sha256:f4ed18f111bfe2d599bca7468e7f9251042c1c2118f762c8de2766a56d773c60", + "sha256:fbc00cee7bdbb9291979ddc9d08034a29dfcda4932628c9bbc28c1edd589df0c" + ], + "index": "pypi", + "markers": "python_version >= '3.10'", + "version": "==2.3.0" + }, + "mypy-extensions": { + "hashes": [ + "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", + "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558" + ], + "markers": "python_version >= '3.8'", + "version": "==1.1.0" + }, + "packaging": { + "hashes": [ + "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", + "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661" + ], + "markers": "python_version >= '3.8'", + "version": "==26.2" + }, + "pathspec": { + "hashes": [ + "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", + "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189" + ], + "markers": "python_version >= '3.9'", + "version": "==1.1.1" + }, + "pipx": { + "hashes": [ + "sha256:4cba49e6f5ba7e894058f50fb12f0944868940e712bc68bcd17b405d2ef2beee", + "sha256:a6968fb9cf941535c601c08c331b0cd2a6db3b67bb59fbf5a9552cc1bafdab03" + ], + "index": "pypi", + "markers": "python_version >= '3.10'", + "version": "==1.16.2" + }, + "platformdirs": { + "hashes": [ + "sha256:0555d18370482847566ffabcaa53ad7c6c1c29f195989ae1ed634a05f76ea1e0", + "sha256:360ccded2b7fce0af0ff80cc8f5942a1c5d99b0e856033acb030bfc634709e74" + ], + "markers": "python_version >= '3.10'", + "version": "==4.11.0" + }, + "pluggy": { + "hashes": [ + "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", + "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746" + ], + "markers": "python_version >= '3.9'", + "version": "==1.6.0" + }, + "pycodestyle": { + "hashes": [ + "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", + "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d" + ], + "markers": "python_version >= '3.9'", + "version": "==2.14.0" + }, + "pyflakes": { + "hashes": [ + "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", + "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f" + ], + "markers": "python_version >= '3.9'", + "version": "==3.4.0" + }, + "pygments": { + "hashes": [ + "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", + "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176" + ], + "markers": "python_version >= '3.9'", + "version": "==2.20.0" + }, + "pyproject-api": { + "hashes": [ + "sha256:860060c8832dce983b5eec6f41c4c43eb3ec06ff7332387a63acdf5ca27b68d8", + "sha256:b8807d85a293e6c9f133e6575946fed45f1d42b22d58c780b33aa2421a799549" + ], + "markers": "python_version >= '3.10'", + "version": "==1.11.0" + }, + "pyproject-hooks": { + "hashes": [ + "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", + "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913" + ], + "markers": "python_version >= '3.7'", + "version": "==1.2.0" + }, + "pytest": { + "hashes": [ + "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", + "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c" + ], + "index": "pypi", + "markers": "python_version >= '3.10'", + "version": "==9.1.1" + }, + "pytest-cov": { + "hashes": [ + "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", + "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==7.1.0" + }, + "pytest-randomly": { + "hashes": [ + "sha256:47f1d9746c3bc3efabd53ae1ebfb8bb385cf3d4df4b505b6d58d9c97a3dfe70f", + "sha256:f55e89e53367b090c0c053697d7f9d77595543d0e0516c93978b50c0f6b252f9" + ], + "index": "pypi", + "markers": "python_version >= '3.10'", + "version": "==4.1.0" + }, + "python-discovery": { + "hashes": [ + "sha256:3e014c6327154d3dda27939a9a0dc9c5c000439f1906d3f303b48f984bd2ecef", + "sha256:70c4fc61b4e7404e44f01d6fc44a715c4d685ca6cea83d295922f05891877c98" + ], + "markers": "python_version >= '3.8'", + "version": "==1.5.0" + }, + "pytokens": { + "hashes": [ + "sha256:0fc71786e629cef478cbf29d7ea1923299181d0699dbe7c3c0f4a583811d9fc1", + "sha256:11edda0942da80ff58c4408407616a310adecae1ddd22eef8c692fe266fa5009", + "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", + "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", + "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", + "sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2", + "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", + "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", + "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5", + "sha256:30f51edd9bb7f85c748979384165601d028b84f7bd13fe14d3e065304093916a", + "sha256:34bcc734bd2f2d5fe3b34e7b3c0116bfb2397f2d9666139988e7a3eb5f7400e3", + "sha256:3ad72b851e781478366288743198101e5eb34a414f1d5627cdd585ca3b25f1db", + "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", + "sha256:42f144f3aafa5d92bad964d471a581651e28b24434d184871bd02e3a0d956037", + "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", + "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc", + "sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7", + "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", + "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", + "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", + "sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c", + "sha256:682fa37ff4d8e95f7df6fe6fe6a431e8ed8e788023c6bcc0f0880a12eab80ad1", + "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", + "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", + "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", + "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", + "sha256:941d4343bf27b605e9213b26bfa1c4bf197c9c599a9627eb7305b0defcfe40c1", + "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", + "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", + "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", + "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", + "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe", + "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", + "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d", + "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", + "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440", + "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16", + "sha256:da5baeaf7116dced9c6bb76dc31ba04a2dc3695f3d9f74741d7910122b456edc", + "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", + "sha256:dcafc12c30dbaf1e2af0490978352e0c4041a7cde31f4f81435c2a5e8b9cabb6", + "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6", + "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324" + ], + "markers": "python_version >= '3.8'", + "version": "==0.4.1" + }, + "pyupgrade": { + "hashes": [ + "sha256:1a361bea39deda78d1460f65d9dd548d3a36ff8171d2482298539b9dc11c9c06", + "sha256:2ac7b95cbd176475041e4dfe8ef81298bd4654a244f957167bd68af37d52be9f" + ], + "index": "pypi", + "markers": "python_version >= '3.10'", + "version": "==3.21.2" + }, + "requests": { + "hashes": [ + "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", + "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed" + ], + "markers": "python_version >= '3.10'", + "version": "==2.34.2" + }, + "tokenize-rt": { + "hashes": [ + "sha256:8439c042b330c553fdbe1758e4a05c0ed460dbbbb24a606f11f0dee75da4cad6", + "sha256:a152bf4f249c847a66497a4a95f63376ed68ac6abf092a2f7cfb29d044ecff44" + ], + "markers": "python_version >= '3.9'", + "version": "==6.2.0" + }, + "tomli-w": { + "hashes": [ + "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", + "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021" + ], + "markers": "python_version >= '3.9'", + "version": "==1.2.0" + }, + "tox": { + "hashes": [ + "sha256:ab0b126a04dd56bc18e6d216386db09335247f2289b54cf534deb5c4ae3a8d2e", + "sha256:dcae21f5f015f3a67658e35644cce0d1aa0dedcd06f3927f95d84e1717f6cea5" + ], + "index": "pypi", + "markers": "python_version >= '3.10'", + "version": "==4.58.0" + }, + "typing-extensions": { + "hashes": [ + "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", + "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5" + ], + "markers": "python_version >= '3.9'", + "version": "==4.16.0" + }, + "urllib3": { + "hashes": [ + "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", + "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897" + ], + "markers": "python_version >= '3.10'", + "version": "==2.7.0" + }, + "userpath": { + "hashes": [ + "sha256:2cbf01a23d655a1ff8fc166dfb78da1b641d1ceabf0fe5f970767d380b14e89d", + "sha256:6c52288dab069257cc831846d15d48133522455d4677ee69a9781f11dbefd815" + ], + "markers": "python_version >= '3.7'", + "version": "==1.9.2" + }, + "virtualenv": { + "hashes": [ + "sha256:7f9519b9432ff11b6e1a3e94061664efc2ff99ea21780e3cf4f6bd0a5da8b37c", + "sha256:a8370c1c5530fbabf955e40b8fbbc68a431648b10f9433faa587db30a06e51dd" + ], + "markers": "python_version >= '3.9'", + "version": "==21.7.0" + } + } +} diff --git a/README.md b/README.md index ebc2b6690..7624f55f6 100644 --- a/README.md +++ b/README.md @@ -1,120 +1,184 @@ -python-patterns -=============== +# python-patterns A collection of design patterns and idioms in Python. -Current Patterns ----------------- +Remember that each pattern has its own trade-offs. And you need to pay attention more to why you're choosing a certain pattern than to how to implement it. -__Creational Patterns__: +## Creational Patterns + +> Patterns that deal with **object creation** — abstracting and controlling how instances are made. + +```mermaid +graph LR + Client -->|requests object| AbstractFactory + AbstractFactory -->|delegates to| ConcreteFactory + ConcreteFactory -->|produces| Product + + Builder -->|step-by-step| Director + Director -->|returns| BuiltObject + + FactoryMethod -->|subclass decides| ConcreteProduct + Pool -->|reuses| PooledInstance +``` | Pattern | Description | |:-------:| ----------- | | [abstract_factory](patterns/creational/abstract_factory.py) | use a generic function with specific factories | | [borg](patterns/creational/borg.py) | a singleton with shared-state among instances | | [builder](patterns/creational/builder.py) | instead of using multiple constructors, builder object receives parameters and returns constructed objects | -| [factory_method](patterns/creational/factory_method.py) | delegate a specialized function/method to create instances | +| [factory](patterns/creational/factory.py) | delegate a specialized function/method to create instances | | [lazy_evaluation](patterns/creational/lazy_evaluation.py) | lazily-evaluated property pattern in Python | | [pool](patterns/creational/pool.py) | preinstantiate and maintain a group of instances of the same type | | [prototype](patterns/creational/prototype.py) | use a factory and clones of a prototype for new instances (if instantiation is expensive) | -__Structural Patterns__: - -| Pattern | Description | -|:-------:| ----------- | -| [3-tier](patterns/structural/3-tier.py) | data<->business logic<->presentation separation (strict relationships) | -| [adapter](patterns/structural/adapter.py) | adapt one interface to another using a white-list | -| [bridge](patterns/structural/bridge.py) | a client-provider middleman to soften interface changes | -| [composite](patterns/structural/composite.py) | lets clients treat individual objects and compositions uniformly | -| [decorator](patterns/structural/decorator.py) | wrap functionality with other functionality in order to affect outputs | -| [facade](patterns/structural/facade.py) | use one class as an API to a number of others | -| [flyweight](patterns/structural/flyweight.py) | transparently reuse existing instances of objects with similar/identical state | -| [front_controller](patterns/structural/front_controller.py) | single handler requests coming to the application | -| [mvc](patterns/structural/mvc.py) | model<->view<->controller (non-strict relationships) | -| [proxy](patterns/structural/proxy.py) | an object funnels operations to something else | - -__Behavioral Patterns__: - -| Pattern | Description | -|:-------:| ----------- | -| [chain_of_responsibility](patterns/behavioral/chain_of_responsibility.py) | apply a chain of successive handlers to try and process the data | -| [catalog](patterns/behavioral/catalog.py) | general methods will call different specialized methods based on construction parameter | -| [chaining_method](patterns/behavioral/chaining_method.py) | continue callback next object method | -| [command](patterns/behavioral/command.py) | bundle a command and arguments to call later | -| [iterator](patterns/behavioral/iterator.py) | traverse a container and access the container's elements | -| [mediator](patterns/behavioral/mediator.py) | an object that knows how to connect other objects and act as a proxy | -| [memento](patterns/behavioral/memento.py) | generate an opaque token that can be used to go back to a previous state | -| [observer](patterns/behavioral/observer.py) | provide a callback for notification of events/changes to data | -| [publish_subscribe](patterns/behavioral/publish_subscribe.py) | a source syndicates events/data to 0+ registered listeners | -| [registry](patterns/behavioral/registry.py) | keep track of all subclasses of a given class | -| [specification](patterns/behavioral/specification.py) | business rules can be recombined by chaining the business rules together using boolean logic | -| [state](patterns/behavioral/state.py) | logic is organized into a discrete number of potential states and the next state that can be transitioned to | -| [strategy](patterns/behavioral/strategy.py) | selectable operations over the same data | -| [template](patterns/behavioral/template.py) | an object imposes a structure but takes pluggable components | -| [visitor](patterns/behavioral/visitor.py) | invoke a callback for all items of a collection | - -__Design for Testability Patterns__: - -| Pattern | Description | -|:-------:| ----------- | -| [setter_injection](patterns/dft/setter_injection.py) | the client provides the depended-on object to the SUT via the setter injection (implementation variant of dependency injection) | - -__Fundamental Patterns__: - -| Pattern | Description | -|:-------:| ----------- | +## Structural Patterns + +> Patterns that define **how classes and objects are composed** to form larger, flexible structures. + +```mermaid +graph TD + Client --> Facade + Facade --> SubsystemA + Facade --> SubsystemB + Facade --> SubsystemC + + Client2 --> Adapter + Adapter --> LegacyService + + Client3 --> Proxy + Proxy -->|controls access to| RealSubject + + Component --> Composite + Composite --> Leaf1 + Composite --> Leaf2 +``` + +| Pattern | Description | +|:-----------------------------------------------------------:|--------------------------------------------------------------------------------| +| [3-tier](patterns/structural/3-tier.py) | data<->business logic<->presentation separation (strict relationships) | +| [adapter](patterns/structural/adapter.py) | adapt one interface to another using a white-list | +| [bridge](patterns/structural/bridge.py) | a client-provider middleman to soften interface changes | +| [composite](patterns/structural/composite.py) | lets clients treat individual objects and compositions uniformly | +| [decorator](patterns/structural/decorator.py) | wrap functionality with other functionality in order to affect outputs | +| [facade](patterns/structural/facade.py) | use one class as an API to a number of others | +| [flyweight](patterns/structural/flyweight.py) | transparently reuse existing instances of objects with similar/identical state | +| [front_controller](patterns/structural/front_controller.py) | single handler requests coming to the application | +| [mvc](patterns/structural/mvc.py) | model<->view<->controller (non-strict relationships) | +| [proxy](patterns/structural/proxy.py) | an object funnels operations to something else | + +## Behavioral Patterns + +> Patterns concerned with **communication and responsibility** between objects. + +```mermaid +graph LR + Sender -->|sends event| Observer1 + Sender -->|sends event| Observer2 + + Request --> Handler1 + Handler1 -->|passes if unhandled| Handler2 + Handler2 -->|passes if unhandled| Handler3 + + Context -->|delegates to| Strategy + Strategy -->|executes| Algorithm + + Originator -->|saves state to| Memento + Caretaker -->|holds| Memento +``` + +| Pattern | Description | +|:-------------------------------------------------------------------------:|--------------------------------------------------------------------------------------------------------------| +| [chain_of_responsibility](patterns/behavioral/chain_of_responsibility.py) | apply a chain of successive handlers to try and process the data | +| [catalog](patterns/behavioral/catalog.py) | general methods will call different specialized methods based on construction parameter | +| [chaining_method](patterns/behavioral/chaining_method.py) | continue callback next object method | +| [command](patterns/behavioral/command.py) | bundle a command and arguments to call later | +| [interpreter](patterns/behavioral/interpreter.py) | define a grammar for a language and use it to interpret statements | +| [iterator](patterns/behavioral/iterator.py) | traverse a container and access the container's elements | +| [iterator](patterns/behavioral/iterator_alt.py) (alt. impl.) | traverse a container and access the container's elements | +| [mediator](patterns/behavioral/mediator.py) | an object that knows how to connect other objects and act as a proxy | +| [memento](patterns/behavioral/memento.py) | generate an opaque token that can be used to go back to a previous state | +| [observer](patterns/behavioral/observer.py) | provide a callback for notification of events/changes to data | +| [publish_subscribe](patterns/behavioral/publish_subscribe.py) | a source syndicates events/data to 0+ registered listeners | +| [registry](patterns/behavioral/registry.py) | keep track of all subclasses of a given class | +| [servant](patterns/behavioral/servant.py) | provide common functionality to a group of classes without using inheritance | +| [specification](patterns/behavioral/specification.py) | business rules can be recombined by chaining the business rules together using boolean logic | +| [state](patterns/behavioral/state.py) | logic is organized into a discrete number of potential states and the next state that can be transitioned to | +| [strategy](patterns/behavioral/strategy.py) | selectable operations over the same data | +| [template](patterns/behavioral/template.py) | an object imposes a structure but takes pluggable components | +| [visitor](patterns/behavioral/visitor.py) | invoke a callback for all items of a collection | + +## Design for Testability Patterns + +| Pattern | Description | +|:--------------------------------------------------------:|------------------------------------| +| [dependency_injection](patterns/dependency_injection.py) | 3 variants of dependency injection | + +## Fundamental Patterns + +| Pattern | Description | +|:----------------------------------------------------------------:|-----------------------------------------------------------------------------| | [delegation_pattern](patterns/fundamental/delegation_pattern.py) | an object handles a request by delegating to a second object (the delegate) | -__Others__: +## Others -| Pattern | Description | -|:-------:| ----------- | -| [blackboard](patterns/other/blackboard.py) | architectural model, assemble different sub-system knowledge to build a solution, AI approach - non gang of four pattern | -| [graph_search](patterns/other/graph_search.py) | graphing algorithms - non gang of four pattern | -| [hsm](patterns/other/hsm/hsm.py) | hierarchical state machine - non gang of four pattern | +| Pattern | Description | +|:----------------------------------------------:|--------------------------------------------------------------------------------------------------------------------------| +| [blackboard](patterns/other/blackboard.py) | architectural model, assemble different sub-system knowledge to build a solution, AI approach - non gang of four pattern | +| [graph_search](patterns/other/graph_search.py) | graphing algorithms - non gang of four pattern | +| [hsm](patterns/other/hsm/hsm.py) | hierarchical state machine - non gang of four pattern | +## 🚫 Anti-Patterns -Videos ------- -[Design Patterns in Python by Peter Ullrich](https://www.youtube.com/watch?v=bsyjSW46TDg) +This section lists some common design patterns that are **not recommended** in Python and explains why. -[Sebastian Buczyński - Why you don't need design patterns in Python?](https://www.youtube.com/watch?v=G5OeYHCJuv0) +### 🧱 Singleton +**Why not:** +- Python modules are already singletons — every module is imported only once. +- Explicit singleton classes add unnecessary complexity. +- Better alternatives: use module-level variables or dependency injection. -[You Don't Need That!](https://www.youtube.com/watch?v=imW-trt0i9I) +### 🌀 God Object +**Why not:** +- Centralizes too much logic in a single class. +- Makes code harder to test and maintain. +- Better alternative: split functionality into smaller, cohesive classes. -[Pluggable Libs Through Design Patterns](https://www.youtube.com/watch?v=PfgEU3W0kyU) +### 🔁 Inheritance overuse +**Why not:** +- Deep inheritance trees make code brittle. +- Prefer composition and delegation. +- “Favor composition over inheritance.” +## Videos -Contributing ------------- -When an implementation is added or modified, please review the following guidelines: +* [Design Patterns in Python by Peter Ullrich](https://www.youtube.com/watch?v=bsyjSW46TDg) +* [Sebastian Buczyński - Why you don't need design patterns in Python?](https://www.youtube.com/watch?v=G5OeYHCJuv0) +* [You Don't Need That!](https://www.youtube.com/watch?v=imW-trt0i9I) +* [Pluggable Libs Through Design Patterns](https://www.youtube.com/watch?v=PfgEU3W0kyU) -##### Output -All files with example patterns have `### OUTPUT ###` section at the bottom -(migration to OUTPUT = """...""" is in progress). +## Contributing -Run `append_output.sh` (e.g. `./append_output.sh borg.py`) to generate/update it. +When an implementation is added or modified, please review the following guidelines: ##### Docstrings -Add module level description in form of a docstring with links to corresponding references or other useful information. - +Add module level description in form of a docstring with links to corresponding references or other useful information. Add "Examples in Python ecosystem" section if you know some. It shows how patterns could be applied to real-world problems. +[facade.py](patterns/structural/facade.py) has a good example of detailed description, but sometimes the shorter one as in [template.py](patterns/behavioral/template.py) would suffice. -[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. - - -##### Python2/3 compatibility -Try to keep it (discussion is held in [issue #208](https://github.com/faif/python-patterns/issues/208)) -- use new style classes (inherit from `object`) -- use `from __future__ import print_function` +##### 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 `flake8` and `pytest` commands locally to be sure your changes will pass CI . +Please run the following before submitting a patch: +- If you have a bash-compatible shell, use `./lint.sh` to run formatting, + linting, type checking, and tests. + +Development dependencies are pinned in `Pipfile.lock`. After changing +`Pipfile`, activate a Python 3.10 virtual environment and run `make lock` to +regenerate it. + +## Contributing via issue triage [![Open Source Helpers](https://www.codetriage.com/faif/python-patterns/badges/users.svg)](https://www.codetriage.com/faif/python-patterns) +You can triage issues and pull requests on [CodeTriage](https://www.codetriage.com/faif/python-patterns). diff --git a/append_output.sh b/append_output.sh deleted file mode 100755 index 8e576d0f4..000000000 --- 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=$(python "$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 000000000..98306ea93 --- /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 536a52d79..e109555b4 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/config_backup/tox.ini b/config_backup/tox.ini new file mode 100644 index 000000000..f0d8b6c1f --- /dev/null +++ b/config_backup/tox.ini @@ -0,0 +1,30 @@ +[tox] +envlist = py,cov-report +skip_missing_interpreters = true +usedevelop = true + +[testenv] +setenv = + COVERAGE_FILE = .coverage.{envname} +deps = + pipenv +commands_pre = + pipenv sync --dev --system +allowlist_externals = + pytest + flake8 + mypy +commands = + flake8 --exclude="venv/,.tox/" patterns/ + ; `randomly-seed` option from `pytest-randomly` helps with deterministic outputs for examples like `other/blackboard.py` + pytest --randomly-seed=1234 --doctest-modules patterns/ + pytest -s -vv --cov=patterns/ --log-level=INFO tests/ + + +[testenv:cov-report] +setenv = + COVERAGE_FILE = .coverage +deps = coverage +commands = + coverage combine + coverage report diff --git a/lint.sh b/lint.sh new file mode 100755 index 000000000..3139587ad --- /dev/null +++ b/lint.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +set -euo pipefail + +project_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +venv_dir="${project_dir}/.venv" +python_bin="${PYTHON:-python3}" + +if [[ ! -x "${venv_dir}/bin/python" ]]; then + "${python_bin}" -m venv "${venv_dir}" + "${venv_dir}/bin/python" -m pip install --upgrade pip +fi + +venv_python="${venv_dir}/bin/python" +venv_bin="${venv_dir}/bin" + +"${venv_python}" -m pip install pipenv +PIPENV_IGNORE_VIRTUALENVS=1 \ + PIPENV_VENV_IN_PROJECT=1 \ + "${venv_bin}/pipenv" sync --dev +"${venv_python}" -m pip install --no-build-isolation --no-deps -e "${project_dir}" + +source_dir="${project_dir}/patterns" +tests_dir="${project_dir}/tests" + +"${venv_bin}/codespell" --quiet-level=2 "${source_dir}" "${tests_dir}" "${project_dir}/README.md" +"${venv_bin}/flake8" \ + "${source_dir}" \ + "${tests_dir}" \ + --max-line-length=120 \ + --extend-ignore=E266,E731,W503 \ + --count \ + --show-source \ + --statistics +"${venv_bin}/isort" --profile black --check-only "${source_dir}" "${tests_dir}" +"${venv_bin}/black" --check "${source_dir}" "${tests_dir}" +"${venv_bin}/mypy" "${source_dir}" +"${venv_bin}/pytest" "${tests_dir}" "${source_dir}" diff --git a/patterns/behavioral/catalog.py b/patterns/behavioral/catalog.py index 476b0eca1..4074c1d21 100644 --- a/patterns/behavioral/catalog.py +++ b/patterns/behavioral/catalog.py @@ -1,177 +1,173 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - """ -A class that uses different static function depending of a parameter passed in -init. Note the use of a single dictionary instead of multiple conditions +A class that uses different static functions depending on a parameter passed +during initialization. Uses a single dictionary instead of multiple conditions. """ __author__ = "Ibrahim Diop " -class Catalog(object): - """catalog of multiple static methods that are executed depending on an init - - parameter - """ - - def __init__(self, param): +class Catalog: + """catalog of multiple static methods that are executed depending on an init parameter""" + def __init__(self, param: str) -> None: # dictionary that will be used to determine which static method is # to be executed but that will be also used to store possible param # value - self._static_method_choices = {'param_value_1': self._static_method_1, 'param_value_2': self._static_method_2} + 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(): - print("executed method 1!") + def _static_method_1() -> str: + return "executed method 1!" @staticmethod - def _static_method_2(): - print("executed method 2!") + def _static_method_2() -> str: + return "executed method 2!" - def main_method(self): + def main_method(self) -> str: """will execute either _static_method_1 or _static_method_2 depending on self.param value """ - self._static_method_choices[self.param]() + return self._static_method_choices[self.param]() # Alternative implementation for different levels of methods -class CatalogInstance(object): - +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) -> str: + return f"Value {self.x1}" - def _instance_method_2(self): - print("Value {}".format(self.x2)) + def _instance_method_2(self) -> str: + return f"Value {self.x2}" - _instance_method_choices = {'param_value_1': _instance_method_1, 'param_value_2': _instance_method_2} + _instance_method_choices = { + "param_value_1": _instance_method_1, + "param_value_2": _instance_method_2, + } - def main_method(self): + def main_method(self) -> str: """will execute either _instance_method_1 or _instance_method_2 depending on self.param value """ - self._instance_method_choices[self.param].__get__(self)() - + return self._instance_method_choices[self.param].__get__(self)() # type: ignore + # type ignore reason: https://github.com/python/mypy/issues/10206 -class CatalogClass(object): +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) -> str: + return f"Value {cls.x1}" @classmethod - def _class_method_2(cls): - print("Value {}".format(cls.x2)) + def _class_method_2(cls) -> str: + return f"Value {cls.x2}" - _class_method_choices = {'param_value_1': _class_method_1, 'param_value_2': _class_method_2} + _class_method_choices = { + "param_value_1": _class_method_1, + "param_value_2": _class_method_2, + } - def main_method(self): + def main_method(self) -> str: """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__)() - + return self._class_method_choices[self.param].__get__(None, self.__class__)() # type: ignore + # type ignore reason: https://github.com/python/mypy/issues/10206 -class CatalogStatic(object): +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(): - print("executed method 1!") + def _static_method_1() -> str: + return "executed method 1!" @staticmethod - def _static_method_2(): - print("executed method 2!") + def _static_method_2() -> str: + return "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) -> str: """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__)() + + return self._static_method_choices[self.param].__get__(None, self.__class__)() # type: ignore + # type ignore reason: https://github.com/python/mypy/issues/10206 def main(): """ - >>> c = Catalog('param_value_1').main_method() - executed method 1! - >>> Catalog('param_value_2').main_method() - executed method 2! - """ + >>> test = Catalog('param_value_2') + >>> test.main_method() + 'executed method 2!' - test = Catalog('param_value_2') - test.main_method() + >>> test = CatalogInstance('param_value_1') + >>> test.main_method() + 'Value x1' - test = CatalogInstance('param_value_1') - test.main_method() + >>> test = CatalogClass('param_value_2') + >>> test.main_method() + 'Value x2' - test = CatalogClass('param_value_2') - test.main_method() - - test = CatalogStatic('param_value_1') - test.main_method() + >>> test = CatalogStatic('param_value_1') + >>> test.main_method() + 'executed method 1!' + """ if __name__ == "__main__": - main() + import doctest - -OUTPUT = """ -executed method 2! -Value x1 -Value x2 -executed method 1! -""" + doctest.testmod() diff --git a/patterns/behavioral/chain_of_responsibility.py b/patterns/behavioral/chain_of_responsibility.py index 8e374d5ec..a44a9210e 100644 --- a/patterns/behavioral/chain_of_responsibility.py +++ b/patterns/behavioral/chain_of_responsibility.py @@ -1,6 +1,3 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - """ *What is this pattern about? @@ -17,20 +14,23 @@ As a variation some receivers may be capable of sending requests out in several directions, forming a `tree of responsibility`. -*TL;DR80 +*Examples in Python ecosystem: +Django Middleware: https://docs.djangoproject.com/en/stable/topics/http/middleware/ +The middleware components act as a chain where each processes the request/response. + +*TL;DR Allow a request to pass down a chain of receivers until it is handled. """ -import abc +from abc import ABC, abstractmethod +from typing import Optional -class Handler(object): - __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. @@ -42,8 +42,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) -> bool | None: """Compare passed value to predefined interval""" @@ -53,10 +53,11 @@ class ConcreteHandler0(Handler): """ @staticmethod - def check_range(request): + def check_range(request: int) -> bool | None: 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): @@ -64,57 +65,59 @@ class ConcreteHandler1(Handler): start, end = 10, 20 - def check_range(self, request): + def check_range(self, request: int) -> bool | None: 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) -> bool | None: 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) -> bool | None: + print(f"end of chain, no handler for {request}") return False def main(): - h0 = ConcreteHandler0() - h1 = ConcreteHandler1() - h2 = ConcreteHandler2(FallbackHandler()) - h0.successor = h1 - h1.successor = h2 - - requests = [2, 5, 14, 22, 18, 3, 35, 27, 20] - for request in requests: - h0.handle(request) + """ + >>> h0 = ConcreteHandler0() + >>> h1 = ConcreteHandler1() + >>> h2 = ConcreteHandler2(FallbackHandler()) + >>> h0.successor = h1 + >>> h1.successor = h2 + + >>> requests = [2, 5, 14, 22, 18, 3, 35, 27, 20] + >>> for request in requests: + ... h0.handle(request) + request 2 handled in handler 0 + request 5 handled in handler 0 + request 14 handled in handler 1 + request 22 handled in handler 2 + request 18 handled in handler 1 + request 3 handled in handler 0 + end of chain, no handler for 35 + request 27 handled in handler 2 + request 20 handled in handler 2 + """ if __name__ == "__main__": - main() - - -OUTPUT = """ -request 2 handled in handler 0 -request 5 handled in handler 0 -request 14 handled in handler 1 -request 22 handled in handler 2 -request 18 handled in handler 1 -request 3 handled in handler 0 -end of chain, no handler for 35 -request 27 handled in handler 2 -request 20 handled in handler 2 -""" + import doctest + + doctest.testmod(optionflags=doctest.ELLIPSIS) diff --git a/patterns/behavioral/chaining_method.py b/patterns/behavioral/chaining_method.py index 3a2f397d8..26f110181 100644 --- a/patterns/behavioral/chaining_method.py +++ b/patterns/behavioral/chaining_method.py @@ -1,41 +1,37 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- +from __future__ import annotations -from __future__ import print_function - -class Person(object): - def __init__(self, name, action): +class Person: + 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(object): - def __init__(self, name): +class Action: + 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() - + """ + >>> move = Action('move') + >>> person = Person('Jack') + >>> person.do_action(move).amount('5m').stop() + Jack move 5m then stop + """ -if __name__ == '__main__': - main() +if __name__ == "__main__": + import doctest -OUTPUT = """ -Jack move 5m then stop -""" + doctest.testmod() diff --git a/patterns/behavioral/command.py b/patterns/behavioral/command.py index 74fcfc9c6..d0c3944e4 100644 --- a/patterns/behavioral/command.py +++ b/patterns/behavioral/command.py @@ -1,69 +1,105 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - """ -*TL;DR80 -Encapsulates all information needed to perform an action or trigger an event. +Command pattern decouples the object invoking a job from the one who knows +how to do it. As mentioned in the GoF book, a good example is in menu items. +You have a menu that has lots of items. Each item is responsible for doing a +special thing and you want your menu item just call the execute method when +it is pressed. To achieve this you implement a command object with the execute +method for each menu item and pass to it. + +*About the example +We have a menu containing two items. Each item accepts a file name, one hides the file +and the other deletes it. Both items have an undo option. +Each item is a MenuItem class that accepts the corresponding command as input and executes +it's execute method when it is pressed. + +*TL;DR +Object oriented implementation of callback functions. *Examples in Python ecosystem: -Django HttpRequest (without `execute` method): - https://docs.djangoproject.com/en/2.1/ref/request-response/#httprequest-objects +Django HttpRequest (without execute method): +https://docs.djangoproject.com/en/2.1/ref/request-response/#httprequest-objects """ -from __future__ import print_function -import os -from os.path import lexists +class HideFileCommand: + """ + A command to hide a file given its name + """ + + def __init__(self) -> None: + # an array of files hidden, to undo them as needed + self._hidden_files: list[str] = [] + + def execute(self, filename: str) -> None: + print(f"hiding {filename}") + self._hidden_files.append(filename) + + def undo(self) -> None: + filename = self._hidden_files.pop() + print(f"un-hiding {filename}") + + +class DeleteFileCommand: + """ + A command to delete a file given its name + """ + + def __init__(self) -> None: + # an array of deleted files, to undo them as needed + self._deleted_files: list[str] = [] -class MoveFileCommand(object): - def __init__(self, src, dest): - self.src = src - self.dest = dest + def execute(self, filename: str) -> None: + print(f"deleting {filename}") + self._deleted_files.append(filename) - def execute(self): - self.rename(self.src, self.dest) + def undo(self) -> None: + filename = self._deleted_files.pop() + print(f"restoring {filename}") - def undo(self): - self.rename(self.dest, self.src) - def rename(self, src, dest): - print(u"renaming %s to %s" % (src, dest)) - os.rename(src, dest) +class MenuItem: + """ + The invoker class. Here it is items in a menu. + """ + + def __init__(self, command: HideFileCommand | DeleteFileCommand) -> None: + self._command = command + + def on_do_press(self, filename: str) -> None: + self._command.execute(filename) + + def on_undo_press(self) -> None: + self._command.undo() def main(): - command_stack = [] + """ + >>> item1 = MenuItem(DeleteFileCommand()) - # commands are just pushed into the command stack - command_stack.append(MoveFileCommand('foo.txt', 'bar.txt')) - command_stack.append(MoveFileCommand('bar.txt', 'baz.txt')) + >>> item2 = MenuItem(HideFileCommand()) - # verify that none of the target files exist - assert not lexists("foo.txt") - assert not lexists("bar.txt") - assert not lexists("baz.txt") - try: - with open("foo.txt", "w"): # Creating the file - pass + # create a file named `test-file` to work with + >>> test_file_name = 'test-file' - # they can be executed later on - for cmd in command_stack: - cmd.execute() + # deleting `test-file` + >>> item1.on_do_press(test_file_name) + deleting test-file - # and can also be undone at will - for cmd in reversed(command_stack): - cmd.undo() - finally: - os.unlink("foo.txt") + # restoring `test-file` + >>> item1.on_undo_press() + restoring test-file + # hiding `test-file` + >>> item2.on_do_press(test_file_name) + hiding test-file -if __name__ == "__main__": - main() + # un-hiding `test-file` + >>> item2.on_undo_press() + un-hiding test-file + """ -OUTPUT = """ -renaming foo.txt to bar.txt -renaming bar.txt to baz.txt -renaming baz.txt to bar.txt -renaming bar.txt to foo.txt -""" +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/behavioral/iterator.py b/patterns/behavioral/iterator.py index 167979ec7..3ed4043b9 100644 --- a/patterns/behavioral/iterator.py +++ b/patterns/behavioral/iterator.py @@ -1,46 +1,47 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - """ http://ginstrom.com/scribbles/2007/10/08/design-patterns-python-style/ Implementation of the iterator pattern with a generator -*TL;DR80 +*TL;DR Traverses a container and accesses the container's elements. """ -from __future__ import print_function - -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 main(): - print('Counting to two...') - for number in count_to_two(): - print(number, end=' ') +def count_to_five() -> None: + return count_to(5) - print('\nCounting to five...') - for number in count_to_five(): - print(number, end=' ') +def main(): + """ + # Counting to two... + >>> for number in count_to_two(): + ... print(number) + one + two + + # Counting to five... + >>> for number in count_to_five(): + ... print(number) + one + two + three + four + five + """ -if __name__ == "__main__": - main() +if __name__ == "__main__": + import doctest -OUTPUT = """ -Counting to two... -one two -Counting to five... -one two three four five -""" # noqa + doctest.testmod() diff --git a/patterns/behavioral/iterator_alt.py b/patterns/behavioral/iterator_alt.py new file mode 100644 index 000000000..a2a71d823 --- /dev/null +++ b/patterns/behavioral/iterator_alt.py @@ -0,0 +1,62 @@ +""" +Implementation of the iterator pattern using the iterator protocol from Python + +*TL;DR +Traverses a container and accesses the container's elements. +""" + +from __future__ import annotations + + +class NumberWords: + """Counts by word numbers, up to a maximum of five""" + + _WORD_MAP = ( + "one", + "two", + "three", + "four", + "five", + ) + + def __init__(self, start: int, stop: int) -> None: + self.start = start + self.stop = stop + + def __iter__(self) -> NumberWords: # this makes the class an Iterable + return self + + def __next__(self) -> str: # this makes the class an Iterator + if self.start > self.stop or self.start > len(self._WORD_MAP): + raise StopIteration + current = self.start + self.start += 1 + return self._WORD_MAP[current - 1] + + +# Test the iterator + + +def main(): + """ + # Counting to two... + >>> for number in NumberWords(start=1, stop=2): + ... print(number) + one + two + + # Counting to five... + >>> for number in NumberWords(start=1, stop=5): + ... print(number) + one + two + three + four + five + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/behavioral/mediator.py b/patterns/behavioral/mediator.py index 9aee4a180..122852b77 100644 --- a/patterns/behavioral/mediator.py +++ b/patterns/behavioral/mediator.py @@ -1,54 +1,53 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - """ https://www.djangospin.com/design-patterns-python/mediator/ Objects in a system communicate through a Mediator instead of directly with each other. This reduces the dependencies between communicating objects, thereby reducing coupling. -*TL;DR80 +*TL;DR Encapsulates how a set of objects interact. """ +from __future__ import annotations + -class ChatRoom(object): +class ChatRoom: """Mediator class""" - def display_message(self, user, message): - print("[{} says]: {}".format(user, message)) + def display_message(self, user: User, message: str) -> str: + return f"[{user} says]: {message}" -class User(object): +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): - self.chat_room.display_message(self, message) + def say(self, message: str) -> str: + return self.chat_room.display_message(self, message) - def __str__(self): + def __str__(self) -> str: return self.name def main(): - molly = User('Molly') - mark = User('Mark') - ethan = User('Ethan') - - molly.say("Hi Team! Meeting at 3 PM today.") - mark.say("Roger that!") - ethan.say("Alright.") + """ + >>> molly = User('Molly') + >>> mark = User('Mark') + >>> ethan = User('Ethan') + >>> molly.say("Hi Team! Meeting at 3 PM today.") + '[Molly says]: Hi Team! Meeting at 3 PM today.' + >>> mark.say("Roger that!") + '[Mark says]: Roger that!' + >>> ethan.say("Alright.") + '[Ethan says]: Alright.' + """ -if __name__ == '__main__': - main() +if __name__ == "__main__": + import doctest -OUTPUT = """ -[Molly says]: Hi Team! Meeting at 3 PM today. -[Mark says]: Roger that! -[Ethan says]: Alright. -""" # noqa + doctest.testmod() diff --git a/patterns/behavioral/memento.py b/patterns/behavioral/memento.py index 9ad6198b4..a14ea8a0d 100644 --- a/patterns/behavioral/memento.py +++ b/patterns/behavioral/memento.py @@ -1,59 +1,64 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - """ http://code.activestate.com/recipes/413838-memento-closure/ -*TL;DR80 +*TL;DR Provides the ability to restore an object to its previous state. """ -from copy import copy -from copy import deepcopy +from collections.abc import Callable +from copy import copy, deepcopy +from typing import Any -def memento(obj, deep=False): +def memento(obj: Any, deep: bool = False) -> Callable: state = deepcopy(obj.__dict__) if deep else copy(obj.__dict__) - def restore(): + def restore() -> None: obj.__dict__.clear() obj.__dict__.update(state) return restore -class Transaction(object): +class Transaction: """A transaction guard. This is, in fact, just syntactic sugar around a memento closure. """ deep = False - states = [] + states: list[Callable[[], None]] = [] - def __init__(self, deep, *targets): + def __init__(self, deep: bool, *targets: Any) -> None: self.deep = deep self.targets = targets self.commit() - def commit(self): + def commit(self) -> None: self.states = [memento(target, self.deep) for target in self.targets] - def rollback(self): + def rollback(self) -> None: for a_state in self.states: a_state() -class Transactional(object): +class Transactional: """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): + def __init__(self, method: Callable) -> None: self.method = method - def __get__(self, obj, T): + def __get__(self, obj: Any, T: type) -> Callable: + """ + A decorator that makes a function transactional. + + :param method: The function to be decorated. + """ + def transaction(*args, **kwargs): state = memento(obj) try: @@ -65,19 +70,19 @@ def transaction(*args, **kwargs): return transaction -class NumObj(object): - def __init__(self, value): +class NumObj: + def __init__(self, value: int) -> None: self.value = value - def __repr__(self): - return '<%s: %r>' % (self.__class__.__name__, self.value) + def __repr__(self) -> str: + return f"<{self.__class__.__name__}: {self.value!r}>" - def increment(self): + def increment(self) -> None: self.value += 1 @Transactional - def do_stuff(self): - self.value = '1111' # <- invalid value + def do_stuff(self) -> None: + self.__dict__["value"] = "1111" # <- intentionally invalid value self.increment() # <- will fail and rollback @@ -137,4 +142,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 b93c0c4e9..9ea97bfc0 100644 --- a/patterns/behavioral/observer.py +++ b/patterns/behavioral/observer.py @@ -1,115 +1,135 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - """ http://code.activestate.com/recipes/131499-observer-pattern/ -*TL;DR80 +*TL;DR Maintains a list of dependents and notifies them of any state changes. *Examples in Python ecosystem: -Django Signals: https://docs.djangoproject.com/en/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 print_function +# observer.py + +from __future__ import annotations + + +class Observer: + def update(self, subject: Subject) -> None: + """ + Receive update from the subject. + + Args: + subject (Subject): The subject instance sending the update. + """ + pass -class Subject(object): - def __init__(self): +class Subject: + _observers: list[Observer] + + def __init__(self) -> None: + """ + Initialize the subject with an empty observer list. + """ self._observers = [] - def attach(self, observer): + def attach(self, observer: Observer) -> None: + """ + Attach an observer to the subject. + + Args: + observer (Observer): The observer instance to attach. + """ if observer not in self._observers: self._observers.append(observer) - def detach(self, observer): + def detach(self, observer: Observer) -> None: + """ + Detach an observer from the subject. + + Args: + observer (Observer): The observer instance to detach. + """ try: self._observers.remove(observer) except ValueError: pass - def notify(self, modifier=None): + def notify(self) -> None: + """ + Notify all attached observers by calling their update method. + """ for observer in self._observers: - if modifier != observer: - observer.update(self) + observer.update(self) -# Example usage 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(u'HexViewer: Subject %s has data 0x%x' % (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(u'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}") -# Example usage... def main(): - data1 = Data('Data 1') - data2 = Data('Data 2') - view1 = DecimalViewer() - view2 = HexViewer() - data1.attach(view1) - data1.attach(view2) - data2.attach(view2) - data2.attach(view1) - - print(u"Setting Data 1 = 10") - data1.data = 10 - print(u"Setting Data 2 = 15") - data2.data = 15 - print(u"Setting Data 1 = 3") - data1.data = 3 - print(u"Setting Data 2 = 5") - data2.data = 5 - print(u"Detach HexViewer from data1 and data2.") - data1.detach(view2) - data2.detach(view2) - print(u"Setting Data 1 = 10") - data1.data = 10 - print(u"Setting Data 2 = 15") - data2.data = 15 - - -if __name__ == '__main__': - main() - - -OUTPUT = """ -Setting Data 1 = 10 -DecimalViewer: Subject Data 1 has data 10 -HexViewer: Subject Data 1 has data 0xa -Setting Data 2 = 15 -HexViewer: Subject Data 2 has data 0xf -DecimalViewer: Subject Data 2 has data 15 -Setting Data 1 = 3 -DecimalViewer: Subject Data 1 has data 3 -HexViewer: Subject Data 1 has data 0x3 -Setting Data 2 = 5 -HexViewer: Subject Data 2 has data 0x5 -DecimalViewer: Subject Data 2 has data 5 -Detach HexViewer from data1 and data2. -Setting Data 1 = 10 -DecimalViewer: Subject Data 1 has data 10 -Setting Data 2 = 15 -DecimalViewer: Subject Data 2 has data 15 -""" + """ + >>> data1 = Data('Data 1') + >>> data2 = Data('Data 2') + >>> view1 = DecimalViewer() + >>> view2 = HexViewer() + >>> data1.attach(view1) + >>> data1.attach(view2) + >>> data2.attach(view2) + >>> data2.attach(view1) + + >>> data1.data = 10 + DecimalViewer: Subject Data 1 has data 10 + HexViewer: Subject Data 1 has data 0xa + + >>> data2.data = 15 + HexViewer: Subject Data 2 has data 0xf + DecimalViewer: Subject Data 2 has data 15 + + >>> data1.data = 3 + DecimalViewer: Subject Data 1 has data 3 + HexViewer: Subject Data 1 has data 0x3 + + >>> data2.data = 5 + HexViewer: Subject Data 2 has data 0x5 + DecimalViewer: Subject Data 2 has data 5 + + # Detach HexViewer from data1 and data2 + >>> data1.detach(view2) + >>> data2.detach(view2) + + >>> data1.data = 10 + DecimalViewer: Subject Data 1 has data 10 + + >>> data2.data = 15 + DecimalViewer: Subject Data 2 has data 15 + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/behavioral/publish_subscribe.py b/patterns/behavioral/publish_subscribe.py index 131181e08..91e74ab67 100644 --- a/patterns/behavioral/publish_subscribe.py +++ b/patterns/behavioral/publish_subscribe.py @@ -1,27 +1,27 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- """ Reference: http://www.slideshare.net/ishraqabd/publish-subscribe-model-overview-13368808 Author: https://github.com/HanWenfang """ +from __future__ import annotations + class Provider: - def __init__(self): - self.msg_queue = [] - self.subscribers = {} + def __init__(self) -> None: + self.msg_queue: list[str] = [] + self.subscribers: dict[str, list[Subscriber]] = {} - 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) @@ -29,64 +29,67 @@ 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(): - message_center = Provider() - - fftv = Publisher(message_center) - - jim = Subscriber("jim", message_center) - jim.subscribe("cartoon") - jack = Subscriber("jack", message_center) - jack.subscribe("music") - gee = Subscriber("gee", message_center) - gee.subscribe("movie") - vani = Subscriber("vani", message_center) - vani.subscribe("movie") - vani.unsubscribe("movie") - - fftv.publish("cartoon") - fftv.publish("music") - fftv.publish("ads") - fftv.publish("movie") - fftv.publish("cartoon") - fftv.publish("cartoon") - fftv.publish("movie") - fftv.publish("blank") - - message_center.update() + """ + >>> message_center = Provider() + + >>> fftv = Publisher(message_center) + + >>> jim = Subscriber("jim", message_center) + >>> jim.subscribe("cartoon") + >>> jack = Subscriber("jack", message_center) + >>> jack.subscribe("music") + >>> gee = Subscriber("gee", message_center) + >>> gee.subscribe("movie") + >>> vani = Subscriber("vani", message_center) + >>> vani.subscribe("movie") + >>> vani.unsubscribe("movie") + + # Note that no one subscribed to `ads` + # and that vani changed their mind + + >>> fftv.publish("cartoon") + >>> fftv.publish("music") + >>> fftv.publish("ads") + >>> fftv.publish("movie") + >>> fftv.publish("cartoon") + >>> fftv.publish("cartoon") + >>> fftv.publish("movie") + >>> fftv.publish("blank") + + >>> message_center.update() + jim got cartoon + jack got music + gee got movie + jim got cartoon + jim got cartoon + gee got movie + """ if __name__ == "__main__": - main() + import doctest - -OUTPUT = """ -jim got cartoon -jack got music -gee got movie -jim got cartoon -jim got cartoon -gee got movie -""" + doctest.testmod() diff --git a/patterns/behavioral/registry.py b/patterns/behavioral/registry.py index 9c31b834d..1288c1f6a 100644 --- a/patterns/behavioral/registry.py +++ b/patterns/behavioral/registry.py @@ -1,10 +1,5 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - - class RegistryHolder(type): - - REGISTRY = {} + REGISTRY: dict[str, "RegistryHolder"] = {} def __new__(cls, name, bases, attrs): new_cls = type.__new__(cls, name, bases, attrs) @@ -20,32 +15,31 @@ def get_registry(cls): return dict(cls.REGISTRY) -class BaseRegisteredClass(object): - __metaclass__ = RegistryHolder +class BaseRegisteredClass(metaclass=RegistryHolder): + """ + Any class that will inherits from BaseRegisteredClass will be included + inside the dict RegistryHolder.REGISTRY, the key being the name of the + class and the associated value, the class itself. + """ + + +def main(): """ - Any class that will inherits from BaseRegisteredClass will be included - inside the dict RegistryHolder.REGISTRY, the key being the name of the - class and the associated value, the class itself. + Before subclassing + >>> sorted(RegistryHolder.REGISTRY) + ['BaseRegisteredClass'] + + >>> class ClassRegistree(BaseRegisteredClass): + ... def __init__(self, *args, **kwargs): + ... pass + + After subclassing + >>> sorted(RegistryHolder.REGISTRY) + ['BaseRegisteredClass', 'ClassRegistree'] """ - pass if __name__ == "__main__": - print("Before subclassing: ") - for k in RegistryHolder.REGISTRY: - print(k) - - class ClassRegistree(BaseRegisteredClass): - def __init__(self, *args, **kwargs): - pass - - print("After subclassing: ") - for k in RegistryHolder.REGISTRY: - print(k) - -### OUTPUT ### -# Before subclassing: -# BaseRegisteredClass -# After subclassing: -# BaseRegisteredClass -# ClassRegistree + import doctest + + doctest.testmod(optionflags=doctest.ELLIPSIS) diff --git a/patterns/behavioral/servant.py b/patterns/behavioral/servant.py new file mode 100644 index 000000000..776c4126f --- /dev/null +++ b/patterns/behavioral/servant.py @@ -0,0 +1,131 @@ +""" +Implementation of the Servant design pattern. + +The Servant design pattern is a behavioral pattern used to offer functionality +to a group of classes without requiring them to inherit from a base class. + +This pattern involves creating a Servant class that provides certain services +or functionalities. These services are used by other classes which do not need +to be related through a common parent class. It is particularly useful in +scenarios where adding the desired functionality through inheritance is impractical +or would lead to a rigid class hierarchy. + +This pattern is characterized by the following: + +- A Servant class that provides specific services or actions. +- Client classes that need these services, but do not derive from the Servant class. +- The use of the Servant class by the client classes to perform actions on their behalf. + +References: +- https://en.wikipedia.org/wiki/Servant_(design_pattern) +""" + +import math + + +class Position: + """Representation of a 2D position with x and y coordinates.""" + + def __init__(self, x, y): + self.x = x + self.y = y + + +class Circle: + """Representation of a circle defined by a radius and a position.""" + + def __init__(self, radius, position: Position): + self.radius = radius + self.position = position + + +class Rectangle: + """Representation of a rectangle defined by width, height, and a position.""" + + def __init__(self, width, height, position: Position): + self.width = width + self.height = height + self.position = position + + +class GeometryTools: + """ + Servant class providing geometry-related services, including area and + perimeter calculations and position updates. + """ + + @staticmethod + def calculate_area(shape): + """ + Calculate the area of a given shape. + + Args: + shape: The geometric shape whose area is to be calculated. + + Returns: + The area of the shape. + + Raises: + ValueError: If the shape type is unsupported. + """ + if isinstance(shape, Circle): + return math.pi * shape.radius**2 + elif isinstance(shape, Rectangle): + return shape.width * shape.height + else: + raise ValueError("Unsupported shape type") + + @staticmethod + def calculate_perimeter(shape): + """ + Calculate the perimeter of a given shape. + + Args: + shape: The geometric shape whose perimeter is to be calculated. + + Returns: + The perimeter of the shape. + + Raises: + ValueError: If the shape type is unsupported. + """ + if isinstance(shape, Circle): + return 2 * math.pi * shape.radius + elif isinstance(shape, Rectangle): + return 2 * (shape.width + shape.height) + else: + raise ValueError("Unsupported shape type") + + @staticmethod + def move_to(shape, new_position: Position): + """ + Move a given shape to a new position. + + Args: + shape: The geometric shape to be moved. + new_position: The new position to move the shape to. + """ + shape.position = new_position + print(f"Moved to ({shape.position.x}, {shape.position.y})") + + +def main(): + """ + >>> servant = GeometryTools() + >>> circle = Circle(5, Position(0, 0)) + >>> rectangle = Rectangle(3, 4, Position(0, 0)) + >>> servant.calculate_area(circle) + 78.53981633974483 + >>> servant.calculate_perimeter(rectangle) + 14 + >>> servant.move_to(circle, Position(3, 4)) + Moved to (3, 4) + >>> servant.move_to(rectangle, Position(5, 6)) + Moved to (5, 6) + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/behavioral/specification.py b/patterns/behavioral/specification.py index 5c77370e2..10d226894 100644 --- a/patterns/behavioral/specification.py +++ b/patterns/behavioral/specification.py @@ -1,17 +1,15 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - """ @author: Gordeev Andrey -*TL;DR80 +*TL;DR Provides recombination business logic by chaining together using boolean logic. """ from abc import abstractmethod +from typing import Union -class Specification(object): +class Specification: def and_specification(self, candidate): raise NotImplementedError() @@ -31,85 +29,82 @@ class CompositeSpecification(Specification): def is_satisfied_by(self, candidate): pass - def and_specification(self, candidate): + def and_specification(self, candidate: "Specification") -> "AndSpecification": return AndSpecification(self, candidate) - def or_specification(self, candidate): + def or_specification(self, candidate: "Specification") -> "OrSpecification": return OrSpecification(self, candidate) - def not_specification(self): + def not_specification(self) -> "NotSpecification": return NotSpecification(self) class AndSpecification(CompositeSpecification): - _one = Specification() - _other = Specification() + def __init__(self, one: "Specification", other: "Specification") -> None: + self._one: Specification = one + self._other: Specification = other - def __init__(self, one, other): - self._one = one - self._other = other - - def is_satisfied_by(self, candidate): - return bool(self._one.is_satisfied_by(candidate) and self._other.is_satisfied_by(candidate)) + def is_satisfied_by(self, candidate: Union["User", str]) -> bool: + return bool( + self._one.is_satisfied_by(candidate) + and self._other.is_satisfied_by(candidate) + ) class OrSpecification(CompositeSpecification): - _one = Specification() - _other = Specification() - - def __init__(self, one, other): - self._one = one - self._other = other + def __init__(self, one: "Specification", other: "Specification") -> None: + 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)) + def is_satisfied_by(self, candidate: Union["User", str]): + return bool( + self._one.is_satisfied_by(candidate) + or self._other.is_satisfied_by(candidate) + ) class NotSpecification(CompositeSpecification): - _wrapped = Specification() - - def __init__(self, wrapped): - self._wrapped = wrapped + def __init__(self, wrapped: "Specification"): + self._wrapped: Specification = wrapped - def is_satisfied_by(self, candidate): + def is_satisfied_by(self, candidate: Union["User", str]): return bool(not self._wrapped.is_satisfied_by(candidate)) -class User(object): - def __init__(self, super_user=False): +class User: + def __init__(self, super_user: bool = False) -> None: self.super_user = super_user class UserSpecification(CompositeSpecification): - def is_satisfied_by(self, candidate): + def is_satisfied_by(self, candidate: Union["User", str]) -> bool: return isinstance(candidate, User) class SuperUserSpecification(CompositeSpecification): - def is_satisfied_by(self, candidate): - return getattr(candidate, 'super_user', False) + def is_satisfied_by(self, candidate: "User") -> bool: + return getattr(candidate, "super_user", False) def main(): - print('Specification') - andrey = User() - ivan = User(super_user=True) - vasiliy = 'not User instance' - - root_specification = UserSpecification().and_specification(SuperUserSpecification()) + """ + >>> andrey = User() + >>> ivan = User(super_user=True) + >>> vasiliy = 'not User instance' - print(root_specification.is_satisfied_by(andrey)) - print(root_specification.is_satisfied_by(ivan)) - print(root_specification.is_satisfied_by(vasiliy)) + >>> root_specification = UserSpecification().and_specification(SuperUserSpecification()) + # Is specification satisfied by + >>> root_specification.is_satisfied_by(andrey), 'andrey' + (False, 'andrey') + >>> root_specification.is_satisfied_by(ivan), 'ivan' + (True, 'ivan') + >>> root_specification.is_satisfied_by(vasiliy), 'vasiliy' + (False, 'vasiliy') + """ -if __name__ == '__main__': - main() +if __name__ == "__main__": + import doctest -OUTPUT = """ -Specification -False -True -False -""" + doctest.testmod() diff --git a/patterns/behavioral/state.py b/patterns/behavioral/state.py index 46508c3a9..8d792de93 100644 --- a/patterns/behavioral/state.py +++ b/patterns/behavioral/state.py @@ -1,95 +1,96 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - """ Implementation of the state pattern http://ginstrom.com/scribbles/2007/10/08/design-patterns-python-style/ -*TL;DR80 +*TL;DR Implements state as a derived class of the state pattern interface. Implements state transitions by invoking methods from the pattern's superclass. """ -from __future__ import print_function - +from __future__ import annotations -class State(object): +class State: """Base state. This is to share functionality""" - def scan(self): + name: str + pos: int + stations: list[str] + + def toggle_amfm(self) -> None: + raise NotImplementedError + + def scan(self) -> None: """Scan the dial to the next station""" self.pos += 1 if self.pos == len(self.stations): self.pos = 0 - print(u"Scanning... Station is %s %s" % (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): - print(u"Switching to FM") + 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): - print(u"Switching to AM") + def toggle_amfm(self) -> None: + print("Switching to AM") self.radio.state = self.radio.amstate -class Radio(object): - +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 + self.state: 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() -# Test our radio out def main(): - radio = Radio() - actions = [radio.scan] * 2 + [radio.toggle_amfm] + [radio.scan] * 2 - actions *= 2 - - for action in actions: - action() - - -if __name__ == '__main__': - main() - - -OUTPUT = """ -Scanning... Station is 1380 AM -Scanning... Station is 1510 AM -Switching to FM -Scanning... Station is 89.1 FM -Scanning... Station is 103.9 FM -Scanning... Station is 81.3 FM -Scanning... Station is 89.1 FM -Switching to AM -Scanning... Station is 1250 AM -Scanning... Station is 1380 AM -""" + """ + >>> radio = Radio() + >>> actions = [radio.scan] * 2 + [radio.toggle_amfm] + [radio.scan] * 2 + >>> actions *= 2 + + >>> for action in actions: + ... action() + Scanning... Station is 1380 AM + Scanning... Station is 1510 AM + Switching to FM + Scanning... Station is 89.1 FM + Scanning... Station is 103.9 FM + Scanning... Station is 81.3 FM + Scanning... Station is 89.1 FM + Switching to AM + Scanning... Station is 1250 AM + Scanning... Station is 1380 AM + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/behavioral/strategy.py b/patterns/behavioral/strategy.py index 94a5d50eb..13b1c0f5d 100644 --- a/patterns/behavioral/strategy.py +++ b/patterns/behavioral/strategy.py @@ -1,56 +1,99 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - """ *What is this pattern about? Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it. -*TL;DR80 +*TL;DR Enables selecting an algorithm at runtime. """ +from __future__ import annotations + +from collections.abc import Callable +from typing import TypeAlias + +DiscountStrategy: TypeAlias = Callable[["Order"], float] + + +class DiscountStrategyValidator: # Descriptor class for check perform + @staticmethod + def validate(obj: Order, value: DiscountStrategy) -> 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: DiscountStrategy | None = None) -> None: + if value is not None and self.validate(obj, value): + setattr(obj, self.private_name, value) + else: + setattr(obj, self.private_name, None) + + def __get__( + self, obj: Order, objtype: type[Order] | None = None + ) -> DiscountStrategy | 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: DiscountStrategy | None = 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(): - order0 = Order(100) - order1 = Order(100, discount_strategy=ten_percent_discount) - order2 = Order(1000, discount_strategy=on_sale_discount) - print(order0) - print(order1) - print(order2) + """ + >>> 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__": - main() - + import doctest -OUTPUT = """ - - - -""" + doctest.testmod() diff --git a/patterns/behavioral/template.py b/patterns/behavioral/template.py index 1c6940ebc..76fc136bc 100644 --- a/patterns/behavioral/template.py +++ b/patterns/behavioral/template.py @@ -1,10 +1,7 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - """ An example of the Template pattern in Python -*TL;DR80 +*TL;DR Defines the skeleton of a base algorithm, deferring definition of exact steps to subclasses. @@ -13,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) @@ -46,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(): @@ -72,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 cee9fdaf1..ab2f5f713 100644 --- a/patterns/behavioral/visitor.py +++ b/patterns/behavioral/visitor.py @@ -1,10 +1,7 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - """ http://peter-hoffmann.com/2010/extrinsic-visitor-pattern-python-inheritance.html -*TL;DR80 +*TL;DR Separates an algorithm from an object structure on which it operates. An interesting recipe could be found in @@ -19,7 +16,7 @@ """ -class Node(object): +class Node: pass @@ -35,11 +32,11 @@ class C(A, B): pass -class Visitor(object): - def visit(self, node, *args, **kwargs): +class Visitor: + def visit(self, node: Node, *args, **kwargs) -> str: 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 @@ -48,29 +45,30 @@ def visit(self, node, *args, **kwargs): meth = self.generic_visit return meth(node, *args, **kwargs) - def generic_visit(self, node, *args, **kwargs): - print('generic_visit ' + node.__class__.__name__) + def generic_visit(self, node: Node, *args, **kwargs) -> str: + return "generic_visit " + node.__class__.__name__ - def visit_B(self, node, *args, **kwargs): - print('visit_B ' + node.__class__.__name__) + def visit_B(self, node: Node, *args, **kwargs) -> str: + return "visit_B " + node.__class__.__name__ def main(): - a = A() - b = B() - c = C() - visitor = Visitor() - visitor.visit(a) - visitor.visit(b) - visitor.visit(c) + """ + >>> a, b, c = A(), B(), C() + >>> visitor = Visitor() + >>> visitor.visit(a) + 'generic_visit A' -if __name__ == "__main__": - main() + >>> visitor.visit(b) + 'visit_B B' + >>> visitor.visit(c) + 'visit_B C' + """ -OUTPUT = """ -generic_visit A -visit_B B -visit_B C -""" + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/creational/abstract_factory.py b/patterns/creational/abstract_factory.py index f1c80ac3c..2362d8fef 100644 --- a/patterns/creational/abstract_factory.py +++ b/patterns/creational/abstract_factory.py @@ -1,6 +1,3 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - """ *What is this pattern about? @@ -29,78 +26,73 @@ https://sourcemaking.com/design_patterns/abstract_factory http://ginstrom.com/scribbles/2007/10/08/design-patterns-python-style/ -*TL;DR80 +*TL;DR Provides a way to encapsulate a group of individual factories. """ import random -class PetShop(object): +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(object): - 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(object): - 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 44afa449f..7a8073e69 100644 --- a/patterns/creational/borg.py +++ b/patterns/creational/borg.py @@ -1,6 +1,3 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - """ *What is this pattern about? The Borg pattern (also known as the Monostate pattern) is a way to @@ -16,77 +13,97 @@ 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 dictionary itself is shared (which is __shared_state), all other attributes will also be shared. -For this reason, when the attribute self.state is modified using -instance rm2, the value of self.state in instance rm1 also changes. The -same happens if self.state is modified using rm3, which is an -instance from a subclass. -Notice that even though they share attributes, the instances are not -the same, as seen by their ids. *Where is the pattern used practically? Sharing state is useful in applications like managing database connections: https://github.com/onetwopunch/pythonDbTemplate/blob/master/database.py *References: -https://fkromer.github.io/python-pattern-references/design/#singleton +- https://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;DR80 +*TL;DR Provides singleton-like behavior sharing state between instances. """ -class Borg(object): - __shared_state = {} +class Borg: + _shared_state: dict[str, str] = {} + + def __init__(self) -> None: + self.__dict__ = self._shared_state - def __init__(self): - self.__dict__ = self.__shared_state - self.state = 'Init' - def __str__(self): +class YourBorg(Borg): + def __init__(self, state: str | None = 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 -class YourBorg(Borg): - pass +def main(): + """ + >>> rm1 = YourBorg() + >>> rm2 = YourBorg() + + >>> rm1.state = 'Idle' + >>> rm2.state = 'Running' + + >>> print('rm1: {0}'.format(rm1)) + rm1: Running + >>> print('rm2: {0}'.format(rm2)) + rm2: Running + + # When the `state` attribute is modified from instance `rm2`, + # the value of `state` in instance `rm1` also changes + >>> rm2.state = 'Zombie' + >>> print('rm1: {0}'.format(rm1)) + rm1: Zombie + >>> print('rm2: {0}'.format(rm2)) + rm2: Zombie -if __name__ == '__main__': - rm1 = Borg() - rm2 = Borg() + # Even though `rm1` and `rm2` share attributes, the instances are not the same + >>> rm1 is rm2 + False - rm1.state = 'Idle' - rm2.state = 'Running' + # New instances also get the same shared state + >>> rm3 = YourBorg() - print('rm1: {0}'.format(rm1)) - print('rm2: {0}'.format(rm2)) + >>> print('rm1: {0}'.format(rm1)) + rm1: Zombie + >>> print('rm2: {0}'.format(rm2)) + rm2: Zombie + >>> print('rm3: {0}'.format(rm3)) + rm3: Zombie - rm2.state = 'Zombie' + # A new instance can explicitly change the state during creation + >>> rm4 = YourBorg('Running') - print('rm1: {0}'.format(rm1)) - print('rm2: {0}'.format(rm2)) + >>> print('rm4: {0}'.format(rm4)) + rm4: Running - print('rm1 id: {0}'.format(id(rm1))) - print('rm2 id: {0}'.format(id(rm2))) + # Existing instances reflect that change as well + >>> print('rm3: {0}'.format(rm3)) + rm3: Running + """ - rm3 = YourBorg() - print('rm1: {0}'.format(rm1)) - print('rm2: {0}'.format(rm2)) - print('rm3: {0}'.format(rm3)) +if __name__ == "__main__": + import doctest -### OUTPUT ### -# rm1: Running -# rm2: Running -# rm1: Zombie -# rm2: Zombie -# rm1 id: 140732837899224 -# rm2 id: 140732837899296 -# rm1: Init -# rm2: Init -# rm3: Init + doctest.testmod() diff --git a/patterns/creational/builder.py b/patterns/creational/builder.py index 72ff0bc18..c5253ad30 100644 --- a/patterns/creational/builder.py +++ b/patterns/creational/builder.py @@ -1,16 +1,12 @@ -#!/usr/bin/python -# -*- coding : utf-8 -*- - """ -*What is this pattern about? +What is this pattern about? It decouples the creation of a complex object and its representation, so that the same process can be reused to build objects from the same family. This is useful when you must separate the specification of an object from its actual representation (generally for abstraction). -*What does this example do? - +What does this example do? The first example achieves this by using an abstract base class for a building, where the initializer (__init__ method) specifies the steps needed, and the concrete subclasses implement these steps. @@ -25,19 +21,20 @@ class for a building, where the initializer (__init__ method) specifies the In general, in Python this won't be necessary, but a second example showing this kind of arrangement is also included. -*Where is the pattern used practically? +Where is the pattern used practically? +See: https://sourcemaking.com/design_patterns/builder -*References: -https://sourcemaking.com/design_patterns/builder - -*TL;DR80 +TL;DR Decouples the creation of a complex object and its representation. """ # Abstract Building -class Building(object): - def __init__(self): +class Building: + floor: str + size: str + + def __init__(self) -> None: self.build_floor() self.build_size() @@ -47,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 @@ -74,38 +71,47 @@ def build_size(self): # a concrete class does not have a useful constructor) -class ComplexBuilding(object): - def __repr__(self): - return 'Floor: {0.floor} | Size: {0.size}'.format(self) +class ComplexBuilding: + floor: str + size: str + + 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() return building -# Client -if __name__ == "__main__": - house = House() - print(house) - flat = Flat() - print(flat) +def main(): + """ + >>> house = House() + >>> house + Floor: One | Size: Big + + >>> flat = Flat() + >>> flat + Floor: More than One | Size: Small # Using an external constructor function: - complex_house = construct_building(ComplexHouse) - print(complex_house) + >>> complex_house = construct_building(ComplexHouse) + >>> complex_house + Floor: One | Size: Big and fancy + """ + + +if __name__ == "__main__": + import doctest -### OUTPUT ### -# Floor: One | Size: Big -# Floor: More than One | Size: Small -# Floor: One | Size: Big and fancy + doctest.testmod() diff --git a/patterns/creational/factory.py b/patterns/creational/factory.py new file mode 100644 index 000000000..801575bdf --- /dev/null +++ b/patterns/creational/factory.py @@ -0,0 +1,78 @@ +"""*What is this pattern about? +A Factory is an object for creating other objects. + +*What does this example do? +The code shows a way to localize words in two languages: English and +Greek. "get_localizer" is the factory function that constructs a +localizer depending on the language chosen. The localizer object will +be an instance from a different class according to the language +localized. However, the main code does not have to worry about which +localizer will be instantiated, since the method "localize" will be called +in the same way independently of the language. + +*Where can the pattern be used practically? +The Factory Method can be seen in the popular web framework Django: +https://docs.djangoproject.com/en/4.0/topics/forms/formsets/ +For example, different types of forms are created using a formset_factory + +*References: +http://ginstrom.com/scribbles/2007/10/08/design-patterns-python-style/ + +*TL;DR +Creates objects without having to specify the exact class. +""" + +from typing import Protocol + + +class Localizer(Protocol): + def localize(self, msg: str) -> str: ... + + +class GreekLocalizer: + """A simple localizer a la gettext""" + + def __init__(self) -> None: + self.translations = {"dog": "σκύλος", "cat": "γάτα"} + + def localize(self, msg: str) -> str: + """We'll punt if we don't have a translation""" + return self.translations.get(msg, msg) + + +class EnglishLocalizer: + """Simply echoes the message""" + + def localize(self, msg: str) -> str: + return msg + + +def get_localizer(language: str = "English") -> Localizer: + """Factory""" + localizers: dict[str, type[Localizer]] = { + "English": EnglishLocalizer, + "Greek": GreekLocalizer, + } + + return localizers.get(language, EnglishLocalizer)() + + +def main(): + """ + # Create our localizers + >>> e, g = get_localizer(language="English"), get_localizer(language="Greek") + + # Localize some text + >>> for msg in "dog parrot cat bear".split(): + ... print(e.localize(msg), g.localize(msg)) + dog σκύλος + parrot parrot + cat γάτα + bear bear + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/creational/factory_method.py b/patterns/creational/factory_method.py deleted file mode 100644 index 741547845..000000000 --- a/patterns/creational/factory_method.py +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -"""*What is this pattern about? -The Factory Method pattern can be used to create an interface for a -method, leaving the implementation to the class that gets -instantiated. - -*What does this example do? -The code shows a way to localize words in two languages: English and -Greek. "getLocalizer" is the factory method that constructs a -localizer depending on the language chosen. The localizer object will -be an instance from a different class according to the language -localized. However, the main code does not have to worry about which -localizer will be instantiated, since the method "get" will be called -in the same way independently of the language. - -*Where can the pattern be used practically? -The Factory Method can be seen in the popular web framework Django: -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. - -*References: -http://ginstrom.com/scribbles/2007/10/08/design-patterns-python-style/ -https://fkromer.github.io/python-pattern-references/design/#factory-method -https://sourcemaking.com/design_patterns/factory_method - -*TL;DR80 -Creates objects without having to specify the exact class. -""" - - -class GreekGetter(object): - - """A simple localizer a la gettext""" - - def __init__(self): - self.trans = dict(dog="σκύλος", cat="γάτα") - - def get(self, msgid): - """We'll punt if we don't have a translation""" - return self.trans.get(msgid, str(msgid)) - - -class EnglishGetter(object): - - """Simply echoes the msg ids""" - - def get(self, msgid): - return str(msgid) - - -def get_localizer(language="English"): - """The factory method""" - languages = dict(English=EnglishGetter, Greek=GreekGetter) - return languages[language]() - - -if __name__ == '__main__': - # Create our localizers - e, g = get_localizer(language="English"), get_localizer(language="Greek") - # Localize some text - for msgid in "dog parrot cat bear".split(): - print(e.get(msgid), g.get(msgid)) - -### OUTPUT ### -# dog σκύλος -# parrot parrot -# cat γάτα -# bear bear diff --git a/patterns/creational/lazy_evaluation.py b/patterns/creational/lazy_evaluation.py index 62919a601..b9945d998 100644 --- a/patterns/creational/lazy_evaluation.py +++ b/patterns/creational/lazy_evaluation.py @@ -1,6 +1,3 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - """ Lazily-evaluated property pattern in Python. @@ -13,25 +10,34 @@ 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 -*TL;DR80 +*TL;DR Delays the eval of an expr until its value is needed and avoids repeated evals. """ -from __future__ import print_function import functools +from collections.abc import Callable +from typing import Any, overload -class lazy_property(object): - def __init__(self, function): +class lazy_property: + def __init__(self, function: Callable[["Person"], str]) -> None: self.function = function - functools.update_wrapper(self, function) + functools.update_wrapper(self, function) # type: ignore[arg-type] + + @overload + def __get__(self, obj: None, type_: type["Person"]) -> "lazy_property": ... - def __get__(self, obj, type_): + @overload + def __get__(self, obj: "Person", type_: type["Person"]) -> str: ... + + def __get__( + self, obj: "Person | None", type_: type["Person"] + ) -> "lazy_property | str": if obj is None: return self val = self.function(obj) @@ -39,61 +45,76 @@ def __get__(self, obj, type_): return val -def lazy_property2(fn): - attr = '_lazy__' + fn.__name__ +def lazy_property2(fn: Callable[[Any], str]) -> property: + """ + A lazy property decorator. + + The function decorated is called the first time to retrieve the result and + then that calculated result is used the next time you access the value. + """ + attr = "_lazy__" + fn.__name__ - @property - def _lazy_property(self): + def _lazy_property(self: Any) -> str: if not hasattr(self, attr): setattr(self, attr, fn(self)) return getattr(self, attr) - return _lazy_property + return property(_lazy_property) -class Person(object): - def __init__(self, name, occupation): +class Person: + def __init__(self, name: str, occupation: str) -> None: self.name = name self.occupation = occupation self.call_count2 = 0 @lazy_property - def relatives(self): + def relatives(self) -> str: # Get all relatives, let's assume that it costs much time. relatives = "Many relatives." return relatives @lazy_property2 - def parents(self): + def parents(self) -> str: self.call_count2 += 1 return "Father and mother" def main(): - Jhon = Person('Jhon', 'Coder') - print(u"Name: {0} Occupation: {1}".format(Jhon.name, Jhon.occupation)) - print(u"Before we access `relatives`:") - print(Jhon.__dict__) - print(u"Jhon's relatives: {0}".format(Jhon.relatives)) - print(u"After we've accessed `relatives`:") - print(Jhon.__dict__) - print(Jhon.parents) - print(Jhon.__dict__) - print(Jhon.parents) - print(Jhon.call_count2) - - -if __name__ == '__main__': - main() - -### OUTPUT ### -# Name: Jhon Occupation: Coder -# Before we access `relatives`: -# {'call_count2': 0, 'name': 'Jhon', 'occupation': 'Coder'} -# Jhon's relatives: Many relatives. -# After we've accessed `relatives`: -# {'relatives': 'Many relatives.', 'call_count2': 0, 'name': 'Jhon', 'occupation': 'Coder'} -# Father and mother -# {'_lazy__parents': 'Father and mother', 'relatives': 'Many relatives.', 'call_count2': 1, 'name': 'Jhon', 'occupation': 'Coder'} # noqa flake8 -# Father and mother -# 1 + """ + >>> Jhon = Person('Jhon', 'Coder') + + >>> Jhon.name + 'Jhon' + >>> Jhon.occupation + 'Coder' + + # Before we access `relatives` + >>> sorted(Jhon.__dict__.items()) + [('call_count2', 0), ('name', 'Jhon'), ('occupation', 'Coder')] + + >>> Jhon.relatives + 'Many relatives.' + + # After we've accessed `relatives` + >>> sorted(Jhon.__dict__.items()) + [('call_count2', 0), ..., ('relatives', 'Many relatives.')] + + >>> Jhon.parents + 'Father and mother' + + >>> sorted(Jhon.__dict__.items()) + [('_lazy__parents', 'Father and mother'), ('call_count2', 1), ..., ('relatives', 'Many relatives.')] + + >>> Jhon.parents + 'Father and mother' + + >>> Jhon.call_count2 + 1 + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod(optionflags=doctest.ELLIPSIS) diff --git a/patterns/creational/pool.py b/patterns/creational/pool.py index 1650cd271..684b11857 100644 --- a/patterns/creational/pool.py +++ b/patterns/creational/pool.py @@ -1,6 +1,3 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - """ *What is this pattern about? This pattern is used when creating an object is costly (and they are @@ -18,7 +15,7 @@ As we can see, the first string object put in "yam" is USED by the with statement. But because it is released back into the pool afterwards it is reused by the explicit call to sample_queue.get(). -Same thing happens with "sam", when the ObjectPool created insided the +Same thing happens with "sam", when the ObjectPool created inside the function is deleted (by the GC) and the object is returned. *Where is the pattern used practically? @@ -27,62 +24,71 @@ http://stackoverflow.com/questions/1514120/python-implementation-of-the-object-pool-design-pattern https://sourcemaking.com/design_patterns/object_pool -*TL;DR80 +*TL;DR Stores a set of initialized objects kept ready to use. """ +from queue import Queue +from types import TracebackType + -class ObjectPool(object): - def __init__(self, queue, auto_get=False): +class ObjectPool: + def __init__(self, queue: Queue, auto_get: bool = False) -> None: self._queue = queue self.item = self._queue.get() if auto_get else None - def __enter__(self): + def __enter__(self) -> str: if self.item is None: self.item = self._queue.get() return self.item - def __exit__(self, Type, value, traceback): + def __exit__( + self, + Type: type[BaseException] | None, + value: BaseException | None, + traceback: TracebackType | None, + ) -> None: if self.item is not None: self._queue.put(self.item) self.item = None - def __del__(self): + def __del__(self) -> None: if self.item is not None: self._queue.put(self.item) self.item = None def main(): - try: - import queue - except ImportError: # python 2.x compatibility - import Queue as queue + """ + >>> import queue + + >>> def test_object(queue): + ... pool = ObjectPool(queue, True) + ... print('Inside func: {}'.format(pool.item)) + + >>> sample_queue = queue.Queue() - def test_object(queue): - pool = ObjectPool(queue, True) - print('Inside func: {}'.format(pool.item)) + >>> sample_queue.put('yam') + >>> with ObjectPool(sample_queue) as obj: + ... print('Inside with: {}'.format(obj)) + Inside with: yam - sample_queue = queue.Queue() + >>> print('Outside with: {}'.format(sample_queue.get())) + Outside with: yam - 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('sam') + >>> test_object(sample_queue) + Inside func: sam - sample_queue.put('sam') - test_object(sample_queue) - print('Outside func: {}'.format(sample_queue.get())) + >>> 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 b4d25184c..4c2dd7ed1 100644 --- a/patterns/creational/prototype.py +++ b/patterns/creational/prototype.py @@ -1,13 +1,10 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - """ *What is this pattern about? This patterns aims to reduce the number of classes required by an application. Instead of relying on subclasses it creates objects by copying a prototypical instance at run-time. -This is useful as it make it easier to derive new kinds of objects, +This is useful as it makes it easier to derive new kinds of objects, when instances of the class have only a few different combinations of state, and when instantiation is expensive. @@ -20,55 +17,67 @@ Below provides an example of such Dispatcher, which contains three copies of the prototype: 'default', 'objecta' and 'objectb'. -*TL;DR80 +*TL;DR Creates new object instances by cloning prototype. """ +from __future__ import annotations + +from typing import Any -class Prototype(object): - 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 -class PrototypeDispatcher(object): +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(): - dispatcher = PrototypeDispatcher() - prototype = Prototype() +def main() -> None: + """ + >>> dispatcher = PrototypeDispatcher() + >>> prototype = Prototype() + + >>> d = prototype.clone() + >>> a = prototype.clone(value='a-value', category='a') + >>> b = a.clone(value='b-value', is_checked=True) + >>> dispatcher.register_object('objecta', a) + >>> dispatcher.register_object('objectb', b) + >>> dispatcher.register_object('default', d) + + >>> [{n: p.value} for n, p in dispatcher.get_objects().items()] + [{'objecta': 'a-value'}, {'objectb': 'b-value'}, {'default': 'default'}] - d = prototype.clone() - a = prototype.clone(value='a-value', category='a') - b = prototype.clone(value='b-value', is_checked=True) - dispatcher.register_object('objecta', a) - dispatcher.register_object('objectb', b) - dispatcher.register_object('default', d) - print([{n: p.value} for n, p in dispatcher.get_objects().items()]) + >>> print(b.category, b.is_checked) + a True + """ -if __name__ == '__main__': - main() +if __name__ == "__main__": + import doctest -### OUTPUT ### -# [{'objectb': 'b-value'}, {'default': 'default'}, {'objecta': 'a-value'}] + doctest.testmod() diff --git a/patterns/dependency_injection.py b/patterns/dependency_injection.py new file mode 100644 index 000000000..4246d11a2 --- /dev/null +++ b/patterns/dependency_injection.py @@ -0,0 +1,116 @@ +""" +Dependency Injection (DI) is a technique whereby one object supplies the dependencies (services) +to another object (client). +It allows to decouple objects: no need to change client code simply because an object it depends on +needs to be changed to a different one. (Open/Closed principle) + +Port of the Java example of Dependency Injection" in +"xUnit Test Patterns - Refactoring Test Code" by Gerard Meszaros +(ISBN-10: 0131495054, ISBN-13: 978-0131495050) + +In the following example `time_provider` (service) is embedded into TimeDisplay (client). +If such service performed an expensive operation you would like to substitute or mock it in tests. + +class TimeDisplay(object): + + def __init__(self): + self.time_provider = datetime.datetime.now + + def get_current_time_as_html_fragment(self): + current_time = self.time_provider() + current_time_as_html_fragment = "{}".format(current_time) + return current_time_as_html_fragment + +""" + +import datetime +from collections.abc import Callable + + +class ConstructorInjection: + def __init__(self, time_provider: Callable) -> None: + self.time_provider = time_provider + + def get_current_time_as_html_fragment(self) -> str: + current_time = self.time_provider() + current_time_as_html_fragment = '{}'.format( + current_time + ) + return current_time_as_html_fragment + + +class ParameterInjection: + def __init__(self) -> None: + pass + + def get_current_time_as_html_fragment(self, time_provider: Callable) -> str: + current_time = time_provider() + current_time_as_html_fragment = '{}'.format( + current_time + ) + return current_time_as_html_fragment + + +class SetterInjection: + """Setter Injection""" + + def __init__(self): + pass + + def set_time_provider(self, time_provider: Callable): + self.time_provider = time_provider + + def get_current_time_as_html_fragment(self): + current_time = self.time_provider() + current_time_as_html_fragment = '{}'.format( + current_time + ) + return current_time_as_html_fragment + + +def production_code_time_provider() -> str: + """ + Production code version of the time provider (just a wrapper for formatting + datetime for this example). + """ + current_time = datetime.datetime.now() + current_time_formatted = f"{current_time.hour}:{current_time.minute}" + return current_time_formatted + + +def midnight_time_provider() -> str: + """Hard-coded stub""" + return "24:01" + + +def main(): + """ + >>> time_with_ci1 = ConstructorInjection(midnight_time_provider) + >>> time_with_ci1.get_current_time_as_html_fragment() + '24:01' + + >>> time_with_ci2 = ConstructorInjection(production_code_time_provider) + >>> time_with_ci2.get_current_time_as_html_fragment() + '...' + + >>> time_with_pi = ParameterInjection() + >>> time_with_pi.get_current_time_as_html_fragment(midnight_time_provider) + '24:01' + + >>> time_with_si = SetterInjection() + + >>> time_with_si.get_current_time_as_html_fragment() + Traceback (most recent call last): + ... + AttributeError: ... + + >>> time_with_si.set_time_provider(midnight_time_provider) + >>> time_with_si.get_current_time_as_html_fragment() + '24:01' + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod(optionflags=doctest.ELLIPSIS) diff --git a/patterns/dft/__init__.py b/patterns/dft/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/patterns/dft/constructor_injection.py b/patterns/dft/constructor_injection.py deleted file mode 100644 index 7194a2eae..000000000 --- a/patterns/dft/constructor_injection.py +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/python -# -*- coding : utf-8 -*- -import datetime - -""" -Port of the Java example of "Constructor Injection" in -"xUnit Test Patterns - Refactoring Test Code" by Gerard Meszaros -(ISBN-10: 0131495054, ISBN-13: 978-0131495050) - -production code which is untestable: - -class TimeDisplay(object): - - def __init__(self): - self.time_provider = datetime.datetime - - def get_current_time_as_html_fragment(self): - current_time = self.time_provider.now() - current_time_as_html_fragment = "{}".format(current_time) - return current_time_as_html_fragment -""" - - -class TimeDisplay(object): - def __init__(self, time_provider): - self.time_provider = time_provider - - def get_current_time_as_html_fragment(self): - current_time = self.time_provider.now() - current_time_as_html_fragment = "{}".format(current_time) - return current_time_as_html_fragment - - -class ProductionCodeTimeProvider(object): - """ - Production code version of the time provider (just a wrapper for formatting - datetime for this example). - """ - - def now(self): - current_time = datetime.datetime.now() - current_time_formatted = "{}:{}".format(current_time.hour, current_time.minute) - return current_time_formatted - - -class MidnightTimeProvider(object): - """ - Class implemented as hard-coded stub (in contrast to configurable stub). - """ - - def now(self): - current_time_is_always_midnight = "24:01" - return current_time_is_always_midnight diff --git a/patterns/dft/parameter_injection.py b/patterns/dft/parameter_injection.py deleted file mode 100644 index c15927362..000000000 --- a/patterns/dft/parameter_injection.py +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/python -# -*- coding : utf-8 -*- -import datetime - -""" -Port of the Java example of "Parameter Injection" in -"xUnit Test Patterns - Refactoring Test Code" by Gerard Meszaros -(ISBN-10: 0131495054, ISBN-13: 978-0131495050) accessible in outdated version on -http://xunitpatterns.com/Dependency%20Injection.html. - -production code which is untestable: - -class TimeDisplay(object): - - def __init__(self): - self.time_provider = datetime.datetime - - def get_current_time_as_html_fragment(self): - current_time = self.time_provider.now() - current_time_as_html_fragment = "{}".format(current_time) - return current_time_as_html_fragment -""" - - -class TimeDisplay(object): - def __init__(self): - pass - - def get_current_time_as_html_fragment(self, time_provider): - current_time = time_provider.now() - current_time_as_html_fragment = "{}".format(current_time) - return current_time_as_html_fragment - - -class ProductionCodeTimeProvider(object): - """ - Production code version of the time provider (just a wrapper for formatting - datetime for this example). - """ - - def now(self): - current_time = datetime.datetime.now() - current_time_formatted = "{}:{}".format(current_time.hour, current_time.minute) - return current_time_formatted - - -class MidnightTimeProvider(object): - """ - Class implemented as hard-coded stub (in contrast to configurable stub). - """ - - def now(self): - current_time_is_always_midnight = "24:01" - return current_time_is_always_midnight diff --git a/patterns/dft/setter_injection.py b/patterns/dft/setter_injection.py deleted file mode 100644 index f14a2a24d..000000000 --- a/patterns/dft/setter_injection.py +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/python -# -*- coding : utf-8 -*- -import datetime - -""" -Port of the Java example of "Setter Injection" in -"xUnit Test Patterns - Refactoring Test Code" by Gerard Meszaros -(ISBN-10: 0131495054, ISBN-13: 978-0131495050) accessible in outdated version on -http://xunitpatterns.com/Dependency%20Injection.html. - -production code which is untestable: - -class TimeDisplay(object): - - def __init__(self): - self.time_provider = datetime.datetime - - def get_current_time_as_html_fragment(self): - current_time = self.time_provider.now() - current_time_as_html_fragment = "{}".format(current_time) - return current_time_as_html_fragment -""" - - -class TimeDisplay(object): - def __init__(self): - pass - - def set_time_provider(self, time_provider): - self.time_provider = time_provider - - def get_current_time_as_html_fragment(self): - current_time = self.time_provider.now() - current_time_as_html_fragment = "{}".format(current_time) - return current_time_as_html_fragment - - -class ProductionCodeTimeProvider(object): - """ - Production code version of the time provider (just a wrapper for formatting - datetime for this example). - """ - - def now(self): - current_time = datetime.datetime.now() - current_time_formatted = "{}:{}".format(current_time.hour, current_time.minute) - return current_time_formatted - - -class MidnightTimeProvider(object): - """ - Class implemented as hard-coded stub (in contrast to configurable stub). - """ - - def now(self): - current_time_is_always_midnight = "24:01" - return current_time_is_always_midnight diff --git a/patterns/fundamental/delegation_pattern.py b/patterns/fundamental/delegation_pattern.py index ad41ac1ea..c035da202 100644 --- a/patterns/fundamental/delegation_pattern.py +++ b/patterns/fundamental/delegation_pattern.py @@ -1,16 +1,18 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - """ Reference: https://en.wikipedia.org/wiki/Delegation_pattern Author: https://github.com/IuryAlves -*TL;DR80 +*TL;DR Allows object composition to achieve the same code reuse as inheritance. """ +from __future__ import annotations + +from collections.abc import Callable +from typing import Any -class Delegator(object): + +class Delegator: """ >>> delegator = Delegator(Delegate()) >>> delegator.p1 @@ -18,19 +20,21 @@ class Delegator(object): >>> delegator.p2 Traceback (most recent call last): ... - AttributeError: 'Delegate' object has no attribute 'p2' + AttributeError: ... >>> 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: ... """ - def __init__(self, delegate): + def __init__(self, delegate: Delegate) -> None: self.delegate = delegate - def __getattr__(self, name): + def __getattr__(self, name: str) -> Any | Callable: attr = getattr(self.delegate, name) if not callable(attr): @@ -38,18 +42,19 @@ def __getattr__(self, name): def wrapper(*args, **kwargs): return attr(*args, **kwargs) + return wrapper -class Delegate(object): - def __init__(self): +class Delegate: + def __init__(self) -> None: self.p1 = 123 - def do_something(self, something): - return "Doing %s" % something + def do_something(self, something: str, kw=None) -> str: + return f"Doing {something}{kw or ''}" -if __name__ == '__main__': +if __name__ == "__main__": import doctest doctest.testmod() diff --git a/patterns/other/blackboard.py b/patterns/other/blackboard.py index f47ecfe40..e69582ec3 100644 --- a/patterns/other/blackboard.py +++ b/patterns/other/blackboard.py @@ -1,6 +1,3 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - """ @author: Eugene Duboviy | github.com/duboviy @@ -12,112 +9,130 @@ https://en.wikipedia.org/wiki/Blackboard_system """ -import abc import random +from abc import ABC, abstractmethod + + +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(object): - def __init__(self): - self.experts = [] + +class Blackboard: + """The blackboard system that holds the common state.""" + + def __init__(self) -> None: + self.experts: list = [] self.common_state = { - 'problems': 0, - 'suggestions': 0, - 'contributions': [], - 'progress': 0, # percentage, if 100 -> task is finished + "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(object): - def __init__(self, blackboard): +class Controller: + """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(object): - - __metaclass__ = abc.ABCMeta - - def __init__(self, blackboard): - self.blackboard = blackboard + return self.blackboard.common_state["contributions"] - @abc.abstractproperty - 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.') +class Student(AbstractExpert): + """Concrete class for a student expert.""" + def __init__(self, blackboard) -> None: + super().__init__(blackboard) -class Student(AbstractExpert): @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 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) - - -if __name__ == '__main__': - blackboard = Blackboard() - - blackboard.add_expert(Student(blackboard)) - blackboard.add_expert(Scientist(blackboard)) - blackboard.add_expert(Professor(blackboard)) - - c = Controller(blackboard) - contributions = c.run_loop() - - from pprint import pprint - - pprint(contributions) - -### OUTPUT ### -# ['Student', -# 'Student', -# 'Scientist', -# 'Student', -# 'Scientist', -# 'Student', -# 'Scientist', -# 'Student', -# 'Scientist', -# 'Student', -# 'Scientist', -# 'Professor'] + def is_eager_to_contribute(self) -> bool: + return True if self.blackboard.common_state["problems"] > 100 else False + + def contribute(self) -> None: + self.blackboard.common_state["problems"] += random.randint(1, 2) + self.blackboard.common_state["suggestions"] += random.randint(10, 20) + self.blackboard.common_state["contributions"] += [self.__class__.__name__] + self.blackboard.common_state["progress"] += random.randint(10, 100) + + +def main(): + """ + >>> random.seed(1234) + >>> blackboard = Blackboard() + >>> blackboard.add_expert(Student(blackboard)) + >>> blackboard.add_expert(Scientist(blackboard)) + >>> blackboard.add_expert(Professor(blackboard)) + + >>> c = Controller(blackboard) + >>> contributions = c.run_loop() + + >>> contributions[0], contributions[-1] + ('Student', 'Professor') + >>> set(contributions) == {"Student", "Scientist", "Professor"} + True + """ + + +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 35ad4f10e..e3c921409 100644 --- a/patterns/other/graph_search.py +++ b/patterns/other/graph_search.py @@ -1,18 +1,16 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -"" - - 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): + def __init__(self, graph: dict[str, list[str]]) -> None: self.graph = graph - def find_path(self, start, end, path=None): + def find_path_dfs( + self, start: str, end: str, path: list[str] | None = None + ) -> list[str] | None: path = path or [] path.append(start) @@ -20,23 +18,28 @@ 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 + return None - def find_all_path(self, start, end, path=None): + def find_all_paths_dfs( + self, start: str, end: str, path: list[str] | None = None + ) -> list[list[str]]: path = path or [] path.append(start) if start == end: return [path] - paths = [] + paths: list[list[str]] = [] 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: str, end: str, path: list[str] | None = None + ) -> list[str] | None: path = path or [] path.append(start) @@ -45,25 +48,111 @@ 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: str, end: str) -> list[str] | None: + """ + 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: list[str] = [start] + dist_to: dict[str, int] = {start: 0} + edge_to: dict[str, str] = {} -# example of graph usage -graph = {'A': ['B', 'C'], 'B': ['C', 'D'], 'C': ['D'], 'D': ['C'], 'E': ['F'], 'F': ['C']} - -# initialization of new graph search object -graph1 = GraphSearch(graph) - - -print(graph1.find_path('A', 'D')) -print(graph1.find_all_path('A', 'D')) -print(graph1.find_shortest_path('A', 'D')) - -### OUTPUT ### -# ['A', 'B', 'C', 'D'] -# [['A', 'B', 'C', 'D'], ['A', 'B', 'D'], ['A', 'C', 'D']] -# ['A', 'B', 'D'] + 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: list[str] = [] + node = end + while dist_to[node] != 0: + path.insert(0, node) + node = edge_to[node] + path.insert(0, start) + return path + return None + + +def main(): + """ + # example of graph usage + >>> graph = { + ... 'A': ['B', 'C'], + ... 'B': ['C', 'D'], + ... 'C': ['D', 'G'], + ... 'D': ['C'], + ... 'E': ['F'], + ... 'F': ['C'], + ... 'G': ['E'], + ... 'H': ['C'] + ... } + + # initialization of new graph search object + >>> graph_search = GraphSearch(graph) + + >>> print(graph_search.find_path_dfs('A', 'D')) + ['A', 'B', 'C', 'D'] + + # start the search somewhere in the middle + >>> print(graph_search.find_path_dfs('G', 'F')) + ['G', 'E', 'F'] + + # unreachable node + >>> print(graph_search.find_path_dfs('C', 'H')) + None + + # non existing node + >>> print(graph_search.find_path_dfs('C', 'X')) + None + + >>> print(graph_search.find_all_paths_dfs('A', 'D')) + [['A', 'B', 'C', 'D'], ['A', 'B', 'D'], ['A', 'C', 'D']] + >>> print(graph_search.find_shortest_path_dfs('A', 'D')) + ['A', 'B', 'D'] + >>> print(graph_search.find_shortest_path_dfs('A', 'F')) + ['A', 'C', 'G', 'E', 'F'] + + >>> print(graph_search.find_shortest_path_bfs('A', 'D')) + ['A', 'B', 'D'] + >>> print(graph_search.find_shortest_path_bfs('A', 'F')) + ['A', 'C', 'G', 'E', 'F'] + + # start the search somewhere in the middle + >>> print(graph_search.find_shortest_path_bfs('G', 'F')) + ['G', 'E', 'F'] + + # unreachable node + >>> print(graph_search.find_shortest_path_bfs('A', 'H')) + None + + # non existing node + >>> print(graph_search.find_shortest_path_bfs('A', 'X')) + None + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/other/hsm/hsm.py b/patterns/other/hsm/hsm.py index 3665b3242..44498014e 100644 --- a/patterns/other/hsm/hsm.py +++ b/patterns/other/hsm/hsm.py @@ -21,7 +21,7 @@ class UnsupportedTransition(BaseException): pass -class HierachicalStateMachine(object): +class HierachicalStateMachine: def __init__(self): self._active_state = Active(self) # Unit.Inservice.Active() self._standby_state = Standby(self) # Unit.Inservice.Standby() @@ -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(): @@ -85,7 +85,7 @@ def on_message(self, message_type): # message ignored raise UnsupportedMessageType -class Unit(object): +class Unit: def __init__(self, HierachicalStateMachine): self.hsm = HierachicalStateMachine @@ -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() @@ -125,12 +125,12 @@ def __init__(self, HierachicalStateMachine): self._hsm = HierachicalStateMachine def on_fault_trigger(self): - super(Active, self).perform_switchover() - super(Active, self).on_fault_trigger() + super().perform_switchover() + super().on_fault_trigger() def on_switchover(self): self._hsm.on_switchover() # message ignored - self._hsm.next_state('standby') + self._hsm.next_state("standby") class Standby(Inservice): @@ -138,8 +138,8 @@ def __init__(self, HierachicalStateMachine): self._hsm = HierachicalStateMachine def on_switchover(self): - super(Standby, self).on_switchover() # message ignored - self._hsm._next_state('active') + super().on_switchover() # message ignored + 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): @@ -157,17 +157,17 @@ def __init__(self, HierachicalStateMachine): self._hsm = HierachicalStateMachine def on_diagnostics_failed(self): - super(Suspect, self).send_diagnostics_failure_report() - super(Suspect, self).next_state('failed') + super().send_diagnostics_failure_report() + super().next_state("failed") def on_diagnostics_passed(self): - super(Suspect, self).send_diagnostics_pass_report() - super(Suspect, self).clear_alarm() # loss of redundancy alarm - super(Suspect, self).next_state('standby') + super().send_diagnostics_pass_report() + super().clear_alarm() # loss of redundancy alarm + super().next_state("standby") def on_operator_inservice(self): - super(Suspect, self).abort_diagnostics() - super(Suspect, self).on_operator_inservice() # message ignored + super().abort_diagnostics() + super().on_operator_inservice() # message ignored class Failed(OutOfService): diff --git a/patterns/structural/3-tier.py b/patterns/structural/3-tier.py index d5a7a148c..3c266f514 100644 --- a/patterns/structural/3-tier.py +++ b/patterns/structural/3-tier.py @@ -1,90 +1,95 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - """ -*TL;DR80 +*TL;DR Separates presentation, application processing, and data management functions. """ +from collections.abc import KeysView + -class Data(object): - """ Data Store Class """ +class Data: + """Data Store Class""" products = { - 'milk': {'price': 1.50, 'quantity': 10}, - 'eggs': {'price': 0.20, 'quantity': 100}, - 'cheese': {'price': 2.00, 'quantity': 10}, + "milk": {"price": 1.50, "quantity": 10}, + "eggs": {"price": 0.20, "quantity": 100}, + "cheese": {"price": 2.00, "quantity": 10}, } def __get__(self, obj, klas): print("(Fetching from Data Store)") - return {'products': self.products} + return {"products": self.products} -class BusinessLogic(object): - """ Business logic holding data store instances """ +class BusinessLogic: + """Business logic holding data store instances""" data = Data() - def product_list(self): - return self.data['products'].keys() + def product_list(self) -> KeysView[str]: + return self.data["products"].keys() - def product_information(self, product): - return self.data['products'].get(product, None) + def product_information(self, product: str) -> dict[str, int | float] | None: + return self.data["products"].get(product, None) -class Ui(object): - """ UI interaction class """ +class Ui: + """UI interaction class""" - def __init__(self): + def __init__(self) -> None: self.business_logic = BusinessLogic() - def get_product_list(self): - print('PRODUCT LIST:') + def get_product_list(self) -> None: + print("PRODUCT LIST:") for product in self.business_logic.product_list(): print(product) - print('') + print("") - def get_product_information(self, product): + def get_product_information(self, product: str) -> None: product_info = self.business_logic.product_information(product) if product_info: - print('PRODUCT INFORMATION:') + print("PRODUCT INFORMATION:") print( - 'Name: {0}, Price: {1:.2f}, Quantity: {2:}'.format( - product.title(), product_info.get('price', 0), product_info.get('quantity', 0) - ) + f"Name: {product.title()}, " + + f"Price: {product_info.get('price', 0):.2f}, " + + f"Quantity: {product_info.get('quantity', 0):}" ) else: - print('That product "{0}" does not exist in the records'.format(product)) + print(f"That product '{product}' does not exist in the records") def main(): - ui = Ui() - ui.get_product_list() - ui.get_product_information('cheese') - ui.get_product_information('eggs') - ui.get_product_information('milk') - ui.get_product_information('arepas') - - -if __name__ == '__main__': - main() - -### OUTPUT ### -# PRODUCT LIST: -# (Fetching from Data Store) -# cheese -# eggs -# milk -# -# (Fetching from Data Store) -# PRODUCT INFORMATION: -# Name: Cheese, Price: 2.00, Quantity: 10 -# (Fetching from Data Store) -# PRODUCT INFORMATION: -# Name: Eggs, Price: 0.20, Quantity: 100 -# (Fetching from Data Store) -# PRODUCT INFORMATION: -# Name: Milk, Price: 1.50, Quantity: 10 -# (Fetching from Data Store) -# That product "arepas" does not exist in the records + """ + >>> ui = Ui() + >>> ui.get_product_list() + PRODUCT LIST: + (Fetching from Data Store) + milk + eggs + cheese + + + >>> ui.get_product_information("cheese") + (Fetching from Data Store) + PRODUCT INFORMATION: + Name: Cheese, Price: 2.00, Quantity: 10 + + >>> ui.get_product_information("eggs") + (Fetching from Data Store) + PRODUCT INFORMATION: + Name: Eggs, Price: 0.20, Quantity: 100 + + >>> ui.get_product_information("milk") + (Fetching from Data Store) + PRODUCT INFORMATION: + Name: Milk, Price: 1.50, Quantity: 10 + + >>> ui.get_product_information("arepas") + (Fetching from Data Store) + That product 'arepas' does not exist in the records + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/structural/adapter.py b/patterns/structural/adapter.py index 5fd32cfe9..5afe42faf 100644 --- a/patterns/structural/adapter.py +++ b/patterns/structural/adapter.py @@ -1,6 +1,3 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - """ *What is this pattern about? The Adapter pattern provides a different interface for a class. We can @@ -27,62 +24,68 @@ https://sourcemaking.com/design_patterns/adapter http://python-3-patterns-idioms-test.readthedocs.io/en/latest/ChangeInterface.html#adapter -*TL;DR80 +*TL;DR Allows the interface of an existing class to be used as another interface. """ +from collections.abc import Callable +from typing import Any, TypeVar + +T = TypeVar("T") + -class Dog(object): - def __init__(self): +class Dog: + def __init__(self) -> None: self.name = "Dog" - def bark(self): + def bark(self) -> str: return "woof!" -class Cat(object): - def __init__(self): +class Cat: + def __init__(self) -> None: self.name = "Cat" - def meow(self): + def meow(self) -> str: return "meow!" -class Human(object): - def __init__(self): +class Human: + def __init__(self) -> None: self.name = "Human" - def speak(self): + def speak(self) -> str: return "'hello'" -class Car(object): - def __init__(self): +class Car: + 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(object): - """ - Adapts an object by replacing methods. - Usage: +class Adapter: + """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[..., Any]) -> None: + """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""" + def __getattr__(self, attr: str) -> Any: + """All non-adapted calls are passed to the object.""" return getattr(self.obj, attr) - def original_dict(self): - """Print original object dict""" + def original_dict(self) -> dict[str, Any]: + """Print original object dict.""" return self.obj.__dict__ @@ -119,4 +122,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 879205dd2..0990efc38 100644 --- a/patterns/structural/bridge.py +++ b/patterns/structural/bridge.py @@ -1,55 +1,56 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - """ *References: http://en.wikibooks.org/wiki/Computer_Science_Design_Patterns/Bridge_Pattern#Python -*TL;DR80 +*TL;DR Decouples an abstraction from its implementation. """ # ConcreteImplementor 1/2 -class DrawingAPI1(object): - def draw_circle(self, x, y, radius): - print('API1.circle at {}:{} radius {}'.format(x, y, radius)) +class DrawingAPI1: + def draw_circle(self, x: int, y: int, radius: float) -> None: + print(f"API1.circle at {x}:{y} radius {radius}") # ConcreteImplementor 2/2 -class DrawingAPI2(object): - def draw_circle(self, x, y, radius): - print('API2.circle at {}:{} radius {}'.format(x, y, radius)) +class DrawingAPI2: + def draw_circle(self, x: int, y: int, radius: float) -> None: + print(f"API2.circle at {x}:{y} radius {radius}") # Refined Abstraction -class CircleShape(object): - def __init__(self, x, y, radius, drawing_api): +class CircleShape: + def __init__( + self, x: int, y: int, radius: float, drawing_api: DrawingAPI2 | DrawingAPI1 + ) -> None: self._x = x self._y = y - self._radius = radius + self._radius: float = radius self._drawing_api = drawing_api # low-level i.e. Implementation specific - def draw(self): + def draw(self) -> None: self._drawing_api.draw_circle(self._x, self._y, self._radius) # high-level i.e. Abstraction specific - def scale(self, pct): + def scale(self, pct: float) -> None: self._radius *= pct def main(): - shapes = (CircleShape(1, 2, 3, DrawingAPI1()), CircleShape(5, 7, 11, DrawingAPI2())) + """ + >>> shapes = (CircleShape(1, 2, 3, DrawingAPI1()), CircleShape(5, 7, 11, DrawingAPI2())) - for shape in shapes: - shape.scale(2.5) - shape.draw() + >>> for shape in shapes: + ... shape.scale(2.5) + ... shape.draw() + API1.circle at 1:2 radius 7.5 + API2.circle at 5:7 radius 27.5 + """ -if __name__ == '__main__': - main() +if __name__ == "__main__": + import doctest -### OUTPUT ### -# API1.circle at 1:2 radius 7.5 -# API2.circle at 5:7 radius 27.5 + doctest.testmod() diff --git a/patterns/structural/composite.py b/patterns/structural/composite.py index 1244b7856..ee0520c6b 100644 --- a/patterns/structural/composite.py +++ b/patterns/structural/composite.py @@ -1,6 +1,3 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - """ *What is this pattern about? The composite pattern describes a group of objects that is treated the @@ -25,62 +22,71 @@ https://en.wikipedia.org/wiki/Composite_pattern https://infinitescript.com/2014/10/the-23-gang-of-three-design-patterns/ -*TL;DR80 +*TL;DR Describes a group of objects that is treated as a single instance. """ +from abc import ABC, abstractmethod + -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 e5252f9c0..a32e2b06d 100644 --- a/patterns/structural/decorator.py +++ b/patterns/structural/decorator.py @@ -1,6 +1,3 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - """ *What is this pattern about? The Decorator pattern is used to dynamically add a new feature to an @@ -23,49 +20,55 @@ *References: https://sourcemaking.com/design_patterns/decorator -*TL;DR80 +*TL;DR Adds behaviour to object without affecting its class. """ -from __future__ import print_function - -class TextTag(object): +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()}" + + +def main(): + """ + >>> simple_hello = TextTag("hello, world!") + >>> special_hello = ItalicWrapper(BoldWrapper(simple_hello)) + + >>> print("before:", simple_hello.render()) + before: hello, world! + + >>> print("after:", special_hello.render()) + after: hello, world! + """ -if __name__ == '__main__': - simple_hello = TextTag("hello, world!") - special_hello = ItalicWrapper(BoldWrapper(simple_hello)) - print("before:", simple_hello.render()) - print("after:", special_hello.render()) +if __name__ == "__main__": + import doctest -### OUTPUT ### -# before: hello, world! -# after: hello, world! + doctest.testmod() diff --git a/patterns/structural/facade.py b/patterns/structural/facade.py index 7a6c57c80..f7b00be32 100644 --- a/patterns/structural/facade.py +++ b/patterns/structural/facade.py @@ -1,7 +1,7 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - """ +Example from https://en.wikipedia.org/wiki/Facade_pattern#Python + + *What is this pattern about? The Facade pattern is a way to provide a simpler unified interface to a more complex system. It provides an easier way to access functions @@ -13,17 +13,6 @@ serves as an unified interface to all the underlying procedures to turn on a computer. -*What does this example do? -The code defines three classes (TC1, TC2, TC3) that represent complex -parts to be tested. Instead of testing each class separately, the -TestRunner class acts as the facade to run all tests with only one -call to the method runAll. By doing that, the client part only needs -to instantiate the class TestRunner and call the runAll method. -As seen in the example, the interface provided by the Facade pattern -is independent from the underlying implementation. Since the client -just calls the runAll method, we can modify the classes TC1, TC2 or -TC3 without impact on the way the client uses the system. - *Where is the pattern used practically? This pattern can be seen in the Python standard library when we use the isdir function. Although a user simply uses this function to know @@ -35,89 +24,74 @@ https://fkromer.github.io/python-pattern-references/design/#facade http://python-3-patterns-idioms-test.readthedocs.io/en/latest/ChangeInterface.html#facade -*TL;DR80 +*TL;DR Provides a simpler unified interface to a complex system. """ -from __future__ import print_function -import time - -SLEEP = 0.1 - - -# Complex Parts -class TC1: - def run(self): - print(u"###### In Test 1 ######") - time.sleep(SLEEP) - print(u"Setting up") - time.sleep(SLEEP) - print(u"Running test") - time.sleep(SLEEP) - print(u"Tearing down") - time.sleep(SLEEP) - print(u"Test Finished\n") - - -class TC2: - def run(self): - print(u"###### In Test 2 ######") - time.sleep(SLEEP) - print(u"Setting up") - time.sleep(SLEEP) - print(u"Running test") - time.sleep(SLEEP) - print(u"Tearing down") - time.sleep(SLEEP) - print(u"Test Finished\n") - - -class TC3: - def run(self): - print(u"###### In Test 3 ######") - time.sleep(SLEEP) - print(u"Setting up") - time.sleep(SLEEP) - print(u"Running test") - time.sleep(SLEEP) - print(u"Tearing down") - time.sleep(SLEEP) - print(u"Test Finished\n") - - -# Facade -class TestRunner: + +# Complex computer parts +class CPU: + """ + Simple CPU representation. + """ + + def freeze(self) -> None: + print("Freezing processor.") + + def jump(self, position: str) -> None: + print("Jumping to:", position) + + def execute(self) -> None: + print("Executing.") + + +class Memory: + """ + Simple memory representation. + """ + + def load(self, position: str, data: str) -> None: + print(f"Loading from {position} data: '{data}'.") + + +class SolidStateDrive: + """ + Simple solid state drive representation. + """ + + def read(self, lba: str, size: str) -> str: + return f"Some data from sector {lba} with size {size}" + + +class ComputerFacade: + """ + Represents a facade for various computer parts. + """ + def __init__(self): - self.tc1 = TC1() - self.tc2 = TC2() - self.tc3 = TC3() - self.tests = [self.tc1, self.tc2, self.tc3] - - def runAll(self): - [i.run() for i in self.tests] - - -# Client -if __name__ == '__main__': - testrunner = TestRunner() - testrunner.runAll() - -### OUTPUT ### -# ###### In Test 1 ###### -# Setting up -# Running test -# Tearing down -# Test Finished -# -# ###### In Test 2 ###### -# Setting up -# Running test -# Tearing down -# Test Finished -# -# ###### In Test 3 ###### -# Setting up -# Running test -# Tearing down -# Test Finished -# + self.cpu = CPU() + self.memory = Memory() + self.ssd = SolidStateDrive() + + def start(self): + self.cpu.freeze() + self.memory.load("0x00", self.ssd.read("100", "1024")) + self.cpu.jump("0x00") + self.cpu.execute() + + +def main(): + """ + >>> computer_facade = ComputerFacade() + >>> computer_facade.start() + Freezing processor. + Loading from 0x00 data: 'Some data from sector 100 with size 1024'. + Jumping to: 0x00 + Executing. + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod(optionflags=doctest.ELLIPSIS) diff --git a/patterns/structural/flyweight.py b/patterns/structural/flyweight.py index c8fba6a23..148a5bc05 100644 --- a/patterns/structural/flyweight.py +++ b/patterns/structural/flyweight.py @@ -1,6 +1,3 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - """ *What is this pattern about? This pattern aims to minimise the number of objects that are needed by @@ -19,122 +16,73 @@ *References: http://codesnipers.com/?q=python-flyweights +https://python-patterns.guide/gang-of-four/flyweight/ + +*Examples in Python ecosystem: +https://docs.python.org/3/library/sys.html#sys.intern -*TL;DR80 +*TL;DR Minimizes memory usage by sharing data with other similar objects. """ import weakref -class FlyweightMeta(type): - def __new__(mcs, name, parents, dct): - """ - Set up object pool - - :param name: class name - :param parents: class parents - :param dct: dict: includes class attributes, class methods, - static methods, etc - :return: new class - """ - dct['pool'] = weakref.WeakValueDictionary() - return super(FlyweightMeta, mcs).__new__(mcs, name, parents, dct) - - @staticmethod - def _serialize_params(cls, *args, **kwargs): - """ - Serialize input parameters to a key. - Simple implementation is just to serialize it as a string - """ - args_list = list(map(str, args)) - args_list.extend([str(kwargs), cls.__name__]) - key = ''.join(args_list) - return key - - def __call__(cls, *args, **kwargs): - key = FlyweightMeta._serialize_params(cls, *args, **kwargs) - pool = getattr(cls, 'pool', {}) - - instance = pool.get(key) - if instance is None: - instance = super(FlyweightMeta, cls).__call__(*args, **kwargs) - pool[key] = instance - return instance - - -class Card(object): - - """The object pool. Has builtin reference counting""" - - _CardPool = weakref.WeakValueDictionary() - - """Flyweight implementation. If the object exists in the - pool just return it (instead of creating a new one)""" - - def __new__(cls, value, suit): - obj = Card._CardPool.get(value + suit) - if not obj: - obj = object.__new__(cls) - Card._CardPool[value + suit] = obj +class Card: + """The Flyweight""" + + value: str + suit: str + + # Could be a simple dict. + # With WeakValueDictionary garbage collection can reclaim the object + # when there are no other references to it. + _pool: weakref.WeakValueDictionary[str, "Card"] = weakref.WeakValueDictionary() + + def __new__(cls, value: str, suit: str) -> "Card": + # If the object exists in the pool - just return it + obj = cls._pool.get(value + suit) + # otherwise - create new one (and add it to the pool) + if obj is None: + obj = object.__new__(Card) + cls._pool[value + suit] = obj + # This row does the part we usually see in `__init__` obj.value, obj.suit = value, suit return obj + # If you uncomment `__init__` and comment-out `__new__` - + # Card becomes normal (non-flyweight). # def __init__(self, value, suit): # self.value, self.suit = value, suit - def __repr__(self): - return "" % (self.value, self.suit) - - -def with_metaclass(meta, *bases): - """ Provide python cross-version metaclass compatibility. """ - return meta("NewBase", bases, {}) - - -class Card2(with_metaclass(FlyweightMeta)): - def __init__(self, *args, **kwargs): - # print('Init {}: {}'.format(self.__class__, (args, kwargs))) - pass - - -if __name__ == '__main__': - # comment __new__ and uncomment __init__ to see the difference - c1 = Card('9', 'h') - c2 = Card('9', 'h') - print(c1, c2) - print(c1 == c2, c1 is c2) - print(id(c1), id(c2)) + def __repr__(self) -> str: + return f"" - c1.temp = None - c3 = Card('9', 'h') - print(hasattr(c3, 'temp')) - c1 = c2 = c3 = None - c3 = Card('9', 'h') - print(hasattr(c3, 'temp')) - # Tests with metaclass - instances_pool = getattr(Card2, 'pool') - cm1 = Card2('10', 'h', a=1) - cm2 = Card2('10', 'h', a=1) - cm3 = Card2('10', 'h', a=2) +def main(): + """ + >>> c1 = Card('9', 'h') + >>> c2 = Card('9', 'h') + >>> c1, c2 + (, ) + >>> c1 == c2 + True + >>> c1 is c2 + True - assert (cm1 == cm2) and (cm1 != cm3) - assert (cm1 is cm2) and (cm1 is not cm3) - assert len(instances_pool) == 2 + >>> c1.new_attr = 'temp' + >>> c3 = Card('9', 'h') + >>> hasattr(c3, 'new_attr') + True - del cm1 - assert len(instances_pool) == 2 + >>> Card._pool.clear() + >>> c4 = Card('9', 'h') + >>> hasattr(c4, 'new_attr') + False + """ - del cm2 - assert len(instances_pool) == 1 - del cm3 - assert len(instances_pool) == 0 +if __name__ == "__main__": + import doctest -### OUTPUT ### -# (, ) -# (True, True) -# (31903856, 31903856) -# True -# False + doctest.testmod() diff --git a/patterns/structural/flyweight_with_metaclass.py b/patterns/structural/flyweight_with_metaclass.py new file mode 100644 index 000000000..ced8d9159 --- /dev/null +++ b/patterns/structural/flyweight_with_metaclass.py @@ -0,0 +1,63 @@ +import weakref + + +class FlyweightMeta(type): + def __new__(mcs, name, parents, dct): + """ + Set up object pool + + :param name: class name + :param parents: class parents + :param dct: dict: includes class attributes, class methods, + static methods, etc + :return: new class + """ + dct["pool"] = weakref.WeakValueDictionary() + return super().__new__(mcs, name, parents, dct) + + @staticmethod + def _serialize_params(cls, *args, **kwargs): + """ + Serialize input parameters to a key. + Simple implementation is just to serialize it as a string + """ + args_list = list(map(str, args)) + args_list.extend([str(kwargs), cls.__name__]) + key = "".join(args_list) + return key + + def __call__(cls, *args, **kwargs): + key = FlyweightMeta._serialize_params(cls, *args, **kwargs) + pool = getattr(cls, "pool", {}) + + instance = pool.get(key) + if instance is None: + instance = super().__call__(*args, **kwargs) + pool[key] = instance + return instance + + +class Card2(metaclass=FlyweightMeta): + def __init__(self, *args, **kwargs): + # print('Init {}: {}'.format(self.__class__, (args, kwargs))) + pass + + +if __name__ == "__main__": + instances_pool = getattr(Card2, "pool") + cm1 = Card2("10", "h", a=1) + cm2 = Card2("10", "h", a=1) + cm3 = Card2("10", "h", a=2) + + assert (cm1 == cm2) and (cm1 != cm3) + assert (cm1 is cm2) and (cm1 is not cm3) + assert len(instances_pool) == 2 + + del cm1 + assert len(instances_pool) == 2 + + del cm2 + assert len(instances_pool) == 1 + + del cm3 + assert len(instances_pool) == 0 diff --git a/patterns/structural/front_controller.py b/patterns/structural/front_controller.py index 580f57fe5..92f58b213 100644 --- a/patterns/structural/front_controller.py +++ b/patterns/structural/front_controller.py @@ -1,56 +1,66 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - """ @author: Gordeev Andrey -*TL;DR80 +*TL;DR Provides a centralized entry point that controls and manages request handling. """ +from __future__ import annotations + +from typing import Any -class MobileView(object): - def show_index_page(self): - print('Displaying mobile index page') +class MobileView: + def show_index_page(self) -> None: + print("Displaying mobile index page") -class TabletView(object): - def show_index_page(self): - print('Displaying tablet index page') +class TabletView: + def show_index_page(self) -> None: + print("Displaying tablet index page") -class Dispatcher(object): - def __init__(self): + +class Dispatcher: + 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(object): - """ front controller """ +class RequestController: + """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(object): - """ request """ +class Request: + """request""" - mobile_type = 'mobile' - tablet_type = 'tablet' + mobile_type = "mobile" + tablet_type = "tablet" def __init__(self, request): self.type = None @@ -61,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 6c7aa9ef4..5726b0897 100644 --- a/patterns/structural/mvc.py +++ b/patterns/structural/mvc.py @@ -1,135 +1,216 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - """ -*TL;DR80 +*TL;DR Separates data in GUIs from the ways it is presented, and accepted. """ +from abc import ABC, abstractmethod +from inspect import signature +from sys import argv +from typing import Any + -class Model(object): - def __iter__(self): - raise NotImplementedError +class Model(ABC): + """The Model is the data layer of the application.""" - def get(self, item): + @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.")) + raise KeyError(str(e) + " not in the model's item list.") -class View(object): - def show_item_list(self, item_type, item_list): - raise NotImplementedError +class View(ABC): + """The View is the presentation layer of the application.""" - def show_item_information(self, item_type, item_name, item_info): + @abstractmethod + def show_item_list(self, item_type: str, item_list: list) -> None: + pass + + @abstractmethod + def show_item_information( + self, item_type: str, item_name: str, item_info: dict + ) -> None: """Will look for item information by iterating over key,value pairs yielded by item_info.items()""" - 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 %s "%s" does not exist in the records' % (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: + """The Controller is the intermediary between the Model and the View.""" -class Controller(object): - def __init__(self, model, view): - self.model = model - self.view = 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 9890a2c37..c12fb051c 100644 --- a/patterns/structural/proxy.py +++ b/patterns/structural/proxy.py @@ -1,59 +1,89 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - """ -*TL;DR80 -Provides an interface to resource that is expensive to duplicate. +*What is this pattern about? +Proxy is used in places where you want to add functionality to a class without +changing its interface. The main class is called `Real Subject`. A client should +use the proxy or the real subject without any code change, so both must have the +same interface. Logging and controlling access to the real subject are some of +the proxy pattern usages. + +*References: +https://refactoring.guru/design-patterns/proxy/python/example +https://python-3-patterns-idioms-test.readthedocs.io/en/latest/Fronting.html + +*TL;DR +Add functionality or logic (e.g. logging, caching, authorization) to a resource +without changing its interface. """ -from __future__ import print_function -import time + +class Subject: + """ + As mentioned in the document, interfaces of both RealSubject and Proxy should + be the same, because the client should be able to use RealSubject or Proxy with + no code change. + + Not all times this interface is necessary. The point is the client should be + able to use RealSubject or Proxy interchangeably with no change in code. + """ + + def do_the_job(self, user: str) -> None: + raise NotImplementedError() -class SalesManager: - def talk(self): - print("Sales Manager ready to talk") +class RealSubject(Subject): + """ + This is the main job doer. External services like payment gateways can be a + good example. + """ + def do_the_job(self, user: str) -> None: + print(f"I am doing the job for {user}") -class Proxy: - def __init__(self): - self.busy = 'No' - self.sales = None - def talk(self): - print("Proxy checking for Sales Manager availability") - if self.busy == 'No': - self.sales = SalesManager() - time.sleep(0.1) - self.sales.talk() +class Proxy(Subject): + def __init__(self) -> None: + self._real_subject = RealSubject() + + def do_the_job(self, user: str) -> None: + """ + logging and controlling access are some examples of proxy usages. + """ + + print(f"[log] Doing the job for {user} is requested.") + + if user == "admin": + self._real_subject.do_the_job(user) else: - time.sleep(0.1) - print("Sales Manager is busy") - - -class NoTalkProxy(Proxy): - def talk(self): - print("Proxy checking for Sales Manager availability") - time.sleep(0.1) - print("This Sales Manager will not talk to you", "whether he/she is busy or not") - - -if __name__ == '__main__': - p = Proxy() - p.talk() - p.busy = 'Yes' - p.talk() - p = NoTalkProxy() - p.talk() - p.busy = 'Yes' - p.talk() - -### OUTPUT ### -# Proxy checking for Sales Manager availability -# Sales Manager ready to talk -# Proxy checking for Sales Manager availability -# Sales Manager is busy -# Proxy checking for Sales Manager availability -# This Sales Manager will not talk to you whether he/she is busy or not -# Proxy checking for Sales Manager availability -# This Sales Manager will not talk to you whether he/she is busy or not + print("[log] I can do the job just for `admins`.") + + +def client(job_doer: RealSubject | Proxy, user: str) -> None: + job_doer.do_the_job(user) + + +def main(): + """ + >>> proxy = Proxy() + + >>> real_subject = RealSubject() + + >>> client(proxy, 'admin') + [log] Doing the job for admin is requested. + I am doing the job for admin + + >>> client(proxy, 'anonymous') + [log] Doing the job for anonymous is requested. + [log] I can do the job just for `admins`. + + >>> client(real_subject, 'admin') + I am doing the job for admin + + >>> client(real_subject, 'anonymous') + I am doing the job for anonymous + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..1e4740463 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,112 @@ +[build-system] +requires = ["setuptools >= 77.0.3"] +build-backend = "setuptools.build_meta" + +[project] +name = "python-patterns" +description = "A collection of design patterns and idioms in Python." +version = "0.1.0" +readme = "README.md" +requires-python = ">=3.10" +classifiers = [ + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", +] +dependencies= [ +] + +maintainers=[ + { name="faif" } +] + +[project.urls] +Homepage = "https://github.com/faif/python-patterns" +Repository = "https://github.com/faif/python-patterns" +"Bug Tracker" = "https://github.com/faif/python-patterns/issues" +Contributors = "https://github.com/faif/python-patterns/graphs/contributors" + +[project.optional-dependencies] +dev = [ + "codecov", + "codespell", + "mypy", + "pipx>=1.7.1", + "pipenv", + "pyupgrade", + "pytest>=6.2.0", + "pytest-cov>=2.11.0", + "pytest-randomly>=3.1.0", + "black>=25.1.0", + "build>=1.2.2", + "isort>=5.7.0", + "flake8>=7.1.0", + "tox>=4.25.0" +] + +[tool.setuptools] +packages = ["patterns"] + +[tool.pytest.ini_options] +filterwarnings = [ + "ignore::Warning:.*test class 'TestRunner'.*" +] +# Adding settings from tox.ini for pytest +testpaths = ["tests"] +#testpaths = ["tests", "patterns"] +python_files = ["test_*.py", "*_test.py"] +# Enable doctest discovery in patterns directory +addopts = "--doctest-modules --randomly-seed=1234 --cov=patterns --cov-report=term-missing" +doctest_optionflags = ["ELLIPSIS", "NORMALIZE_WHITESPACE"] +log_level = "INFO" + +[tool.coverage.run] +branch = true +source = ["./"] +#source = ["patterns"] +# Ensure coverage data is collected properly +relative_files = true +parallel = true +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.black] +target-version = ["py310"] + +[tool.tox] +legacy_tox_ini = """ +[tox] +envlist = py + +[testenv] +package = editable +deps = pipenv +commands_pre = pipenv sync --dev --system +commands = pytest {posargs:tests patterns} +""" diff --git a/pytest_local.ini b/pytest_local.ini new file mode 100644 index 000000000..154db6e67 --- /dev/null +++ b/pytest_local.ini @@ -0,0 +1,3 @@ +[pytest] +addopts = -q +testpaths = tests diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index c33c1dbce..000000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,6 +0,0 @@ --e . -pytest~=4.1 -pytest-cov~=2.6 -flake8~=3.6 -codecov~=2.0 -mock~=2.0 diff --git a/run_all.sh b/run_all.sh deleted file mode 100755 index 50922f3d2..000000000 --- a/run_all.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash -# -# Little helper to run all the scripts, under python coverage if coverage is available -# - -set -eu -failed="" - -if which coverage > /dev/null; then - COVERAGE="`which coverage` run -a" -else - COVERAGE='' -fi -for f in */[^_]*py; do - PYTHONPATH=. python $COVERAGE $f || failed+=" $f" - echo "I: done $f. Exit code $?" -done; - -if [ ! -z "$failed" ]; then - echo "Failed: $failed"; - exit 1 -fi \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index 07c495dc5..000000000 --- a/setup.py +++ /dev/null @@ -1,13 +0,0 @@ -from setuptools import setup - -setup( - name="python-patterns", - 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", - ], -) diff --git a/tests/behavioral/test_catalog.py b/tests/behavioral/test_catalog.py new file mode 100644 index 000000000..9f86db330 --- /dev/null +++ b/tests/behavioral/test_catalog.py @@ -0,0 +1,30 @@ +from patterns.behavioral.catalog import ( + Catalog, + CatalogClass, + CatalogInstance, + CatalogStatic, +) + + +def test_catalog_multiple_methods(): + test = Catalog("param_value_2") + token = test.main_method() + assert token == "executed method 2!" + + +def test_catalog_multiple_instance_methods(): + test = CatalogInstance("param_value_1") + token = test.main_method() + assert token == "Value x1" + + +def test_catalog_multiple_class_methods(): + test = CatalogClass("param_value_2") + token = test.main_method() + assert token == "Value x2" + + +def test_catalog_multiple_static_methods(): + test = CatalogStatic("param_value_1") + token = test.main_method() + assert token == "executed method 1!" diff --git a/tests/behavioral/test_command.py b/tests/behavioral/test_command.py deleted file mode 100644 index 2532af029..000000000 --- a/tests/behavioral/test_command.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import os -import shutil -import unittest -from patterns.behavioral.command import MoveFileCommand - - -class CommandTest(unittest.TestCase): - @classmethod - def __get_test_directory(self): - """ - Get the temporary directory for the tests. - """ - self.test_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'test_command') - - @classmethod - def setUpClass(self): - """ - - Create a temporary directory and file - /test_command - /foo.txt - - get the temporary test directory - - and initializes the command stack. - """ - os.mkdir('tests/behavioral/test_command') - open('tests/behavioral/test_command/foo.txt', 'w').close() - self.__get_test_directory() - self.command_stack = [] - self.command_stack.append( - MoveFileCommand(os.path.join(self.test_dir, 'foo.txt'), os.path.join(self.test_dir, 'bar.txt')) - ) - self.command_stack.append( - MoveFileCommand(os.path.join(self.test_dir, 'bar.txt'), os.path.join(self.test_dir, 'baz.txt')) - ) - - def test_sequential_execution(self): - self.command_stack[0].execute() - output_after_first_execution = os.listdir(self.test_dir) - self.assertEqual(output_after_first_execution[0], 'bar.txt') - self.command_stack[1].execute() - output_after_second_execution = os.listdir(self.test_dir) - self.assertEqual(output_after_second_execution[0], 'baz.txt') - - def test_sequential_undo(self): - self.command_stack = list(reversed(self.command_stack)) - self.command_stack[0].undo() - output_after_first_undo = os.listdir(self.test_dir) - self.assertEqual(output_after_first_undo[0], 'bar.txt') - self.command_stack[1].undo() - output_after_second_undo = os.listdir(self.test_dir) - self.assertEqual(output_after_second_undo[0], 'foo.txt') - - @classmethod - def tearDownClass(self): - """ - Remove the temporary directory /test_command and its content. - """ - shutil.rmtree('tests/behavioral/test_command') diff --git a/tests/behavioral/test_mediator.py b/tests/behavioral/test_mediator.py new file mode 100644 index 000000000..28e862442 --- /dev/null +++ b/tests/behavioral/test_mediator.py @@ -0,0 +1,15 @@ +from patterns.behavioral.mediator import User + + +def test_mediated_comments(): + molly = User("Molly") + mediated_comment = molly.say("Hi Team! Meeting at 3 PM today.") + assert mediated_comment == "[Molly says]: Hi Team! Meeting at 3 PM today." + + mark = User("Mark") + mediated_comment = mark.say("Roger that!") + assert mediated_comment == "[Mark says]: Roger that!" + + ethan = User("Ethan") + mediated_comment = ethan.say("Alright.") + assert mediated_comment == "[Ethan says]: Alright." diff --git a/tests/behavioral/test_memento.py b/tests/behavioral/test_memento.py new file mode 100644 index 000000000..1ae11703b --- /dev/null +++ b/tests/behavioral/test_memento.py @@ -0,0 +1,32 @@ +import pytest + +from patterns.behavioral.memento import NumObj, Transaction + + +def test_object_creation(): + num_obj = NumObj(-1) + assert repr(num_obj) == "", "Object representation not as expected" + + +def test_rollback_on_transaction(): + num_obj = NumObj(-1) + a_transaction = Transaction(True, num_obj) + for _i in range(3): + num_obj.increment() + a_transaction.commit() + assert num_obj.value == 2 + + for _i in range(3): + num_obj.increment() + try: + num_obj.value += "x" # will fail + except TypeError: + a_transaction.rollback() + assert num_obj.value == 2, "Transaction did not rollback as expected" + + +def test_rollback_with_transactional_annotation(): + num_obj = NumObj(2) + with pytest.raises(TypeError): + num_obj.do_stuff() + assert num_obj.value == 2 diff --git a/tests/behavioral/test_observer.py b/tests/behavioral/test_observer.py index 2264a9ee2..821f97a61 100644 --- a/tests/behavioral/test_observer.py +++ b/tests/behavioral/test_observer.py @@ -1,63 +1,33 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import unittest -from patterns.behavioral.observer import Subject, Data, DecimalViewer, HexViewer - -try: - from unittest.mock import patch -except ImportError: - from mock import patch - - -class TestSubject(unittest.TestCase): - @classmethod - def setUpClass(cls): - cls.s = Subject() - cls.dec_obs = DecimalViewer() - cls.hex_obs = HexViewer() - - def test_a_observer_list_shall_be_empty_initially(cls): - cls.assertEqual(len(cls.s._observers), 0) - - def test_b_observers_shall_be_attachable(cls): - cls.s.attach(cls.dec_obs) - cls.assertEqual(isinstance(cls.s._observers[0], DecimalViewer), True) - cls.assertEqual(len(cls.s._observers), 1) - cls.s.attach(cls.hex_obs) - cls.assertEqual(isinstance(cls.s._observers[1], HexViewer), True) - cls.assertEqual(len(cls.s._observers), 2) - - def test_c_observers_shall_be_detachable(cls): - cls.s.detach(cls.dec_obs) - # hex viewer shall be remaining if dec viewer is detached first - cls.assertEqual(isinstance(cls.s._observers[0], HexViewer), True) - cls.assertEqual(len(cls.s._observers), 1) - cls.s.detach(cls.hex_obs) - cls.assertEqual(len(cls.s._observers), 0) - - -class TestData(unittest.TestCase): - @classmethod - def setUpClass(cls): - cls.dec_obs = DecimalViewer() - cls.hex_obs = HexViewer() - cls.sub = Data('Data') - # inherited behavior already tested with TestSubject - cls.sub.attach(cls.dec_obs) - cls.sub.attach(cls.hex_obs) - - def test_data_change_shall_notify_all_observers_once(cls): - with patch.object(cls.dec_obs, 'update') as mock_dec_obs_update, patch.object( - cls.hex_obs, 'update' - ) as mock_hex_obs_update: - cls.sub.data = 10 - cls.assertEqual(mock_dec_obs_update.call_count, 1) - cls.assertEqual(mock_hex_obs_update.call_count, 1) - - def test_data_value_shall_be_changeable(cls): - cls.sub.data = 20 - cls.assertEqual(cls.sub._data, 20) - - def test_data_name_shall_be_changeable(cls): - cls.sub.name = 'New Data Name' - cls.assertEqual(cls.sub.name, 'New Data Name') +from unittest.mock import Mock, patch + +import pytest + +from patterns.behavioral.observer import Data, DecimalViewer, HexViewer + + +@pytest.fixture +def observable(): + return Data("some data") + + +def test_attach_detach(observable): + decimal_viewer = DecimalViewer() + assert len(observable._observers) == 0 + + observable.attach(decimal_viewer) + assert decimal_viewer in observable._observers + + observable.detach(decimal_viewer) + assert decimal_viewer not in observable._observers + + +def test_one_data_change_notifies_each_observer_once(observable): + observable.attach(DecimalViewer()) + observable.attach(HexViewer()) + + with patch( + "patterns.behavioral.observer.DecimalViewer.update", new_callable=Mock() + ) as mocked_update: + assert mocked_update.call_count == 0 + observable.data = 10 + assert mocked_update.call_count == 1 diff --git a/tests/behavioral/test_publish_subscribe.py b/tests/behavioral/test_publish_subscribe.py index 20bd93c54..6f142855d 100644 --- a/tests/behavioral/test_publish_subscribe.py +++ b/tests/behavioral/test_publish_subscribe.py @@ -1,67 +1,69 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import unittest -from patterns.behavioral.publish_subscribe import Provider, Publisher, Subscriber +from unittest.mock import call, patch -try: - from unittest.mock import patch, call -except ImportError: - from mock import patch, call +from patterns.behavioral.publish_subscribe import Provider, Publisher, Subscriber -class TestProvider(unittest.TestCase): +class TestProvider: """ Integration tests ~ provider class with as little mocking as possible. """ 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) + assert len(pro.subscribers) == 0 + sub = Subscriber("sub name", pro) sub.subscribe(subscription) - cls.assertEqual(len(pro.subscribers[subscription]), 1) + assert 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) + assert len(pro.subscribers[subscription]) == 1 sub.unsubscribe(subscription) - cls.assertEqual(len(pro.subscribers[subscription]), 0) + assert 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) - cls.assertEqual(len(pro.msg_queue), 0) + Subscriber("sub name", pro) + assert len(pro.msg_queue) == 0 pub.publish(expected_msg) - cls.assertEqual(len(pro.msg_queue), 1) - cls.assertEqual(pro.msg_queue[0], expected_msg) + assert len(pro.msg_queue) == 1 + assert 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: + assert mock_subscriber1_run.call_count == 0 + assert mock_subscriber2_run.call_count == 0 + pub.publish("sub 1 msg 1") + pub.publish("sub 1 msg 2") + pub.publish("sub 2 msg 1") + pub.publish("sub 2 msg 2") + with ( + patch.object(sub1, "run") as mock_subscriber1_run, + patch.object(sub2, "run") as mock_subscriber2_run, + ): pro.update() - expected_sub1_calls = [call('sub 1 msg 1'), call('sub 1 msg 2')] + 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_servant.py b/tests/behavioral/test_servant.py new file mode 100644 index 000000000..b3ab45d91 --- /dev/null +++ b/tests/behavioral/test_servant.py @@ -0,0 +1,41 @@ +import math + +import pytest + +from patterns.behavioral.servant import Circle, GeometryTools, Position, Rectangle + + +@pytest.fixture +def circle(): + return Circle(3, Position(0, 0)) + + +@pytest.fixture +def rectangle(): + return Rectangle(4, 5, Position(0, 0)) + + +def test_calculate_area(circle, rectangle): + assert GeometryTools.calculate_area(circle) == math.pi * 3**2 + assert GeometryTools.calculate_area(rectangle) == 4 * 5 + + with pytest.raises(ValueError): + GeometryTools.calculate_area("invalid shape") + + +def test_calculate_perimeter(circle, rectangle): + assert GeometryTools.calculate_perimeter(circle) == 2 * math.pi * 3 + assert GeometryTools.calculate_perimeter(rectangle) == 2 * (4 + 5) + + with pytest.raises(ValueError): + GeometryTools.calculate_perimeter("invalid shape") + + +def test_move_to(circle, rectangle): + new_position = Position(1, 1) + GeometryTools.move_to(circle, new_position) + assert circle.position == new_position + + new_position = Position(1, 1) + GeometryTools.move_to(rectangle, new_position) + assert rectangle.position == new_position diff --git a/tests/behavioral/test_state.py b/tests/behavioral/test_state.py index 4009820b0..77473f519 100644 --- a/tests/behavioral/test_state.py +++ b/tests/behavioral/test_state.py @@ -1,56 +1,27 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import unittest +import pytest + from patterns.behavioral.state import Radio -class RadioTest(unittest.TestCase): - """ - Attention: Test case results depend on test case execution. The test cases - in this integration test class should be executed in an explicit order: - http://stackoverflow.com/questions/5387299/python-unittest-testcase-execution-order - """ - - @classmethod - def setUpClass(self): - self.radio = Radio() - - def test_initial_state(self): - state = self.radio.state.name - expected_state_name = 'AM' - self.assertEqual(state, expected_state_name) - - def test_initial_am_station(self): - station = self.radio.state.stations[self.radio.state.pos] - expected_station = '1250' - self.assertEqual(station, expected_station) - - def test_2nd_am_station_after_scan(self): - self.radio.scan() - station = self.radio.state.stations[self.radio.state.pos] - expected_station = '1380' - self.assertEqual(station, expected_station) - - def test_3rd_am_station_after_scan(self): - self.radio.scan() - station = self.radio.state.stations[self.radio.state.pos] - expected_station = '1510' - self.assertEqual(station, expected_station) - - def test_am_station_overflow_after_scan(self): - self.radio.scan() - station = self.radio.state.stations[self.radio.state.pos] - expected_station = '1250' - self.assertEqual(station, expected_station) - - def test_shall_toggle_from_am_to_fm(self): - self.radio.toggle_amfm() - state = self.radio.state.name - expected_state_name = 'FM' - self.assertEqual(state, expected_state_name) - - def test_shall_toggle_from_fm_to_am(self): - self.radio.toggle_amfm() - state = self.radio.state.name - expected_state_name = 'AM' - self.assertEqual(state, expected_state_name) +@pytest.fixture +def radio(): + return Radio() + + +def test_initial_state(radio): + assert radio.state.name == "AM" + + +def test_initial_am_station(radio): + initial_pos = radio.state.pos + assert radio.state.stations[initial_pos] == "1250" + + +def test_toggle_amfm(radio): + assert radio.state.name == "AM" + + radio.toggle_amfm() + assert radio.state.name == "FM" + + radio.toggle_amfm() + assert radio.state.name == "AM" diff --git a/tests/behavioral/test_strategy.py b/tests/behavioral/test_strategy.py new file mode 100644 index 000000000..53976f389 --- /dev/null +++ b/tests/behavioral/test_strategy.py @@ -0,0 +1,41 @@ +import pytest + +from patterns.behavioral.strategy import Order, on_sale_discount, ten_percent_discount + + +@pytest.fixture +def order(): + return Order(100) + + +@pytest.mark.parametrize( + "func, discount", [(ten_percent_discount, 10.0), (on_sale_discount, 45.0)] +) +def test_discount_function_return(func, order, discount): + assert func(order) == discount + + +@pytest.mark.parametrize( + "func, price", [(ten_percent_discount, 100), (on_sale_discount, 100)] +) +def test_order_discount_strategy_validate_success(func, price): + order = Order(price, func) + + assert order.price == price + assert order.discount_strategy == func + + +def test_order_discount_strategy_validate_error(): + order = Order(10, discount_strategy=on_sale_discount) + + assert order.discount_strategy is None + + +@pytest.mark.parametrize( + "func, price, discount", + [(ten_percent_discount, 100, 90.0), (on_sale_discount, 100, 55.0)], +) +def test_discount_apply_success(func, price, discount): + order = Order(price, func) + + assert order.apply_discount() == discount diff --git a/tests/behavioral/test_visitor.py b/tests/behavioral/test_visitor.py new file mode 100644 index 000000000..6826679b4 --- /dev/null +++ b/tests/behavioral/test_visitor.py @@ -0,0 +1,26 @@ +import pytest + +from patterns.behavioral.visitor import A, B, C, Visitor + + +@pytest.fixture +def visitor(): + return Visitor() + + +def test_visiting_generic_node(visitor): + a = A() + token = visitor.visit(a) + assert token == "generic_visit A", "The expected generic object was not called" + + +def test_visiting_specific_nodes(visitor): + b = B() + token = visitor.visit(b) + assert token == "visit_B B", "The expected specific object was not called" + + +def test_visiting_inherited_nodes(visitor): + c = C() + token = visitor.visit(c) + assert token == "visit_B C", "The expected inherited object was not called" diff --git a/tests/creational/test_abstract_factory.py b/tests/creational/test_abstract_factory.py index 5ecaf00ae..e717fafcd 100644 --- a/tests/creational/test_abstract_factory.py +++ b/tests/creational/test_abstract_factory.py @@ -1,17 +1,12 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import unittest -from patterns.creational.abstract_factory import PetShop, Dog +from unittest.mock import patch -try: - from unittest.mock import patch -except ImportError: - from mock import patch +from patterns.creational.abstract_factory import Dog, PetShop -class TestPetShop(unittest.TestCase): +class TestPetShop: 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() - self.assertEqual(mock_Dog_speak.call_count, 1) + with patch.object(Dog, "speak") as mock_Dog_speak: + pet = dog_pet_shop.buy_pet("") + pet.speak() + assert mock_Dog_speak.call_count == 1 diff --git a/tests/creational/test_borg.py b/tests/creational/test_borg.py index 62e9efe43..61785020a 100644 --- a/tests/creational/test_borg.py +++ b/tests/creational/test_borg.py @@ -1,24 +1,26 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import unittest from patterns.creational.borg import Borg, YourBorg -class BorgTest(unittest.TestCase): - def setUp(self): +class TestBorg: + def setup_method(self): self.b1 = Borg() self.b2 = Borg() + # creating YourBorg instance implicitly sets the state attribute + # for all borg instances. self.ib1 = YourBorg() + def teardown_method(self): + self.ib1.state = "Init" + def test_initial_borg_state_shall_be_init(self): b = Borg() - self.assertEqual(b.state, 'Init') + assert 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" + assert self.b1.state == "Running" + assert self.b2.state == "Running" + assert self.ib1.state == "Running" def test_instances_shall_have_own_ids(self): - self.assertNotEqual(id(self.b1), id(self.b2), id(self.ib1)) + assert id(self.b1) != id(self.b2), id(self.ib1) diff --git a/tests/creational/test_builder.py b/tests/creational/test_builder.py index 4b63531a7..539d7994b 100644 --- a/tests/creational/test_builder.py +++ b/tests/creational/test_builder.py @@ -1,23 +1,20 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -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): +class TestSimple: def test_house(self): house = House() - self.assertEqual(house.size, 'Big') - self.assertEqual(house.floor, 'One') + assert house.size == "Big" + assert house.floor == "One" def test_flat(self): flat = Flat() - self.assertEqual(flat.size, 'Small') - self.assertEqual(flat.floor, 'More than One') + assert flat.size == "Small" + assert flat.floor == "More than One" -class TestComplex(unittest.TestCase): +class TestComplex: def test_house(self): house = construct_building(ComplexHouse) - self.assertEqual(house.size, 'Big and fancy') - self.assertEqual(house.floor, 'One') + assert house.size == "Big and fancy" + assert house.floor == "One" diff --git a/tests/creational/test_factory.py b/tests/creational/test_factory.py new file mode 100644 index 000000000..7d18211d8 --- /dev/null +++ b/tests/creational/test_factory.py @@ -0,0 +1,30 @@ +from patterns.creational.factory import EnglishLocalizer, GreekLocalizer, get_localizer + + +class TestFactory: + def test_get_localizer_greek(self): + localizer = get_localizer("Greek") + assert isinstance(localizer, GreekLocalizer) + assert localizer.localize("dog") == "σκύλος" + assert localizer.localize("cat") == "γάτα" + # Test unknown word returns the word itself + assert localizer.localize("monkey") == "monkey" + + def test_get_localizer_english(self): + localizer = get_localizer("English") + assert isinstance(localizer, EnglishLocalizer) + assert localizer.localize("dog") == "dog" + assert localizer.localize("cat") == "cat" + + def test_get_localizer_default(self): + # Test default argument + localizer = get_localizer() + assert isinstance(localizer, EnglishLocalizer) + + def test_get_localizer_unknown_language(self): + # Test fallback for unknown language if applicable, + # or just verify what happens. + # Based on implementation: localizers.get(language, EnglishLocalizer)() + # It defaults to EnglishLocalizer for unknown keys. + localizer = get_localizer("Spanish") + assert isinstance(localizer, EnglishLocalizer) diff --git a/tests/creational/test_factory_method.py b/tests/creational/test_factory_method.py deleted file mode 100644 index dd6ae66e6..000000000 --- a/tests/creational/test_factory_method.py +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import unittest -from patterns.creational.factory_method import get_localizer - - -class TestLocalizer(unittest.TestCase): - def setUp(self): - self.e, self.g = get_localizer(language="English"), get_localizer(language="Greek") - - def test_parrot_eng_localization(self): - self.assertEqual(self.e.get('parrot'), 'parrot') - - def test_parrot_greek_localization(self): - self.assertEqual(self.g.get('parrot'), 'parrot') - - def test_dog_eng_localization(self): - self.assertEqual(self.e.get('dog'), 'dog') - - def test_dog_greek_localization(self): - self.assertEqual(self.g.get('dog'), 'σκύλος') - - def test_cat_eng_localization(self): - self.assertEqual(self.e.get('cat'), 'cat') - - def test_cat_greek_localization(self): - self.assertEqual(self.g.get('cat'), 'γάτα') - - def test_bear_eng_localization(self): - self.assertEqual(self.e.get('bear'), 'bear') - - def test_bear_greek_localization(self): - self.assertEqual(self.g.get('bear'), 'bear') diff --git a/tests/creational/test_lazy.py b/tests/creational/test_lazy.py index c61ee9a16..d1d02a1ba 100644 --- a/tests/creational/test_lazy.py +++ b/tests/creational/test_lazy.py @@ -1,32 +1,34 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -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') +class TestDynamicExpanding: + def setup_method(self): + self.John = Person("John", "Coder") def test_innate_properties(self): - self.assertDictEqual({'name': 'John', 'occupation': 'Coder', 'call_count2': 0}, self.John.__dict__) + assert { + "name": "John", + "occupation": "Coder", + "call_count2": 0, + } == self.John.__dict__ def test_relatives_not_in_properties(self): - self.assertNotIn('relatives', self.John.__dict__) + assert "relatives" not in self.John.__dict__ def test_extended_properties(self): - print(u"John's relatives: {0}".format(self.John.relatives)) - self.assertDictEqual( - {'name': 'John', 'occupation': 'Coder', 'relatives': 'Many relatives.', 'call_count2': 0}, - self.John.__dict__, - ) + print(f"John's relatives: {self.John.relatives}") + assert { + "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}") + assert "relatives" in self.John.__dict__ def test_parents(self): for _ in range(2): - self.assertEqual(self.John.parents, "Father and mother") - self.assertEqual(self.John.call_count2, 1) + assert self.John.parents == "Father and mother" + assert self.John.call_count2 == 1 diff --git a/tests/creational/test_pool.py b/tests/creational/test_pool.py index 1217ee2cd..97b9d269e 100644 --- a/tests/creational/test_pool.py +++ b/tests/creational/test_pool.py @@ -1,53 +1,46 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import unittest - -try: - import queue -except ImportError: # python 2.x compatibility - import Queue as queue +import queue + from patterns.creational.pool import ObjectPool -class TestPool(unittest.TestCase): - def setUp(self): +class TestPool: + def setup_method(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.assertFalse(self.sample_queue.empty()) - self.assertTrue(self.sample_queue.get() == 'first') - self.assertTrue(self.sample_queue.empty()) + assert pool == "first" + assert self.sample_queue.get() == "second" + assert not self.sample_queue.empty() + assert self.sample_queue.get() == "first" + assert self.sample_queue.empty() def test_frozen_pool(self): with ObjectPool(self.sample_queue) as pool: - self.assertEqual(pool, 'first') - self.assertEqual(pool, 'first') - self.assertTrue(self.sample_queue.get() == 'second') - self.assertFalse(self.sample_queue.empty()) - self.assertTrue(self.sample_queue.get() == 'first') - self.assertTrue(self.sample_queue.empty()) - + assert pool == "first" + assert pool == "first" + assert self.sample_queue.get() == "second" + assert not self.sample_queue.empty() + assert self.sample_queue.get() == "first" + assert self.sample_queue.empty() -class TestNaitivePool(unittest.TestCase): +class TestNaitivePool: """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.assertFalse(sample_queue.empty()) - self.assertTrue(sample_queue.get() == 'yam') - self.assertTrue(sample_queue.empty()) + assert obj == "yam" + assert not sample_queue.empty() + assert sample_queue.get() == "yam" + assert sample_queue.empty() # sample_queue.put('sam') # test_object(sample_queue) diff --git a/tests/creational/test_prototype.py b/tests/creational/test_prototype.py index 2e462d1c2..ca5d77724 100644 --- a/tests/creational/test_prototype.py +++ b/tests/creational/test_prototype.py @@ -1,49 +1,48 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import unittest +import pytest + from patterns.creational.prototype import Prototype, PrototypeDispatcher -class TestPrototypeFeatures(unittest.TestCase): - def setUp(self): +class TestPrototypeFeatures: + def setup_method(self): self.prototype = Prototype() def test_cloning_propperty_innate_values(self): sample_object_1 = self.prototype.clone() sample_object_2 = self.prototype.clone() - self.assertEqual(sample_object_1.value, sample_object_2.value) + assert sample_object_1.value == sample_object_2.value def test_extended_property_values_cloning(self): sample_object_1 = self.prototype.clone() - sample_object_1.some_value = 'test string' + sample_object_1.some_value = "test string" sample_object_2 = self.prototype.clone() - self.assertRaises(AttributeError, lambda: sample_object_2.some_value) + pytest.raises(AttributeError, lambda: sample_object_2.some_value) def test_cloning_propperty_assigned_values(self): sample_object_1 = self.prototype.clone() - sample_object_2 = self.prototype.clone(value='re-assigned') - self.assertNotEqual(sample_object_1.value, sample_object_2.value) + sample_object_2 = self.prototype.clone(value="re-assigned") + assert sample_object_1.value != sample_object_2.value -class TestDispatcherFeatures(unittest.TestCase): - def setUp(self): +class TestDispatcherFeatures: + def setup_method(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) + assert 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') + assert self.dispatcher.get_objects()["A"].value == "a-value" + assert self.dispatcher.get_objects()["B"].value == "b-value" + assert 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) + assert self.dispatcher.get_objects()["A"].ext_value == "E" + assert self.dispatcher.get_objects()["B"].diff diff --git a/tests/dft/test_constructor_injection.py b/tests/dft/test_constructor_injection.py deleted file mode 100644 index 6ee836019..000000000 --- a/tests/dft/test_constructor_injection.py +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import unittest - -from patterns.dft.constructor_injection import TimeDisplay, MidnightTimeProvider, ProductionCodeTimeProvider, datetime - -""" -Port of the Java example of "Constructor Injection" in -"xUnit Test Patterns - Refactoring Test Code" by Gerard Meszaros -(ISBN-10: 0131495054, ISBN-13: 978-0131495050) - -Test code which will almost always fail (if not exactly 12:01) when untestable -production code (production code time provider is datetime) is used: - - def test_display_current_time_at_midnight(self): - class_under_test = TimeDisplay() - expected_time = "24:01" - result = class_under_test.get_current_time_as_as_html_fragment() - self.assertEqual(result, expected_time) -""" - - -class ConstructorInjectionTest(unittest.TestCase): - def test_display_current_time_at_midnight(self): - """ - Will almost always fail (despite of right at/after midnight). - """ - time_provider_stub = MidnightTimeProvider() - class_under_test = TimeDisplay(time_provider_stub) - expected_time = "24:01" - self.assertEqual(class_under_test.get_current_time_as_html_fragment(), expected_time) - - def test_display_current_time_at_current_time(self): - """ - Just as justification for working example. (Will always pass.) - """ - production_code_time_provider = ProductionCodeTimeProvider() - class_under_test = TimeDisplay(production_code_time_provider) - current_time = datetime.datetime.now() - expected_time = "{}:{}".format(current_time.hour, current_time.minute) - self.assertEqual(class_under_test.get_current_time_as_html_fragment(), expected_time) diff --git a/tests/dft/test_parameter_injection.py b/tests/dft/test_parameter_injection.py deleted file mode 100644 index da340b937..000000000 --- a/tests/dft/test_parameter_injection.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import unittest - -from patterns.dft.parameter_injection import TimeDisplay, MidnightTimeProvider, ProductionCodeTimeProvider, datetime - -""" -Port of the Java example of "Parameter Injection" in -"xUnit Test Patterns - Refactoring Test Code" by Gerard Meszaros -(ISBN-10: 0131495054, ISBN-13: 978-0131495050) accessible in outdated version on -http://xunitpatterns.com/Dependency%20Injection.html. - -Test code which will almost always fail (if not exactly 12:01) when untestable -production code (have a look into constructor_injection.py) is used: - - def test_display_current_time_at_midnight(self): - class_under_test = TimeDisplay() - expected_time = "24:01" - result = class_under_test.get_current_time_as_as_html_fragment() - self.assertEqual(result, expected_time) -""" - - -class ParameterInjectionTest(unittest.TestCase): - def test_display_current_time_at_midnight(self): - """ - Would almost always fail (despite of right at/after midnight) if - untestable production code would have been used. - """ - time_provider_stub = MidnightTimeProvider() - class_under_test = TimeDisplay() - expected_time = "24:01" - self.assertEqual(class_under_test.get_current_time_as_html_fragment(time_provider_stub), expected_time) - - def test_display_current_time_at_current_time(self): - """ - Just as justification for working example with the time provider used in - production. (Will always pass.) - """ - production_code_time_provider = ProductionCodeTimeProvider() - class_under_test = TimeDisplay() - current_time = datetime.datetime.now() - expected_time = "{}:{}".format(current_time.hour, current_time.minute) - self.assertEqual( - class_under_test.get_current_time_as_html_fragment(production_code_time_provider), expected_time - ) diff --git a/tests/dft/test_setter_injection.py b/tests/dft/test_setter_injection.py deleted file mode 100644 index fce7b2c9f..000000000 --- a/tests/dft/test_setter_injection.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import unittest - -from patterns.dft.setter_injection import TimeDisplay, MidnightTimeProvider, ProductionCodeTimeProvider, datetime - -""" -Port of the Java example of "Setter Injection" in -"xUnit Test Patterns - Refactoring Test Code" by Gerard Meszaros -(ISBN-10: 0131495054, ISBN-13: 978-0131495050) accessible in outdated version on -http://xunitpatterns.com/Dependency%20Injection.html. - -Test code which will almost always fail (if not exactly 12:01) when untestable -production code (have a look into constructor_injection.py) is used: - - def test_display_current_time_at_midnight(self): - class_under_test = TimeDisplay() - expected_time = "24:01" - result = class_under_test.get_current_time_as_as_html_fragment() - self.assertEqual(result, expected_time) -""" - - -class ParameterInjectionTest(unittest.TestCase): - def test_display_current_time_at_midnight(self): - """ - Would almost always fail (despite of right at/after midnight) if - untestable production code would have been used. - """ - time_provider_stub = MidnightTimeProvider() - class_under_test = TimeDisplay() - class_under_test.set_time_provider(time_provider_stub) - expected_time = "24:01" - self.assertEqual(class_under_test.get_current_time_as_html_fragment(), expected_time) - - def test_display_current_time_at_current_time(self): - """ - Just as justification for working example with the time provider used in - production. (Will always pass.) - """ - production_code_time_provider = ProductionCodeTimeProvider() - class_under_test = TimeDisplay() - class_under_test.set_time_provider(production_code_time_provider) - current_time = datetime.datetime.now() - expected_time = "{}:{}".format(current_time.hour, current_time.minute) - self.assertEqual(class_under_test.get_current_time_as_html_fragment(), expected_time) diff --git a/tests/fundamental/test_delegation.py b/tests/fundamental/test_delegation.py new file mode 100644 index 000000000..f7124b14a --- /dev/null +++ b/tests/fundamental/test_delegation.py @@ -0,0 +1,16 @@ +import pytest + +from patterns.fundamental.delegation_pattern import Delegate, Delegator + + +def test_delegator_delegates_attribute_and_call(): + d = Delegator(Delegate()) + assert d.p1 == 123 + assert d.do_something("something") == "Doing something" + assert d.do_something("something", kw=", hi") == "Doing something, hi" + + +def test_delegator_missing_attribute_raises(): + d = Delegator(Delegate()) + with pytest.raises(AttributeError): + _ = d.p2 diff --git a/tests/structural/test_adapter.py b/tests/structural/test_adapter.py index 667c2fb53..042a32f26 100644 --- a/tests/structural/test_adapter.py +++ b/tests/structural/test_adapter.py @@ -1,11 +1,8 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -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): - def setUp(self): +class TestClass: + def setup_method(self): self.dog = Dog() self.cat = Cat() self.human = Human() @@ -14,57 +11,57 @@ def setUp(self): def test_dog_shall_bark(self): noise = self.dog.bark() expected_noise = "woof!" - self.assertEqual(noise, expected_noise) + assert noise == expected_noise def test_cat_shall_meow(self): noise = self.cat.meow() expected_noise = "meow!" - self.assertEqual(noise, expected_noise) + assert noise == expected_noise def test_human_shall_speak(self): noise = self.human.speak() expected_noise = "'hello'" - self.assertEqual(noise, expected_noise) + assert noise == expected_noise def test_car_shall_make_loud_noise(self): noise = self.car.make_noise(1) expected_noise = "vroom!" - self.assertEqual(noise, expected_noise) + assert noise == expected_noise def test_car_shall_make_very_loud_noise(self): noise = self.car.make_noise(10) expected_noise = "vroom!!!!!!!!!!" - self.assertEqual(noise, expected_noise) + assert noise == expected_noise -class AdapterTest(unittest.TestCase): +class TestAdapter: def test_dog_adapter_shall_make_noise(self): dog = Dog() dog_adapter = Adapter(dog, make_noise=dog.bark) noise = dog_adapter.make_noise() expected_noise = "woof!" - self.assertEqual(noise, expected_noise) + assert noise == expected_noise def test_cat_adapter_shall_make_noise(self): cat = Cat() cat_adapter = Adapter(cat, make_noise=cat.meow) noise = cat_adapter.make_noise() expected_noise = "meow!" - self.assertEqual(noise, expected_noise) + assert noise == expected_noise def test_human_adapter_shall_make_noise(self): human = Human() human_adapter = Adapter(human, make_noise=human.speak) noise = human_adapter.make_noise() expected_noise = "'hello'" - self.assertEqual(noise, expected_noise) + assert noise == expected_noise def test_car_adapter_shall_make_loud_noise(self): car = Car() car_adapter = Adapter(car, make_noise=car.make_noise) noise = car_adapter.make_noise(1) expected_noise = "vroom!" - self.assertEqual(noise, expected_noise) + assert noise == expected_noise def test_car_adapter_shall_make_very_loud_noise(self): car = Car() @@ -72,4 +69,4 @@ def test_car_adapter_shall_make_very_loud_noise(self): noise = car_adapter.make_noise(10) expected_noise = "vroom!!!!!!!!!!" - self.assertEqual(noise, expected_noise) + assert noise == expected_noise diff --git a/tests/structural/test_bridge.py b/tests/structural/test_bridge.py index 5541624c9..bcf2a46d6 100644 --- a/tests/structural/test_bridge.py +++ b/tests/structural/test_bridge.py @@ -1,27 +1,22 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import unittest -from patterns.structural.bridge import DrawingAPI1, DrawingAPI2, CircleShape +from unittest.mock import patch -try: - from unittest.mock import patch -except ImportError: - from mock import patch +from patterns.structural.bridge import CircleShape, DrawingAPI1, DrawingAPI2 -class BridgeTest(unittest.TestCase): +class TestBridge: def test_bridge_shall_draw_with_concrete_api_implementation(cls): ci1 = DrawingAPI1() ci2 = DrawingAPI2() - with patch.object(ci1, 'draw_circle') as mock_ci1_draw_circle, patch.object( - ci2, 'draw_circle' - ) as mock_ci2_draw_circle: + with ( + patch.object(ci1, "draw_circle") as mock_ci1_draw_circle, + patch.object(ci2, "draw_circle") as mock_ci2_draw_circle, + ): sh1 = CircleShape(1, 2, 3, ci1) sh1.draw() - cls.assertEqual(mock_ci1_draw_circle.call_count, 1) + assert mock_ci1_draw_circle.call_count == 1 sh2 = CircleShape(1, 2, 3, ci2) sh2.draw() - cls.assertEqual(mock_ci2_draw_circle.call_count, 1) + assert mock_ci2_draw_circle.call_count == 1 def test_bridge_shall_scale_both_api_circles_with_own_implementation(cls): SCALE_FACTOR = 2 @@ -36,10 +31,13 @@ def test_bridge_shall_scale_both_api_circles_with_own_implementation(cls): sh2 = CircleShape(1, 2, CIRCLE2_RADIUS, ci2) sh1.scale(SCALE_FACTOR) sh2.scale(SCALE_FACTOR) - cls.assertEqual(sh1._radius, EXPECTED_CIRCLE1_RADIUS) - cls.assertEqual(sh2._radius, EXPECTED_CIRCLE2_RADIUS) - with patch.object(sh1, 'scale') as mock_sh1_scale_circle, patch.object(sh2, 'scale') as mock_sh2_scale_circle: + assert sh1._radius == EXPECTED_CIRCLE1_RADIUS + assert sh2._radius == EXPECTED_CIRCLE2_RADIUS + with ( + patch.object(sh1, "scale") as mock_sh1_scale_circle, + patch.object(sh2, "scale") as mock_sh2_scale_circle, + ): sh1.scale(2) sh2.scale(2) - cls.assertEqual(mock_sh1_scale_circle.call_count, 1) - cls.assertEqual(mock_sh2_scale_circle.call_count, 1) + assert mock_sh1_scale_circle.call_count == 1 + assert mock_sh2_scale_circle.call_count == 1 diff --git a/tests/structural/test_decorator.py b/tests/structural/test_decorator.py index 2b6933c7f..04a7a112e 100644 --- a/tests/structural/test_decorator.py +++ b/tests/structural/test_decorator.py @@ -1,18 +1,18 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -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') +class TestTextWrapping: + def setup_method(self): + self.raw_string = TextTag("raw but not cruel") def test_italic(self): - self.assertEqual(ItalicWrapper(self.raw_string).render(), 'raw but not cruel') + assert ItalicWrapper(self.raw_string).render() == "raw but not cruel" def test_bold(self): - self.assertEqual(BoldWrapper(self.raw_string).render(), 'raw but not cruel') + assert 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') + assert ( + BoldWrapper(ItalicWrapper(self.raw_string)).render() + == "raw but not cruel" + ) diff --git a/tests/structural/test_facade.py b/tests/structural/test_facade.py index a3765d28a..2ff24ca3a 100644 --- a/tests/structural/test_facade.py +++ b/tests/structural/test_facade.py @@ -1,56 +1,11 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import unittest -import sys +from patterns.structural.facade import ComputerFacade -try: - from io import StringIO -except ImportError: - from StringIO import StringIO -from patterns.structural.facade import TestRunner, TC1, TC2, TC3 - -class TestRunnerFacilities(unittest.TestCase): - def setUp(self): - self.tc1 = TC1() - self.tc2 = TC2() - self.tc3 = TC3() - self.average_result_tc1 = ( - "###### In Test 1 ######\n" + "Setting up\n" + "Running test\n" + "Tearing down\n" + "Test Finished" - ) - self.average_result_tc2 = ( - "###### In Test 2 ######\n" + "Setting up\n" + "Running test\n" + "Tearing down\n" + "Test Finished" - ) - self.average_result_tc3 = ( - "###### In Test 3 ######\n" + "Setting up\n" + "Running test\n" + "Tearing down\n" + "Test Finished" - ) - self.runner = TestRunner() - self.out = StringIO() - self.saved_stdout = sys.stdout - sys.stdout = self.out - - def tearDown(self): - self.out.close() - sys.stdout = self.saved_stdout - - def test_tc1_output(self): - self.tc1.run() - output = self.out.getvalue().strip() - self.assertEqual(output, self.average_result_tc1) - - def test_tc2_output(self): - self.tc2.run() - output = self.out.getvalue().strip() - self.assertEqual(output, self.average_result_tc2) - - def test_tc3_output(self): - self.tc3.run() - output = self.out.getvalue().strip() - self.assertEqual(output, self.average_result_tc3) - - def test_bunch_launch(self): - self.runner.runAll() - output = self.out.getvalue().strip() - self.assertEqual( - output, str(self.average_result_tc1 + '\n\n' + self.average_result_tc2 + '\n\n' + self.average_result_tc3) - ) +def test_computer_facade_start(capsys): + cf = ComputerFacade() + cf.start() + out = capsys.readouterr().out + assert "Freezing processor." in out + assert "Loading from 0x00 data:" in out + assert "Jumping to: 0x00" in out + assert "Executing." in out diff --git a/tests/structural/test_flyweight.py b/tests/structural/test_flyweight.py index 82cdefea5..a200203f9 100644 --- a/tests/structural/test_flyweight.py +++ b/tests/structural/test_flyweight.py @@ -1,37 +1,20 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import unittest from patterns.structural.flyweight import Card -class TestCard(unittest.TestCase): - def test_instances_shall_reference_same_object(self): - c1 = Card('9', 'h') - c2 = Card('9', 'h') - self.assertEqual(c1, c2) - self.assertEqual(id(c1), id(c2)) +def test_card_flyweight_identity_and_repr(): + c1 = Card("9", "h") + c2 = Card("9", "h") + assert c1 is c2 + assert repr(c1) == "" - def test_instances_with_different_suit(self): - """ - shall reference different objects - """ - c1 = Card('9', 'a') - c2 = Card('9', 'b') - self.assertNotEqual(id(c1), id(c2)) - def test_instances_with_different_values(self): - """ - shall reference different objects - """ - c1 = Card('9', 'h') - c2 = Card('A', 'h') - self.assertNotEqual(id(c1), id(c2)) +def test_card_attribute_persistence_and_pool_clear(): + Card._pool.clear() + c1 = Card("A", "s") + c1.temp = "t" + c2 = Card("A", "s") + assert hasattr(c2, "temp") - def test_instances_shall_share_additional_attributes(self): - expected_attribute_name = 'attr' - expected_attribute_value = 'value of attr' - c1 = Card('9', 'h') - c1.attr = expected_attribute_value - c2 = Card('9', 'h') - self.assertEqual(hasattr(c2, expected_attribute_name), True) - self.assertEqual(c2.attr, expected_attribute_value) + Card._pool.clear() + c3 = Card("A", "s") + assert not hasattr(c3, "temp") diff --git a/tests/structural/test_mvc.py b/tests/structural/test_mvc.py new file mode 100644 index 000000000..3206cc7b6 --- /dev/null +++ b/tests/structural/test_mvc.py @@ -0,0 +1,67 @@ +import pytest + +from patterns.structural.mvc import ( + ConsoleView, + Controller, + ProductModel, + Router, +) + + +def test_productmodel_iteration_and_price_str(): + pm = ProductModel() + items = list(pm) + assert set(items) == {"milk", "eggs", "cheese"} + + info = pm.get("cheese") + assert info["quantity"] == 10 + assert str(info["price"]) == "2.00" + + +def test_productmodel_get_raises_keyerror(): + pm = ProductModel() + with pytest.raises(KeyError) as exc: + pm.get("unknown_item") + assert "not in the model's item list." in str(exc.value) + + +def test_consoleview_capitalizer_and_list_and_info(capsys): + view = ConsoleView() + # capitalizer + assert view.capitalizer("heLLo") == "Hello" + + # show item list + view.show_item_list("product", ["x", "y"]) + out = capsys.readouterr().out + assert "PRODUCT LIST:" in out + assert "x" in out and "y" in out + + # show item information formatting + pm = ProductModel() + controller = Controller(pm, view) + controller.show_item_information("milk") + out = capsys.readouterr().out + assert "PRODUCT INFORMATION:" in out + assert "Name: milk" in out + assert "Price: 1.50" in out + assert "Quantity: 10" in out + + +def test_show_item_information_missing_calls_item_not_found(capsys): + view = ConsoleView() + pm = ProductModel() + controller = Controller(pm, view) + + controller.show_item_information("arepas") + out = capsys.readouterr().out + assert 'That product "arepas" does not exist in the records' in out + + +def test_router_register_resolve_and_unknown(): + router = Router() + router.register("products", Controller, ProductModel, ConsoleView) + controller = router.resolve("products") + assert isinstance(controller, Controller) + + with pytest.raises(KeyError): + router.resolve("no-such-path") diff --git a/tests/structural/test_proxy.py b/tests/structural/test_proxy.py index d8ab44fb5..a8965e2cf 100644 --- a/tests/structural/test_proxy.py +++ b/tests/structural/test_proxy.py @@ -1,99 +1,17 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import sys -from time import time -import unittest -from patterns.structural.proxy import Proxy, NoTalkProxy - -if sys.version_info[0] == 2: - from StringIO import StringIO -else: - from io import StringIO - - -class ProxyTest(unittest.TestCase): - @classmethod - def setUpClass(cls): - """ Class scope setup. """ - cls.p = Proxy() - - def setUp(cls): - """ Function/test case scope setup. """ - cls.output = StringIO() - cls.saved_stdout = sys.stdout - sys.stdout = cls.output - - def tearDown(cls): - """ Function/test case scope teardown. """ - cls.output.close() - sys.stdout = cls.saved_stdout - - def test_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) +from patterns.structural.proxy import Proxy, client + + +class TestProxy: + def test_do_the_job_for_admin_shall_pass(self, capsys): + client(Proxy(), "admin") + assert capsys.readouterr().out == ( + "[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, capsys): + client(Proxy(), "anonymous") + assert capsys.readouterr().out == ( + "[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 a03d1f590..4ddd4d331 100644 --- a/tests/test_hsm.py +++ b/tests/test_hsm.py @@ -1,96 +1,99 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import unittest +from unittest.mock import patch + +import pytest + from patterns.other.hsm.hsm import ( + Active, HierachicalStateMachine, + Standby, + Suspect, UnsupportedMessageType, UnsupportedState, UnsupportedTransition, - Active, - Standby, - Suspect, ) -try: - from unittest.mock import patch -except ImportError: - from mock import patch - -class HsmMethodTest(unittest.TestCase): +class TestHsmMethod: @classmethod - def setUpClass(cls): + def setup_class(cls): cls.hsm = HierachicalStateMachine() def test_initial_state_shall_be_standby(cls): - cls.assertEqual(isinstance(cls.hsm._current_state, Standby), True) + assert isinstance(cls.hsm._current_state, Standby) def test_unsupported_state_shall_raise_exception(cls): - with cls.assertRaises(UnsupportedState): - cls.hsm._next_state('missing') + with pytest.raises(UnsupportedState): + cls.hsm._next_state("missing") def test_unsupported_message_type_shall_raise_exception(cls): - with cls.assertRaises(UnsupportedMessageType): - cls.hsm.on_message('trigger') + with pytest.raises(UnsupportedMessageType): + cls.hsm.on_message("trigger") def test_calling_next_state_shall_change_current_state(cls): cls.hsm._current_state = Standby # initial state - cls.hsm._next_state('active') - cls.assertEqual(isinstance(cls.hsm._current_state, Active), True) + cls.hsm._next_state("active") + assert isinstance(cls.hsm._current_state, Active) 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' - cls.assertEqual(return_value, expected_return_value) + expected_return_value = "perform switchover" + assert return_value == expected_return_value -class StandbyStateTest(unittest.TestCase): - """ Exemplary 2nd level state test class (here: Standby state). Add missing - state test classes... """ +class TestStandbyState: + """Exemplary 2nd level state test class (here: Standby state). Add missing + state test classes...""" @classmethod - def setUpClass(cls): + def setup_class(cls): cls.hsm = HierachicalStateMachine() - def setUp(cls): + def setup_method(cls): cls.hsm._current_state = Standby(cls.hsm) def test_given_standby_on_message_switchover_shall_set_active(cls): - cls.hsm.on_message('switchover') - cls.assertEqual(isinstance(cls.hsm._current_state, Active), True) + cls.hsm.on_message("switchover") + assert isinstance(cls.hsm._current_state, Active) def test_given_standby_on_message_switchover_shall_call_hsm_methods(cls): - with patch.object(cls.hsm, '_perform_switchover') as mock_perform_switchover, patch.object( - cls.hsm, '_check_mate_status' - ) as mock_check_mate_status, patch.object( - cls.hsm, '_send_switchover_response' - ) as mock_send_switchover_response, patch.object( - cls.hsm, '_next_state' - ) as mock_next_state: - cls.hsm.on_message('switchover') - cls.assertEqual(mock_perform_switchover.call_count, 1) - cls.assertEqual(mock_check_mate_status.call_count, 1) - cls.assertEqual(mock_send_switchover_response.call_count, 1) - cls.assertEqual(mock_next_state.call_count, 1) + with ( + patch.object(cls.hsm, "_perform_switchover") as mock_perform_switchover, + patch.object(cls.hsm, "_check_mate_status") as mock_check_mate_status, + patch.object( + cls.hsm, "_send_switchover_response" + ) as mock_send_switchover_response, + patch.object(cls.hsm, "_next_state") as mock_next_state, + ): + cls.hsm.on_message("switchover") + assert mock_perform_switchover.call_count == 1 + assert mock_check_mate_status.call_count == 1 + assert mock_send_switchover_response.call_count == 1 + assert mock_next_state.call_count == 1 def test_given_standby_on_message_fault_trigger_shall_set_suspect(cls): - cls.hsm.on_message('fault trigger') - cls.assertEqual(isinstance(cls.hsm._current_state, Suspect), True) - - def test_given_standby_on_message_diagnostics_failed_shall_raise_exception_and_keep_in_state(cls): - with cls.assertRaises(UnsupportedTransition): - cls.hsm.on_message('diagnostics failed') - cls.assertEqual(isinstance(cls.hsm._current_state, Standby), True) - - def test_given_standby_on_message_diagnostics_passed_shall_raise_exception_and_keep_in_state(cls): - with cls.assertRaises(UnsupportedTransition): - cls.hsm.on_message('diagnostics passed') - cls.assertEqual(isinstance(cls.hsm._current_state, Standby), True) - - def test_given_standby_on_message_operator_inservice_shall_raise_exception_and_keep_in_state(cls): - with cls.assertRaises(UnsupportedTransition): - cls.hsm.on_message('operator inservice') - cls.assertEqual(isinstance(cls.hsm._current_state, Standby), True) + cls.hsm.on_message("fault trigger") + assert isinstance(cls.hsm._current_state, Suspect) + + def test_given_standby_on_message_diagnostics_failed_shall_raise_exception_and_keep_in_state( + cls, + ): + with pytest.raises(UnsupportedTransition): + cls.hsm.on_message("diagnostics failed") + assert isinstance(cls.hsm._current_state, Standby) + + def test_given_standby_on_message_diagnostics_passed_shall_raise_exception_and_keep_in_state( + cls, + ): + with pytest.raises(UnsupportedTransition): + cls.hsm.on_message("diagnostics passed") + assert isinstance(cls.hsm._current_state, Standby) + + def test_given_standby_on_message_operator_inservice_shall_raise_exception_and_keep_in_state( + cls, + ): + with pytest.raises(UnsupportedTransition): + cls.hsm.on_message("operator inservice") + assert isinstance(cls.hsm._current_state, Standby) diff --git a/tests/test_outputs.py b/tests/test_outputs.py deleted file mode 100644 index eaa879974..000000000 --- a/tests/test_outputs.py +++ /dev/null @@ -1,60 +0,0 @@ -try: - from contextlib import redirect_stdout -except: - pass - -import io -import sys - -import pytest - -from patterns.behavioral.catalog import main as catalog_main -from patterns.behavioral.catalog import OUTPUT as catalog_output -from patterns.behavioral.chain_of_responsibility import main as chain_main -from patterns.behavioral.chain_of_responsibility import OUTPUT as chain_output -from patterns.behavioral.chaining_method import main as chaining_method_main -from patterns.behavioral.chaining_method import OUTPUT as chaining_method_output -from patterns.behavioral.command import main as command_main -from patterns.behavioral.command import OUTPUT as command_output -from patterns.behavioral.iterator import main as iterator_main -from patterns.behavioral.iterator import OUTPUT as iterator_output -from patterns.behavioral.mediator import main as mediator_main -from patterns.behavioral.mediator import OUTPUT as mediator_output -from patterns.behavioral.observer import main as observer_main -from patterns.behavioral.observer import OUTPUT as observer_output -from patterns.behavioral.publish_subscribe import main as publish_subscribe_main -from patterns.behavioral.publish_subscribe import OUTPUT as publish_subscribe_output -from patterns.behavioral.specification import main as specification_main -from patterns.behavioral.specification import OUTPUT as specification_output -from patterns.behavioral.state import main as state_main -from patterns.behavioral.state import OUTPUT as state_output -from patterns.behavioral.strategy import main as strategy_main -from patterns.behavioral.strategy import OUTPUT as strategy_output -from patterns.behavioral.visitor import main as visitor_main -from patterns.behavioral.visitor import OUTPUT as visitor_output - - -@pytest.mark.skipif(sys.version_info < (3,4), - reason="requires python3.4 or higher") -@pytest.mark.parametrize("main,output", [ - (catalog_main, catalog_output), - (chain_main, chain_output), - (chaining_method_main, chaining_method_output), - (command_main, command_output), - (iterator_main, iterator_output), - (mediator_main, mediator_output), - (observer_main, observer_output), - (publish_subscribe_main, publish_subscribe_output), - (specification_main, specification_output), - (state_main, state_output), - (strategy_main, strategy_output), - (visitor_main, visitor_output), -]) -def test_output(main, output): - f = io.StringIO() - with redirect_stdout(f): - main() - - real_output = f.getvalue().strip() - expected_output = output.strip() - assert real_output == expected_output