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/.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 b16e1f661..ecd1fb556 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,34 +1,24 @@ -# vim ft=yaml -# travis-ci.org definition for python-patterns build +os: linux +dist: noble language: python -sudo: false - python: - - "2.7" - - "3.3" - - "3.4" - - "3.5" - - "3.6" -# Disabled for now since cause more pain than gain -# - "pypy" -# - "pypy3" + - "3.10" + - "3.11" + - "3.12" + - "3.13" + - "3.14" cache: - pip install: - - travis_retry pip install -q coveralls codecov six - - pip install flake8 # eventually worth + - pip install pipenv + - pipenv sync --dev --system + - pip install --no-build-isolation --no-deps -e . script: - # Run tests - - PYTHONPATH=. nosetests -s -v --with-doctest --with-cov --cover-package . --logging-level=INFO -v . - # Actually run all the scripts, contributing to coverage - - PYTHONPATH=. ./run_all.sh - # for now failure in flaking is ignored - - flake8 *py || echo "PEP8 the code" + - tox after_success: - - coveralls - 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 68f284fb5..7624f55f6 100644 --- a/README.md +++ b/README.md @@ -1,77 +1,184 @@ -python-patterns -=============== +# python-patterns A collection of design patterns and idioms in Python. -When an implementation is added or modified, be sure to update this file and -rerun `append_output.sh` (eg. ./append_output.sh borg.py) to keep the output -comments at the bottom up to date. +Remember that each pattern has its own trade-offs. And you need to pay attention more to why you're choosing a certain pattern than to how to implement it. -Current Patterns: +## Creational Patterns -__Creational Patterns__: +> Patterns that deal with **object creation** — abstracting and controlling how instances are made. -| Pattern | Description | -|:-------:| ----------- | -| [abstract_factory](creational/abstract_factory.py) | use a generic function with specific factories | -| [borg](creational/borg.py) | a singleton with shared-state among instances | -| [builder](creational/builder.py) | instead of using multiple constructors, builder object receives parameters and returns constructed objects | -| [factory_method](creational/factory_method.py) | delegate a specialized function/method to create instances | -| [lazy_evaluation](creational/lazy_evaluation.py) | lazily-evaluated property pattern in Python | -| [pool](creational/pool.py) | preinstantiate and maintain a group of instances of the same type | -| [prototype](creational/prototype.py) | use a factory and clones of a prototype for new instances (if instantiation is expensive) | +```mermaid +graph LR + Client -->|requests object| AbstractFactory + AbstractFactory -->|delegates to| ConcreteFactory + ConcreteFactory -->|produces| Product -__Structural Patterns__: + Builder -->|step-by-step| Director + Director -->|returns| BuiltObject -| Pattern | Description | -|:-------:| ----------- | -| [3-tier](structural/3-tier.py) | data<->business logic<->presentation separation (strict relationships) | -| [adapter](structural/adapter.py) | adapt one interface to another using a white-list | -| [bridge](structural/bridge.py) | a client-provider middleman to soften interface changes | -| [composite](structural/composite.py) | lets clients treat individual objects and compositions uniformly | -| [decorator](structural/decorator.py) | wrap functionality with other functionality in order to affect outputs | -| [facade](structural/facade.py) | use one class as an API to a number of others | -| [flyweight](structural/flyweight.py) | transparently reuse existing instances of objects with similar/identical state | -| [front_controller](structural/front_controller.py) | single handler requests coming to the application | -| [mvc](structural/mvc.py) | model<->view<->controller (non-strict relationships) | -| [proxy](structural/proxy.py) | an object funnels operations to something else | - -__Behavioral Patterns__: + FactoryMethod -->|subclass decides| ConcreteProduct + Pool -->|reuses| PooledInstance +``` | Pattern | Description | |:-------:| ----------- | -| [chain](behavioral/chain.py) | apply a chain of successive handlers to try and process the data | -| [catalog](behavioral/catalog.py) | general methods will call different specialized methods based on construction parameter | -| [chaining_method](behavioral/chaining_method.py) | continue callback next object method | -| [command](behavioral/command.py) | bundle a command and arguments to call later | -| [iterator](behavioral/iterator.py) | traverse a container and access the container's elements | -| [mediator](behavioral/mediator.py) | an object that knows how to connect other objects and act as a proxy | -| [memento](behavioral/memento.py) | generate an opaque token that can be used to go back to a previous state | -| [observer](behavioral/observer.py) | provide a callback for notification of events/changes to data | -| [publish_subscribe](behavioral/publish_subscribe.py) | a source syndicates events/data to 0+ registered listeners | -| [registry](behavioral/registry.py) | keep track of all subclasses of a given class | -| [specification](behavioral/specification.py) | business rules can be recombined by chaining the business rules together using boolean logic | -| [state](behavioral/state.py) | logic is organized into a discrete number of potential states and the next state that can be transitioned to | -| [strategy](behavioral/strategy.py) | selectable operations over the same data | -| [template](behavioral/template.py) | an object imposes a structure but takes pluggable components | -| [visitor](behavioral/visitor.py) | invoke a callback for all items of a collection | - -__Design for Testability Patterns__: +| [abstract_factory](patterns/creational/abstract_factory.py) | use a generic function with specific factories | +| [borg](patterns/creational/borg.py) | a singleton with shared-state among instances | +| [builder](patterns/creational/builder.py) | instead of using multiple constructors, builder object receives parameters and returns constructed objects | +| [factory](patterns/creational/factory.py) | delegate a specialized function/method to create instances | +| [lazy_evaluation](patterns/creational/lazy_evaluation.py) | lazily-evaluated property pattern in Python | +| [pool](patterns/creational/pool.py) | preinstantiate and maintain a group of instances of the same type | +| [prototype](patterns/creational/prototype.py) | use a factory and clones of a prototype for new instances (if instantiation is expensive) | -| Pattern | Description | -|:-------:| ----------- | -| [setter_injection](dft/setter_injection.py) | the client provides the depended-on object to the SUT via the setter injection (implementation variant of dependency injection) | +## Structural Patterns -__Fundamental Patterns__: +> Patterns that define **how classes and objects are composed** to form larger, flexible structures. -| Pattern | Description | -|:-------:| ----------- | -| [delegation_pattern](fundamental/delegation_pattern.py) | an object handles a request by delegating to a second object (the delegate) | +```mermaid +graph TD + Client --> Facade + Facade --> SubsystemA + Facade --> SubsystemB + Facade --> SubsystemC -__Others__: + Client2 --> Adapter + Adapter --> LegacyService -| Pattern | Description | -|:-------:| ----------- | -| [blackboard](other/blackboard.py) | architectural model, assemble different sub-system knowledge to build a solution, AI approach - non gang of four pattern | -| [graph_search](other/graph_search.py) | graphing algorithms - non gang of four pattern | -| [hsm](other/hsm/hsm.py) | hierarchical state machine - non gang of four pattern | + Client3 --> Proxy + Proxy -->|controls access to| RealSubject + + Component --> Composite + Composite --> Leaf1 + Composite --> Leaf2 +``` + +| Pattern | Description | +|:-----------------------------------------------------------:|--------------------------------------------------------------------------------| +| [3-tier](patterns/structural/3-tier.py) | data<->business logic<->presentation separation (strict relationships) | +| [adapter](patterns/structural/adapter.py) | adapt one interface to another using a white-list | +| [bridge](patterns/structural/bridge.py) | a client-provider middleman to soften interface changes | +| [composite](patterns/structural/composite.py) | lets clients treat individual objects and compositions uniformly | +| [decorator](patterns/structural/decorator.py) | wrap functionality with other functionality in order to affect outputs | +| [facade](patterns/structural/facade.py) | use one class as an API to a number of others | +| [flyweight](patterns/structural/flyweight.py) | transparently reuse existing instances of objects with similar/identical state | +| [front_controller](patterns/structural/front_controller.py) | single handler requests coming to the application | +| [mvc](patterns/structural/mvc.py) | model<->view<->controller (non-strict relationships) | +| [proxy](patterns/structural/proxy.py) | an object funnels operations to something else | + +## Behavioral Patterns + +> Patterns concerned with **communication and responsibility** between objects. + +```mermaid +graph LR + Sender -->|sends event| Observer1 + Sender -->|sends event| Observer2 + + Request --> Handler1 + Handler1 -->|passes if unhandled| Handler2 + Handler2 -->|passes if unhandled| Handler3 + + Context -->|delegates to| Strategy + Strategy -->|executes| Algorithm + + Originator -->|saves state to| Memento + Caretaker -->|holds| Memento +``` + +| Pattern | Description | +|:-------------------------------------------------------------------------:|--------------------------------------------------------------------------------------------------------------| +| [chain_of_responsibility](patterns/behavioral/chain_of_responsibility.py) | apply a chain of successive handlers to try and process the data | +| [catalog](patterns/behavioral/catalog.py) | general methods will call different specialized methods based on construction parameter | +| [chaining_method](patterns/behavioral/chaining_method.py) | continue callback next object method | +| [command](patterns/behavioral/command.py) | bundle a command and arguments to call later | +| [interpreter](patterns/behavioral/interpreter.py) | define a grammar for a language and use it to interpret statements | +| [iterator](patterns/behavioral/iterator.py) | traverse a container and access the container's elements | +| [iterator](patterns/behavioral/iterator_alt.py) (alt. impl.) | traverse a container and access the container's elements | +| [mediator](patterns/behavioral/mediator.py) | an object that knows how to connect other objects and act as a proxy | +| [memento](patterns/behavioral/memento.py) | generate an opaque token that can be used to go back to a previous state | +| [observer](patterns/behavioral/observer.py) | provide a callback for notification of events/changes to data | +| [publish_subscribe](patterns/behavioral/publish_subscribe.py) | a source syndicates events/data to 0+ registered listeners | +| [registry](patterns/behavioral/registry.py) | keep track of all subclasses of a given class | +| [servant](patterns/behavioral/servant.py) | provide common functionality to a group of classes without using inheritance | +| [specification](patterns/behavioral/specification.py) | business rules can be recombined by chaining the business rules together using boolean logic | +| [state](patterns/behavioral/state.py) | logic is organized into a discrete number of potential states and the next state that can be transitioned to | +| [strategy](patterns/behavioral/strategy.py) | selectable operations over the same data | +| [template](patterns/behavioral/template.py) | an object imposes a structure but takes pluggable components | +| [visitor](patterns/behavioral/visitor.py) | invoke a callback for all items of a collection | + +## Design for Testability Patterns + +| Pattern | Description | +|:--------------------------------------------------------:|------------------------------------| +| [dependency_injection](patterns/dependency_injection.py) | 3 variants of dependency injection | + +## Fundamental Patterns + +| Pattern | Description | +|:----------------------------------------------------------------:|-----------------------------------------------------------------------------| +| [delegation_pattern](patterns/fundamental/delegation_pattern.py) | an object handles a request by delegating to a second object (the delegate) | + +## Others + +| Pattern | Description | +|:----------------------------------------------:|--------------------------------------------------------------------------------------------------------------------------| +| [blackboard](patterns/other/blackboard.py) | architectural model, assemble different sub-system knowledge to build a solution, AI approach - non gang of four pattern | +| [graph_search](patterns/other/graph_search.py) | graphing algorithms - non gang of four pattern | +| [hsm](patterns/other/hsm/hsm.py) | hierarchical state machine - non gang of four pattern | + +## 🚫 Anti-Patterns + +This section lists some common design patterns that are **not recommended** in Python and explains why. + +### 🧱 Singleton +**Why not:** +- Python modules are already singletons — every module is imported only once. +- Explicit singleton classes add unnecessary complexity. +- Better alternatives: use module-level variables or dependency injection. + +### 🌀 God Object +**Why not:** +- Centralizes too much logic in a single class. +- Makes code harder to test and maintain. +- Better alternative: split functionality into smaller, cohesive classes. + +### 🔁 Inheritance overuse +**Why not:** +- Deep inheritance trees make code brittle. +- Prefer composition and delegation. +- “Favor composition over inheritance.” + +## Videos + +* [Design Patterns in Python by Peter Ullrich](https://www.youtube.com/watch?v=bsyjSW46TDg) +* [Sebastian Buczyński - Why you don't need design patterns in Python?](https://www.youtube.com/watch?v=G5OeYHCJuv0) +* [You Don't Need That!](https://www.youtube.com/watch?v=imW-trt0i9I) +* [Pluggable Libs Through Design Patterns](https://www.youtube.com/watch?v=PfgEU3W0kyU) + +## Contributing + +When an implementation is added or modified, please review the following guidelines: + +##### Docstrings +Add module level description in form of a docstring with links to corresponding references or other useful information. +Add "Examples in Python ecosystem" section if you know some. It shows how patterns could be applied to real-world problems. +[facade.py](patterns/structural/facade.py) has a good example of detailed description, but sometimes the shorter one as in [template.py](patterns/behavioral/template.py) would suffice. + +##### Python 2 compatibility +To see Python 2 compatible versions of some patterns please check-out the [legacy](https://github.com/faif/python-patterns/tree/legacy) tag. + +##### Update README +When everything else is done - update corresponding part of README. + +##### Travis CI +Please run the following before submitting a patch: +- 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 a3b7948b9..000000000 --- a/append_output.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -set -e - -src=$(sed -n -e '/### OUTPUT ###/,$!p' "$1") -output=$(python "$1" | sed 's/^/# /') - -# These are done separately to avoid having to insert a newline, which causes -# problems when the text itself has '\n' in strings -echo "$src" > $1 -echo -e "\n### OUTPUT ###" >> $1 -echo "$output" >> $1 diff --git a/behavioral/catalog.py b/behavioral/catalog.py deleted file mode 100644 index e9d1e0589..000000000 --- a/behavioral/catalog.py +++ /dev/null @@ -1,179 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -A class that uses different static function depending of a parameter passed in -init. Note the use of a single dictionary instead of multiple conditions -""" - -__author__ = "Ibrahim Diop " - - -class Catalog(object): - """catalog of multiple static methods that are executed depending on an init - - parameter - """ - - def __init__(self, param): - - # dictionary that will be used to determine which static method is - # to be executed but that will be also used to store possible param - # value - self._static_method_choices = {'param_value_1': self._static_method_1, - 'param_value_2': self._static_method_2} - - # simple test to validate param value - if param in self._static_method_choices.keys(): - self.param = param - else: - raise ValueError("Invalid Value for Param: {0}".format(param)) - - @staticmethod - def _static_method_1(): - print("executed method 1!") - - @staticmethod - def _static_method_2(): - print("executed method 2!") - - def main_method(self): - """will execute either _static_method_1 or _static_method_2 - - depending on self.param value - """ - self._static_method_choices[self.param]() - - -# Alternative implementation for different levels of methods -class CatalogInstance(object): - - """catalog of multiple methods that are executed depending on an init - - parameter - """ - - def __init__(self, param): - 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)) - - def _instance_method_1(self): - print("Value {}".format(self.x1)) - - def _instance_method_2(self): - print("Value {}".format(self.x2)) - - _instance_method_choices = {'param_value_1': _instance_method_1, - 'param_value_2': _instance_method_2} - - def main_method(self): - """will execute either _instance_method_1 or _instance_method_2 - - depending on self.param value - """ - self._instance_method_choices[self.param].__get__(self)() - - -class CatalogClass(object): - - """catalog of multiple class methods that are executed depending on an init - - parameter - """ - - x1 = 'x1' - x2 = 'x2' - - def __init__(self, param): - # 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)) - - @classmethod - def _class_method_1(cls): - print("Value {}".format(cls.x1)) - - @classmethod - def _class_method_2(cls): - print("Value {}".format(cls.x2)) - - _class_method_choices = {'param_value_1': _class_method_1, - 'param_value_2': _class_method_2} - - def main_method(self): - """will execute either _class_method_1 or _class_method_2 - - depending on self.param value - """ - self._class_method_choices[self.param].__get__(None, self.__class__)() - - -class CatalogStatic(object): - - """catalog of multiple static methods that are executed depending on an init - - parameter - """ - - def __init__(self, param): - # 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)) - - @staticmethod - def _static_method_1(): - print("executed method 1!") - - @staticmethod - def _static_method_2(): - print("executed method 2!") - - _static_method_choices = {'param_value_1': _static_method_1, - 'param_value_2': _static_method_2} - - def main_method(self): - """will execute either _static_method_1 or _static_method_2 - - depending on self.param value - """ - self._static_method_choices[self.param].__get__(None, self.__class__)() - - -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() - - test = CatalogInstance('param_value_1') - test.main_method() - - test = CatalogClass('param_value_2') - test.main_method() - - test = CatalogStatic('param_value_1') - test.main_method() - -if __name__ == "__main__": - - main() - -### OUTPUT ### -# executed method 2! -# Value x1 -# Value x2 -# executed method 1! diff --git a/behavioral/chain.py b/behavioral/chain.py deleted file mode 100644 index 989a3072c..000000000 --- a/behavioral/chain.py +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -http://www.dabeaz.com/coroutines/ -""" - -from contextlib import contextmanager -import os -import sys -import time -import abc - - -class Handler(object): - __metaclass__ = abc.ABCMeta - - def __init__(self, successor=None): - self._successor = successor - - def handle(self, request): - res = self._handle(request) - if not res: - self._successor.handle(request) - - @abc.abstractmethod - def _handle(self, request): - raise NotImplementedError('Must provide implementation in subclass.') - - -class ConcreteHandler1(Handler): - - def _handle(self, request): - if 0 < request <= 10: - print('request {} handled in handler 1'.format(request)) - return True - - -class ConcreteHandler2(Handler): - - def _handle(self, request): - if 10 < request <= 20: - print('request {} handled in handler 2'.format(request)) - return True - - -class ConcreteHandler3(Handler): - - def _handle(self, request): - if 20 < request <= 30: - print('request {} handled in handler 3'.format(request)) - return True - - -class DefaultHandler(Handler): - - def _handle(self, request): - print('end of chain, no handler for {}'.format(request)) - return True - - -class Client(object): - - def __init__(self): - self.handler = ConcreteHandler1( - ConcreteHandler3(ConcreteHandler2(DefaultHandler()))) - - def delegate(self, requests): - for request in requests: - self.handler.handle(request) - - -def coroutine(func): - def start(*args, **kwargs): - cr = func(*args, **kwargs) - next(cr) - return cr - return start - - -@coroutine -def coroutine1(target): - while True: - request = yield - if 0 < request <= 10: - print('request {} handled in coroutine 1'.format(request)) - else: - target.send(request) - - -@coroutine -def coroutine2(target): - while True: - request = yield - if 10 < request <= 20: - print('request {} handled in coroutine 2'.format(request)) - else: - target.send(request) - - -@coroutine -def coroutine3(target): - while True: - request = yield - if 20 < request <= 30: - print('request {} handled in coroutine 3'.format(request)) - else: - target.send(request) - - -@coroutine -def default_coroutine(): - while True: - request = yield - print('end of chain, no coroutine for {}'.format(request)) - - -class ClientCoroutine: - - def __init__(self): - self.target = coroutine1(coroutine3(coroutine2(default_coroutine()))) - - def delegate(self, requests): - for request in requests: - self.target.send(request) - - -def timeit(func): - - def count(*args, **kwargs): - start = time.time() - res = func(*args, **kwargs) - count._time = time.time() - start - return res - return count - - -@contextmanager -def suppress_stdout(): - try: - stdout, sys.stdout = sys.stdout, open(os.devnull, 'w') - yield - finally: - sys.stdout = stdout - - -if __name__ == "__main__": - client1 = Client() - client2 = ClientCoroutine() - requests = [2, 5, 14, 22, 18, 3, 35, 27, 20] - - client1.delegate(requests) - print('-' * 30) - client2.delegate(requests) - - requests *= 10000 - client1_delegate = timeit(client1.delegate) - client2_delegate = timeit(client2.delegate) - with suppress_stdout(): - client1_delegate(requests) - client2_delegate(requests) - # lets check which is faster - print(client1_delegate._time, client2_delegate._time) - -### OUTPUT ### -# request 2 handled in handler 1 -# request 5 handled in handler 1 -# request 14 handled in handler 2 -# request 22 handled in handler 3 -# request 18 handled in handler 2 -# request 3 handled in handler 1 -# end of chain, no handler for 35 -# request 27 handled in handler 3 -# request 20 handled in handler 2 -# ------------------------------ -# request 2 handled in coroutine 1 -# request 5 handled in coroutine 1 -# request 14 handled in coroutine 2 -# request 22 handled in coroutine 3 -# request 18 handled in coroutine 2 -# request 3 handled in coroutine 1 -# end of chain, no coroutine for 35 -# request 27 handled in coroutine 3 -# request 20 handled in coroutine 2 -# (0.2369999885559082, 0.16199994087219238) diff --git a/behavioral/chaining_method.py b/behavioral/chaining_method.py deleted file mode 100644 index 02f9527fa..000000000 --- a/behavioral/chaining_method.py +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import print_function - - -class Person(object): - - def __init__(self, name, action): - self.name = name - self.action = action - - def do_action(self): - print(self.name, self.action.name, end=' ') - return self.action - - -class Action(object): - - def __init__(self, name): - self.name = name - - def amount(self, val): - print(val, end=' ') - return self - - def stop(self): - print('then stop') - -if __name__ == '__main__': - - move = Action('move') - person = Person('Jack', move) - person.do_action().amount('5m').stop() - -### OUTPUT ### -# Jack move 5m then stop diff --git a/behavioral/command.py b/behavioral/command.py deleted file mode 100644 index 98a53546f..000000000 --- a/behavioral/command.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -*TL;DR80 -Encapsulates all information needed to perform an action or trigger an event. -""" - -from __future__ import print_function -import os -from os.path import lexists - - -class MoveFileCommand(object): - - def __init__(self, src, dest): - self.src = src - self.dest = dest - - def execute(self): - self.rename(self.src, self.dest) - - 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) - - -def main(): - command_stack = [] - - # commands are just pushed into the command stack - command_stack.append(MoveFileCommand('foo.txt', 'bar.txt')) - command_stack.append(MoveFileCommand('bar.txt', 'baz.txt')) - - # 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 - - # they can be executed later on - for cmd in command_stack: - cmd.execute() - - # and can also be undone at will - for cmd in reversed(command_stack): - cmd.undo() - finally: - os.unlink("foo.txt") - -if __name__ == "__main__": - main() - -### OUTPUT ### -# renaming foo.txt to bar.txt -# renaming bar.txt to baz.txt -# renaming baz.txt to bar.txt -# renaming bar.txt to foo.txt diff --git a/behavioral/iterator.py b/behavioral/iterator.py deleted file mode 100644 index 815259208..000000000 --- a/behavioral/iterator.py +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -http://ginstrom.com/scribbles/2007/10/08/design-patterns-python-style/ -Implementation of the iterator pattern with a generator - -*TL;DR80 -Traverses a container and accesses the container's elements. -""" - -from __future__ import print_function - - -def count_to(count): - """Counts by word numbers, up to a maximum of five""" - numbers = ["one", "two", "three", "four", "five"] - for number in numbers[:count]: - yield number - -# Test the generator -count_to_two = lambda: count_to(2) -count_to_five = lambda: count_to(5) - -print('Counting to two...') -for number in count_to_two(): - print(number, end=' ') - -print() - -print('Counting to five...') -for number in count_to_five(): - print(number, end=' ') - -print() - -### OUTPUT ### -# Counting to two... -# one two -# Counting to five... -# one two three four five diff --git a/behavioral/mediator.py b/behavioral/mediator.py deleted file mode 100644 index b3871157a..000000000 --- a/behavioral/mediator.py +++ /dev/null @@ -1,148 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -http://web.archive.org/web/20120309135549/http://dpip.testingperspective.com/?p=28 - -*TL;DR80 -Encapsulates how a set of objects interact. -""" - -import random -import time - - -class TC: - - def __init__(self): - self._tm = None - self._bProblem = 0 - - def setup(self): - print("Setting up the Test") - time.sleep(0.1) - self._tm.prepareReporting() - - def execute(self): - if not self._bProblem: - print("Executing the test") - time.sleep(0.1) - else: - print("Problem in setup. Test not executed.") - - def tearDown(self): - if not self._bProblem: - print("Tearing down") - time.sleep(0.1) - self._tm.publishReport() - else: - print("Test not executed. No tear down required.") - - def setTM(self, tm): - self._tm = tm - - def setProblem(self, value): - self._bProblem = value - - -class Reporter: - - def __init__(self): - self._tm = None - - def prepare(self): - print("Reporter Class is preparing to report the results") - time.sleep(0.1) - - def report(self): - print("Reporting the results of Test") - time.sleep(0.1) - - def setTM(self, tm): - self._tm = tm - - -class DB: - - def __init__(self): - self._tm = None - - def insert(self): - print("Inserting the execution begin status in the Database") - time.sleep(0.1) - # Following code is to simulate a communication from DB to TC - if random.randrange(1, 4) == 3: - return -1 - - def update(self): - print("Updating the test results in Database") - time.sleep(0.1) - - def setTM(self, tm): - self._tm = tm - - -class TestManager: - - def __init__(self): - self._reporter = None - self._db = None - self._tc = None - - def prepareReporting(self): - rvalue = self._db.insert() - if rvalue == -1: - self._tc.setProblem(1) - self._reporter.prepare() - - def setReporter(self, reporter): - self._reporter = reporter - - def setDB(self, db): - self._db = db - - def publishReport(self): - self._db.update() - self._reporter.report() - - def setTC(self, tc): - self._tc = tc - - -if __name__ == '__main__': - reporter = Reporter() - db = DB() - tm = TestManager() - tm.setReporter(reporter) - tm.setDB(db) - reporter.setTM(tm) - db.setTM(tm) - # For simplification we are looping on the same test. - # Practically, it could be about various unique test classes and their - # objects - for i in range(3): - tc = TC() - tc.setTM(tm) - tm.setTC(tc) - tc.setup() - tc.execute() - tc.tearDown() - -### OUTPUT ### -# Setting up the Test -# Inserting the execution begin status in the Database -# Executing the test -# Tearing down -# Updating the test results in Database -# Reporting the results of Test -# Setting up the Test -# Inserting the execution begin status in the Database -# Reporter Class is preparing to report the results -# Problem in setup. Test not executed. -# Test not executed. No tear down required. -# Setting up the Test -# Inserting the execution begin status in the Database -# Executing the test -# Tearing down -# Updating the test results in Database -# Reporting the results of Test diff --git a/behavioral/memento.py b/behavioral/memento.py deleted file mode 100644 index c53daa2ad..000000000 --- a/behavioral/memento.py +++ /dev/null @@ -1,143 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -http://code.activestate.com/recipes/413838-memento-closure/ - -*TL;DR80 -Provides the ability to restore an object to its previous state. -""" - -from copy import copy -from copy import deepcopy - - -def memento(obj, deep=False): - state = deepcopy(obj.__dict__) if deep else copy(obj.__dict__) - - def restore(): - obj.__dict__.clear() - obj.__dict__.update(state) - - return restore - - -class Transaction(object): - """A transaction guard. - - This is, in fact, just syntactic sugar around a memento closure. - """ - deep = False - states = [] - - def __init__(self, deep, *targets): - self.deep = deep - self.targets = targets - self.commit() - - def commit(self): - self.states = [memento(target, self.deep) for target in self.targets] - - def rollback(self): - for a_state in self.states: - a_state() - - -class Transactional(object): - """Adds transactional semantics to methods. Methods decorated with - - @Transactional will rollback to entry-state upon exceptions. - """ - - def __init__(self, method): - self.method = method - - def __get__(self, obj, T): - def transaction(*args, **kwargs): - state = memento(obj) - try: - return self.method(obj, *args, **kwargs) - except Exception as e: - state() - raise e - - return transaction - - -class NumObj(object): - - def __init__(self, value): - self.value = value - - def __repr__(self): - return '<%s: %r>' % (self.__class__.__name__, self.value) - - def increment(self): - self.value += 1 - - @Transactional - def do_stuff(self): - self.value = '1111' # <- invalid value - self.increment() # <- will fail and rollback - - -if __name__ == '__main__': - num_obj = NumObj(-1) - print(num_obj) - - a_transaction = Transaction(True, num_obj) - try: - for i in range(3): - num_obj.increment() - print(num_obj) - a_transaction.commit() - print('-- committed') - - for i in range(3): - num_obj.increment() - print(num_obj) - num_obj.value += 'x' # will fail - print(num_obj) - except Exception as e: - a_transaction.rollback() - print('-- rolled back') - print(num_obj) - - print('-- now doing stuff ...') - try: - num_obj.do_stuff() - except Exception as e: - print('-> doing stuff failed!') - import sys - import traceback - - traceback.print_exc(file=sys.stdout) - print(num_obj) - - -### OUTPUT ### -# -# -# -# -# -- committed -# -# -# -# -- rolled back -# -# -- now doing stuff ... -# -> doing stuff failed! -# Traceback (most recent call last): -# File "memento.py", line 97, in -# num_obj.do_stuff() -# File "memento.py", line 52, in transaction -# raise e -# File "memento.py", line 49, in transaction -# return self.method(obj, *args, **kwargs) -# File "memento.py", line 70, in do_stuff -# self.increment() # <- will fail and rollback -# File "memento.py", line 65, in increment -# self.value += 1 -# TypeError: Can't convert 'int' object to str implicitly -# diff --git a/behavioral/observer.py b/behavioral/observer.py deleted file mode 100644 index 23b13aeee..000000000 --- a/behavioral/observer.py +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -http://code.activestate.com/recipes/131499-observer-pattern/ - -*TL;DR80 -Maintains a list of dependents and notifies them of any state changes. -""" - -from __future__ import print_function - - -class Subject(object): - - def __init__(self): - self._observers = [] - - def attach(self, observer): - if observer not in self._observers: - self._observers.append(observer) - - def detach(self, observer): - try: - self._observers.remove(observer) - except ValueError: - pass - - def notify(self, modifier=None): - for observer in self._observers: - if modifier != observer: - observer.update(self) - - -# Example usage -class Data(Subject): - - def __init__(self, name=''): - Subject.__init__(self) - self.name = name - self._data = 0 - - @property - def data(self): - return self._data - - @data.setter - def data(self, value): - self._data = value - self.notify() - - -class HexViewer: - - def update(self, subject): - print(u'HexViewer: Subject %s has data 0x%x' % - (subject.name, subject.data)) - - -class DecimalViewer: - - def update(self, subject): - print(u'DecimalViewer: Subject %s has data %d' % - (subject.name, subject.data)) - - -# Example usage... -def main(): - data1 = Data('Data 1') - data2 = Data('Data 2') - view1 = DecimalViewer() - view2 = HexViewer() - data1.attach(view1) - data1.attach(view2) - data2.attach(view2) - data2.attach(view1) - - print(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 diff --git a/behavioral/publish_subscribe.py b/behavioral/publish_subscribe.py deleted file mode 100644 index b41fbaf3c..000000000 --- a/behavioral/publish_subscribe.py +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -Reference: -http://www.slideshare.net/ishraqabd/publish-subscribe-model-overview-13368808 -Author: https://github.com/HanWenfang -""" - - -class Provider: - - def __init__(self): - self.msg_queue = [] - self.subscribers = {} - - def notify(self, msg): - self.msg_queue.append(msg) - - def subscribe(self, msg, subscriber): - self.subscribers.setdefault(msg, []).append(subscriber) - - def unsubscribe(self, msg, subscriber): - self.subscribers[msg].remove(subscriber) - - def update(self): - for msg in self.msg_queue: - for sub in self.subscribers.get(msg, []): - sub.run(msg) - self.msg_queue = [] - - -class Publisher: - - def __init__(self, msg_center): - self.provider = msg_center - - def publish(self, msg): - self.provider.notify(msg) - - -class Subscriber: - - def __init__(self, name, msg_center): - self.name = name - self.provider = msg_center - - def subscribe(self, msg): - self.provider.subscribe(msg, self) - - def unsubscribe(self, msg): - self.provider.unsubscribe(msg, self) - - def run(self, msg): - print("{} got {}".format(self.name, msg)) - - -def main(): - message_center = Provider() - - fftv = Publisher(message_center) - - jim = Subscriber("jim", message_center) - jim.subscribe("cartoon") - jack = Subscriber("jack", message_center) - jack.subscribe("music") - gee = Subscriber("gee", message_center) - gee.subscribe("movie") - 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() - - -if __name__ == "__main__": - main() - -### OUTPUT ### -# jim got cartoon -# jack got music -# gee got movie -# jim got cartoon -# jim got cartoon -# gee got movie diff --git a/behavioral/registry.py b/behavioral/registry.py deleted file mode 100644 index 3e8310eb0..000000000 --- a/behavioral/registry.py +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - - -class RegistryHolder(type): - - REGISTRY = {} - - def __new__(cls, name, bases, attrs): - new_cls = type.__new__(cls, name, bases, attrs) - """ - Here the name of the class is used as key but it could be any class - parameter. - """ - cls.REGISTRY[new_cls.__name__] = new_cls - return new_cls - - @classmethod - def get_registry(cls): - return dict(cls.REGISTRY) - - -class BaseRegisteredClass(object): - __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. - """ - 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 diff --git a/behavioral/specification.py b/behavioral/specification.py deleted file mode 100644 index 89a4ed1d7..000000000 --- a/behavioral/specification.py +++ /dev/null @@ -1,118 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -@author: Gordeev Andrey - -*TL;DR80 -Provides recombination business logic by chaining together using boolean logic. -""" - -from abc import abstractmethod - - -class Specification(object): - - def and_specification(self, candidate): - raise NotImplementedError() - - def or_specification(self, candidate): - raise NotImplementedError() - - def not_specification(self): - raise NotImplementedError() - - @abstractmethod - def is_satisfied_by(self, candidate): - pass - - -class CompositeSpecification(Specification): - - @abstractmethod - def is_satisfied_by(self, candidate): - pass - - def and_specification(self, candidate): - return AndSpecification(self, candidate) - - def or_specification(self, candidate): - return OrSpecification(self, candidate) - - def not_specification(self): - return NotSpecification(self) - - -class AndSpecification(CompositeSpecification): - _one = Specification() - _other = Specification() - - 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)) - - -class OrSpecification(CompositeSpecification): - _one = Specification() - _other = Specification() - - 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) or - self._other.is_satisfied_by(candidate)) - - -class NotSpecification(CompositeSpecification): - _wrapped = Specification() - - def __init__(self, wrapped): - self._wrapped = wrapped - - def is_satisfied_by(self, candidate): - return bool(not self._wrapped.is_satisfied_by(candidate)) - - -class User(object): - - def __init__(self, super_user=False): - self.super_user = super_user - - -class UserSpecification(CompositeSpecification): - - def is_satisfied_by(self, candidate): - return isinstance(candidate, User) - - -class SuperUserSpecification(CompositeSpecification): - - def is_satisfied_by(self, candidate): - return getattr(candidate, 'super_user', False) - - -if __name__ == '__main__': - print('Specification') - andrey = User() - ivan = User(super_user=True) - vasiliy = 'not User instance' - - root_specification = UserSpecification().\ - and_specification(SuperUserSpecification()) - - print(root_specification.is_satisfied_by(andrey)) - print(root_specification.is_satisfied_by(ivan)) - print(root_specification.is_satisfied_by(vasiliy)) - - -### OUTPUT ### -# Specification -# False -# True -# False diff --git a/behavioral/state.py b/behavioral/state.py deleted file mode 100644 index 5b9d78c10..000000000 --- a/behavioral/state.py +++ /dev/null @@ -1,92 +0,0 @@ -#!/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 -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 - - -class State(object): - - """Base state. This is to share functionality""" - - def scan(self): - """Scan the dial to the next station""" - self.pos += 1 - if self.pos == len(self.stations): - self.pos = 0 - print(u"Scanning... Station is %s %s" % - (self.stations[self.pos], self.name)) - - -class AmState(State): - - def __init__(self, radio): - self.radio = radio - self.stations = ["1250", "1380", "1510"] - self.pos = 0 - self.name = "AM" - - def toggle_amfm(self): - print(u"Switching to FM") - self.radio.state = self.radio.fmstate - - -class FmState(State): - - def __init__(self, radio): - self.radio = radio - self.stations = ["81.3", "89.1", "103.9"] - self.pos = 0 - self.name = "FM" - - def toggle_amfm(self): - print(u"Switching to AM") - self.radio.state = self.radio.amstate - - -class Radio(object): - - """A radio. It has a scan button, and an AM/FM toggle switch.""" - - def __init__(self): - """We have an AM state and an FM state""" - self.amstate = AmState(self) - self.fmstate = FmState(self) - self.state = self.amstate - - def toggle_amfm(self): - self.state.toggle_amfm() - - def scan(self): - self.state.scan() - - -# Test our radio out -if __name__ == '__main__': - radio = Radio() - actions = [radio.scan] * 2 + [radio.toggle_amfm] + [radio.scan] * 2 - actions *= 2 - - for action in actions: - action() - -### OUTPUT ### -# Scanning... Station is 1380 AM -# Scanning... Station is 1510 AM -# Switching to FM -# Scanning... Station is 89.1 FM -# Scanning... Station is 103.9 FM -# Scanning... Station is 81.3 FM -# Scanning... Station is 89.1 FM -# Switching to AM -# Scanning... Station is 1250 AM -# Scanning... Station is 1380 AM diff --git a/behavioral/strategy.py b/behavioral/strategy.py deleted file mode 100644 index 45b87601c..000000000 --- a/behavioral/strategy.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -http://stackoverflow.com/questions/963965/how-is-this-strategy-pattern - -written-in-python-the-sample-in-wikipedia -In most of other languages Strategy pattern is implemented via creating some -base strategy interface/abstract class and subclassing it with a number of -concrete strategies (as we can see at -http://en.wikipedia.org/wiki/Strategy_pattern), however Python supports -higher-order functions and allows us to have only one class and inject -functions into it's instances, as shown in this example. - -*TL;DR80 -Enables selecting an algorithm at runtime. -""" - -import types - - -class StrategyExample: - - def __init__(self, func=None): - self.name = 'Strategy Example 0' - if func is not None: - self.execute = types.MethodType(func, self) - - def execute(self): - print(self.name) - - -def execute_replacement1(self): - print(self.name + ' from execute 1') - - -def execute_replacement2(self): - print(self.name + ' from execute 2') - - -if __name__ == '__main__': - strat0 = StrategyExample() - - strat1 = StrategyExample(execute_replacement1) - strat1.name = 'Strategy Example 1' - - strat2 = StrategyExample(execute_replacement2) - strat2.name = 'Strategy Example 2' - - strat0.execute() - strat1.execute() - strat2.execute() - -### OUTPUT ### -# Strategy Example 0 -# Strategy Example 1 from execute 1 -# Strategy Example 2 from execute 2 diff --git a/behavioral/template.py b/behavioral/template.py deleted file mode 100644 index ef4df8a1c..000000000 --- a/behavioral/template.py +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -http://ginstrom.com/scribbles/2007/10/08/design-patterns-python-style/ - -An example of the Template pattern in Python - -*TL;DR80 -Defines the skeleton of an algorithm, deferring steps to subclasses. -""" - -ingredients = "spam eggs apple" -line = '-' * 10 - - -# Skeletons -def iter_elements(getter, action): - """Template skeleton that iterates items""" - for element in getter(): - action(element) - print(line) - - -def rev_elements(getter, action): - """Template skeleton that iterates items in reverse order""" - for element in getter()[::-1]: - action(element) - print(line) - - -# Getters -def get_list(): - return ingredients.split() - - -def get_lists(): - return [list(x) for x in ingredients.split()] - - -# Actions -def print_item(item): - print(item) - - -def reverse_item(item): - print(item[::-1]) - - -# Makes templates -def make_template(skeleton, getter, action): - """Instantiate a template method with getter and action""" - def template(): - skeleton(getter, action) - return template - -# Create our template functions -templates = [make_template(s, g, a) - for g in (get_list, get_lists) - for a in (print_item, reverse_item) - for s in (iter_elements, rev_elements)] - -# Execute them -for template in templates: - template() - -### OUTPUT ### -# spam -# ---------- -# eggs -# ---------- -# apple -# ---------- -# apple -# ---------- -# eggs -# ---------- -# spam -# ---------- -# maps -# ---------- -# sgge -# ---------- -# elppa -# ---------- -# elppa -# ---------- -# sgge -# ---------- -# maps -# ---------- -# ['s', 'p', 'a', 'm'] -# ---------- -# ['e', 'g', 'g', 's'] -# ---------- -# ['a', 'p', 'p', 'l', 'e'] -# ---------- -# ['a', 'p', 'p', 'l', 'e'] -# ---------- -# ['e', 'g', 'g', 's'] -# ---------- -# ['s', 'p', 'a', 'm'] -# ---------- -# ['m', 'a', 'p', 's'] -# ---------- -# ['s', 'g', 'g', 'e'] -# ---------- -# ['e', 'l', 'p', 'p', 'a'] -# ---------- -# ['e', 'l', 'p', 'p', 'a'] -# ---------- -# ['s', 'g', 'g', 'e'] -# ---------- -# ['m', 'a', 'p', 's'] -# ---------- diff --git a/behavioral/visitor.py b/behavioral/visitor.py deleted file mode 100644 index a7d4abce8..000000000 --- a/behavioral/visitor.py +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -http://peter-hoffmann.com/2010/extrinsic-visitor-pattern-python-inheritance.html - -*TL;DR80 -Separates an algorithm from an object structure on which it operates. -""" - - -class Node(object): - pass - - -class A(Node): - pass - - -class B(Node): - pass - - -class C(A, B): - pass - - -class Visitor(object): - - def visit(self, node, *args, **kwargs): - meth = None - for cls in node.__class__.__mro__: - meth_name = 'visit_' + cls.__name__ - meth = getattr(self, meth_name, None) - if meth: - break - - if not meth: - meth = self.generic_visit - return meth(node, *args, **kwargs) - - def generic_visit(self, node, *args, **kwargs): - print('generic_visit ' + node.__class__.__name__) - - def visit_B(self, node, *args, **kwargs): - print('visit_B ' + node.__class__.__name__) - - -a = A() -b = B() -c = C() -visitor = Visitor() -visitor.visit(a) -visitor.visit(b) -visitor.visit(c) - -### OUTPUT ### -# generic_visit A -# visit_B B -# visit_B C 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/config_backup/setup.cfg b/config_backup/setup.cfg new file mode 100644 index 000000000..e109555b4 --- /dev/null +++ b/config_backup/setup.cfg @@ -0,0 +1,13 @@ +[flake8] +max-line-length = 120 +ignore = E266 E731 W503 +exclude = venv* + +[tool:pytest] +filterwarnings = + ; ignore TestRunner class from facade example + ignore:.*test class 'TestRunner'.*:Warning + +[mypy] +python_version = 3.12 +ignore_missing_imports = True diff --git a/config_backup/tox.ini b/config_backup/tox.ini new file mode 100644 index 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/creational/borg.py b/creational/borg.py deleted file mode 100644 index d8008a59e..000000000 --- a/creational/borg.py +++ /dev/null @@ -1,92 +0,0 @@ -#!/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 -implement singleton behavior, but instead of having only one instance -of a class, there are multiple instances that share the same state. In -other words, the focus is on sharing state instead of sharing instance -identity. - -*What does this example do? -To understand the implementation of this pattern in Python, it is -important to know that, in Python, instance attributes are stored in a -attribute dictionary called __dict__. Usually, each instance will have -its own dictionary, but the Borg pattern modifies this so that all -instances have the same dictionary. -In this example, the __shared_state attribute will be the dictionary -shared between all instances, and this is ensured by assigining -__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 chages. The -same happends 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 - -*TL;DR80 -Provides singletone-like behavior sharing state between instances. -""" - - -class Borg(object): - __shared_state = {} - - def __init__(self): - self.__dict__ = self.__shared_state - self.state = 'Init' - - def __str__(self): - return self.state - - -class YourBorg(Borg): - pass - - -if __name__ == '__main__': - rm1 = Borg() - rm2 = Borg() - - rm1.state = 'Idle' - rm2.state = 'Running' - - print('rm1: {0}'.format(rm1)) - print('rm2: {0}'.format(rm2)) - - rm2.state = 'Zombie' - - print('rm1: {0}'.format(rm1)) - print('rm2: {0}'.format(rm2)) - - print('rm1 id: {0}'.format(id(rm1))) - print('rm2 id: {0}'.format(id(rm2))) - - rm3 = YourBorg() - - print('rm1: {0}'.format(rm1)) - print('rm2: {0}'.format(rm2)) - print('rm3: {0}'.format(rm3)) - -### OUTPUT ### -# rm1: Running -# rm2: Running -# rm1: Zombie -# rm2: Zombie -# rm1 id: 140732837899224 -# rm2 id: 140732837899296 -# rm1: Init -# rm2: Init -# rm3: Init diff --git a/creational/builder.py b/creational/builder.py deleted file mode 100644 index 2c642d7a6..000000000 --- a/creational/builder.py +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/python -# -*- coding : utf-8 -*- - -""" -*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? -This particular example uses a director function to abtract the -construction of a building. The user specifies a Builder (House or -Flat) and the director specifies the methods in the order necessary -creating a different building depending on the specified -specification (through the Builder class). - -@author: Diogenes Augusto Fernandes Herminio -https://gist.github.com/420905#file_builder_python.py - -*Where is the pattern used practically? - -*References: -https://sourcemaking.com/design_patterns/builder - -*TL;DR80 -Decouples the creation of a complex object and its representation. -""" - - -def construct_building(builder): - builder.new_building() - builder.build_floor() - builder.build_size() - return builder.building - - -# Abstract Builder -class Builder(object): - - def __init__(self): - self.building = None - - def new_building(self): - self.building = Building() - - def build_floor(self): - raise NotImplementedError - - def build_size(self): - raise NotImplementedError - -# Concrete Builder - - -class BuilderHouse(Builder): - - def build_floor(self): - self.building.floor = 'One' - - def build_size(self): - self.building.size = 'Big' - - -class BuilderFlat(Builder): - - def build_floor(self): - self.building.floor = 'More than One' - - def build_size(self): - self.building.size = 'Small' - - -# Product -class Building(object): - - def __init__(self): - self.floor = None - self.size = None - - def __repr__(self): - return 'Floor: {0.floor} | Size: {0.size}'.format(self) - - -# Client -if __name__ == "__main__": - building = construct_building(BuilderHouse()) - print(building) - building = construct_building(BuilderFlat()) - print(building) - -### OUTPUT ### -# Floor: One | Size: Big -# Floor: More than One | Size: Small diff --git a/creational/factory_method.py b/creational/factory_method.py deleted file mode 100644 index 741547845..000000000 --- a/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/creational/lazy_evaluation.py b/creational/lazy_evaluation.py deleted file mode 100644 index 84e69644d..000000000 --- a/creational/lazy_evaluation.py +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -Lazily-evaluated property pattern in Python. - -https://en.wikipedia.org/wiki/Lazy_evaluation - -*References: -bottle -https://github.com/bottlepy/bottle/blob/cafc15419cbb4a6cb748e6ecdccf92893bb25ce5/bottle.py#L270 -django -https://github.com/django/django/blob/ffd18732f3ee9e6f0374aff9ccf350d85187fac2/django/utils/functional.py#L19 -pip -https://github.com/pypa/pip/blob/cb75cca785629e15efb46c35903827b3eae13481/pip/utils/__init__.py#L821 -pyramimd -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 -Delays the eval of an expr until its value is needed and avoids repeated evals. -""" - -from __future__ import print_function -import functools - - -class lazy_property(object): - - def __init__(self, function): - self.function = function - functools.update_wrapper(self, function) - - def __get__(self, obj, type_): - if obj is None: - return self - val = self.function(obj) - obj.__dict__[self.function.__name__] = val - return val - - -def lazy_property2(fn): - attr = '_lazy__' + fn.__name__ - - @property - def _lazy_property(self): - if not hasattr(self, attr): - setattr(self, attr, fn(self)) - return getattr(self, attr) - return _lazy_property - - -class Person(object): - - def __init__(self, name, occupation): - self.name = name - self.occupation = occupation - self.call_count2 = 0 - - @lazy_property - def relatives(self): - # Get all relatives, let's assume that it costs much time. - relatives = "Many relatives." - return relatives - - @lazy_property2 - def parents(self): - self.call_count2 += 1 - return "Father and mother" - - -def main(): - Jhon = Person('Jhon', 'Coder') - 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'} -# Father and mother -# 1 diff --git a/creational/prototype.py b/creational/prototype.py deleted file mode 100644 index 4e99871d5..000000000 --- a/creational/prototype.py +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -*TL;DR80 -Creates new object instances by cloning prototype. -""" - - -class Prototype(object): - - value = 'default' - - def clone(self, **attrs): - """Clone a prototype and update inner attributes dictionary""" - # Python in Practice, Mark Summerfield - obj = self.__class__() - obj.__dict__.update(attrs) - return obj - - -class PrototypeDispatcher(object): - - def __init__(self): - self._objects = {} - - def get_objects(self): - """Get all objects""" - return self._objects - - def register_object(self, name, obj): - """Register an object""" - self._objects[name] = obj - - def unregister_object(self, name): - """Unregister an object""" - del self._objects[name] - - -def main(): - dispatcher = PrototypeDispatcher() - prototype = Prototype() - - 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()]) - - -if __name__ == '__main__': - main() - -### OUTPUT ### -# [{'objectb': 'b-value'}, {'default': 'default'}, {'objecta': 'a-value'}] diff --git a/dft/constructor_injection.py b/dft/constructor_injection.py deleted file mode 100644 index cb061fa8d..000000000 --- a/dft/constructor_injection.py +++ /dev/null @@ -1,55 +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/dft/parameter_injection.py b/dft/parameter_injection.py deleted file mode 100644 index 74487260d..000000000 --- a/dft/parameter_injection.py +++ /dev/null @@ -1,52 +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/dft/setter_injection.py b/dft/setter_injection.py deleted file mode 100644 index a8f6e2724..000000000 --- a/dft/setter_injection.py +++ /dev/null @@ -1,59 +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/fundamental/delegation_pattern.py b/fundamental/delegation_pattern.py deleted file mode 100644 index 85b78641b..000000000 --- a/fundamental/delegation_pattern.py +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -Reference: https://en.wikipedia.org/wiki/Delegation_pattern -Author: https://github.com/IuryAlves - -*TL;DR80 -Allows object composition to achieve the same code reuse as inheritance. -""" - - -class Delegator(object): - """ - >>> delegator = Delegator(Delegate()) - >>> delegator.do_something("nothing") - 'Doing nothing' - >>> delegator.do_anything() - - """ - - def __init__(self, delegate): - self.delegate = delegate - - def __getattr__(self, name): - def wrapper(*args, **kwargs): - if hasattr(self.delegate, name): - attr = getattr(self.delegate, name) - if callable(attr): - return attr(*args, **kwargs) - return wrapper - - -class Delegate(object): - - def do_something(self, something): - return "Doing %s" % something - - -if __name__ == '__main__': - import doctest - doctest.testmod() 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/other/blackboard.py b/other/blackboard.py deleted file mode 100644 index 1c9fa1d07..000000000 --- a/other/blackboard.py +++ /dev/null @@ -1,127 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -@author: Eugene Duboviy | github.com/duboviy - -In Blackboard pattern several specialised sub-systems (knowledge sources) -assemble their knowledge to build a possibly partial or approximate solution. -In this way, the sub-systems work together to solve the problem, -where the solution is the sum of its parts. - -https://en.wikipedia.org/wiki/Blackboard_system -""" - -import abc -import random - - -class Blackboard(object): - - def __init__(self): - self.experts = [] - self.common_state = { - 'problems': 0, - 'suggestions': 0, - 'contributions': [], - 'progress': 0 # percentage, if 100 -> task is finished - } - - def add_expert(self, expert): - self.experts.append(expert) - - -class Controller(object): - - def __init__(self, blackboard): - self.blackboard = blackboard - - def run_loop(self): - 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 - - @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): - - @property - def is_eager_to_contribute(self): - 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) - - -class Scientist(AbstractExpert): - - @property - def is_eager_to_contribute(self): - 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) - - -class Professor(AbstractExpert): - - @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'] diff --git a/other/graph_search.py b/other/graph_search.py deleted file mode 100644 index 3ccf883be..000000000 --- a/other/graph_search.py +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -"" - -class GraphSearch: - - """Graph search emulation in python, from source - http://www.python.org/doc/essays/graphs/""" - - def __init__(self, graph): - self.graph = graph - - def find_path(self, start, end, path=None): - path = path or [] - - path.append(start) - if start == end: - return path - for node in self.graph.get(start, []): - if node not in path: - newpath = self.find_path(node, end, path) - if newpath: - return newpath - - def find_all_path(self, start, end, path=None): - path = path or [] - path.append(start) - if start == end: - return [path] - paths = [] - for node in self.graph.get(start, []): - if node not in path: - newpaths = self.find_all_path(node, end, path[:]) - paths.extend(newpaths) - return paths - - def find_shortest_path(self, start, end, path=None): - path = path or [] - path.append(start) - - if start == end: - return path - shortest = None - for node in self.graph.get(start, []): - if node not in path: - newpath = self.find_shortest_path(node, end, path[:]) - if newpath: - if not shortest or len(newpath) < len(shortest): - shortest = newpath - return shortest - -# example of graph usage -graph = {'A': ['B', 'C'], - 'B': ['C', 'D'], - 'C': ['D'], - 'D': ['C'], - 'E': ['F'], - 'F': ['C'] - } - -# initialization of new graph search object -graph1 = GraphSearch(graph) - - -print(graph1.find_path('A', 'D')) -print(graph1.find_all_path('A', 'D')) -print(graph1.find_shortest_path('A', 'D')) - -### OUTPUT ### -# ['A', 'B', 'C', 'D'] -# [['A', 'B', 'C', 'D'], ['A', 'B', 'D'], ['A', 'C', 'D']] -# ['A', 'B', 'D'] diff --git a/behavioral/__init__.py b/patterns/__init__.py similarity index 100% rename from behavioral/__init__.py rename to patterns/__init__.py diff --git a/creational/__init__.py b/patterns/behavioral/__init__.py similarity index 100% rename from creational/__init__.py rename to patterns/behavioral/__init__.py diff --git a/patterns/behavioral/catalog.py b/patterns/behavioral/catalog.py new file mode 100644 index 000000000..4074c1d21 --- /dev/null +++ b/patterns/behavioral/catalog.py @@ -0,0 +1,173 @@ +""" +A class that uses different static functions depending on a parameter passed +during initialization. Uses a single dictionary instead of multiple conditions. +""" + +__author__ = "Ibrahim Diop " + + +class Catalog: + """catalog of multiple static methods that are executed depending on an init parameter""" + + def __init__(self, param: str) -> None: + # dictionary that will be used to determine which static method is + # to be executed but that will be also used to store possible param + # value + self._static_method_choices = { + "param_value_1": self._static_method_1, + "param_value_2": self._static_method_2, + } + + # simple test to validate param value + if param in self._static_method_choices.keys(): + self.param = param + else: + raise ValueError(f"Invalid Value for Param: {param}") + + @staticmethod + def _static_method_1() -> str: + return "executed method 1!" + + @staticmethod + def _static_method_2() -> str: + return "executed method 2!" + + def main_method(self) -> str: + """will execute either _static_method_1 or _static_method_2 + + depending on self.param value + """ + return self._static_method_choices[self.param]() + + +# Alternative implementation for different levels of methods +class CatalogInstance: + """catalog of multiple methods that are executed depending on an init + parameter + """ + + def __init__(self, param: str) -> None: + self.x1 = "x1" + self.x2 = "x2" + # simple test to validate param value + if param in self._instance_method_choices: + self.param = param + else: + raise ValueError(f"Invalid Value for Param: {param}") + + def _instance_method_1(self) -> str: + return f"Value {self.x1}" + + def _instance_method_2(self) -> str: + return f"Value {self.x2}" + + _instance_method_choices = { + "param_value_1": _instance_method_1, + "param_value_2": _instance_method_2, + } + + def main_method(self) -> str: + """will execute either _instance_method_1 or _instance_method_2 + + depending on self.param value + """ + return self._instance_method_choices[self.param].__get__(self)() # type: ignore + # type ignore reason: https://github.com/python/mypy/issues/10206 + + +class CatalogClass: + """catalog of multiple class methods that are executed depending on an init + parameter + """ + + x1 = "x1" + x2 = "x2" + + def __init__(self, param: str) -> None: + # simple test to validate param value + if param in self._class_method_choices: + self.param = param + else: + raise ValueError(f"Invalid Value for Param: {param}") + + @classmethod + def _class_method_1(cls) -> str: + return f"Value {cls.x1}" + + @classmethod + def _class_method_2(cls) -> str: + return f"Value {cls.x2}" + + _class_method_choices = { + "param_value_1": _class_method_1, + "param_value_2": _class_method_2, + } + + def main_method(self) -> str: + """will execute either _class_method_1 or _class_method_2 + + depending on self.param value + """ + return self._class_method_choices[self.param].__get__(None, self.__class__)() # type: ignore + # type ignore reason: https://github.com/python/mypy/issues/10206 + + +class CatalogStatic: + """catalog of multiple static methods that are executed depending on an init + parameter + """ + + def __init__(self, param: str) -> None: + # simple test to validate param value + if param in self._static_method_choices: + self.param = param + else: + raise ValueError(f"Invalid Value for Param: {param}") + + @staticmethod + def _static_method_1() -> str: + return "executed method 1!" + + @staticmethod + def _static_method_2() -> str: + return "executed method 2!" + + _static_method_choices = { + "param_value_1": _static_method_1, + "param_value_2": _static_method_2, + } + + def main_method(self) -> str: + """will execute either _static_method_1 or _static_method_2 + + depending on self.param value + """ + + return self._static_method_choices[self.param].__get__(None, self.__class__)() # type: ignore + # type ignore reason: https://github.com/python/mypy/issues/10206 + + +def main(): + """ + >>> test = Catalog('param_value_2') + >>> test.main_method() + 'executed method 2!' + + >>> test = CatalogInstance('param_value_1') + >>> test.main_method() + 'Value x1' + + >>> test = CatalogClass('param_value_2') + >>> test.main_method() + 'Value x2' + + >>> test = CatalogStatic('param_value_1') + >>> test.main_method() + 'executed method 1!' + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/behavioral/chain_of_responsibility.py b/patterns/behavioral/chain_of_responsibility.py new file mode 100644 index 000000000..a44a9210e --- /dev/null +++ b/patterns/behavioral/chain_of_responsibility.py @@ -0,0 +1,123 @@ +""" +*What is this pattern about? + +The Chain of responsibility is an object oriented version of the +`if ... elif ... elif ... else ...` idiom, with the +benefit that the condition–action blocks can be dynamically rearranged +and reconfigured at runtime. + +This pattern aims to decouple the senders of a request from its +receivers by allowing request to move through chained +receivers until it is handled. + +Request receiver in simple form keeps a reference to a single successor. +As a variation some receivers may be capable of sending requests out +in several directions, forming a `tree of responsibility`. + +*Examples in Python ecosystem: +Django Middleware: https://docs.djangoproject.com/en/stable/topics/http/middleware/ +The middleware components act as a chain where each processes the request/response. + +*TL;DR +Allow a request to pass down a chain of receivers until it is handled. +""" + +from abc import ABC, abstractmethod +from typing import Optional + + +class Handler(ABC): + def __init__(self, successor: Optional["Handler"] = None): + self.successor = successor + + def handle(self, request: int) -> None: + """ + Handle request and stop. + If can't - call next handler in chain. + + As an alternative you might even in case of success + call the next handler. + """ + res = self.check_range(request) + if not res and self.successor: + self.successor.handle(request) + + @abstractmethod + def check_range(self, request: int) -> bool | None: + """Compare passed value to predefined interval""" + + +class ConcreteHandler0(Handler): + """Each handler can be different. + Be simple and static... + """ + + @staticmethod + def check_range(request: int) -> bool | None: + if 0 <= request < 10: + print(f"request {request} handled in handler 0") + return True + return None + + +class ConcreteHandler1(Handler): + """... With it's own internal state""" + + start, end = 10, 20 + + def check_range(self, request: int) -> bool | None: + if self.start <= request < self.end: + print(f"request {request} handled in handler 1") + return True + return None + + +class ConcreteHandler2(Handler): + """... With helper methods.""" + + def check_range(self, request: int) -> bool | None: + start, end = self.get_interval_from_db() + if start <= request < end: + print(f"request {request} handled in handler 2") + return True + return None + + @staticmethod + def get_interval_from_db() -> tuple[int, int]: + return (20, 30) + + +class FallbackHandler(Handler): + @staticmethod + def check_range(request: int) -> 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) + request 2 handled in handler 0 + request 5 handled in handler 0 + request 14 handled in handler 1 + request 22 handled in handler 2 + request 18 handled in handler 1 + request 3 handled in handler 0 + end of chain, no handler for 35 + request 27 handled in handler 2 + request 20 handled in handler 2 + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod(optionflags=doctest.ELLIPSIS) diff --git a/patterns/behavioral/chaining_method.py b/patterns/behavioral/chaining_method.py new file mode 100644 index 000000000..26f110181 --- /dev/null +++ b/patterns/behavioral/chaining_method.py @@ -0,0 +1,37 @@ +from __future__ import annotations + + +class Person: + def __init__(self, name: str) -> None: + self.name = name + + def do_action(self, action: Action) -> Action: + print(self.name, action.name, end=" ") + return action + + +class Action: + def __init__(self, name: str) -> None: + self.name = name + + def amount(self, val: str) -> Action: + print(val, end=" ") + return self + + def stop(self) -> None: + print("then stop") + + +def main(): + """ + >>> move = Action('move') + >>> person = Person('Jack') + >>> person.do_action(move).amount('5m').stop() + Jack move 5m then stop + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/behavioral/command.py b/patterns/behavioral/command.py new file mode 100644 index 000000000..d0c3944e4 --- /dev/null +++ b/patterns/behavioral/command.py @@ -0,0 +1,105 @@ +""" +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 +""" + + +class HideFileCommand: + """ + A command to hide a file given its name + """ + + def __init__(self) -> None: + # an array of files hidden, to undo them as needed + self._hidden_files: list[str] = [] + + def execute(self, filename: str) -> None: + print(f"hiding {filename}") + self._hidden_files.append(filename) + + def undo(self) -> None: + filename = self._hidden_files.pop() + print(f"un-hiding {filename}") + + +class DeleteFileCommand: + """ + A command to delete a file given its name + """ + + def __init__(self) -> None: + # an array of deleted files, to undo them as needed + self._deleted_files: list[str] = [] + + def execute(self, filename: str) -> None: + print(f"deleting {filename}") + self._deleted_files.append(filename) + + def undo(self) -> None: + filename = self._deleted_files.pop() + print(f"restoring {filename}") + + +class MenuItem: + """ + The invoker class. Here it is items in a menu. + """ + + def __init__(self, command: HideFileCommand | DeleteFileCommand) -> None: + self._command = command + + def on_do_press(self, filename: str) -> None: + self._command.execute(filename) + + def on_undo_press(self) -> None: + self._command.undo() + + +def main(): + """ + >>> item1 = MenuItem(DeleteFileCommand()) + + >>> item2 = MenuItem(HideFileCommand()) + + # create a file named `test-file` to work with + >>> test_file_name = 'test-file' + + # deleting `test-file` + >>> item1.on_do_press(test_file_name) + deleting test-file + + # restoring `test-file` + >>> item1.on_undo_press() + restoring test-file + + # hiding `test-file` + >>> item2.on_do_press(test_file_name) + hiding test-file + + # un-hiding `test-file` + >>> item2.on_undo_press() + un-hiding test-file + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/behavioral/iterator.py b/patterns/behavioral/iterator.py new file mode 100644 index 000000000..3ed4043b9 --- /dev/null +++ b/patterns/behavioral/iterator.py @@ -0,0 +1,47 @@ +""" +http://ginstrom.com/scribbles/2007/10/08/design-patterns-python-style/ +Implementation of the iterator pattern with a generator + +*TL;DR +Traverses a container and accesses the container's elements. +""" + + +def count_to(count: int): + """Counts by word numbers, up to a maximum of five""" + numbers = ["one", "two", "three", "four", "five"] + yield from numbers[:count] + + +# Test the generator +def count_to_two() -> None: + return count_to(2) + + +def count_to_five() -> None: + return count_to(5) + + +def main(): + """ + # Counting to two... + >>> for number in count_to_two(): + ... print(number) + one + two + + # Counting to five... + >>> for number in count_to_five(): + ... print(number) + one + two + three + four + five + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/behavioral/iterator_alt.py b/patterns/behavioral/iterator_alt.py new file mode 100644 index 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 new file mode 100644 index 000000000..122852b77 --- /dev/null +++ b/patterns/behavioral/mediator.py @@ -0,0 +1,53 @@ +""" +https://www.djangospin.com/design-patterns-python/mediator/ + +Objects in a system communicate through a Mediator instead of directly with each other. +This reduces the dependencies between communicating objects, thereby reducing coupling. + +*TL;DR +Encapsulates how a set of objects interact. +""" + +from __future__ import annotations + + +class ChatRoom: + """Mediator class""" + + def display_message(self, user: User, message: str) -> str: + return f"[{user} says]: {message}" + + +class User: + """A class whose instances want to interact with each other""" + + def __init__(self, name: str) -> None: + self.name = name + self.chat_room = ChatRoom() + + def say(self, message: str) -> str: + return self.chat_room.display_message(self, message) + + def __str__(self) -> str: + return self.name + + +def main(): + """ + >>> molly = User('Molly') + >>> mark = User('Mark') + >>> ethan = User('Ethan') + + >>> molly.say("Hi Team! Meeting at 3 PM today.") + '[Molly says]: Hi Team! Meeting at 3 PM today.' + >>> mark.say("Roger that!") + '[Mark says]: Roger that!' + >>> ethan.say("Alright.") + '[Ethan says]: Alright.' + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/behavioral/memento.py b/patterns/behavioral/memento.py new file mode 100644 index 000000000..a14ea8a0d --- /dev/null +++ b/patterns/behavioral/memento.py @@ -0,0 +1,146 @@ +""" +http://code.activestate.com/recipes/413838-memento-closure/ + +*TL;DR +Provides the ability to restore an object to its previous state. +""" + +from collections.abc import Callable +from copy import copy, deepcopy +from typing import Any + + +def memento(obj: Any, deep: bool = False) -> Callable: + state = deepcopy(obj.__dict__) if deep else copy(obj.__dict__) + + def restore() -> None: + obj.__dict__.clear() + obj.__dict__.update(state) + + return restore + + +class Transaction: + """A transaction guard. + + This is, in fact, just syntactic sugar around a memento closure. + """ + + deep = False + states: list[Callable[[], None]] = [] + + def __init__(self, deep: bool, *targets: Any) -> None: + self.deep = deep + self.targets = targets + self.commit() + + def commit(self) -> None: + self.states = [memento(target, self.deep) for target in self.targets] + + def rollback(self) -> None: + for a_state in self.states: + a_state() + + +class Transactional: + """Adds transactional semantics to methods. Methods decorated with + @Transactional will roll back to entry-state upon exceptions. + + :param method: The function to be decorated. + """ + + def __init__(self, method: Callable) -> None: + self.method = method + + def __get__(self, obj: Any, T: type) -> Callable: + """ + A decorator that makes a function transactional. + + :param method: The function to be decorated. + """ + + def transaction(*args, **kwargs): + state = memento(obj) + try: + return self.method(obj, *args, **kwargs) + except Exception as e: + state() + raise e + + return transaction + + +class NumObj: + def __init__(self, value: int) -> None: + self.value = value + + def __repr__(self) -> str: + return f"<{self.__class__.__name__}: {self.value!r}>" + + def increment(self) -> None: + self.value += 1 + + @Transactional + def do_stuff(self) -> None: + self.__dict__["value"] = "1111" # <- intentionally invalid value + self.increment() # <- will fail and rollback + + +def main(): + """ + >>> num_obj = NumObj(-1) + >>> print(num_obj) + + + >>> a_transaction = Transaction(True, num_obj) + + >>> try: + ... for i in range(3): + ... num_obj.increment() + ... print(num_obj) + ... a_transaction.commit() + ... print('-- committed') + ... for i in range(3): + ... num_obj.increment() + ... print(num_obj) + ... num_obj.value += 'x' # will fail + ... print(num_obj) + ... except Exception: + ... a_transaction.rollback() + ... print('-- rolled back') + + + + -- committed + + + + -- rolled back + + >>> print(num_obj) + + + >>> print('-- now doing stuff ...') + -- now doing stuff ... + + >>> try: + ... num_obj.do_stuff() + ... except Exception: + ... print('-> doing stuff failed!') + ... import sys + ... import traceback + ... traceback.print_exc(file=sys.stdout) + -> doing stuff failed! + Traceback (most recent call last): + ... + TypeError: ...str...int... + + >>> print(num_obj) + + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod(optionflags=doctest.ELLIPSIS) diff --git a/patterns/behavioral/observer.py b/patterns/behavioral/observer.py new file mode 100644 index 000000000..9ea97bfc0 --- /dev/null +++ b/patterns/behavioral/observer.py @@ -0,0 +1,135 @@ +""" +http://code.activestate.com/recipes/131499-observer-pattern/ + +*TL;DR +Maintains a list of dependents and notifies them of any state changes. + +*Examples in Python ecosystem: +Django Signals: https://docs.djangoproject.com/en/3.1/topics/signals/ +Flask Signals: https://flask.palletsprojects.com/en/1.1.x/signals/ +""" + +# observer.py + +from __future__ import annotations + + +class Observer: + def update(self, subject: Subject) -> None: + """ + Receive update from the subject. + + Args: + subject (Subject): The subject instance sending the update. + """ + pass + + +class Subject: + _observers: list[Observer] + + def __init__(self) -> None: + """ + Initialize the subject with an empty observer list. + """ + self._observers = [] + + def attach(self, observer: Observer) -> None: + """ + Attach an observer to the subject. + + Args: + observer (Observer): The observer instance to attach. + """ + if observer not in self._observers: + self._observers.append(observer) + + def detach(self, observer: Observer) -> None: + """ + Detach an observer from the subject. + + Args: + observer (Observer): The observer instance to detach. + """ + try: + self._observers.remove(observer) + except ValueError: + pass + + def notify(self) -> None: + """ + Notify all attached observers by calling their update method. + """ + for observer in self._observers: + observer.update(self) + + +class Data(Subject): + def __init__(self, name: str = "") -> None: + super().__init__() + self.name = name + self._data = 0 + + @property + def data(self) -> int: + return self._data + + @data.setter + def data(self, value: int) -> None: + self._data = value + self.notify() + + +class HexViewer: + def update(self, subject: Data) -> None: + print(f"HexViewer: Subject {subject.name} has data 0x{subject.data:x}") + + +class DecimalViewer: + def update(self, subject: Data) -> None: + print(f"DecimalViewer: Subject {subject.name} has data {subject.data}") + + +def main(): + """ + >>> data1 = Data('Data 1') + >>> data2 = Data('Data 2') + >>> view1 = DecimalViewer() + >>> view2 = HexViewer() + >>> data1.attach(view1) + >>> data1.attach(view2) + >>> data2.attach(view2) + >>> data2.attach(view1) + + >>> data1.data = 10 + DecimalViewer: Subject Data 1 has data 10 + HexViewer: Subject Data 1 has data 0xa + + >>> data2.data = 15 + HexViewer: Subject Data 2 has data 0xf + DecimalViewer: Subject Data 2 has data 15 + + >>> data1.data = 3 + DecimalViewer: Subject Data 1 has data 3 + HexViewer: Subject Data 1 has data 0x3 + + >>> data2.data = 5 + HexViewer: Subject Data 2 has data 0x5 + DecimalViewer: Subject Data 2 has data 5 + + # Detach HexViewer from data1 and data2 + >>> data1.detach(view2) + >>> data2.detach(view2) + + >>> data1.data = 10 + DecimalViewer: Subject Data 1 has data 10 + + >>> data2.data = 15 + DecimalViewer: Subject Data 2 has data 15 + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/behavioral/publish_subscribe.py b/patterns/behavioral/publish_subscribe.py new file mode 100644 index 000000000..91e74ab67 --- /dev/null +++ b/patterns/behavioral/publish_subscribe.py @@ -0,0 +1,95 @@ +""" +Reference: +http://www.slideshare.net/ishraqabd/publish-subscribe-model-overview-13368808 +Author: https://github.com/HanWenfang +""" + +from __future__ import annotations + + +class Provider: + def __init__(self) -> None: + self.msg_queue: list[str] = [] + self.subscribers: dict[str, list[Subscriber]] = {} + + def notify(self, msg: str) -> None: + self.msg_queue.append(msg) + + def subscribe(self, msg: str, subscriber: Subscriber) -> None: + self.subscribers.setdefault(msg, []).append(subscriber) + + def unsubscribe(self, msg: str, subscriber: Subscriber) -> None: + self.subscribers[msg].remove(subscriber) + + def update(self) -> None: + for msg in self.msg_queue: + for sub in self.subscribers.get(msg, []): + sub.run(msg) + self.msg_queue = [] + + +class Publisher: + def __init__(self, msg_center: Provider) -> None: + self.provider = msg_center + + def publish(self, msg: str) -> None: + self.provider.notify(msg) + + +class Subscriber: + def __init__(self, name: str, msg_center: Provider) -> None: + self.name = name + self.provider = msg_center + + def subscribe(self, msg: str) -> None: + self.provider.subscribe(msg, self) + + def unsubscribe(self, msg: str) -> None: + self.provider.unsubscribe(msg, self) + + def run(self, msg: str) -> None: + print(f"{self.name} got {msg}") + + +def main(): + """ + >>> message_center = Provider() + + >>> fftv = Publisher(message_center) + + >>> jim = Subscriber("jim", message_center) + >>> jim.subscribe("cartoon") + >>> jack = Subscriber("jack", message_center) + >>> jack.subscribe("music") + >>> gee = Subscriber("gee", message_center) + >>> gee.subscribe("movie") + >>> vani = Subscriber("vani", message_center) + >>> vani.subscribe("movie") + >>> vani.unsubscribe("movie") + + # Note that no one subscribed to `ads` + # and that vani changed their mind + + >>> fftv.publish("cartoon") + >>> fftv.publish("music") + >>> fftv.publish("ads") + >>> fftv.publish("movie") + >>> fftv.publish("cartoon") + >>> fftv.publish("cartoon") + >>> fftv.publish("movie") + >>> fftv.publish("blank") + + >>> message_center.update() + jim got cartoon + jack got music + gee got movie + jim got cartoon + jim got cartoon + gee got movie + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/behavioral/registry.py b/patterns/behavioral/registry.py new file mode 100644 index 000000000..1288c1f6a --- /dev/null +++ b/patterns/behavioral/registry.py @@ -0,0 +1,45 @@ +class RegistryHolder(type): + REGISTRY: dict[str, "RegistryHolder"] = {} + + def __new__(cls, name, bases, attrs): + new_cls = type.__new__(cls, name, bases, attrs) + """ + Here the name of the class is used as key but it could be any class + parameter. + """ + cls.REGISTRY[new_cls.__name__] = new_cls + return new_cls + + @classmethod + def get_registry(cls): + return dict(cls.REGISTRY) + + +class BaseRegisteredClass(metaclass=RegistryHolder): + """ + Any class that will inherits from BaseRegisteredClass will be included + inside the dict RegistryHolder.REGISTRY, the key being the name of the + class and the associated value, the class itself. + """ + + +def main(): + """ + Before subclassing + >>> sorted(RegistryHolder.REGISTRY) + ['BaseRegisteredClass'] + + >>> class ClassRegistree(BaseRegisteredClass): + ... def __init__(self, *args, **kwargs): + ... pass + + After subclassing + >>> sorted(RegistryHolder.REGISTRY) + ['BaseRegisteredClass', 'ClassRegistree'] + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod(optionflags=doctest.ELLIPSIS) diff --git a/patterns/behavioral/servant.py b/patterns/behavioral/servant.py new file mode 100644 index 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 new file mode 100644 index 000000000..10d226894 --- /dev/null +++ b/patterns/behavioral/specification.py @@ -0,0 +1,110 @@ +""" +@author: Gordeev Andrey + +*TL;DR +Provides recombination business logic by chaining together using boolean logic. +""" + +from abc import abstractmethod +from typing import Union + + +class Specification: + def and_specification(self, candidate): + raise NotImplementedError() + + def or_specification(self, candidate): + raise NotImplementedError() + + def not_specification(self): + raise NotImplementedError() + + @abstractmethod + def is_satisfied_by(self, candidate): + pass + + +class CompositeSpecification(Specification): + @abstractmethod + def is_satisfied_by(self, candidate): + pass + + def and_specification(self, candidate: "Specification") -> "AndSpecification": + return AndSpecification(self, candidate) + + def or_specification(self, candidate: "Specification") -> "OrSpecification": + return OrSpecification(self, candidate) + + def not_specification(self) -> "NotSpecification": + return NotSpecification(self) + + +class AndSpecification(CompositeSpecification): + def __init__(self, one: "Specification", other: "Specification") -> None: + self._one: Specification = one + self._other: Specification = other + + def is_satisfied_by(self, candidate: Union["User", str]) -> bool: + return bool( + self._one.is_satisfied_by(candidate) + and self._other.is_satisfied_by(candidate) + ) + + +class OrSpecification(CompositeSpecification): + def __init__(self, one: "Specification", other: "Specification") -> None: + self._one: Specification = one + self._other: Specification = other + + def is_satisfied_by(self, candidate: Union["User", str]): + return bool( + self._one.is_satisfied_by(candidate) + or self._other.is_satisfied_by(candidate) + ) + + +class NotSpecification(CompositeSpecification): + def __init__(self, wrapped: "Specification"): + self._wrapped: Specification = wrapped + + def is_satisfied_by(self, candidate: Union["User", str]): + return bool(not self._wrapped.is_satisfied_by(candidate)) + + +class User: + def __init__(self, super_user: bool = False) -> None: + self.super_user = super_user + + +class UserSpecification(CompositeSpecification): + def is_satisfied_by(self, candidate: Union["User", str]) -> bool: + return isinstance(candidate, User) + + +class SuperUserSpecification(CompositeSpecification): + def is_satisfied_by(self, candidate: "User") -> bool: + return getattr(candidate, "super_user", False) + + +def main(): + """ + >>> andrey = User() + >>> ivan = User(super_user=True) + >>> vasiliy = 'not User instance' + + >>> root_specification = UserSpecification().and_specification(SuperUserSpecification()) + + # Is specification satisfied by + >>> root_specification.is_satisfied_by(andrey), 'andrey' + (False, 'andrey') + >>> root_specification.is_satisfied_by(ivan), 'ivan' + (True, 'ivan') + >>> root_specification.is_satisfied_by(vasiliy), 'vasiliy' + (False, 'vasiliy') + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/behavioral/state.py b/patterns/behavioral/state.py new file mode 100644 index 000000000..8d792de93 --- /dev/null +++ b/patterns/behavioral/state.py @@ -0,0 +1,96 @@ +""" +Implementation of the state pattern + +http://ginstrom.com/scribbles/2007/10/08/design-patterns-python-style/ + +*TL;DR +Implements state as a derived class of the state pattern interface. +Implements state transitions by invoking methods from the pattern's superclass. +""" + +from __future__ import annotations + + +class State: + """Base state. This is to share functionality""" + + 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(f"Scanning... Station is {self.stations[self.pos]} {self.name}") + + +class AmState(State): + def __init__(self, radio: Radio) -> None: + self.radio = radio + self.stations = ["1250", "1380", "1510"] + self.pos = 0 + self.name = "AM" + + def toggle_amfm(self) -> None: + print("Switching to FM") + self.radio.state = self.radio.fmstate + + +class FmState(State): + def __init__(self, radio: Radio) -> None: + self.radio = radio + self.stations = ["81.3", "89.1", "103.9"] + self.pos = 0 + self.name = "FM" + + def toggle_amfm(self) -> None: + print("Switching to AM") + self.radio.state = self.radio.amstate + + +class Radio: + """A radio. It has a scan button, and an AM/FM toggle switch.""" + + def __init__(self) -> None: + """We have an AM state and an FM state""" + self.amstate = AmState(self) + self.fmstate = FmState(self) + self.state: State = self.amstate + + def toggle_amfm(self) -> None: + self.state.toggle_amfm() + + def scan(self) -> None: + self.state.scan() + + +def main(): + """ + >>> radio = Radio() + >>> actions = [radio.scan] * 2 + [radio.toggle_amfm] + [radio.scan] * 2 + >>> actions *= 2 + + >>> for action in actions: + ... action() + Scanning... Station is 1380 AM + Scanning... Station is 1510 AM + Switching to FM + Scanning... Station is 89.1 FM + Scanning... Station is 103.9 FM + Scanning... Station is 81.3 FM + Scanning... Station is 89.1 FM + Switching to AM + Scanning... Station is 1250 AM + Scanning... Station is 1380 AM + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/behavioral/strategy.py b/patterns/behavioral/strategy.py new file mode 100644 index 000000000..13b1c0f5d --- /dev/null +++ b/patterns/behavioral/strategy.py @@ -0,0 +1,99 @@ +""" +*What is this pattern about? +Define a family of algorithms, encapsulate each one, and make them interchangeable. +Strategy lets the algorithm vary independently from clients that use it. + +*TL;DR +Enables selecting an algorithm at runtime. +""" + +from __future__ import annotations + +from 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: + discount_strategy = DiscountStrategyValidator() + + def __init__( + self, price: float, discount_strategy: DiscountStrategy | None = None + ) -> None: + self.price: float = price + self.discount_strategy = discount_strategy + + def apply_discount(self) -> float: + if self.discount_strategy: + discount = self.discount_strategy(self) + else: + discount = 0 + + return self.price - discount + + def __repr__(self) -> str: + strategy = getattr(self.discount_strategy, "__name__", None) + return f"" + + +def ten_percent_discount(order: Order) -> float: + return order.price * 0.10 + + +def on_sale_discount(order: Order) -> float: + return order.price * 0.25 + 20 + + +def main(): + """ + >>> order = Order(100, discount_strategy=ten_percent_discount) + >>> print(order) + + >>> print(order.apply_discount()) + 90.0 + >>> order = Order(100, discount_strategy=on_sale_discount) + >>> print(order) + + >>> print(order.apply_discount()) + 55.0 + >>> order = Order(10, discount_strategy=on_sale_discount) + Discount cannot be applied due to negative price resulting. on_sale_discount + >>> print(order) + + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/behavioral/template.py b/patterns/behavioral/template.py new file mode 100644 index 000000000..76fc136bc --- /dev/null +++ b/patterns/behavioral/template.py @@ -0,0 +1,73 @@ +""" +An example of the Template pattern in Python + +*TL;DR +Defines the skeleton of a base algorithm, deferring definition of exact +steps to subclasses. + +*Examples in Python ecosystem: +Django class based views: https://docs.djangoproject.com/en/2.1/topics/class-based-views/ +""" + + +def get_text() -> str: + return "plain-text" + + +def get_pdf() -> str: + return "pdf" + + +def get_csv() -> str: + return "csv" + + +def convert_to_text(data: str) -> str: + print("[CONVERT]") + return f"{data} as text" + + +def saver() -> None: + print("[SAVE]") + + +def template_function(getter, converter=False, to_save=False) -> None: + data = getter() + print(f"Got `{data}`") + + if len(data) <= 3 and converter: + data = converter(data) + else: + print("Skip conversion") + + if to_save: + saver() + + print(f"`{data}` was processed") + + +def main(): + """ + >>> template_function(get_text, to_save=True) + Got `plain-text` + Skip conversion + [SAVE] + `plain-text` was processed + + >>> template_function(get_pdf, converter=convert_to_text) + Got `pdf` + [CONVERT] + `pdf as text` was processed + + >>> template_function(get_csv, to_save=True) + Got `csv` + Skip conversion + [SAVE] + `csv` was processed + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/behavioral/visitor.py b/patterns/behavioral/visitor.py new file mode 100644 index 000000000..ab2f5f713 --- /dev/null +++ b/patterns/behavioral/visitor.py @@ -0,0 +1,74 @@ +""" +http://peter-hoffmann.com/2010/extrinsic-visitor-pattern-python-inheritance.html + +*TL;DR +Separates an algorithm from an object structure on which it operates. + +An interesting recipe could be found in +Brian Jones, David Beazley "Python Cookbook" (2013): +- "8.21. Implementing the Visitor Pattern" +- "8.22. Implementing the Visitor Pattern Without Recursion" + +*Examples in Python ecosystem: +- Python's ast.NodeVisitor: https://github.com/python/cpython/blob/master/Lib/ast.py#L250 +which is then being used e.g. in tools like `pyflakes`. +- `Black` formatter tool implements it's own: https://github.com/ambv/black/blob/master/black.py#L718 +""" + + +class Node: + pass + + +class A(Node): + pass + + +class B(Node): + pass + + +class C(A, B): + pass + + +class Visitor: + def visit(self, node: Node, *args, **kwargs) -> str: + meth = None + for cls in node.__class__.__mro__: + meth_name = "visit_" + cls.__name__ + meth = getattr(self, meth_name, None) + if meth: + break + + if not meth: + meth = self.generic_visit + return meth(node, *args, **kwargs) + + def generic_visit(self, node: Node, *args, **kwargs) -> str: + return "generic_visit " + node.__class__.__name__ + + def visit_B(self, node: Node, *args, **kwargs) -> str: + return "visit_B " + node.__class__.__name__ + + +def main(): + """ + >>> a, b, c = A(), B(), C() + >>> visitor = Visitor() + + >>> visitor.visit(a) + 'generic_visit A' + + >>> visitor.visit(b) + 'visit_B B' + + >>> visitor.visit(c) + 'visit_B C' + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/behavioral/viz/catalog.py.png b/patterns/behavioral/viz/catalog.py.png similarity index 100% rename from behavioral/viz/catalog.py.png rename to patterns/behavioral/viz/catalog.py.png diff --git a/behavioral/viz/chain.py.png b/patterns/behavioral/viz/chain.py.png similarity index 100% rename from behavioral/viz/chain.py.png rename to patterns/behavioral/viz/chain.py.png diff --git a/behavioral/viz/chaining_method.py.png b/patterns/behavioral/viz/chaining_method.py.png similarity index 100% rename from behavioral/viz/chaining_method.py.png rename to patterns/behavioral/viz/chaining_method.py.png diff --git a/behavioral/viz/command.py.png b/patterns/behavioral/viz/command.py.png similarity index 100% rename from behavioral/viz/command.py.png rename to patterns/behavioral/viz/command.py.png diff --git a/behavioral/viz/iterator.py.png b/patterns/behavioral/viz/iterator.py.png similarity index 100% rename from behavioral/viz/iterator.py.png rename to patterns/behavioral/viz/iterator.py.png diff --git a/behavioral/viz/mediator.py.png b/patterns/behavioral/viz/mediator.py.png similarity index 100% rename from behavioral/viz/mediator.py.png rename to patterns/behavioral/viz/mediator.py.png diff --git a/behavioral/viz/memento.py.png b/patterns/behavioral/viz/memento.py.png similarity index 100% rename from behavioral/viz/memento.py.png rename to patterns/behavioral/viz/memento.py.png diff --git a/behavioral/viz/observer.py.png b/patterns/behavioral/viz/observer.py.png similarity index 100% rename from behavioral/viz/observer.py.png rename to patterns/behavioral/viz/observer.py.png diff --git a/behavioral/viz/publish_subscribe.py.png b/patterns/behavioral/viz/publish_subscribe.py.png similarity index 100% rename from behavioral/viz/publish_subscribe.py.png rename to patterns/behavioral/viz/publish_subscribe.py.png diff --git a/behavioral/viz/registry.py.png b/patterns/behavioral/viz/registry.py.png similarity index 100% rename from behavioral/viz/registry.py.png rename to patterns/behavioral/viz/registry.py.png diff --git a/behavioral/viz/specification.py.png b/patterns/behavioral/viz/specification.py.png similarity index 100% rename from behavioral/viz/specification.py.png rename to patterns/behavioral/viz/specification.py.png diff --git a/behavioral/viz/state.py.png b/patterns/behavioral/viz/state.py.png similarity index 100% rename from behavioral/viz/state.py.png rename to patterns/behavioral/viz/state.py.png diff --git a/behavioral/viz/strategy.py.png b/patterns/behavioral/viz/strategy.py.png similarity index 100% rename from behavioral/viz/strategy.py.png rename to patterns/behavioral/viz/strategy.py.png diff --git a/behavioral/viz/template.py.png b/patterns/behavioral/viz/template.py.png similarity index 100% rename from behavioral/viz/template.py.png rename to patterns/behavioral/viz/template.py.png diff --git a/behavioral/viz/visitor.py.png b/patterns/behavioral/viz/visitor.py.png similarity index 100% rename from behavioral/viz/visitor.py.png rename to patterns/behavioral/viz/visitor.py.png diff --git a/dft/__init__.py b/patterns/creational/__init__.py similarity index 100% rename from dft/__init__.py rename to patterns/creational/__init__.py diff --git a/creational/abstract_factory.py b/patterns/creational/abstract_factory.py similarity index 56% rename from creational/abstract_factory.py rename to patterns/creational/abstract_factory.py index 6c781b86a..2362d8fef 100644 --- a/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? @@ -11,7 +8,7 @@ The idea is to abstract the creation of objects depending on business logic, platform choice, etc. -In Python, we interface we use is simply a callable, which is "builtin" interface +In Python, the interface we use is simply a callable, which is "builtin" interface in Python, and in normal circumstances we can simply use the class itself as that callable, because classes are first class objects in Python. @@ -29,80 +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): +class Cat(Pet): + def speak(self) -> None: + print("meow") - def speak(self): - return "woof" + def __str__(self) -> str: + return f"Cat<{self.name}>" - def __str__(self): - return "Dog" +class PetShop: + """A pet shop""" -class Cat(object): + def __init__(self, animal_factory: type[Pet]) -> None: + """pet_factory is our abstract factory. We can set it at will.""" - def speak(self): - return "meow" + self.pet_factory = animal_factory - def __str__(self): - return "Cat" + def buy_pet(self, name: str) -> Pet: + """Creates and shows a pet using the abstract factory""" + pet = self.pet_factory(name) + print(f"Here is your lovely {pet}") + return pet -# Additional factories: -# Create a random animal -def random_animal(): - """Let's be dynamic!""" - return random.choice([Dog, Cat])() +# Show pets with various factories +def main() -> None: + """ + # A Shop that sells only cats + >>> cat_shop = PetShop(Cat) + >>> pet = cat_shop.buy_pet("Lucy") + Here is your lovely Cat + >>> pet.speak() + meow + """ -# Show pets with various factories if __name__ == "__main__": + animals = [Dog, Cat] + random_animal: type[Pet] = random.choice(animals) - # A Shop that sells only cats - cat_shop = PetShop(Cat) - cat_shop.show_pet() - print("") - - # 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 new file mode 100644 index 000000000..7a8073e69 --- /dev/null +++ b/patterns/creational/borg.py @@ -0,0 +1,109 @@ +""" +*What is this pattern about? +The Borg pattern (also known as the Monostate pattern) is a way to +implement singleton behavior, but instead of having only one instance +of a class, there are multiple instances that share the same state. In +other words, the focus is on sharing state instead of sharing instance +identity. + +*What does this example do? +To understand the implementation of this pattern in Python, it is +important to know that, in Python, instance attributes are stored in a +attribute dictionary called __dict__. Usually, each instance will have +its own dictionary, but the Borg pattern modifies this so that all +instances have the same dictionary. +In this example, the __shared_state attribute will be the dictionary +shared between all instances, and this is ensured by assigning +__shared_state to the __dict__ variable when initializing a new +instance (i.e., in the __init__ method). Other attributes are usually +added to the instance's attribute dictionary, but, since the attribute +dictionary itself is shared (which is __shared_state), all other +attributes will also be shared. + +*Where is the pattern used practically? +Sharing state is useful in applications like managing database connections: +https://github.com/onetwopunch/pythonDbTemplate/blob/master/database.py + +*References: +- https://fkromer.github.io/python-pattern-references/design/#singleton +- https://learning.oreilly.com/library/view/python-cookbook/0596001673/ch05s23.html +- http://www.aleax.it/5ep.html + +*TL;DR +Provides singleton-like behavior sharing state between instances. +""" + + +class Borg: + _shared_state: dict[str, str] = {} + + def __init__(self) -> None: + self.__dict__ = self._shared_state + + +class YourBorg(Borg): + def __init__(self, state: str | None = None) -> None: + super().__init__() + if state: + self.state = state + else: + # initiate the first instance with default state + if not hasattr(self, "state"): + self.state = "Init" + + def __str__(self) -> str: + return self.state + + +def main(): + """ + >>> rm1 = YourBorg() + >>> rm2 = YourBorg() + + >>> rm1.state = 'Idle' + >>> rm2.state = 'Running' + + >>> print('rm1: {0}'.format(rm1)) + rm1: Running + >>> print('rm2: {0}'.format(rm2)) + rm2: Running + + # When the `state` attribute is modified from instance `rm2`, + # the value of `state` in instance `rm1` also changes + >>> rm2.state = 'Zombie' + + >>> print('rm1: {0}'.format(rm1)) + rm1: Zombie + >>> print('rm2: {0}'.format(rm2)) + rm2: Zombie + + # Even though `rm1` and `rm2` share attributes, the instances are not the same + >>> rm1 is rm2 + False + + # New instances also get the same shared state + >>> rm3 = YourBorg() + + >>> print('rm1: {0}'.format(rm1)) + rm1: Zombie + >>> print('rm2: {0}'.format(rm2)) + rm2: Zombie + >>> print('rm3: {0}'.format(rm3)) + rm3: Zombie + + # A new instance can explicitly change the state during creation + >>> rm4 = YourBorg('Running') + + >>> print('rm4: {0}'.format(rm4)) + rm4: Running + + # Existing instances reflect that change as well + >>> print('rm3: {0}'.format(rm3)) + rm3: Running + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/creational/builder.py b/patterns/creational/builder.py new file mode 100644 index 000000000..c5253ad30 --- /dev/null +++ b/patterns/creational/builder.py @@ -0,0 +1,117 @@ +""" +What is this pattern about? +It decouples the creation of a complex object and its representation, +so that the same process can be reused to build objects from the same +family. +This is useful when you must separate the specification of an object +from its actual representation (generally for abstraction). + +What does this example do? +The first example achieves this by using an abstract base +class for a building, where the initializer (__init__ method) specifies the +steps needed, and the concrete subclasses implement these steps. + +In other programming languages, a more complex arrangement is sometimes +necessary. In particular, you cannot have polymorphic behaviour in a constructor in C++ - +see https://stackoverflow.com/questions/1453131/how-can-i-get-polymorphic-behavior-in-a-c-constructor +- which means this Python technique will not work. The polymorphism +required has to be provided by an external, already constructed +instance of a different class. + +In general, in Python this won't be necessary, but a second example showing +this kind of arrangement is also included. + +Where is the pattern used practically? +See: https://sourcemaking.com/design_patterns/builder + +TL;DR +Decouples the creation of a complex object and its representation. +""" + + +# Abstract Building +class Building: + floor: str + size: str + + def __init__(self) -> None: + self.build_floor() + self.build_size() + + def build_floor(self): + raise NotImplementedError + + def build_size(self): + raise NotImplementedError + + def __repr__(self) -> str: + return "Floor: {0.floor} | Size: {0.size}".format(self) + + +# Concrete Buildings +class House(Building): + def build_floor(self) -> None: + self.floor = "One" + + def build_size(self) -> None: + self.size = "Big" + + +class Flat(Building): + def build_floor(self) -> None: + self.floor = "More than One" + + def build_size(self) -> None: + self.size = "Small" + + +# In some very complex cases, it might be desirable to pull out the building +# logic into another function (or a method on another class), rather than being +# in the base class '__init__'. (This leaves you in the strange situation where +# a concrete class does not have a useful constructor) + + +class ComplexBuilding: + floor: str + size: str + + def __repr__(self) -> str: + return "Floor: {0.floor} | Size: {0.size}".format(self) + + +class ComplexHouse(ComplexBuilding): + def build_floor(self) -> None: + self.floor = "One" + + def build_size(self) -> None: + self.size = "Big and fancy" + + +def construct_building(cls) -> Building: + building = cls() + building.build_floor() + building.build_size() + return building + + +def main(): + """ + >>> house = House() + >>> house + Floor: One | Size: Big + + >>> flat = Flat() + >>> flat + Floor: More than One | Size: Small + + # Using an external constructor function: + >>> complex_house = construct_building(ComplexHouse) + >>> complex_house + Floor: One | Size: Big and fancy + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/creational/factory.py b/patterns/creational/factory.py new file mode 100644 index 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/lazy_evaluation.py b/patterns/creational/lazy_evaluation.py new file mode 100644 index 000000000..b9945d998 --- /dev/null +++ b/patterns/creational/lazy_evaluation.py @@ -0,0 +1,120 @@ +""" +Lazily-evaluated property pattern in Python. + +https://en.wikipedia.org/wiki/Lazy_evaluation + +*References: +bottle +https://github.com/bottlepy/bottle/blob/cafc15419cbb4a6cb748e6ecdccf92893bb25ce5/bottle.py#L270 +django +https://github.com/django/django/blob/ffd18732f3ee9e6f0374aff9ccf350d85187fac2/django/utils/functional.py#L19 +pip +https://github.com/pypa/pip/blob/cb75cca785629e15efb46c35903827b3eae13481/pip/utils/__init__.py#L821 +pyramid +https://github.com/Pylons/pyramid/blob/7909e9503cdfc6f6e84d2c7ace1d3c03ca1d8b73/pyramid/decorator.py#L4 +werkzeug +https://github.com/pallets/werkzeug/blob/5a2bf35441006d832ab1ed5a31963cbc366c99ac/werkzeug/utils.py#L35 + +*TL;DR +Delays the eval of an expr until its value is needed and avoids repeated evals. +""" + +import functools +from collections.abc import Callable +from typing import Any, overload + + +class lazy_property: + def __init__(self, function: Callable[["Person"], str]) -> None: + self.function = function + functools.update_wrapper(self, function) # type: ignore[arg-type] + + @overload + def __get__(self, obj: None, type_: type["Person"]) -> "lazy_property": ... + + @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) + obj.__dict__[self.function.__name__] = val + return val + + +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__ + + def _lazy_property(self: Any) -> str: + if not hasattr(self, attr): + setattr(self, attr, fn(self)) + return getattr(self, attr) + + return property(_lazy_property) + + +class Person: + def __init__(self, name: str, occupation: str) -> None: + self.name = name + self.occupation = occupation + self.call_count2 = 0 + + @lazy_property + def relatives(self) -> str: + # Get all relatives, let's assume that it costs much time. + relatives = "Many relatives." + return relatives + + @lazy_property2 + def parents(self) -> str: + self.call_count2 += 1 + return "Father and mother" + + +def main(): + """ + >>> Jhon = Person('Jhon', 'Coder') + + >>> Jhon.name + 'Jhon' + >>> Jhon.occupation + 'Coder' + + # Before we access `relatives` + >>> sorted(Jhon.__dict__.items()) + [('call_count2', 0), ('name', 'Jhon'), ('occupation', 'Coder')] + + >>> Jhon.relatives + 'Many relatives.' + + # After we've accessed `relatives` + >>> sorted(Jhon.__dict__.items()) + [('call_count2', 0), ..., ('relatives', 'Many relatives.')] + + >>> Jhon.parents + 'Father and mother' + + >>> sorted(Jhon.__dict__.items()) + [('_lazy__parents', 'Father and mother'), ('call_count2', 1), ..., ('relatives', 'Many relatives.')] + + >>> Jhon.parents + 'Father and mother' + + >>> Jhon.call_count2 + 1 + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod(optionflags=doctest.ELLIPSIS) diff --git a/creational/pool.py b/patterns/creational/pool.py similarity index 59% rename from creational/pool.py rename to patterns/creational/pool.py index 2f35e6001..684b11857 100644 --- a/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 @@ -17,8 +14,8 @@ populated with strings. As we can see, the first string object put in "yam" is USED by the with statement. But because it is released back into the pool -aftwerwards it is reused by the explicit call to sample_queue.get(). -Same thing happens with "sam", when the ObjectPool created insided the +afterwards it is reused by the explicit call to sample_queue.get(). +Same thing happens with "sam", when the ObjectPool created inside the function is deleted (by the GC) and the object is returned. *Where is the pattern used practically? @@ -27,63 +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 new file mode 100644 index 000000000..4c2dd7ed1 --- /dev/null +++ b/patterns/creational/prototype.py @@ -0,0 +1,83 @@ +""" +*What is this pattern about? +This patterns aims to reduce the number of classes required by an +application. Instead of relying on subclasses it creates objects by +copying a prototypical instance at run-time. + +This is useful as it makes it easier to derive new kinds of objects, +when instances of the class have only a few different combinations of +state, and when instantiation is expensive. + +*What does this example do? +When the number of prototypes in an application can vary, it can be +useful to keep a Dispatcher (aka, Registry or Manager). This allows +clients to query the Dispatcher for a prototype before cloning a new +instance. + +Below provides an example of such Dispatcher, which contains three +copies of the prototype: 'default', 'objecta' and 'objectb'. + +*TL;DR +Creates new object instances by cloning prototype. +""" + +from __future__ import annotations + +from typing import Any + + +class Prototype: + def __init__(self, value: str = "default", **attrs: Any) -> None: + self.value = value + self.__dict__.update(attrs) + + def clone(self, **attrs: Any) -> Prototype: + """Clone a prototype and update inner attributes dictionary""" + # Python in Practice, Mark Summerfield + # copy.deepcopy can be used instead of next line. + obj = self.__class__(**self.__dict__) + obj.__dict__.update(attrs) + return obj + + +class PrototypeDispatcher: + def __init__(self): + self._objects = {} + + def get_objects(self) -> dict[str, Prototype]: + """Get all objects""" + return self._objects + + def register_object(self, name: str, obj: Prototype) -> None: + """Register an object""" + self._objects[name] = obj + + def unregister_object(self, name: str) -> None: + """Unregister an object""" + del self._objects[name] + + +def main() -> None: + """ + >>> dispatcher = PrototypeDispatcher() + >>> prototype = Prototype() + + >>> d = prototype.clone() + >>> a = prototype.clone(value='a-value', category='a') + >>> b = a.clone(value='b-value', is_checked=True) + >>> dispatcher.register_object('objecta', a) + >>> dispatcher.register_object('objectb', b) + >>> dispatcher.register_object('default', d) + + >>> [{n: p.value} for n, p in dispatcher.get_objects().items()] + [{'objecta': 'a-value'}, {'objectb': 'b-value'}, {'default': 'default'}] + + >>> print(b.category, b.is_checked) + a True + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/creational/viz/abstract_factory.py.png b/patterns/creational/viz/abstract_factory.py.png similarity index 100% rename from creational/viz/abstract_factory.py.png rename to patterns/creational/viz/abstract_factory.py.png diff --git a/creational/viz/borg.py.png b/patterns/creational/viz/borg.py.png similarity index 100% rename from creational/viz/borg.py.png rename to patterns/creational/viz/borg.py.png diff --git a/creational/viz/builder.py.png b/patterns/creational/viz/builder.py.png similarity index 100% rename from creational/viz/builder.py.png rename to patterns/creational/viz/builder.py.png diff --git a/creational/viz/factory_method.py.png b/patterns/creational/viz/factory_method.py.png similarity index 100% rename from creational/viz/factory_method.py.png rename to patterns/creational/viz/factory_method.py.png diff --git a/creational/viz/lazy_evaluation.py.png b/patterns/creational/viz/lazy_evaluation.py.png similarity index 100% rename from creational/viz/lazy_evaluation.py.png rename to patterns/creational/viz/lazy_evaluation.py.png diff --git a/creational/viz/pool.py.png b/patterns/creational/viz/pool.py.png similarity index 100% rename from creational/viz/pool.py.png rename to patterns/creational/viz/pool.py.png diff --git a/creational/viz/prototype.py.png b/patterns/creational/viz/prototype.py.png similarity index 100% rename from creational/viz/prototype.py.png rename to patterns/creational/viz/prototype.py.png 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/fundamental/__init__.py b/patterns/fundamental/__init__.py similarity index 100% rename from fundamental/__init__.py rename to patterns/fundamental/__init__.py diff --git a/patterns/fundamental/delegation_pattern.py b/patterns/fundamental/delegation_pattern.py new file mode 100644 index 000000000..c035da202 --- /dev/null +++ b/patterns/fundamental/delegation_pattern.py @@ -0,0 +1,60 @@ +""" +Reference: https://en.wikipedia.org/wiki/Delegation_pattern +Author: https://github.com/IuryAlves + +*TL;DR +Allows object composition to achieve the same code reuse as inheritance. +""" + +from __future__ import annotations + +from collections.abc import Callable +from typing import Any + + +class Delegator: + """ + >>> delegator = Delegator(Delegate()) + >>> delegator.p1 + 123 + >>> delegator.p2 + Traceback (most recent call last): + ... + 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: ... + """ + + def __init__(self, delegate: Delegate) -> None: + self.delegate = delegate + + def __getattr__(self, name: str) -> Any | Callable: + attr = getattr(self.delegate, name) + + if not callable(attr): + return attr + + def wrapper(*args, **kwargs): + return attr(*args, **kwargs) + + return wrapper + + +class Delegate: + def __init__(self) -> None: + self.p1 = 123 + + def do_something(self, something: str, kw=None) -> str: + return f"Doing {something}{kw or ''}" + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/fundamental/viz/delegation_pattern.py.png b/patterns/fundamental/viz/delegation_pattern.py.png similarity index 100% rename from fundamental/viz/delegation_pattern.py.png rename to patterns/fundamental/viz/delegation_pattern.py.png diff --git a/other/__init__.py b/patterns/other/__init__.py similarity index 100% rename from other/__init__.py rename to patterns/other/__init__.py diff --git a/patterns/other/blackboard.py b/patterns/other/blackboard.py new file mode 100644 index 000000000..e69582ec3 --- /dev/null +++ b/patterns/other/blackboard.py @@ -0,0 +1,138 @@ +""" +@author: Eugene Duboviy | github.com/duboviy + +In Blackboard pattern several specialised sub-systems (knowledge sources) +assemble their knowledge to build a possibly partial or approximate solution. +In this way, the sub-systems work together to solve the problem, +where the solution is the sum of its parts. + +https://en.wikipedia.org/wiki/Blackboard_system +""" + +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: + """The blackboard system that holds the common state.""" + + def __init__(self) -> None: + self.experts: list = [] + self.common_state = { + "problems": 0, + "suggestions": 0, + "contributions": [], + "progress": 0, # percentage, if 100 -> task is finished + } + + def add_expert(self, expert: AbstractExpert) -> None: + self.experts.append(expert) + + +class Controller: + """The controller that manages the blackboard system.""" + + def __init__(self, blackboard: Blackboard) -> None: + self.blackboard = blackboard + + def run_loop(self): + """ + This function is a loop that runs until the progress reaches 100. + It checks if an expert is eager to contribute and then calls its contribute method. + """ + while self.blackboard.common_state["progress"] < 100: + for expert in self.blackboard.experts: + if expert.is_eager_to_contribute: + expert.contribute() + return self.blackboard.common_state["contributions"] + + +class Student(AbstractExpert): + """Concrete class for a student expert.""" + + def __init__(self, blackboard) -> None: + super().__init__(blackboard) + + @property + def is_eager_to_contribute(self) -> bool: + return True + + def contribute(self) -> None: + self.blackboard.common_state["problems"] += random.randint(1, 10) + self.blackboard.common_state["suggestions"] += random.randint(1, 10) + self.blackboard.common_state["contributions"] += [self.__class__.__name__] + self.blackboard.common_state["progress"] += random.randint(1, 2) + + +class Scientist(AbstractExpert): + """Concrete class for a scientist expert.""" + + def __init__(self, blackboard) -> None: + super().__init__(blackboard) + + @property + def is_eager_to_contribute(self) -> int: + return random.randint(0, 1) + + def contribute(self) -> None: + self.blackboard.common_state["problems"] += random.randint(10, 20) + self.blackboard.common_state["suggestions"] += random.randint(10, 20) + self.blackboard.common_state["contributions"] += [self.__class__.__name__] + self.blackboard.common_state["progress"] += random.randint(10, 30) + + +class Professor(AbstractExpert): + def __init__(self, blackboard) -> None: + super().__init__(blackboard) + + @property + def is_eager_to_contribute(self) -> bool: + return True if self.blackboard.common_state["problems"] > 100 else False + + def contribute(self) -> None: + self.blackboard.common_state["problems"] += random.randint(1, 2) + self.blackboard.common_state["suggestions"] += random.randint(10, 20) + self.blackboard.common_state["contributions"] += [self.__class__.__name__] + self.blackboard.common_state["progress"] += random.randint(10, 100) + + +def main(): + """ + >>> 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 new file mode 100644 index 000000000..e3c921409 --- /dev/null +++ b/patterns/other/graph_search.py @@ -0,0 +1,158 @@ +class GraphSearch: + """Graph search emulation in python, from source + http://www.python.org/doc/essays/graphs/ + + dfs stands for Depth First Search + bfs stands for Breadth First Search""" + + def __init__(self, graph: dict[str, list[str]]) -> None: + self.graph = graph + + def find_path_dfs( + self, start: str, end: str, path: list[str] | None = None + ) -> list[str] | None: + path = path or [] + + path.append(start) + if start == end: + return path + for node in self.graph.get(start, []): + if node not in path: + newpath = self.find_path_dfs(node, end, path[:]) + if newpath: + return newpath + return 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: list[list[str]] = [] + for node in self.graph.get(start, []): + if node not in path: + newpaths = self.find_all_paths_dfs(node, end, path[:]) + paths.extend(newpaths) + return paths + + def find_shortest_path_dfs( + self, start: str, end: str, path: list[str] | None = None + ) -> list[str] | None: + path = path or [] + path.append(start) + + if start == end: + return path + shortest = None + for node in self.graph.get(start, []): + if node not in path: + newpath = self.find_shortest_path_dfs(node, end, path[:]) + if newpath: + if not shortest or len(newpath) < len(shortest): + shortest = newpath + return shortest + + def find_shortest_path_bfs(self, start: 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] = {} + + 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/other/hsm/__init__.py b/patterns/other/hsm/__init__.py similarity index 100% rename from other/hsm/__init__.py rename to patterns/other/hsm/__init__.py diff --git a/other/hsm/classes_hsm.png b/patterns/other/hsm/classes_hsm.png similarity index 100% rename from other/hsm/classes_hsm.png rename to patterns/other/hsm/classes_hsm.png diff --git a/other/hsm/classes_test_hsm.png b/patterns/other/hsm/classes_test_hsm.png similarity index 100% rename from other/hsm/classes_test_hsm.png rename to patterns/other/hsm/classes_test_hsm.png diff --git a/other/hsm/hsm.py b/patterns/other/hsm/hsm.py similarity index 64% rename from other/hsm/hsm.py rename to patterns/other/hsm/hsm.py index e40d88c47..44498014e 100644 --- a/other/hsm/hsm.py +++ b/patterns/other/hsm/hsm.py @@ -21,23 +21,26 @@ 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() self._suspect_state = Suspect(self) # Unit.OutOfService.Suspect() self._failed_state = Failed(self) # Unit.OutOfService.Failed() self._current_state = self._standby_state - self.states = {'active': self._active_state, - 'standby': self._standby_state, - 'suspect': self._suspect_state, - 'failed': self._failed_state} - self.message_types = {'fault trigger': self._current_state.on_fault_trigger, - 'switchover': self._current_state.on_switchover, - 'diagnostics passed': self._current_state.on_diagnostics_passed, - 'diagnostics failed': self._current_state.on_diagnostics_failed, - 'operator inservice': self._current_state.on_operator_inservice} + self.states = { + "active": self._active_state, + "standby": self._standby_state, + "suspect": self._suspect_state, + "failed": self._failed_state, + } + self.message_types = { + "fault trigger": self._current_state.on_fault_trigger, + "switchover": self._current_state.on_switchover, + "diagnostics passed": self._current_state.on_diagnostics_passed, + "diagnostics failed": self._current_state.on_diagnostics_failed, + "operator inservice": self._current_state.on_operator_inservice, + } def _next_state(self, state): try: @@ -46,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(): @@ -82,8 +85,7 @@ def on_message(self, message_type): # message ignored raise UnsupportedMessageType -class Unit(object): - +class Unit: def __init__(self, HierachicalStateMachine): self.hsm = HierachicalStateMachine @@ -100,16 +102,15 @@ def on_diagnostics_passed(self): raise UnsupportedTransition def on_operator_inservice(self): - raise UnsupportedTransition + raise UnsupportedTransition class Inservice(Unit): - 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() @@ -120,62 +121,57 @@ def on_switchover(self): class Active(Inservice): - 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): - 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): - def __init__(self, HierachicalStateMachine): self._hsm = HierachicalStateMachine def on_operator_inservice(self): self._hsm.on_switchover() # message ignored self._hsm.send_operator_inservice_response() - self._hsm.next_state('suspect') + self._hsm.next_state("suspect") class Suspect(OutOfService): - 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): - '''No need to override any method.''' + """No need to override any method.""" def __init__(self, HierachicalStateMachine): self._hsm = HierachicalStateMachine - diff --git a/patterns/structural/3-tier.py b/patterns/structural/3-tier.py new file mode 100644 index 000000000..3c266f514 --- /dev/null +++ b/patterns/structural/3-tier.py @@ -0,0 +1,95 @@ +""" +*TL;DR +Separates presentation, application processing, and data management functions. +""" + +from collections.abc import KeysView + + +class Data: + """Data Store Class""" + + products = { + "milk": {"price": 1.50, "quantity": 10}, + "eggs": {"price": 0.20, "quantity": 100}, + "cheese": {"price": 2.00, "quantity": 10}, + } + + def __get__(self, obj, klas): + print("(Fetching from Data Store)") + return {"products": self.products} + + +class BusinessLogic: + """Business logic holding data store instances""" + + data = Data() + + def product_list(self) -> KeysView[str]: + return self.data["products"].keys() + + def product_information(self, product: str) -> dict[str, int | float] | None: + return self.data["products"].get(product, None) + + +class Ui: + """UI interaction class""" + + def __init__(self) -> None: + self.business_logic = BusinessLogic() + + def get_product_list(self) -> None: + print("PRODUCT LIST:") + for product in self.business_logic.product_list(): + print(product) + print("") + + def get_product_information(self, product: str) -> None: + product_info = self.business_logic.product_information(product) + if product_info: + print("PRODUCT INFORMATION:") + print( + f"Name: {product.title()}, " + + f"Price: {product_info.get('price', 0):.2f}, " + + f"Quantity: {product_info.get('quantity', 0):}" + ) + else: + print(f"That product '{product}' does not exist in the records") + + +def main(): + """ + >>> ui = Ui() + >>> ui.get_product_list() + PRODUCT LIST: + (Fetching from Data Store) + milk + eggs + cheese + + + >>> ui.get_product_information("cheese") + (Fetching from Data Store) + PRODUCT INFORMATION: + Name: Cheese, Price: 2.00, Quantity: 10 + + >>> ui.get_product_information("eggs") + (Fetching from Data Store) + PRODUCT INFORMATION: + Name: Eggs, Price: 0.20, Quantity: 100 + + >>> ui.get_product_information("milk") + (Fetching from Data Store) + PRODUCT INFORMATION: + Name: Milk, Price: 1.50, Quantity: 10 + + >>> ui.get_product_information("arepas") + (Fetching from Data Store) + That product 'arepas' does not exist in the records + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/structural/__init__.py b/patterns/structural/__init__.py similarity index 100% rename from structural/__init__.py rename to patterns/structural/__init__.py diff --git a/structural/adapter.py b/patterns/structural/adapter.py similarity index 55% rename from structural/adapter.py rename to patterns/structural/adapter.py index 21a3a6121..5afe42faf 100644 --- a/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,117 +24,103 @@ 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): +class Adapter: + """Adapts an object by replacing methods. + + Usage + ------ + dog = Dog() + dog = Adapter(dog, make_noise=dog.bark) """ - Adapts an object by replacing methods. - Usage: - dog = Dog - dog = Adapter(dog, dict(make_noise=dog.bark)) + def __init__(self, obj: T, **adapted_methods: Callable[..., Any]) -> None: + """We set the adapted methods in the object's dict.""" + self.obj = obj + self.__dict__.update(adapted_methods) + + def __getattr__(self, attr: str) -> Any: + """All non-adapted calls are passed to the object.""" + return getattr(self.obj, attr) + + def original_dict(self) -> dict[str, Any]: + """Print original object dict.""" + return self.obj.__dict__ + + +def main(): + """ >>> objects = [] >>> dog = Dog() >>> print(dog.__dict__) {'name': 'Dog'} + >>> objects.append(Adapter(dog, make_noise=dog.bark)) + + >>> objects[0].__dict__['obj'], objects[0].__dict__['make_noise'] + (<...Dog object at 0x...>, >) + >>> print(objects[0].original_dict()) {'name': 'Dog'} + >>> cat = Cat() >>> objects.append(Adapter(cat, make_noise=cat.meow)) >>> human = Human() >>> objects.append(Adapter(human, make_noise=human.speak)) >>> car = Car() - >>> car_noise = lambda: car.make_noise(3) - >>> objects.append(Adapter(car, make_noise=car_noise)) + >>> objects.append(Adapter(car, make_noise=lambda: car.make_noise(3))) >>> for obj in objects: - ... print('A {} goes {}'.format(obj.name, obj.make_noise())) + ... print("A {0} goes {1}".format(obj.name, obj.make_noise())) A Dog goes woof! A Cat goes meow! A Human goes 'hello' A Car goes vroom!!! """ - def __init__(self, obj, **adapted_methods): - """We set the adapted methods in the object's dict""" - self.obj = obj - self.__dict__.update(adapted_methods) - - def __getattr__(self, attr): - """All non-adapted calls are passed to the object""" - return getattr(self.obj, attr) - - def original_dict(self): - """Print original object dict""" - return self.obj.__dict__ - -def main(): - - objects = [] - dog = Dog() - print(dog.__dict__) - objects.append(Adapter(dog, make_noise=dog.bark)) - print(objects[0].__dict__) - print(objects[0].original_dict()) - cat = Cat() - objects.append(Adapter(cat, make_noise=cat.meow)) - human = Human() - objects.append(Adapter(human, make_noise=human.speak)) - car = Car() - objects.append(Adapter(car, make_noise=lambda: car.make_noise(3))) - - for obj in objects: - print("A {0} goes {1}".format(obj.name, obj.make_noise())) - if __name__ == "__main__": - main() - -### OUTPUT ### -# {'name': 'Dog'} -# {'make_noise': >, 'obj': <__main__.Dog object at 0x7f631ba3fb00>} -# {'name': 'Dog'} -# A Dog goes woof! -# A Cat goes meow! -# A Human goes 'hello' -# A Car goes vroom!!! + import doctest + + doctest.testmod(optionflags=doctest.ELLIPSIS) diff --git a/patterns/structural/bridge.py b/patterns/structural/bridge.py new file mode 100644 index 000000000..0990efc38 --- /dev/null +++ b/patterns/structural/bridge.py @@ -0,0 +1,56 @@ +""" +*References: +http://en.wikibooks.org/wiki/Computer_Science_Design_Patterns/Bridge_Pattern#Python + +*TL;DR +Decouples an abstraction from its implementation. +""" + + +# ConcreteImplementor 1/2 +class DrawingAPI1: + def draw_circle(self, x: int, y: int, radius: float) -> None: + print(f"API1.circle at {x}:{y} radius {radius}") + + +# ConcreteImplementor 2/2 +class DrawingAPI2: + def draw_circle(self, x: int, y: int, radius: float) -> None: + print(f"API2.circle at {x}:{y} radius {radius}") + + +# Refined Abstraction +class CircleShape: + def __init__( + self, x: int, y: int, radius: float, drawing_api: DrawingAPI2 | DrawingAPI1 + ) -> None: + self._x = x + self._y = y + self._radius: float = radius + self._drawing_api = drawing_api + + # low-level i.e. Implementation specific + def draw(self) -> None: + self._drawing_api.draw_circle(self._x, self._y, self._radius) + + # high-level i.e. Abstraction specific + def scale(self, pct: float) -> None: + self._radius *= pct + + +def main(): + """ + >>> shapes = (CircleShape(1, 2, 3, DrawingAPI1()), CircleShape(5, 7, 11, DrawingAPI2())) + + >>> for shape in shapes: + ... shape.scale(2.5) + ... shape.draw() + API1.circle at 1:2 radius 7.5 + API2.circle at 5:7 radius 27.5 + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/structural/composite.py b/patterns/structural/composite.py similarity index 56% rename from structural/composite.py rename to patterns/structural/composite.py index 1244b7856..ee0520c6b 100644 --- a/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/structural/decorator.py b/patterns/structural/decorator.py similarity index 62% rename from structural/decorator.py rename to patterns/structural/decorator.py index 14e72391a..a32e2b06d 100644 --- a/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,43 +20,55 @@ *References: https://sourcemaking.com/design_patterns/decorator -*TL;DR80 +*TL;DR Adds behaviour to object without affecting its class. """ -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 new file mode 100644 index 000000000..f7b00be32 --- /dev/null +++ b/patterns/structural/facade.py @@ -0,0 +1,97 @@ +""" +Example from https://en.wikipedia.org/wiki/Facade_pattern#Python + + +*What is this pattern about? +The Facade pattern is a way to provide a simpler unified interface to +a more complex system. It provides an easier way to access functions +of the underlying system by providing a single entry point. +This kind of abstraction is seen in many real life situations. For +example, we can turn on a computer by just pressing a button, but in +fact there are many procedures and operations done when that happens +(e.g., loading programs from disk to memory). In this case, the button +serves as an unified interface to all the underlying procedures to +turn on a computer. + +*Where is the pattern used practically? +This pattern can be seen in the Python standard library when we use +the isdir function. Although a user simply uses this function to know +whether a path refers to a directory, the system makes a few +operations and calls other modules (e.g., os.stat) to give the result. + +*References: +https://sourcemaking.com/design_patterns/facade +https://fkromer.github.io/python-pattern-references/design/#facade +http://python-3-patterns-idioms-test.readthedocs.io/en/latest/ChangeInterface.html#facade + +*TL;DR +Provides a simpler unified interface to a complex system. +""" + + +# Complex computer parts +class CPU: + """ + Simple CPU representation. + """ + + def freeze(self) -> None: + print("Freezing processor.") + + def jump(self, position: str) -> None: + print("Jumping to:", position) + + def execute(self) -> None: + print("Executing.") + + +class Memory: + """ + Simple memory representation. + """ + + def load(self, position: str, data: str) -> None: + print(f"Loading from {position} data: '{data}'.") + + +class SolidStateDrive: + """ + Simple solid state drive representation. + """ + + def read(self, lba: str, size: str) -> str: + return f"Some data from sector {lba} with size {size}" + + +class ComputerFacade: + """ + Represents a facade for various computer parts. + """ + + def __init__(self): + self.cpu = CPU() + self.memory = Memory() + self.ssd = SolidStateDrive() + + def start(self): + self.cpu.freeze() + self.memory.load("0x00", self.ssd.read("100", "1024")) + self.cpu.jump("0x00") + self.cpu.execute() + + +def main(): + """ + >>> computer_facade = ComputerFacade() + >>> computer_facade.start() + Freezing processor. + Loading from 0x00 data: 'Some data from sector 100 with size 1024'. + Jumping to: 0x00 + Executing. + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod(optionflags=doctest.ELLIPSIS) diff --git a/patterns/structural/flyweight.py b/patterns/structural/flyweight.py new file mode 100644 index 000000000..148a5bc05 --- /dev/null +++ b/patterns/structural/flyweight.py @@ -0,0 +1,88 @@ +""" +*What is this pattern about? +This pattern aims to minimise the number of objects that are needed by +a program at run-time. A Flyweight is an object shared by multiple +contexts, and is indistinguishable from an object that is not shared. + +The state of a Flyweight should not be affected by it's context, this +is known as its intrinsic state. The decoupling of the objects state +from the object's context, allows the Flyweight to be shared. + +*What does this example do? +The example below sets-up an 'object pool' which stores initialised +objects. When a 'Card' is created it first checks to see if it already +exists instead of creating a new one. This aims to reduce the number of +objects initialised by the program. + +*References: +http://codesnipers.com/?q=python-flyweights +https://python-patterns.guide/gang-of-four/flyweight/ + +*Examples in Python ecosystem: +https://docs.python.org/3/library/sys.html#sys.intern + +*TL;DR +Minimizes memory usage by sharing data with other similar objects. +""" + +import weakref + + +class Card: + """The Flyweight""" + + 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) -> str: + return f"" + + +def main(): + """ + >>> c1 = Card('9', 'h') + >>> c2 = Card('9', 'h') + >>> c1, c2 + (, ) + >>> c1 == c2 + True + >>> c1 is c2 + True + + >>> c1.new_attr = 'temp' + >>> c3 = Card('9', 'h') + >>> hasattr(c3, 'new_attr') + True + + >>> Card._pool.clear() + >>> c4 = Card('9', 'h') + >>> hasattr(c4, 'new_attr') + False + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/structural/flyweight_with_metaclass.py b/patterns/structural/flyweight_with_metaclass.py new file mode 100644 index 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 new file mode 100644 index 000000000..92f58b213 --- /dev/null +++ b/patterns/structural/front_controller.py @@ -0,0 +1,95 @@ +""" +@author: Gordeev Andrey + +*TL;DR +Provides a centralized entry point that controls and manages request handling. +""" + +from __future__ import annotations + +from typing import Any + + +class MobileView: + def show_index_page(self) -> None: + print("Displaying mobile index page") + + +class TabletView: + def show_index_page(self) -> None: + print("Displaying tablet index page") + + +class Dispatcher: + def __init__(self) -> None: + self.mobile_view = MobileView() + self.tablet_view = TabletView() + + def dispatch(self, request: Request) -> None: + """ + This function is used to dispatch the request based on the type of device. + If it is a mobile, then mobile view will be called and if it is a tablet, + then tablet view will be called. + Otherwise, an error message will be printed saying that cannot dispatch the request. + """ + if request.type == Request.mobile_type: + self.mobile_view.show_index_page() + elif request.type == Request.tablet_type: + self.tablet_view.show_index_page() + else: + print("Cannot dispatch the request") + + +class RequestController: + """front controller""" + + def __init__(self) -> None: + self.dispatcher = Dispatcher() + + def dispatch_request(self, request: Any) -> None: + """ + This function takes a request object and sends it to the dispatcher. + """ + if isinstance(request, Request): + self.dispatcher.dispatch(request) + else: + print("request must be a Request object") + + +class Request: + """request""" + + mobile_type = "mobile" + tablet_type = "tablet" + + def __init__(self, request): + self.type = None + request = request.lower() + if request == self.mobile_type: + self.type = self.mobile_type + elif request == self.tablet_type: + self.type = self.tablet_type + + +def main(): + """ + >>> front_controller = RequestController() + + >>> front_controller.dispatch_request(Request('mobile')) + Displaying mobile index page + + >>> front_controller.dispatch_request(Request('tablet')) + Displaying tablet index page + + >>> front_controller.dispatch_request(Request('desktop')) + Cannot dispatch the request + + >>> front_controller.dispatch_request('mobile') + request must be a Request object + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/structural/mvc.py b/patterns/structural/mvc.py new file mode 100644 index 000000000..5726b0897 --- /dev/null +++ b/patterns/structural/mvc.py @@ -0,0 +1,216 @@ +""" +*TL;DR +Separates data in GUIs from the ways it is presented, and accepted. +""" + +from abc import ABC, abstractmethod +from inspect import signature +from sys import argv +from typing import Any + + +class Model(ABC): + """The Model is the data layer of the application.""" + + @abstractmethod + def __iter__(self) -> Any: + pass + + @abstractmethod + def get(self, item: str) -> dict: + """Returns an object with a .items() call method + that iterates over key,value pairs of its information.""" + pass + + @property + @abstractmethod + def item_type(self) -> str: + pass + + +class ProductModel(Model): + """The Model is the data layer of the application.""" + + class Price(float): + """A polymorphic way to pass a float with a particular + __str__ functionality.""" + + def __str__(self) -> str: + return f"{self:.2f}" + + products = { + "milk": {"price": Price(1.50), "quantity": 10}, + "eggs": {"price": Price(0.20), "quantity": 100}, + "cheese": {"price": Price(2.00), "quantity": 10}, + } + + item_type = "product" + + def __iter__(self) -> Any: + yield from self.products + + def get(self, product: str) -> dict: + try: + return self.products[product] + except KeyError as e: + raise KeyError(str(e) + " not in the model's item list.") + + +class View(ABC): + """The View is the presentation layer of the application.""" + + @abstractmethod + def show_item_list(self, item_type: str, item_list: list) -> None: + pass + + @abstractmethod + def show_item_information( + self, item_type: str, item_name: str, item_info: dict + ) -> None: + """Will look for item information by iterating over key,value pairs + yielded by item_info.items()""" + pass + + @abstractmethod + def item_not_found(self, item_type: str, item_name: str) -> None: + pass + + +class ConsoleView(View): + """The View is the presentation layer of the application.""" + + def show_item_list(self, item_type: str, item_list: list) -> None: + print(item_type.upper() + " LIST:") + for item in item_list: + print(item) + print("") + + @staticmethod + def capitalizer(string: str) -> str: + """Capitalizes the first letter of a string and lowercases the rest.""" + return string[0].upper() + string[1:].lower() + + def show_item_information( + self, item_type: str, item_name: str, item_info: dict + ) -> None: + """Will look for item information by iterating over key,value pairs""" + print(item_type.upper() + " INFORMATION:") + printout = "Name: %s" % item_name + for key, value in item_info.items(): + printout += ", " + self.capitalizer(str(key)) + ": " + str(value) + printout += "\n" + print(printout) + + def item_not_found(self, item_type: str, item_name: str) -> None: + print(f'That {item_type} "{item_name}" does not exist in the records') + + +class Controller: + """The Controller is the intermediary between the Model and the View.""" + + def __init__(self, model_class: Model, view_class: View) -> None: + self.model: Model = model_class + self.view: View = view_class + + def show_items(self) -> None: + items = list(self.model) + item_type = self.model.item_type + self.view.show_item_list(item_type, items) + + def show_item_information(self, item_name: str) -> None: + """ + Show information about a {item_type} item. + :param str item_name: the name of the {item_type} item to show information about + """ + item_type: str = self.model.item_type + try: + item_info: dict = self.model.get(item_name) + except Exception: + self.view.item_not_found(item_type, item_name) + else: + self.view.show_item_information(item_type, item_name, item_info) + + +class Router: + """The Router is the entry point of the application.""" + + def __init__(self): + self.routes = {} + + def register( + self, + path: str, + controller_class: type[Controller], + model_class: type[Model], + view_class: type[View], + ) -> None: + model_instance: Model = model_class() + view_instance: View = view_class() + self.routes[path] = controller_class(model_instance, view_instance) + + def resolve(self, path: str) -> Controller: + if self.routes.get(path): + controller: Controller = self.routes[path] + return controller + else: + raise KeyError(f"No controller registered for path '{path}'") + + +def main(): + """ + >>> model = ProductModel() + >>> view = ConsoleView() + >>> controller = Controller(model, view) + + >>> controller.show_items() + PRODUCT LIST: + milk + eggs + cheese + + + >>> controller.show_item_information("cheese") + PRODUCT INFORMATION: + Name: cheese, Price: 2.00, Quantity: 10 + + + >>> controller.show_item_information("eggs") + PRODUCT INFORMATION: + Name: eggs, Price: 0.20, Quantity: 100 + + + >>> controller.show_item_information("milk") + PRODUCT INFORMATION: + Name: milk, Price: 1.50, Quantity: 10 + + + >>> controller.show_item_information("arepas") + That product "arepas" does not exist in the records + """ + + +if __name__ == "__main__": + router = Router() + router.register("products", Controller, ProductModel, ConsoleView) + controller: Controller = router.resolve(argv[1]) + + action: str = str(argv[2]) if len(argv) > 2 else "" + args: str = " ".join(map(str, argv[3:])) if len(argv) > 3 else "" + + if hasattr(controller, action): + command = getattr(controller, action) + sig = signature(command) + + if len(sig.parameters) > 0: + if args: + command(args) + else: + print("Command requires arguments.") + else: + command() + else: + print(f"Command {action} not found in the controller.") + + import doctest + + doctest.testmod() diff --git a/patterns/structural/proxy.py b/patterns/structural/proxy.py new file mode 100644 index 000000000..c12fb051c --- /dev/null +++ b/patterns/structural/proxy.py @@ -0,0 +1,89 @@ +""" +*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. +""" + + +class Subject: + """ + As mentioned in the document, interfaces of both RealSubject and Proxy should + be the same, because the client should be able to use RealSubject or Proxy with + no code change. + + Not all times this interface is necessary. The point is the client should be + able to use RealSubject or Proxy interchangeably with no change in code. + """ + + def do_the_job(self, user: str) -> None: + raise NotImplementedError() + + +class RealSubject(Subject): + """ + This is the main job doer. External services like payment gateways can be a + good example. + """ + + def do_the_job(self, user: str) -> None: + print(f"I am doing the job for {user}") + + +class Proxy(Subject): + def __init__(self) -> None: + self._real_subject = RealSubject() + + def do_the_job(self, user: str) -> None: + """ + logging and controlling access are some examples of proxy usages. + """ + + print(f"[log] Doing the job for {user} is requested.") + + if user == "admin": + self._real_subject.do_the_job(user) + else: + print("[log] I can do the job just for `admins`.") + + +def client(job_doer: 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/structural/viz/3-tier.py.png b/patterns/structural/viz/3-tier.py.png similarity index 100% rename from structural/viz/3-tier.py.png rename to patterns/structural/viz/3-tier.py.png diff --git a/structural/viz/adapter.py.png b/patterns/structural/viz/adapter.py.png similarity index 100% rename from structural/viz/adapter.py.png rename to patterns/structural/viz/adapter.py.png diff --git a/structural/viz/bridge.py.png b/patterns/structural/viz/bridge.py.png similarity index 100% rename from structural/viz/bridge.py.png rename to patterns/structural/viz/bridge.py.png diff --git a/structural/viz/composite.py.png b/patterns/structural/viz/composite.py.png similarity index 100% rename from structural/viz/composite.py.png rename to patterns/structural/viz/composite.py.png diff --git a/structural/viz/decorator.py.png b/patterns/structural/viz/decorator.py.png similarity index 100% rename from structural/viz/decorator.py.png rename to patterns/structural/viz/decorator.py.png diff --git a/structural/viz/facade.py.png b/patterns/structural/viz/facade.py.png similarity index 100% rename from structural/viz/facade.py.png rename to patterns/structural/viz/facade.py.png diff --git a/structural/viz/flyweight.py.png b/patterns/structural/viz/flyweight.py.png similarity index 100% rename from structural/viz/flyweight.py.png rename to patterns/structural/viz/flyweight.py.png diff --git a/structural/viz/front_controller.py.png b/patterns/structural/viz/front_controller.py.png similarity index 100% rename from structural/viz/front_controller.py.png rename to patterns/structural/viz/front_controller.py.png diff --git a/structural/viz/mvc.py.png b/patterns/structural/viz/mvc.py.png similarity index 100% rename from structural/viz/mvc.py.png rename to patterns/structural/viz/mvc.py.png diff --git a/structural/viz/proxy.py.png b/patterns/structural/viz/proxy.py.png similarity index 100% rename from structural/viz/proxy.py.png rename to patterns/structural/viz/proxy.py.png 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/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/structural/3-tier.py b/structural/3-tier.py deleted file mode 100644 index ca8c95981..000000000 --- a/structural/3-tier.py +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -*TL;DR80 -Separates presentation, application processing, and data management functions. -""" - - -class Data(object): - """ Data Store Class """ - - products = { - 'milk': {'price': 1.50, 'quantity': 10}, - 'eggs': {'price': 0.20, 'quantity': 100}, - 'cheese': {'price': 2.00, 'quantity': 10} - } - - def __get__(self, obj, klas): - print("(Fetching from Data Store)") - return {'products': self.products} - - -class BusinessLogic(object): - """ Business logic holding data store instances """ - - data = Data() - - def product_list(self): - return self.data['products'].keys() - - def product_information(self, product): - return self.data['products'].get(product, None) - - -class Ui(object): - """ UI interaction class """ - - def __init__(self): - self.business_logic = BusinessLogic() - - def get_product_list(self): - print('PRODUCT LIST:') - for product in self.business_logic.product_list(): - print(product) - print('') - - def get_product_information(self, product): - product_info = self.business_logic.product_information(product) - if product_info: - print('PRODUCT INFORMATION:') - print('Name: {0}, Price: {1:.2f}, Quantity: {2:}'.format( - product.title(), product_info.get('price', 0), - product_info.get('quantity', 0))) - else: - print('That product "{0}" does not exist in the records'.format( - product)) - - -def main(): - ui = Ui() - ui.get_product_list() - ui.get_product_information('cheese') - ui.get_product_information('eggs') - ui.get_product_information('milk') - ui.get_product_information('arepas') - -if __name__ == '__main__': - main() - -### OUTPUT ### -# PRODUCT LIST: -# (Fetching from Data Store) -# cheese -# eggs -# milk -# -# (Fetching from Data Store) -# PRODUCT INFORMATION: -# Name: Cheese, Price: 2.00, Quantity: 10 -# (Fetching from Data Store) -# PRODUCT INFORMATION: -# Name: Eggs, Price: 0.20, Quantity: 100 -# (Fetching from Data Store) -# PRODUCT INFORMATION: -# Name: Milk, Price: 1.50, Quantity: 10 -# (Fetching from Data Store) -# That product "arepas" does not exist in the records diff --git a/structural/bridge.py b/structural/bridge.py deleted file mode 100644 index 63e706064..000000000 --- a/structural/bridge.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -*References: -http://en.wikibooks.org/wiki/Computer_Science_Design_Patterns/Bridge_Pattern#Python - -*TL;DR80 -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)) - - -# ConcreteImplementor 2/2 -class DrawingAPI2(object): - - def draw_circle(self, x, y, radius): - print('API2.circle at {}:{} radius {}'.format(x, y, radius)) - - -# Refined Abstraction -class CircleShape(object): - - def __init__(self, x, y, radius, drawing_api): - self._x = x - self._y = y - self._radius = radius - self._drawing_api = drawing_api - - # low-level i.e. Implementation specific - def draw(self): - self._drawing_api.draw_circle(self._x, self._y, self._radius) - - # high-level i.e. Abstraction specific - def scale(self, pct): - self._radius *= pct - - -def main(): - shapes = ( - CircleShape(1, 2, 3, DrawingAPI1()), - CircleShape(5, 7, 11, DrawingAPI2()) - ) - - for shape in shapes: - shape.scale(2.5) - shape.draw() - - -if __name__ == '__main__': - main() - -### OUTPUT ### -# API1.circle at 1:2 radius 7.5 -# API2.circle at 5:7 radius 27.5 diff --git a/structural/facade.py b/structural/facade.py deleted file mode 100644 index 3ff82dfe1..000000000 --- a/structural/facade.py +++ /dev/null @@ -1,127 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -*What is this pattern about? -The Facade pattern is a way to provide a simpler unified interface to -a more complex system. It provides an easier way to access functions -of the underlying system by providing a single entry point. -This kind of abstraction is seen in many real life situations. For -example, we can turn on a computer by just pressing a button, but in -fact there are many procedures and operations done when that happens -(e.g., loading programs from disk to memory). In this case, the button -serves as an unified interface to all the underlying procedures to -turn on a computer. - -*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 -whether a path refers to a directory, the system makes a few -operations and calls other modules (e.g., os.stat) to give the result. - -*References: -https://sourcemaking.com/design_patterns/facade -https://fkromer.github.io/python-pattern-references/design/#facade -http://python-3-patterns-idioms-test.readthedocs.io/en/latest/ChangeInterface.html#facade - -*TL;DR80 -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: - - 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 -# diff --git a/structural/flyweight.py b/structural/flyweight.py deleted file mode 100644 index d21188edb..000000000 --- a/structural/flyweight.py +++ /dev/null @@ -1,125 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -*References: -http://codesnipers.com/?q=python-flyweights - -*TL;DR80 -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 - obj.value, obj.suit = value, suit - return obj - - # def __init__(self, value, suit): - # self.value, self.suit = value, suit - - def __repr__(self): - return "" % (self.value, self.suit) - - -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)) - - 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) - - assert (cm1 == cm2) != cm3 - assert (cm1 is cm2) 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 - -### OUTPUT ### -# (, ) -# (True, True) -# (31903856, 31903856) -# True -# False diff --git a/structural/front_controller.py b/structural/front_controller.py deleted file mode 100644 index c7bccebda..000000000 --- a/structural/front_controller.py +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -@author: Gordeev Andrey - -*TL;DR80 -Provides a centralized entry point that controls and manages request handling. -""" - -class MobileView(object): - - def show_index_page(self): - print('Displaying mobile index page') - - -class TabletView(object): - - def show_index_page(self): - print('Displaying tablet index page') - - -class Dispatcher(object): - - def __init__(self): - self.mobile_view = MobileView() - self.tablet_view = TabletView() - - def dispatch(self, 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') - - -class RequestController(object): - """ front controller """ - - def __init__(self): - self.dispatcher = Dispatcher() - - def dispatch_request(self, request): - if isinstance(request, Request): - self.dispatcher.dispatch(request) - else: - print('request must be a Request object') - - -class Request(object): - """ request """ - - mobile_type = 'mobile' - tablet_type = 'tablet' - - def __init__(self, request): - self.type = None - request = request.lower() - if request == self.mobile_type: - self.type = self.mobile_type - elif request == self.tablet_type: - self.type = self.tablet_type - - -if __name__ == '__main__': - front_controller = RequestController() - front_controller.dispatch_request(Request('mobile')) - front_controller.dispatch_request(Request('tablet')) - - front_controller.dispatch_request(Request('desktop')) - front_controller.dispatch_request('mobile') - - -### OUTPUT ### -# Displaying mobile index page -# Displaying tablet index page -# cant dispatch the request -# request must be a Request object diff --git a/structural/mvc.py b/structural/mvc.py deleted file mode 100644 index fc6708cf2..000000000 --- a/structural/mvc.py +++ /dev/null @@ -1,146 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -*TL;DR80 -Separates data in GUIs from the ways it is presented, and accepted. -""" - -class Model(object): - - def __iter__(self): - raise NotImplementedError - - def get(self, item): - """Returns an object with a .items() call method - that iterates over key,value pairs of its information.""" - raise NotImplementedError - - @property - def item_type(self): - raise NotImplementedError - - -class ProductModel(Model): - - class Price(float): - """A polymorphic way to pass a float with a particular - __str__ functionality.""" - - def __str__(self): - first_digits_str = str(round(self, 2)) - try: - dot_location = first_digits_str.index('.') - except ValueError: - return (first_digits_str + '.00') - else: - return (first_digits_str + - '0' * (3 + dot_location - len(first_digits_str))) - - products = { - 'milk': {'price': Price(1.50), 'quantity': 10}, - 'eggs': {'price': Price(0.20), 'quantity': 100}, - 'cheese': {'price': Price(2.00), 'quantity': 10} - } - - item_type = 'product' - - def __iter__(self): - for item in self.products: - yield item - - def get(self, product): - try: - return self.products[product] - except KeyError as e: - 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 - - def show_item_information(self, item_type, item_name, item_info): - """Will look for item information by iterating over key,value pairs - yielded by item_info.items()""" - raise NotImplementedError - - def item_not_found(self, item_type, item_name): - raise NotImplementedError - - -class ConsoleView(View): - - def show_item_list(self, item_type, item_list): - print(item_type.upper() + ' LIST:') - for item in item_list: - print(item) - print('') - - @staticmethod - def capitalizer(string): - 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 - for key, value in item_info.items(): - printout += (', ' + self.capitalizer(str(key)) + ': ' + str(value)) - printout += '\n' - print(printout) - - def item_not_found(self, item_type, item_name): - print('That %s "%s" does not exist in the records' % - (item_type, item_name)) - - -class Controller(object): - - def __init__(self, model, view): - self.model = model - self.view = view - - def show_items(self): - 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): - try: - item_info = self.model.get(item_name) - except: - 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 diff --git a/structural/proxy.py b/structural/proxy.py deleted file mode 100644 index 146ba766e..000000000 --- a/structural/proxy.py +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -*TL;DR80 -Provides an interface to resource that is expensive to duplicate. -""" - -from __future__ import print_function -import time - - -class SalesManager: - def talk(self): - print("Sales Manager ready to talk") - - -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() - 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 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_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 new file mode 100644 index 000000000..821f97a61 --- /dev/null +++ b/tests/behavioral/test_observer.py @@ -0,0 +1,33 @@ +from unittest.mock import Mock, patch + +import pytest + +from patterns.behavioral.observer import Data, DecimalViewer, HexViewer + + +@pytest.fixture +def observable(): + return Data("some data") + + +def test_attach_detach(observable): + decimal_viewer = DecimalViewer() + assert len(observable._observers) == 0 + + observable.attach(decimal_viewer) + assert decimal_viewer in observable._observers + + observable.detach(decimal_viewer) + assert decimal_viewer not in observable._observers + + +def test_one_data_change_notifies_each_observer_once(observable): + observable.attach(DecimalViewer()) + observable.attach(HexViewer()) + + with patch( + "patterns.behavioral.observer.DecimalViewer.update", new_callable=Mock() + ) as mocked_update: + assert mocked_update.call_count == 0 + observable.data = 10 + assert mocked_update.call_count == 1 diff --git a/tests/behavioral/test_publish_subscribe.py b/tests/behavioral/test_publish_subscribe.py new file mode 100644 index 000000000..6f142855d --- /dev/null +++ b/tests/behavioral/test_publish_subscribe.py @@ -0,0 +1,69 @@ +from unittest.mock import call, patch + +from patterns.behavioral.publish_subscribe import Provider, Publisher, Subscriber + + +class TestProvider: + """ + Integration tests ~ provider class with as little mocking as possible. + """ + + def test_subscriber_shall_be_attachable_to_subscriptions(cls): + subscription = "sub msg" + pro = Provider() + assert len(pro.subscribers) == 0 + sub = Subscriber("sub name", pro) + sub.subscribe(subscription) + assert len(pro.subscribers[subscription]) == 1 + + def test_subscriber_shall_be_detachable_from_subscriptions(cls): + subscription = "sub msg" + pro = Provider() + sub = Subscriber("sub name", pro) + sub.subscribe(subscription) + assert len(pro.subscribers[subscription]) == 1 + sub.unsubscribe(subscription) + 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" + pro = Provider() + pub = Publisher(pro) + Subscriber("sub name", pro) + assert len(pro.msg_queue) == 0 + pub.publish(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, + ): + pro = Provider() + pub = Publisher(pro) + sub1 = Subscriber("sub 1 name", pro) + sub1.subscribe("sub 1 msg 1") + sub1.subscribe("sub 1 msg 2") + sub2 = Subscriber("sub 2 name", pro) + sub2.subscribe("sub 2 msg 1") + sub2.subscribe("sub 2 msg 2") + with ( + patch.object(sub1, "run") as mock_subscriber1_run, + patch.object(sub2, "run") as mock_subscriber2_run, + ): + pro.update() + 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")] + mock_subscriber1_run.assert_has_calls(expected_sub1_calls) + expected_sub2_calls = [call("sub 2 msg 1"), call("sub 2 msg 2")] + mock_subscriber2_run.assert_has_calls(expected_sub2_calls) diff --git a/tests/behavioral/test_servant.py b/tests/behavioral/test_servant.py new file mode 100644 index 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 new file mode 100644 index 000000000..77473f519 --- /dev/null +++ b/tests/behavioral/test_state.py @@ -0,0 +1,27 @@ +import pytest + +from patterns.behavioral.state import Radio + + +@pytest.fixture +def radio(): + return Radio() + + +def test_initial_state(radio): + assert radio.state.name == "AM" + + +def test_initial_am_station(radio): + initial_pos = radio.state.pos + assert radio.state.stations[initial_pos] == "1250" + + +def test_toggle_amfm(radio): + assert radio.state.name == "AM" + + radio.toggle_amfm() + assert radio.state.name == "FM" + + radio.toggle_amfm() + assert radio.state.name == "AM" diff --git a/tests/behavioral/test_strategy.py b/tests/behavioral/test_strategy.py new file mode 100644 index 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 new file mode 100644 index 000000000..e717fafcd --- /dev/null +++ b/tests/creational/test_abstract_factory.py @@ -0,0 +1,12 @@ +from unittest.mock import patch + +from patterns.creational.abstract_factory import Dog, PetShop + + +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: + 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 new file mode 100644 index 000000000..61785020a --- /dev/null +++ b/tests/creational/test_borg.py @@ -0,0 +1,26 @@ +from patterns.creational.borg import Borg, YourBorg + + +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() + assert b.state == "Init" + + def test_changing_instance_attribute_shall_change_borg_state(self): + 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): + assert id(self.b1) != id(self.b2), id(self.ib1) diff --git a/tests/creational/test_builder.py b/tests/creational/test_builder.py new file mode 100644 index 000000000..539d7994b --- /dev/null +++ b/tests/creational/test_builder.py @@ -0,0 +1,20 @@ +from patterns.creational.builder import ComplexHouse, Flat, House, construct_building + + +class TestSimple: + def test_house(self): + house = House() + assert house.size == "Big" + assert house.floor == "One" + + def test_flat(self): + flat = Flat() + assert flat.size == "Small" + assert flat.floor == "More than One" + + +class TestComplex: + def test_house(self): + house = construct_building(ComplexHouse) + 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_lazy.py b/tests/creational/test_lazy.py new file mode 100644 index 000000000..d1d02a1ba --- /dev/null +++ b/tests/creational/test_lazy.py @@ -0,0 +1,34 @@ +from patterns.creational.lazy_evaluation import Person + + +class TestDynamicExpanding: + def setup_method(self): + self.John = Person("John", "Coder") + + def test_innate_properties(self): + assert { + "name": "John", + "occupation": "Coder", + "call_count2": 0, + } == self.John.__dict__ + + def test_relatives_not_in_properties(self): + assert "relatives" not in self.John.__dict__ + + def test_extended_properties(self): + 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(f"John's relatives: {self.John.relatives}") + assert "relatives" in self.John.__dict__ + + def test_parents(self): + for _ in range(2): + 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 new file mode 100644 index 000000000..97b9d269e --- /dev/null +++ b/tests/creational/test_pool.py @@ -0,0 +1,49 @@ +import queue + +from patterns.creational.pool import ObjectPool + + +class TestPool: + def setup_method(self): + self.sample_queue = queue.Queue() + self.sample_queue.put("first") + self.sample_queue.put("second") + + def test_items_recoil(self): + with ObjectPool(self.sample_queue, True) as pool: + 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: + 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: + """def test_object(queue): + queue_object = QueueObject(queue, True) + print('Inside func: {}'.format(queue_object.object))""" + + def test_pool_behavior_with_single_object_inside(self): + sample_queue = queue.Queue() + sample_queue.put("yam") + with ObjectPool(sample_queue) as obj: + # print('Inside with: {}'.format(obj)) + 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) + # print('Outside func: {}'.format(sample_queue.get())) + + # if not sample_queue.empty(): diff --git a/tests/creational/test_prototype.py b/tests/creational/test_prototype.py new file mode 100644 index 000000000..ca5d77724 --- /dev/null +++ b/tests/creational/test_prototype.py @@ -0,0 +1,48 @@ +import pytest + +from patterns.creational.prototype import Prototype, PrototypeDispatcher + + +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() + 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_2 = self.prototype.clone() + 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") + assert sample_object_1.value != sample_object_2.value + + +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) + + def test_batch_retrieving(self): + assert len(self.dispatcher.get_objects()) == 3 + + def test_particular_properties_retrieving(self): + 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): + assert self.dispatcher.get_objects()["A"].ext_value == "E" + assert self.dispatcher.get_objects()["B"].diff diff --git a/tests/fundamental/test_delegation.py b/tests/fundamental/test_delegation.py new file mode 100644 index 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/test_adapter.py b/tests/structural/test_adapter.py similarity index 71% rename from tests/test_adapter.py rename to tests/structural/test_adapter.py index 4796e48ae..042a32f26 100644 --- a/tests/test_adapter.py +++ b/tests/structural/test_adapter.py @@ -1,12 +1,8 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import unittest -from 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() @@ -15,58 +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() @@ -74,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 new file mode 100644 index 000000000..bcf2a46d6 --- /dev/null +++ b/tests/structural/test_bridge.py @@ -0,0 +1,43 @@ +from unittest.mock import patch + +from patterns.structural.bridge import CircleShape, DrawingAPI1, DrawingAPI2 + + +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, + ): + sh1 = CircleShape(1, 2, 3, ci1) + sh1.draw() + assert mock_ci1_draw_circle.call_count == 1 + sh2 = CircleShape(1, 2, 3, ci2) + sh2.draw() + assert mock_ci2_draw_circle.call_count == 1 + + def test_bridge_shall_scale_both_api_circles_with_own_implementation(cls): + SCALE_FACTOR = 2 + CIRCLE1_RADIUS = 3 + EXPECTED_CIRCLE1_RADIUS = 6 + CIRCLE2_RADIUS = CIRCLE1_RADIUS * CIRCLE1_RADIUS + EXPECTED_CIRCLE2_RADIUS = CIRCLE2_RADIUS * SCALE_FACTOR + + ci1 = DrawingAPI1() + ci2 = DrawingAPI2() + sh1 = CircleShape(1, 2, CIRCLE1_RADIUS, ci1) + sh2 = CircleShape(1, 2, CIRCLE2_RADIUS, ci2) + sh1.scale(SCALE_FACTOR) + sh2.scale(SCALE_FACTOR) + 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) + 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 new file mode 100644 index 000000000..04a7a112e --- /dev/null +++ b/tests/structural/test_decorator.py @@ -0,0 +1,18 @@ +from patterns.structural.decorator import BoldWrapper, ItalicWrapper, TextTag + + +class TestTextWrapping: + def setup_method(self): + self.raw_string = TextTag("raw but not cruel") + + def test_italic(self): + assert ItalicWrapper(self.raw_string).render() == "raw but not cruel" + + def test_bold(self): + assert BoldWrapper(self.raw_string).render() == "raw but not cruel" + + def test_mixed_bold_and_italic(self): + 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 new file mode 100644 index 000000000..2ff24ca3a --- /dev/null +++ b/tests/structural/test_facade.py @@ -0,0 +1,11 @@ +from patterns.structural.facade import ComputerFacade + + +def test_computer_facade_start(capsys): + cf = ComputerFacade() + cf.start() + out = capsys.readouterr().out + assert "Freezing processor." in out + assert "Loading from 0x00 data:" in out + assert "Jumping to: 0x00" in out + assert "Executing." in out diff --git a/tests/structural/test_flyweight.py b/tests/structural/test_flyweight.py new file mode 100644 index 000000000..a200203f9 --- /dev/null +++ b/tests/structural/test_flyweight.py @@ -0,0 +1,20 @@ +from patterns.structural.flyweight import Card + + +def test_card_flyweight_identity_and_repr(): + c1 = Card("9", "h") + c2 = Card("9", "h") + assert c1 is c2 + assert repr(c1) == "" + + +def test_card_attribute_persistence_and_pool_clear(): + Card._pool.clear() + c1 = Card("A", "s") + c1.temp = "t" + c2 = Card("A", "s") + assert hasattr(c2, "temp") + + Card._pool.clear() + c3 = Card("A", "s") + assert not hasattr(c3, "temp") diff --git a/tests/structural/test_mvc.py b/tests/structural/test_mvc.py new file mode 100644 index 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 new file mode 100644 index 000000000..a8965e2cf --- /dev/null +++ b/tests/structural/test_proxy.py @@ -0,0 +1,17 @@ +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_abstract_factory.py b/tests/test_abstract_factory.py deleted file mode 100644 index 0e64c7127..000000000 --- a/tests/test_abstract_factory.py +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import unittest -from creational.abstract_factory import PetShop, Dog -try: - from unittest.mock import patch -except ImportError: - from mock import patch - - -class TestPetShop(unittest.TestCase): - - def test_dog_pet_shop_shall_show_dog_instance(self): - dog_pet_shop = PetShop(Dog) - with patch.object(Dog, 'speak') as mock_Dog_speak: - dog_pet_shop.show_pet() - self.assertEqual(mock_Dog_speak.call_count, 1) diff --git a/tests/test_borg.py b/tests/test_borg.py deleted file mode 100644 index 545ab3ae1..000000000 --- a/tests/test_borg.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import unittest -from creational.borg import Borg, YourBorg - - -class BorgTest(unittest.TestCase): - - def setUp(self): - self.b1 = Borg() - self.b2 = Borg() - self.ib1 = YourBorg() - - def test_initial_borg_state_shall_be_init(self): - b = Borg() - self.assertEqual(b.state, 'Init') - - def test_changing_instance_attribute_shall_change_borg_state(self): - self.b1.state = 'Running' - self.assertEqual(self.b1.state, 'Running') - self.assertEqual(self.b2.state, 'Running') - self.assertEqual(self.ib1.state, 'Running') - - def test_instances_shall_have_own_ids(self): - self.assertNotEqual(id(self.b1), id(self.b2), id(self.ib1)) diff --git a/tests/test_bridge.py b/tests/test_bridge.py deleted file mode 100644 index f639ad8c0..000000000 --- a/tests/test_bridge.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import unittest -from structural.bridge import DrawingAPI1, DrawingAPI2, CircleShape -try: - from unittest.mock import patch -except ImportError: - from mock import patch - - -class BridgeTest(unittest.TestCase): - - def test_bridge_shall_draw_with_concrete_api_implementation(cls): - ci1 = DrawingAPI1() - ci2 = DrawingAPI2() - with patch.object(ci1, 'draw_circle') as mock_ci1_draw_circle,\ - patch.object(ci2, 'draw_circle') as mock_ci2_draw_circle: - sh1 = CircleShape(1, 2, 3, ci1) - sh1.draw() - cls.assertEqual(mock_ci1_draw_circle.call_count, 1) - sh2 = CircleShape(1, 2, 3, ci2) - sh2.draw() - cls.assertEqual(mock_ci2_draw_circle.call_count, 1) - - def test_bridge_shall_scale_both_api_circles_with_own_implementation(cls): - SCALE_FACTOR = 2 - CIRCLE_FACTOR = 3 - CIRCLE1_RADIUS = 3 - EXPECTED_CIRCLE1_RADIUS = 6 - CIRCLE2_RADIUS = CIRCLE1_RADIUS * CIRCLE1_RADIUS - EXPECTED_CIRCLE2_RADIUS = CIRCLE2_RADIUS * SCALE_FACTOR - ci1 = DrawingAPI1() - ci2 = DrawingAPI2() - sh1 = CircleShape(1, 2, CIRCLE1_RADIUS, ci1) - sh2 = CircleShape(1, 2, CIRCLE2_RADIUS, ci2) - sh1.scale(SCALE_FACTOR) - sh2.scale(SCALE_FACTOR) - cls.assertEqual(sh1._radius, EXPECTED_CIRCLE1_RADIUS) - cls.assertEqual(sh2._radius, EXPECTED_CIRCLE2_RADIUS) - with patch.object(sh1, 'scale') as mock_sh1_scale_circle,\ - patch.object(sh2, 'scale') as mock_sh2_scale_circle: - sh1.scale(2) - sh2.scale(2) - cls.assertEqual(mock_sh1_scale_circle.call_count, 1) - cls.assertEqual(mock_sh2_scale_circle.call_count, 1) diff --git a/tests/test_builder.py b/tests/test_builder.py deleted file mode 100644 index 533c11c96..000000000 --- a/tests/test_builder.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import unittest -from creational.builder import construct_building, BuilderHouse, BuilderFlat - - -class TestHouseBuilding(unittest.TestCase): - - def setUp(self): - self.building = construct_building(BuilderHouse()) - - def test_house_size(self): - self.assertEqual(self.building.size, 'Big') - - def test_num_floor_in_house(self): - self.assertEqual(self.building.floor, 'One') - - -class TestFlatBuilding(unittest.TestCase): - - def setUp(self): - self.building = construct_building(BuilderFlat()) - - def test_house_size(self): - self.assertEqual(self.building.size, 'Small') - - def test_num_floor_in_house(self): - self.assertEqual(self.building.floor, 'More than One') diff --git a/tests/test_command.py b/tests/test_command.py deleted file mode 100644 index 9446b961e..000000000 --- a/tests/test_command.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import os -import shutil -import unittest -from 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/test_command') - open('tests/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/test_command') diff --git a/tests/test_constructor_injection.py b/tests/test_constructor_injection.py deleted file mode 100644 index 5ae9ccff2..000000000 --- a/tests/test_constructor_injection.py +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import unittest - -from 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/test_decorator.py b/tests/test_decorator.py deleted file mode 100644 index cc8938f51..000000000 --- a/tests/test_decorator.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import unittest -from structural.decorator import TextTag, BoldWrapper, ItalicWrapper - - -class TestTextWrapping(unittest.TestCase): - - def setUp(self): - self.raw_string = TextTag('raw but not cruel') - - def test_italic(self): - self.assertEqual(ItalicWrapper(self.raw_string).render(), - 'raw but not cruel') - - def test_bold(self): - self.assertEqual(BoldWrapper(self.raw_string).render(), - 'raw but not cruel') - - def test_mixed_bold_and_italic(self): - self.assertEqual( - BoldWrapper(ItalicWrapper(self.raw_string)).render(), - 'raw but not cruel') diff --git a/tests/test_facade.py b/tests/test_facade.py deleted file mode 100644 index c7859161a..000000000 --- a/tests/test_facade.py +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import unittest -import sys -try: - from io import StringIO -except: - from StringIO import StringIO -from 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)) diff --git a/tests/test_factory_method.py b/tests/test_factory_method.py deleted file mode 100644 index 9c0ae7dc2..000000000 --- a/tests/test_factory_method.py +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import unittest -from 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/test_flyweight.py b/tests/test_flyweight.py deleted file mode 100644 index f8fd9cec0..000000000 --- a/tests/test_flyweight.py +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import unittest -from 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_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_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) diff --git a/tests/test_hsm.py b/tests/test_hsm.py index e5b2f88f0..4ddd4d331 100644 --- a/tests/test_hsm.py +++ b/tests/test_hsm.py @@ -1,91 +1,99 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import unittest -from other.hsm.hsm import HierachicalStateMachine,\ - UnsupportedMessageType, UnsupportedState,\ - UnsupportedTransition, Active, Standby, Suspect -try: - from unittest.mock import patch -except ImportError: - from mock import patch +from unittest.mock import patch +import pytest -class HsmMethodTest(unittest.TestCase): +from patterns.other.hsm.hsm import ( + Active, + HierachicalStateMachine, + Standby, + Suspect, + UnsupportedMessageType, + UnsupportedState, + UnsupportedTransition, +) + +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) as context: - 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) as context: - 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) as context: - 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) as context: - 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) as context: - 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_lazy.py b/tests/test_lazy.py deleted file mode 100644 index 8452302ca..000000000 --- a/tests/test_lazy.py +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -from __future__ import print_function -import unittest -from creational.lazy_evaluation import Person - - -class TestDynamicExpanding(unittest.TestCase): - - def setUp(self): - self.John = Person('John', 'Coder') - - def test_innate_properties(self): - self.assertDictEqual( - {'name': 'John', 'occupation': 'Coder', 'call_count2': 0}, - self.John.__dict__ - ) - - def test_relatives_not_in_properties(self): - self.assertNotIn('relatives', self.John.__dict__) - - def test_extended_properties(self): - print(u"John's relatives: {0}".format(self.John.relatives)) - self.assertDictEqual( - {'name': 'John', 'occupation': 'Coder', - 'relatives': 'Many relatives.', 'call_count2': 0}, - self.John.__dict__ - ) - - def test_relatives_after_access(self): - print(u"John's relatives: {0}".format(self.John.relatives)) - self.assertIn('relatives', self.John.__dict__) - - def test_parents(self): - for _ in range(2): - self.assertEqual(self.John.parents, "Father and mother") - self.assertEqual(self.John.call_count2, 1) diff --git a/tests/test_observer.py b/tests/test_observer.py deleted file mode 100644 index 077f33089..000000000 --- a/tests/test_observer.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import unittest -from 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') diff --git a/tests/test_parameter_injection.py b/tests/test_parameter_injection.py deleted file mode 100644 index 9b0d043d7..000000000 --- a/tests/test_parameter_injection.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import unittest - -from 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/test_pool.py b/tests/test_pool.py deleted file mode 100644 index 9f8d07764..000000000 --- a/tests/test_pool.py +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import unittest -try: - import queue -except ImportError: # python 2.x compatibility - import Queue as queue -from creational.pool import ObjectPool - - -class TestPool(unittest.TestCase): - - def setUp(self): - self.sample_queue = queue.Queue() - self.sample_queue.put('first') - self.sample_queue.put('second') - - def test_items_recoil(self): - with ObjectPool(self.sample_queue, True) as pool: - self.assertEqual(pool, 'first') - self.assertTrue(self.sample_queue.get() == 'second') - self.assertFalse(self.sample_queue.empty()) - self.assertTrue(self.sample_queue.get() == 'first') - self.assertTrue(self.sample_queue.empty()) - - def test_frozen_pool(self): - with ObjectPool(self.sample_queue) as pool: - self.assertEqual(pool, 'first') - self.assertEqual(pool, 'first') - self.assertTrue(self.sample_queue.get() == 'second') - self.assertFalse(self.sample_queue.empty()) - self.assertTrue(self.sample_queue.get() == 'first') - self.assertTrue(self.sample_queue.empty()) - - -class TestNaitivePool(unittest.TestCase): - - """def test_object(queue): - queue_object = QueueObject(queue, True) - print('Inside func: {}'.format(queue_object.object))""" - - def test_pool_behavior_with_single_object_inside(self): - sample_queue = queue.Queue() - sample_queue.put('yam') - with ObjectPool(sample_queue) as obj: - # print('Inside with: {}'.format(obj)) - self.assertEqual(obj, 'yam') - self.assertFalse(sample_queue.empty()) - self.assertTrue(sample_queue.get() == 'yam') - self.assertTrue(sample_queue.empty()) - - - # sample_queue.put('sam') - # test_object(sample_queue) - # print('Outside func: {}'.format(sample_queue.get())) - - # if not sample_queue.empty(): diff --git a/tests/test_prototype.py b/tests/test_prototype.py deleted file mode 100644 index 0ad942829..000000000 --- a/tests/test_prototype.py +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import unittest -from creational.prototype import Prototype, PrototypeDispatcher - - -class TestPrototypeFeatures(unittest.TestCase): - - def setUp(self): - self.prototype = Prototype() - - def test_cloning_propperty_innate_values(self): - sample_object_1 = self.prototype.clone() - sample_object_2 = self.prototype.clone() - self.assertEqual(sample_object_1.value, sample_object_2.value) - - def test_extended_property_values_cloning(self): - sample_object_1 = self.prototype.clone() - sample_object_1.some_value = 'test string' - sample_object_2 = self.prototype.clone() - self.assertRaises(AttributeError, lambda: sample_object_2.some_value) - - def test_cloning_propperty_assigned_values(self): - sample_object_1 = self.prototype.clone() - sample_object_2 = self.prototype.clone(value='re-assigned') - self.assertNotEqual(sample_object_1.value, sample_object_2.value) - - -class TestDispatcherFeatures(unittest.TestCase): - - def setUp(self): - self.dispatcher = PrototypeDispatcher() - self.prototype = Prototype() - c = self.prototype.clone() - a = self.prototype.clone(value='a-value', ext_value='E') - b = self.prototype.clone(value='b-value', diff=True) - self.dispatcher.register_object('A', a) - self.dispatcher.register_object('B', b) - self.dispatcher.register_object('C', c) - - def test_batch_retrieving(self): - self.assertEqual(len(self.dispatcher.get_objects()), 3) - - def test_particular_properties_retrieving(self): - self.assertEqual(self.dispatcher.get_objects()['A'].value, 'a-value') - self.assertEqual(self.dispatcher.get_objects()['B'].value, 'b-value') - self.assertEqual(self.dispatcher.get_objects()['C'].value, 'default') - - def test_extended_properties_retrieving(self): - self.assertEqual(self.dispatcher.get_objects()['A'].ext_value, 'E') - self.assertTrue(self.dispatcher.get_objects()['B'].diff) diff --git a/tests/test_proxy.py b/tests/test_proxy.py deleted file mode 100644 index 84c73d059..000000000 --- a/tests/test_proxy.py +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import sys -from time import time -import unittest -from 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) diff --git a/tests/test_publish_subscribe.py b/tests/test_publish_subscribe.py deleted file mode 100644 index f4e001f0c..000000000 --- a/tests/test_publish_subscribe.py +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import unittest -from behavioral.publish_subscribe import Provider, Publisher, Subscriber -try: - from unittest.mock import patch, call -except ImportError: - from mock import patch, call - - -class TestProvider(unittest.TestCase): - """ - Integration tests ~ provider class with as little mocking as possible. - """ - def test_subscriber_shall_be_attachable_to_subscriptions(cls): - subscription = 'sub msg' - pro = Provider() - cls.assertEqual(len(pro.subscribers), 0) - sub = Subscriber('sub name', pro) - sub.subscribe(subscription) - cls.assertEqual(len(pro.subscribers[subscription]), 1) - - def test_subscriber_shall_be_detachable_from_subscriptions(cls): - subscription = 'sub msg' - pro = Provider() - sub = Subscriber('sub name', pro) - sub.subscribe(subscription) - cls.assertEqual(len(pro.subscribers[subscription]), 1) - sub.unsubscribe(subscription) - cls.assertEqual(len(pro.subscribers[subscription]), 0) - - def test_publisher_shall_append_subscription_message_to_queue(cls): - ''' msg_queue ~ Provider.notify(msg) ~ Publisher.publish(msg) ''' - expected_msg = 'expected msg' - pro = Provider() - pub = Publisher(pro) - sub = Subscriber('sub name', pro) - cls.assertEqual(len(pro.msg_queue), 0) - pub.publish(expected_msg) - cls.assertEqual(len(pro.msg_queue), 1) - cls.assertEqual(pro.msg_queue[0], expected_msg) - - def test_provider_shall_update_affected_subscribers_with_published_subscription(cls): - pro = Provider() - pub = Publisher(pro) - sub1 = Subscriber('sub 1 name', pro) - sub1.subscribe('sub 1 msg 1') - sub1.subscribe('sub 1 msg 2') - sub2 = Subscriber('sub 2 name', pro) - sub2.subscribe('sub 2 msg 1') - sub2.subscribe('sub 2 msg 2') - with patch.object(sub1, 'run') as mock_subscriber1_run,\ - patch.object(sub2, 'run') as mock_subscriber2_run: - pro.update() - cls.assertEqual(mock_subscriber1_run.call_count, 0) - cls.assertEqual(mock_subscriber2_run.call_count, 0) - pub.publish('sub 1 msg 1') - pub.publish('sub 1 msg 2') - pub.publish('sub 2 msg 1') - pub.publish('sub 2 msg 2') - with patch.object(sub1, 'run') as mock_subscriber1_run,\ - patch.object(sub2, 'run') as mock_subscriber2_run: - pro.update() - expected_sub1_calls = [call('sub 1 msg 1'), call('sub 1 msg 2')] - mock_subscriber1_run.assert_has_calls(expected_sub1_calls) - expected_sub2_calls = [call('sub 2 msg 1'), call('sub 2 msg 2')] - mock_subscriber2_run.assert_has_calls(expected_sub2_calls) diff --git a/tests/test_setter_injection.py b/tests/test_setter_injection.py deleted file mode 100644 index 5e35d035c..000000000 --- a/tests/test_setter_injection.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import unittest - -from 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/test_state.py b/tests/test_state.py deleted file mode 100644 index 1f5f8c8c0..000000000 --- a/tests/test_state.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import unittest -from 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) diff --git a/tests/test_strategy.py b/tests/test_strategy.py deleted file mode 100644 index 1560386fb..000000000 --- a/tests/test_strategy.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import os -import subprocess -import unittest - - -class StrategyTest(unittest.TestCase): - - def test_print_output(self): - """ - Verifies the print output when strategy.py is executed. - The expected_output is equivalent to the output on the command - line when running 'python strategy.py'. - """ - output = subprocess.check_output(["python", "behavioral/strategy.py"]) - expected_output = os.linesep.join([ - 'Strategy Example 0', - 'Strategy Example 1 from execute 1', - 'Strategy Example 2 from execute 2', - '' - ]) - # byte representation required due to EOF returned subprocess - expected_output_as_bytes = expected_output.encode(encoding='UTF-8') - self.assertEqual(output, expected_output_as_bytes)