diff --git a/.codespellignore b/.codespellignore new file mode 100644 index 000000000..d272a2e1e --- /dev/null +++ b/.codespellignore @@ -0,0 +1,14 @@ +__pycache__ +*.pyc +.idea +*.egg-info/ +.tox/ +env/ +venv/ +.env +.venv +.vscode/ +.python-version +.coverage +build/ +dist/ \ No newline at end of file diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 3778bf3d2..000000000 --- a/.coveragerc +++ /dev/null @@ -1,6 +0,0 @@ -[report] -exclude_lines = - pragma: no cover - # Don't complain if tests don't hit defensive assertion code: - # See: https://stackoverflow.com/a/9212387/3407256 - raise NotImplementedError diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..b565ddcb8 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: + - package-ecosystem: pip + directory: / + schedule: + interval: weekly + versioning-strategy: increase-if-necessary + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly diff --git a/.github/workflows/lint_python.yml b/.github/workflows/lint_python.yml index 637965676..e5ef51b51 100644 --- a/.github/workflows/lint_python.yml +++ b/.github/workflows/lint_python.yml @@ -1,20 +1,62 @@ -name: lint_python -on: [pull_request, push] +name: Python CI + +on: + push: + pull_request: + +permissions: + contents: read + +concurrency: + group: python-ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: - lint_python: - runs-on: ubuntu-latest + quality: + name: Lint, type-check, and test + runs-on: ubuntu-24.04 + timeout-minutes: 15 steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - - run: pip install --upgrade pip - - run: pip install black codespell flake8 isort mypy pytest pyupgrade tox - - run: black --check . - - run: codespell --quiet-level=2 # --ignore-words-list="" --skip="" - - run: flake8 . --count --show-source --statistics - - run: isort --profile black . - - run: tox - - run: pip install -e . - - run: mypy --ignore-missing-imports . || true - - run: pytest . - - run: pytest --doctest-modules . || true - - run: shopt -s globstar && pyupgrade --py36-plus **/*.py + - 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 8b2c28d81..4521242bf 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,15 @@ __pycache__ .idea *.egg-info/ .tox/ -venv +env/ +venv/ +.env +.venv .vscode/ .python-version .coverage +.project +.pydevproject +/.pytest_cache/ +build/ +dist/ diff --git a/.travis.yml b/.travis.yml index ab6ba6bf2..ecd1fb556 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,19 +1,21 @@ os: linux -dist: focal +dist: noble language: python -jobs: - include: - - python: "3.8" - env: TOXENV=py38 - - python: "3.9" - env: TOXENV=py39 +python: + - "3.10" + - "3.11" + - "3.12" + - "3.13" + - "3.14" cache: - pip install: - - pip install codecov tox + - pip install pipenv + - pipenv sync --dev --system + - pip install --no-build-isolation --no-deps -e . script: - tox diff --git a/Makefile b/Makefile index 92ba244aa..5b48aaffb 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ # Usage (line =black line length, path = action path, ignore= exclude folders) # ------ # make pylinter [make pylinter line=88 path=.] -# make pyupgrade +# make lock path := . line := 88 @@ -25,22 +25,11 @@ ifeq ("$(VIRTUAL_ENV)","") exit 1 endif -.PHONY: pyupgrade -pyupgrade: checkvenv -# checks if pip-tools is installed -ifeq ("$(wildcard venv/bin/pip-compile)","") - @echo "Installing Pip-tools..." - @pip install pip-tools -endif - -ifeq ("$(wildcard venv/bin/pip-sync)","") - @echo "Installing Pip-tools..." - @pip install pip-tools -endif - -# pip-tools - # @pip-compile --upgrade requirements-dev.txt - @pip-sync requirements-dev.txt +.PHONY: lock +lock: checkvenv + @command -v pipenv >/dev/null || python -m pip install pipenv + @pipenv lock --dev + @pipenv sync --dev .PHONY: pylinter 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 74b689da4..7624f55f6 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,25 @@ -python-patterns -=============== +# python-patterns A collection of design patterns and idioms in Python. -Current Patterns ----------------- +Remember that each pattern has its own trade-offs. And you need to pay attention more to why you're choosing a certain pattern than to how to implement it. -__Creational Patterns__: +## Creational Patterns + +> Patterns that deal with **object creation** — abstracting and controlling how instances are made. + +```mermaid +graph LR + Client -->|requests object| AbstractFactory + AbstractFactory -->|delegates to| ConcreteFactory + ConcreteFactory -->|produces| Product + + Builder -->|step-by-step| Director + Director -->|returns| BuiltObject + + FactoryMethod -->|subclass decides| ConcreteProduct + Pool -->|reuses| PooledInstance +``` | Pattern | Description | |:-------:| ----------- | @@ -18,85 +31,139 @@ __Creational Patterns__: | [pool](patterns/creational/pool.py) | preinstantiate and maintain a group of instances of the same type | | [prototype](patterns/creational/prototype.py) | use a factory and clones of a prototype for new instances (if instantiation is expensive) | -__Structural Patterns__: - -| Pattern | Description | -|:-------:| ----------- | -| [3-tier](patterns/structural/3-tier.py) | data<->business logic<->presentation separation (strict relationships) | -| [adapter](patterns/structural/adapter.py) | adapt one interface to another using a white-list | -| [bridge](patterns/structural/bridge.py) | a client-provider middleman to soften interface changes | -| [composite](patterns/structural/composite.py) | lets clients treat individual objects and compositions uniformly | -| [decorator](patterns/structural/decorator.py) | wrap functionality with other functionality in order to affect outputs | -| [facade](patterns/structural/facade.py) | use one class as an API to a number of others | -| [flyweight](patterns/structural/flyweight.py) | transparently reuse existing instances of objects with similar/identical state | -| [front_controller](patterns/structural/front_controller.py) | single handler requests coming to the application | -| [mvc](patterns/structural/mvc.py) | model<->view<->controller (non-strict relationships) | -| [proxy](patterns/structural/proxy.py) | an object funnels operations to something else | - -__Behavioral Patterns__: - -| Pattern | Description | -|:-------:| ----------- | -| [chain_of_responsibility](patterns/behavioral/chain_of_responsibility.py) | apply a chain of successive handlers to try and process the data | -| [catalog](patterns/behavioral/catalog.py) | general methods will call different specialized methods based on construction parameter | -| [chaining_method](patterns/behavioral/chaining_method.py) | continue callback next object method | -| [command](patterns/behavioral/command.py) | bundle a command and arguments to call later | -| [iterator](patterns/behavioral/iterator.py) | traverse a container and access the container's elements | -| [iterator](patterns/behavioral/iterator_alt.py) (alt. impl.)| traverse a container and access the container's elements | -| [mediator](patterns/behavioral/mediator.py) | an object that knows how to connect other objects and act as a proxy | -| [memento](patterns/behavioral/memento.py) | generate an opaque token that can be used to go back to a previous state | -| [observer](patterns/behavioral/observer.py) | provide a callback for notification of events/changes to data | -| [publish_subscribe](patterns/behavioral/publish_subscribe.py) | a source syndicates events/data to 0+ registered listeners | -| [registry](patterns/behavioral/registry.py) | keep track of all subclasses of a given class | -| [specification](patterns/behavioral/specification.py) | business rules can be recombined by chaining the business rules together using boolean logic | -| [state](patterns/behavioral/state.py) | logic is organized into a discrete number of potential states and the next state that can be transitioned to | -| [strategy](patterns/behavioral/strategy.py) | selectable operations over the same data | -| [template](patterns/behavioral/template.py) | an object imposes a structure but takes pluggable components | -| [visitor](patterns/behavioral/visitor.py) | invoke a callback for all items of a collection | - -__Design for Testability Patterns__: - -| Pattern | Description | -|:-------:| ----------- | +## Structural Patterns + +> Patterns that define **how classes and objects are composed** to form larger, flexible structures. + +```mermaid +graph TD + Client --> Facade + Facade --> SubsystemA + Facade --> SubsystemB + Facade --> SubsystemC + + Client2 --> Adapter + Adapter --> LegacyService + + Client3 --> Proxy + Proxy -->|controls access to| RealSubject + + Component --> Composite + Composite --> Leaf1 + Composite --> Leaf2 +``` + +| Pattern | Description | +|:-----------------------------------------------------------:|--------------------------------------------------------------------------------| +| [3-tier](patterns/structural/3-tier.py) | data<->business logic<->presentation separation (strict relationships) | +| [adapter](patterns/structural/adapter.py) | adapt one interface to another using a white-list | +| [bridge](patterns/structural/bridge.py) | a client-provider middleman to soften interface changes | +| [composite](patterns/structural/composite.py) | lets clients treat individual objects and compositions uniformly | +| [decorator](patterns/structural/decorator.py) | wrap functionality with other functionality in order to affect outputs | +| [facade](patterns/structural/facade.py) | use one class as an API to a number of others | +| [flyweight](patterns/structural/flyweight.py) | transparently reuse existing instances of objects with similar/identical state | +| [front_controller](patterns/structural/front_controller.py) | single handler requests coming to the application | +| [mvc](patterns/structural/mvc.py) | model<->view<->controller (non-strict relationships) | +| [proxy](patterns/structural/proxy.py) | an object funnels operations to something else | + +## Behavioral Patterns + +> Patterns concerned with **communication and responsibility** between objects. + +```mermaid +graph LR + Sender -->|sends event| Observer1 + Sender -->|sends event| Observer2 + + Request --> Handler1 + Handler1 -->|passes if unhandled| Handler2 + Handler2 -->|passes if unhandled| Handler3 + + Context -->|delegates to| Strategy + Strategy -->|executes| Algorithm + + Originator -->|saves state to| Memento + Caretaker -->|holds| Memento +``` + +| Pattern | Description | +|:-------------------------------------------------------------------------:|--------------------------------------------------------------------------------------------------------------| +| [chain_of_responsibility](patterns/behavioral/chain_of_responsibility.py) | apply a chain of successive handlers to try and process the data | +| [catalog](patterns/behavioral/catalog.py) | general methods will call different specialized methods based on construction parameter | +| [chaining_method](patterns/behavioral/chaining_method.py) | continue callback next object method | +| [command](patterns/behavioral/command.py) | bundle a command and arguments to call later | +| [interpreter](patterns/behavioral/interpreter.py) | define a grammar for a language and use it to interpret statements | +| [iterator](patterns/behavioral/iterator.py) | traverse a container and access the container's elements | +| [iterator](patterns/behavioral/iterator_alt.py) (alt. impl.) | traverse a container and access the container's elements | +| [mediator](patterns/behavioral/mediator.py) | an object that knows how to connect other objects and act as a proxy | +| [memento](patterns/behavioral/memento.py) | generate an opaque token that can be used to go back to a previous state | +| [observer](patterns/behavioral/observer.py) | provide a callback for notification of events/changes to data | +| [publish_subscribe](patterns/behavioral/publish_subscribe.py) | a source syndicates events/data to 0+ registered listeners | +| [registry](patterns/behavioral/registry.py) | keep track of all subclasses of a given class | +| [servant](patterns/behavioral/servant.py) | provide common functionality to a group of classes without using inheritance | +| [specification](patterns/behavioral/specification.py) | business rules can be recombined by chaining the business rules together using boolean logic | +| [state](patterns/behavioral/state.py) | logic is organized into a discrete number of potential states and the next state that can be transitioned to | +| [strategy](patterns/behavioral/strategy.py) | selectable operations over the same data | +| [template](patterns/behavioral/template.py) | an object imposes a structure but takes pluggable components | +| [visitor](patterns/behavioral/visitor.py) | invoke a callback for all items of a collection | + +## Design for Testability Patterns + +| Pattern | Description | +|:--------------------------------------------------------:|------------------------------------| | [dependency_injection](patterns/dependency_injection.py) | 3 variants of dependency injection | -__Fundamental Patterns__: +## Fundamental Patterns -| Pattern | Description | -|:-------:| ----------- | +| Pattern | Description | +|:----------------------------------------------------------------:|-----------------------------------------------------------------------------| | [delegation_pattern](patterns/fundamental/delegation_pattern.py) | an object handles a request by delegating to a second object (the delegate) | -__Others__: +## Others -| Pattern | Description | -|:-------:| ----------- | -| [blackboard](patterns/other/blackboard.py) | architectural model, assemble different sub-system knowledge to build a solution, AI approach - non gang of four pattern | -| [graph_search](patterns/other/graph_search.py) | graphing algorithms - non gang of four pattern | -| [hsm](patterns/other/hsm/hsm.py) | hierarchical state machine - non gang of four pattern | +| Pattern | Description | +|:----------------------------------------------:|--------------------------------------------------------------------------------------------------------------------------| +| [blackboard](patterns/other/blackboard.py) | architectural model, assemble different sub-system knowledge to build a solution, AI approach - non gang of four pattern | +| [graph_search](patterns/other/graph_search.py) | graphing algorithms - non gang of four pattern | +| [hsm](patterns/other/hsm/hsm.py) | hierarchical state machine - non gang of four pattern | +## 🚫 Anti-Patterns -Videos ------- -[Design Patterns in Python by Peter Ullrich](https://www.youtube.com/watch?v=bsyjSW46TDg) +This section lists some common design patterns that are **not recommended** in Python and explains why. -[Sebastian Buczyński - Why you don't need design patterns in Python?](https://www.youtube.com/watch?v=G5OeYHCJuv0) +### 🧱 Singleton +**Why not:** +- Python modules are already singletons — every module is imported only once. +- Explicit singleton classes add unnecessary complexity. +- Better alternatives: use module-level variables or dependency injection. -[You Don't Need That!](https://www.youtube.com/watch?v=imW-trt0i9I) +### 🌀 God Object +**Why not:** +- Centralizes too much logic in a single class. +- Makes code harder to test and maintain. +- Better alternative: split functionality into smaller, cohesive classes. -[Pluggable Libs Through Design Patterns](https://www.youtube.com/watch?v=PfgEU3W0kyU) +### 🔁 Inheritance overuse +**Why not:** +- Deep inheritance trees make code brittle. +- Prefer composition and delegation. +- “Favor composition over inheritance.” +## Videos + +* [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 -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 module level description in form of a docstring with links to corresponding references or other useful information. Add "Examples in Python ecosystem" section if you know some. It shows how patterns could be applied to real-world problems. - -[facade.py](patterns/structural/facade.py) has a good example of detailed description, -but sometimes the shorter one as in [template.py](patterns/behavioral/template.py) would suffice. +[facade.py](patterns/structural/facade.py) has a good example of detailed description, but sometimes the shorter one as in [template.py](patterns/behavioral/template.py) would suffice. ##### 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. @@ -105,10 +172,13 @@ To see Python 2 compatible versions of some patterns please check-out the [legac When everything else is done - update corresponding part of README. ##### Travis CI -Please run `tox` or `tox -e ci37` before submitting a patch to be sure your changes will pass CI. +Please run the following before submitting a patch: +- If you have a bash-compatible shell, use `./lint.sh` to run formatting, + linting, type checking, and tests. -You can also run `flake8` or `pytest` commands manually. Examples can be found in `tox.ini`. +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 which may include reproducing bug reports or asking for vital information, such as version numbers or reproduction instructions. If you would like to start triaging issues, one easy way to get started is to [subscribe to python-patterns on CodeTriage](https://www.codetriage.com/faif/python-patterns). +You can triage issues and pull requests on [CodeTriage](https://www.codetriage.com/faif/python-patterns). diff --git a/config_backup/.coveragerc b/config_backup/.coveragerc new file mode 100644 index 000000000..98306ea93 --- /dev/null +++ b/config_backup/.coveragerc @@ -0,0 +1,25 @@ +[run] +branch = True + +[report] +; Regexes for lines to exclude from consideration +exclude_also = + ; Don't complain about missing debug-only code: + def __repr__ + if self\.debug + + ; Don't complain if tests don't hit defensive assertion code: + raise AssertionError + raise NotImplementedError + + ; Don't complain if non-runnable code isn't run: + if 0: + if __name__ == .__main__.: + + ; Don't complain about abstract methods, they aren't run: + @(abc\.)?abstractmethod + +ignore_errors = True + +[html] +directory = coverage_html_report \ No newline at end of file diff --git a/setup.cfg b/config_backup/setup.cfg similarity index 91% rename from setup.cfg rename to config_backup/setup.cfg index eb556c0a2..e109555b4 100644 --- a/setup.cfg +++ b/config_backup/setup.cfg @@ -9,5 +9,5 @@ filterwarnings = ignore:.*test class 'TestRunner'.*:Warning [mypy] -python_version = 3.8 +python_version = 3.12 ignore_missing_imports = True diff --git a/tox.ini b/config_backup/tox.ini similarity index 62% rename from tox.ini rename to config_backup/tox.ini index 168e2c9d7..f0d8b6c1f 100644 --- a/tox.ini +++ b/config_backup/tox.ini @@ -1,18 +1,24 @@ [tox] -envlist = py38,py39,cov-report +envlist = py,cov-report skip_missing_interpreters = true - +usedevelop = true [testenv] setenv = COVERAGE_FILE = .coverage.{envname} deps = - -r requirements-dev.txt + pipenv +commands_pre = + pipenv sync --dev --system +allowlist_externals = + pytest + flake8 + mypy commands = - flake8 . --exclude="./.*, venv" + flake8 --exclude="venv/,.tox/" patterns/ ; `randomly-seed` option from `pytest-randomly` helps with deterministic outputs for examples like `other/blackboard.py` pytest --randomly-seed=1234 --doctest-modules patterns/ - pytest -s -vv --cov={envsitepackagesdir}/patterns --log-level=INFO tests/ + pytest -s -vv --cov=patterns/ --log-level=INFO tests/ [testenv:cov-report] diff --git a/lint.sh b/lint.sh new file mode 100755 index 000000000..3139587ad --- /dev/null +++ b/lint.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +set -euo pipefail + +project_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +venv_dir="${project_dir}/.venv" +python_bin="${PYTHON:-python3}" + +if [[ ! -x "${venv_dir}/bin/python" ]]; then + "${python_bin}" -m venv "${venv_dir}" + "${venv_dir}/bin/python" -m pip install --upgrade pip +fi + +venv_python="${venv_dir}/bin/python" +venv_bin="${venv_dir}/bin" + +"${venv_python}" -m pip install pipenv +PIPENV_IGNORE_VIRTUALENVS=1 \ + PIPENV_VENV_IN_PROJECT=1 \ + "${venv_bin}/pipenv" sync --dev +"${venv_python}" -m pip install --no-build-isolation --no-deps -e "${project_dir}" + +source_dir="${project_dir}/patterns" +tests_dir="${project_dir}/tests" + +"${venv_bin}/codespell" --quiet-level=2 "${source_dir}" "${tests_dir}" "${project_dir}/README.md" +"${venv_bin}/flake8" \ + "${source_dir}" \ + "${tests_dir}" \ + --max-line-length=120 \ + --extend-ignore=E266,E731,W503 \ + --count \ + --show-source \ + --statistics +"${venv_bin}/isort" --profile black --check-only "${source_dir}" "${tests_dir}" +"${venv_bin}/black" --check "${source_dir}" "${tests_dir}" +"${venv_bin}/mypy" "${source_dir}" +"${venv_bin}/pytest" "${tests_dir}" "${source_dir}" diff --git a/patterns/behavioral/catalog.py b/patterns/behavioral/catalog.py index 7c91aa7d6..4074c1d21 100644 --- a/patterns/behavioral/catalog.py +++ b/patterns/behavioral/catalog.py @@ -1,19 +1,15 @@ """ -A class that uses different static function depending of a parameter passed in -init. Note the use of a single dictionary instead of multiple conditions +A class that uses different static functions depending on a parameter passed +during initialization. Uses a single dictionary instead of multiple conditions. """ __author__ = "Ibrahim Diop " class Catalog: - """catalog of multiple static methods that are executed depending on an init - - parameter - """ + """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 @@ -29,26 +25,24 @@ def __init__(self, param: str) -> None: raise ValueError(f"Invalid Value for Param: {param}") @staticmethod - def _static_method_1() -> None: - print("executed method 1!") + def _static_method_1() -> str: + return "executed method 1!" @staticmethod - def _static_method_2() -> None: - print("executed method 2!") + def _static_method_2() -> str: + return "executed method 2!" - def main_method(self) -> None: + def main_method(self) -> str: """will execute either _static_method_1 or _static_method_2 depending on self.param value """ - self._static_method_choices[self.param]() + return self._static_method_choices[self.param]() # Alternative implementation for different levels of methods class CatalogInstance: - """catalog of multiple methods that are executed depending on an init - parameter """ @@ -61,30 +55,28 @@ def __init__(self, param: str) -> None: else: raise ValueError(f"Invalid Value for Param: {param}") - def _instance_method_1(self) -> None: - print(f"Value {self.x1}") + def _instance_method_1(self) -> str: + return f"Value {self.x1}" - def _instance_method_2(self) -> None: - print(f"Value {self.x2}") + def _instance_method_2(self) -> str: + return f"Value {self.x2}" _instance_method_choices = { "param_value_1": _instance_method_1, "param_value_2": _instance_method_2, } - def main_method(self) -> None: + def main_method(self) -> str: """will execute either _instance_method_1 or _instance_method_2 depending on self.param value """ - self._instance_method_choices[self.param].__get__(self)() # type: ignore + 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 """ @@ -99,31 +91,29 @@ def __init__(self, param: str) -> None: raise ValueError(f"Invalid Value for Param: {param}") @classmethod - def _class_method_1(cls) -> None: - print(f"Value {cls.x1}") + def _class_method_1(cls) -> str: + return f"Value {cls.x1}" @classmethod - def _class_method_2(cls) -> None: - print(f"Value {cls.x2}") + def _class_method_2(cls) -> str: + return f"Value {cls.x2}" _class_method_choices = { "param_value_1": _class_method_1, "param_value_2": _class_method_2, } - def main_method(self): + def main_method(self) -> str: """will execute either _class_method_1 or _class_method_2 depending on self.param value """ - self._class_method_choices[self.param].__get__(None, self.__class__)() # type: ignore + 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 """ @@ -135,25 +125,25 @@ def __init__(self, param: str) -> None: raise ValueError(f"Invalid Value for Param: {param}") @staticmethod - def _static_method_1() -> None: - print("executed method 1!") + def _static_method_1() -> str: + return "executed method 1!" @staticmethod - def _static_method_2() -> None: - print("executed method 2!") + def _static_method_2() -> str: + return "executed method 2!" _static_method_choices = { "param_value_1": _static_method_1, "param_value_2": _static_method_2, } - def main_method(self) -> None: + def main_method(self) -> str: """will execute either _static_method_1 or _static_method_2 depending on self.param value """ - self._static_method_choices[self.param].__get__(None, self.__class__)() # type: ignore + return self._static_method_choices[self.param].__get__(None, self.__class__)() # type: ignore # type ignore reason: https://github.com/python/mypy/issues/10206 @@ -161,19 +151,19 @@ def main(): """ >>> test = Catalog('param_value_2') >>> test.main_method() - executed method 2! + 'executed method 2!' >>> test = CatalogInstance('param_value_1') >>> test.main_method() - Value x1 + 'Value x1' >>> test = CatalogClass('param_value_2') >>> test.main_method() - Value x2 + 'Value x2' >>> test = CatalogStatic('param_value_1') >>> test.main_method() - executed method 1! + 'executed method 1!' """ diff --git a/patterns/behavioral/chain_of_responsibility.py b/patterns/behavioral/chain_of_responsibility.py index 9d46c4a8d..a44a9210e 100644 --- a/patterns/behavioral/chain_of_responsibility.py +++ b/patterns/behavioral/chain_of_responsibility.py @@ -14,12 +14,16 @@ As a variation some receivers may be capable of sending requests out in several directions, forming a `tree of responsibility`. +*Examples in Python ecosystem: +Django Middleware: https://docs.djangoproject.com/en/stable/topics/http/middleware/ +The middleware components act as a chain where each processes the request/response. + *TL;DR Allow a request to pass down a chain of receivers until it is handled. """ from abc import ABC, abstractmethod -from typing import Optional, Tuple +from typing import Optional class Handler(ABC): @@ -39,7 +43,7 @@ def handle(self, request: int) -> None: self.successor.handle(request) @abstractmethod - def check_range(self, request: int) -> Optional[bool]: + def check_range(self, request: int) -> bool | None: """Compare passed value to predefined interval""" @@ -49,7 +53,7 @@ class ConcreteHandler0(Handler): """ @staticmethod - def check_range(request: int) -> Optional[bool]: + def check_range(request: int) -> bool | None: if 0 <= request < 10: print(f"request {request} handled in handler 0") return True @@ -61,7 +65,7 @@ class ConcreteHandler1(Handler): start, end = 10, 20 - def check_range(self, request: int) -> Optional[bool]: + def check_range(self, request: int) -> bool | None: if self.start <= request < self.end: print(f"request {request} handled in handler 1") return True @@ -71,7 +75,7 @@ def check_range(self, request: int) -> Optional[bool]: class ConcreteHandler2(Handler): """... With helper methods.""" - def check_range(self, request: int) -> Optional[bool]: + 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") @@ -79,13 +83,13 @@ def check_range(self, request: int) -> Optional[bool]: return None @staticmethod - def get_interval_from_db() -> Tuple[int, int]: + def get_interval_from_db() -> tuple[int, int]: return (20, 30) class FallbackHandler(Handler): @staticmethod - def check_range(request: int) -> Optional[bool]: + def check_range(request: int) -> bool | None: print(f"end of chain, no handler for {request}") return False diff --git a/patterns/behavioral/chaining_method.py b/patterns/behavioral/chaining_method.py index 195bfa58f..26f110181 100644 --- a/patterns/behavioral/chaining_method.py +++ b/patterns/behavioral/chaining_method.py @@ -2,13 +2,12 @@ class Person: - def __init__(self, name: str, action: Action) -> None: + def __init__(self, name: str) -> None: self.name = name - self.action = action - def do_action(self) -> Action: - print(self.name, self.action.name, end=" ") - return self.action + def do_action(self, action: Action) -> Action: + print(self.name, action.name, end=" ") + return action class Action: @@ -26,8 +25,8 @@ def stop(self) -> None: def main(): """ >>> move = Action('move') - >>> person = Person('Jack', move) - >>> person.do_action().amount('5m').stop() + >>> person = Person('Jack') + >>> person.do_action(move).amount('5m').stop() Jack move 5m then stop """ diff --git a/patterns/behavioral/command.py b/patterns/behavioral/command.py index a88ea8be7..d0c3944e4 100644 --- a/patterns/behavioral/command.py +++ b/patterns/behavioral/command.py @@ -20,8 +20,6 @@ https://docs.djangoproject.com/en/2.1/ref/request-response/#httprequest-objects """ -from typing import List, Union - class HideFileCommand: """ @@ -30,7 +28,7 @@ class HideFileCommand: def __init__(self) -> None: # an array of files hidden, to undo them as needed - self._hidden_files: List[str] = [] + self._hidden_files: list[str] = [] def execute(self, filename: str) -> None: print(f"hiding {filename}") @@ -48,7 +46,7 @@ class DeleteFileCommand: def __init__(self) -> None: # an array of deleted files, to undo them as needed - self._deleted_files: List[str] = [] + self._deleted_files: list[str] = [] def execute(self, filename: str) -> None: print(f"deleting {filename}") @@ -64,7 +62,7 @@ class MenuItem: The invoker class. Here it is items in a menu. """ - def __init__(self, command: Union[HideFileCommand, DeleteFileCommand]) -> None: + def __init__(self, command: HideFileCommand | DeleteFileCommand) -> None: self._command = command def on_do_press(self, filename: str) -> None: diff --git a/patterns/behavioral/iterator.py b/patterns/behavioral/iterator.py index 401624613..3ed4043b9 100644 --- a/patterns/behavioral/iterator.py +++ b/patterns/behavioral/iterator.py @@ -7,7 +7,7 @@ """ -def count_to(count): +def count_to(count: int): """Counts by word numbers, up to a maximum of five""" numbers = ["one", "two", "three", "four", "five"] yield from numbers[:count] diff --git a/patterns/behavioral/iterator_alt.py b/patterns/behavioral/iterator_alt.py index d6fb0df9e..a2a71d823 100644 --- a/patterns/behavioral/iterator_alt.py +++ b/patterns/behavioral/iterator_alt.py @@ -4,6 +4,7 @@ *TL;DR Traverses a container and accesses the container's elements. """ + from __future__ import annotations diff --git a/patterns/behavioral/mediator.py b/patterns/behavioral/mediator.py index e4b3c34ab..122852b77 100644 --- a/patterns/behavioral/mediator.py +++ b/patterns/behavioral/mediator.py @@ -14,8 +14,8 @@ class ChatRoom: """Mediator class""" - def display_message(self, user: User, message: str) -> None: - print(f"[{user} says]: {message}") + def display_message(self, user: User, message: str) -> str: + return f"[{user} says]: {message}" class User: @@ -25,8 +25,8 @@ def __init__(self, name: str) -> None: self.name = name self.chat_room = ChatRoom() - def say(self, message: str) -> None: - self.chat_room.display_message(self, message) + def say(self, message: str) -> str: + return self.chat_room.display_message(self, message) def __str__(self) -> str: return self.name @@ -39,11 +39,11 @@ def main(): >>> ethan = User('Ethan') >>> molly.say("Hi Team! Meeting at 3 PM today.") - [Molly says]: Hi Team! Meeting at 3 PM today. + '[Molly says]: Hi Team! Meeting at 3 PM today.' >>> mark.say("Roger that!") - [Mark says]: Roger that! + '[Mark says]: Roger that!' >>> ethan.say("Alright.") - [Ethan says]: Alright. + '[Ethan says]: Alright.' """ diff --git a/patterns/behavioral/memento.py b/patterns/behavioral/memento.py index e1d42fc24..a14ea8a0d 100644 --- a/patterns/behavioral/memento.py +++ b/patterns/behavioral/memento.py @@ -5,14 +5,15 @@ Provides the ability to restore an object to its previous state. """ -from typing import Callable, List +from collections.abc import Callable from copy import copy, deepcopy +from typing import Any -def memento(obj, deep=False): +def memento(obj: Any, deep: bool = False) -> Callable: state = deepcopy(obj.__dict__) if deep else copy(obj.__dict__) - def restore(): + def restore() -> None: obj.__dict__.clear() obj.__dict__.update(state) @@ -26,31 +27,32 @@ class Transaction: """ deep = False - states: List[Callable[[], None]] = [] + states: list[Callable[[], None]] = [] - def __init__(self, deep, *targets): + def __init__(self, deep: bool, *targets: Any) -> None: self.deep = deep self.targets = targets self.commit() - def commit(self): + def commit(self) -> None: self.states = [memento(target, self.deep) for target in self.targets] - def rollback(self): + def rollback(self) -> None: for a_state in self.states: a_state() class Transactional: """Adds transactional semantics to methods. Methods decorated with + @Transactional will roll back to entry-state upon exceptions. - @Transactional will rollback to entry-state upon exceptions. + :param method: The function to be decorated. """ - def __init__(self, method): + def __init__(self, method: Callable) -> None: self.method = method - def __get__(self, obj, T): + def __get__(self, obj: Any, T: type) -> Callable: """ A decorator that makes a function transactional. @@ -69,18 +71,18 @@ def transaction(*args, **kwargs): class NumObj: - def __init__(self, value): + def __init__(self, value: int) -> None: self.value = value - def __repr__(self): + def __repr__(self) -> str: return f"<{self.__class__.__name__}: {self.value!r}>" - def increment(self): + def increment(self) -> None: self.value += 1 @Transactional - def do_stuff(self): - self.value = "1111" # <- invalid value + def do_stuff(self) -> None: + self.__dict__["value"] = "1111" # <- intentionally invalid value self.increment() # <- will fail and rollback diff --git a/patterns/behavioral/observer.py b/patterns/behavioral/observer.py index 03d970ad6..9ea97bfc0 100644 --- a/patterns/behavioral/observer.py +++ b/patterns/behavioral/observer.py @@ -9,34 +9,59 @@ Flask Signals: https://flask.palletsprojects.com/en/1.1.x/signals/ """ -from __future__ import annotations +# observer.py -from contextlib import suppress -from typing import Protocol +from __future__ import annotations -# define a generic observer type -class Observer(Protocol): +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: - self._observers: list[Observer] = [] + """ + 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: - with suppress(ValueError): + """ + Detach an observer from the subject. + + Args: + observer (Observer): The observer instance to detach. + """ + try: self._observers.remove(observer) + except ValueError: + pass - def notify(self, modifier: Observer | None = None) -> None: + def notify(self) -> None: + """ + Notify all attached observers by calling their update method. + """ for observer in self._observers: - if modifier != observer: - observer.update(self) + observer.update(self) class Data(Subject): diff --git a/patterns/behavioral/publish_subscribe.py b/patterns/behavioral/publish_subscribe.py index 760d8e7b4..91e74ab67 100644 --- a/patterns/behavioral/publish_subscribe.py +++ b/patterns/behavioral/publish_subscribe.py @@ -4,22 +4,24 @@ Author: https://github.com/HanWenfang """ +from __future__ import annotations + class Provider: - def __init__(self): - self.msg_queue = [] - self.subscribers = {} + def __init__(self) -> None: + self.msg_queue: list[str] = [] + self.subscribers: dict[str, list[Subscriber]] = {} - def notify(self, msg): + def notify(self, msg: str) -> None: self.msg_queue.append(msg) - def subscribe(self, msg, subscriber): + def subscribe(self, msg: str, subscriber: Subscriber) -> None: self.subscribers.setdefault(msg, []).append(subscriber) - def unsubscribe(self, msg, subscriber): + def unsubscribe(self, msg: str, subscriber: Subscriber) -> None: self.subscribers[msg].remove(subscriber) - def update(self): + def update(self) -> None: for msg in self.msg_queue: for sub in self.subscribers.get(msg, []): sub.run(msg) @@ -27,25 +29,25 @@ def update(self): class Publisher: - def __init__(self, msg_center): + def __init__(self, msg_center: Provider) -> None: self.provider = msg_center - def publish(self, msg): + def publish(self, msg: str) -> None: self.provider.notify(msg) class Subscriber: - def __init__(self, name, msg_center): + def __init__(self, name: str, msg_center: Provider) -> None: self.name = name self.provider = msg_center - def subscribe(self, msg): + def subscribe(self, msg: str) -> None: self.provider.subscribe(msg, self) - def unsubscribe(self, msg): + def unsubscribe(self, msg: str) -> None: self.provider.unsubscribe(msg, self) - def run(self, msg): + def run(self, msg: str) -> None: print(f"{self.name} got {msg}") diff --git a/patterns/behavioral/registry.py b/patterns/behavioral/registry.py index d44a992e4..1288c1f6a 100644 --- a/patterns/behavioral/registry.py +++ b/patterns/behavioral/registry.py @@ -1,9 +1,5 @@ -from typing import Dict - - class RegistryHolder(type): - - REGISTRY: Dict[str, "RegistryHolder"] = {} + REGISTRY: dict[str, "RegistryHolder"] = {} def __new__(cls, name, bases, attrs): new_cls = type.__new__(cls, name, bases, attrs) diff --git a/patterns/behavioral/servant.py b/patterns/behavioral/servant.py new file mode 100644 index 000000000..776c4126f --- /dev/null +++ b/patterns/behavioral/servant.py @@ -0,0 +1,131 @@ +""" +Implementation of the Servant design pattern. + +The Servant design pattern is a behavioral pattern used to offer functionality +to a group of classes without requiring them to inherit from a base class. + +This pattern involves creating a Servant class that provides certain services +or functionalities. These services are used by other classes which do not need +to be related through a common parent class. It is particularly useful in +scenarios where adding the desired functionality through inheritance is impractical +or would lead to a rigid class hierarchy. + +This pattern is characterized by the following: + +- A Servant class that provides specific services or actions. +- Client classes that need these services, but do not derive from the Servant class. +- The use of the Servant class by the client classes to perform actions on their behalf. + +References: +- https://en.wikipedia.org/wiki/Servant_(design_pattern) +""" + +import math + + +class Position: + """Representation of a 2D position with x and y coordinates.""" + + def __init__(self, x, y): + self.x = x + self.y = y + + +class Circle: + """Representation of a circle defined by a radius and a position.""" + + def __init__(self, radius, position: Position): + self.radius = radius + self.position = position + + +class Rectangle: + """Representation of a rectangle defined by width, height, and a position.""" + + def __init__(self, width, height, position: Position): + self.width = width + self.height = height + self.position = position + + +class GeometryTools: + """ + Servant class providing geometry-related services, including area and + perimeter calculations and position updates. + """ + + @staticmethod + def calculate_area(shape): + """ + Calculate the area of a given shape. + + Args: + shape: The geometric shape whose area is to be calculated. + + Returns: + The area of the shape. + + Raises: + ValueError: If the shape type is unsupported. + """ + if isinstance(shape, Circle): + return math.pi * shape.radius**2 + elif isinstance(shape, Rectangle): + return shape.width * shape.height + else: + raise ValueError("Unsupported shape type") + + @staticmethod + def calculate_perimeter(shape): + """ + Calculate the perimeter of a given shape. + + Args: + shape: The geometric shape whose perimeter is to be calculated. + + Returns: + The perimeter of the shape. + + Raises: + ValueError: If the shape type is unsupported. + """ + if isinstance(shape, Circle): + return 2 * math.pi * shape.radius + elif isinstance(shape, Rectangle): + return 2 * (shape.width + shape.height) + else: + raise ValueError("Unsupported shape type") + + @staticmethod + def move_to(shape, new_position: Position): + """ + Move a given shape to a new position. + + Args: + shape: The geometric shape to be moved. + new_position: The new position to move the shape to. + """ + shape.position = new_position + print(f"Moved to ({shape.position.x}, {shape.position.y})") + + +def main(): + """ + >>> servant = GeometryTools() + >>> circle = Circle(5, Position(0, 0)) + >>> rectangle = Rectangle(3, 4, Position(0, 0)) + >>> servant.calculate_area(circle) + 78.53981633974483 + >>> servant.calculate_perimeter(rectangle) + 14 + >>> servant.move_to(circle, Position(3, 4)) + Moved to (3, 4) + >>> servant.move_to(rectangle, Position(5, 6)) + Moved to (5, 6) + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/patterns/behavioral/specification.py b/patterns/behavioral/specification.py index 303ee513b..10d226894 100644 --- a/patterns/behavioral/specification.py +++ b/patterns/behavioral/specification.py @@ -6,6 +6,7 @@ """ from abc import abstractmethod +from typing import Union class Specification: @@ -28,22 +29,22 @@ class CompositeSpecification(Specification): def is_satisfied_by(self, candidate): pass - def and_specification(self, candidate): + def and_specification(self, candidate: "Specification") -> "AndSpecification": return AndSpecification(self, candidate) - def or_specification(self, candidate): + def or_specification(self, candidate: "Specification") -> "OrSpecification": return OrSpecification(self, candidate) - def not_specification(self): + def not_specification(self) -> "NotSpecification": return NotSpecification(self) class AndSpecification(CompositeSpecification): - def __init__(self, one, other): + def __init__(self, one: "Specification", other: "Specification") -> None: self._one: Specification = one self._other: Specification = other - def is_satisfied_by(self, candidate): + def is_satisfied_by(self, candidate: Union["User", str]) -> bool: return bool( self._one.is_satisfied_by(candidate) and self._other.is_satisfied_by(candidate) @@ -51,11 +52,11 @@ def is_satisfied_by(self, candidate): class OrSpecification(CompositeSpecification): - def __init__(self, one, other): + def __init__(self, one: "Specification", other: "Specification") -> None: self._one: Specification = one self._other: Specification = other - def is_satisfied_by(self, candidate): + def is_satisfied_by(self, candidate: Union["User", str]): return bool( self._one.is_satisfied_by(candidate) or self._other.is_satisfied_by(candidate) @@ -63,25 +64,25 @@ def is_satisfied_by(self, candidate): class NotSpecification(CompositeSpecification): - def __init__(self, wrapped): + def __init__(self, wrapped: "Specification"): self._wrapped: Specification = wrapped - def is_satisfied_by(self, candidate): + def is_satisfied_by(self, candidate: Union["User", str]): return bool(not self._wrapped.is_satisfied_by(candidate)) class User: - def __init__(self, super_user=False): + def __init__(self, super_user: bool = False) -> None: self.super_user = super_user class UserSpecification(CompositeSpecification): - def is_satisfied_by(self, candidate): + def is_satisfied_by(self, candidate: Union["User", str]) -> bool: return isinstance(candidate, User) class SuperUserSpecification(CompositeSpecification): - def is_satisfied_by(self, candidate): + def is_satisfied_by(self, candidate: "User") -> bool: return getattr(candidate, "super_user", False) diff --git a/patterns/behavioral/state.py b/patterns/behavioral/state.py index 423b749e5..8d792de93 100644 --- a/patterns/behavioral/state.py +++ b/patterns/behavioral/state.py @@ -8,12 +8,20 @@ Implements state transitions by invoking methods from the pattern's superclass. """ +from __future__ import annotations -class State: +class State: """Base state. This is to share functionality""" - def scan(self): + 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): @@ -22,43 +30,42 @@ def scan(self): class AmState(State): - def __init__(self, radio): + def __init__(self, radio: Radio) -> None: self.radio = radio self.stations = ["1250", "1380", "1510"] self.pos = 0 self.name = "AM" - def toggle_amfm(self): + def toggle_amfm(self) -> None: print("Switching to FM") self.radio.state = self.radio.fmstate class FmState(State): - def __init__(self, radio): + def __init__(self, radio: Radio) -> None: self.radio = radio self.stations = ["81.3", "89.1", "103.9"] self.pos = 0 self.name = "FM" - def toggle_amfm(self): + def toggle_amfm(self) -> None: print("Switching to AM") self.radio.state = self.radio.amstate class Radio: - """A radio. It has a scan button, and an AM/FM toggle switch.""" - def __init__(self): + def __init__(self) -> None: """We have an AM state and an FM state""" self.amstate = AmState(self) self.fmstate = FmState(self) - self.state = self.amstate + self.state: State = self.amstate - def toggle_amfm(self): + def toggle_amfm(self) -> None: self.state.toggle_amfm() - def scan(self): + def scan(self) -> None: self.state.scan() diff --git a/patterns/behavioral/strategy.py b/patterns/behavioral/strategy.py index 88862fa4c..13b1c0f5d 100644 --- a/patterns/behavioral/strategy.py +++ b/patterns/behavioral/strategy.py @@ -7,15 +7,17 @@ Enables selecting an algorithm at runtime. """ - from __future__ import annotations -from typing import Callable +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: Callable) -> bool: + def validate(obj: Order, value: DiscountStrategy) -> bool: try: if obj.price - value(obj) < 0: raise ValueError( @@ -30,20 +32,24 @@ def validate(obj: Order, value: Callable) -> bool: def __set_name__(self, owner, name: str) -> None: self.private_name = f"_{name}" - def __set__(self, obj: Order, value: Callable = None) -> None: - if value and self.validate(obj, value): + 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: object, objtype: type = 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: Callable = None) -> None: + def __init__( + self, price: float, discount_strategy: DiscountStrategy | None = None + ) -> None: self.price: float = price self.discount_strategy = discount_strategy @@ -56,7 +62,8 @@ def apply_discount(self) -> float: return self.price - discount def __repr__(self) -> str: - return f"" + strategy = getattr(self.discount_strategy, "__name__", None) + return f"" def ten_percent_discount(order: Order) -> float: diff --git a/patterns/behavioral/template.py b/patterns/behavioral/template.py index d2d831742..76fc136bc 100644 --- a/patterns/behavioral/template.py +++ b/patterns/behavioral/template.py @@ -10,28 +10,28 @@ """ -def get_text(): +def get_text() -> str: return "plain-text" -def get_pdf(): +def get_pdf() -> str: return "pdf" -def get_csv(): +def get_csv() -> str: return "csv" -def convert_to_text(data): +def convert_to_text(data: str) -> str: print("[CONVERT]") return f"{data} as text" -def saver(): +def saver() -> None: print("[SAVE]") -def template_function(getter, converter=False, to_save=False): +def template_function(getter, converter=False, to_save=False) -> None: data = getter() print(f"Got `{data}`") diff --git a/patterns/behavioral/visitor.py b/patterns/behavioral/visitor.py index 00d95248d..ab2f5f713 100644 --- a/patterns/behavioral/visitor.py +++ b/patterns/behavioral/visitor.py @@ -33,7 +33,7 @@ class C(A, B): class Visitor: - def visit(self, node, *args, **kwargs): + def visit(self, node: Node, *args, **kwargs) -> str: meth = None for cls in node.__class__.__mro__: meth_name = "visit_" + cls.__name__ @@ -45,11 +45,11 @@ def visit(self, node, *args, **kwargs): meth = self.generic_visit return meth(node, *args, **kwargs) - def generic_visit(self, node, *args, **kwargs): - print("generic_visit " + node.__class__.__name__) + def generic_visit(self, node: Node, *args, **kwargs) -> str: + return "generic_visit " + node.__class__.__name__ - def visit_B(self, node, *args, **kwargs): - print("visit_B " + node.__class__.__name__) + def visit_B(self, node: Node, *args, **kwargs) -> str: + return "visit_B " + node.__class__.__name__ def main(): @@ -58,13 +58,13 @@ def main(): >>> visitor = Visitor() >>> visitor.visit(a) - generic_visit A + 'generic_visit A' >>> visitor.visit(b) - visit_B B + 'visit_B B' >>> visitor.visit(c) - visit_B C + 'visit_B C' """ diff --git a/patterns/creational/abstract_factory.py b/patterns/creational/abstract_factory.py index 3c221a364..2362d8fef 100644 --- a/patterns/creational/abstract_factory.py +++ b/patterns/creational/abstract_factory.py @@ -31,7 +31,6 @@ """ import random -from typing import Type class Pet: @@ -62,10 +61,9 @@ def __str__(self) -> str: class PetShop: - """A pet shop""" - def __init__(self, animal_factory: Type[Pet]) -> None: + def __init__(self, animal_factory: type[Pet]) -> None: """pet_factory is our abstract factory. We can set it at will.""" self.pet_factory = animal_factory @@ -78,14 +76,6 @@ def buy_pet(self, name: str) -> Pet: return pet -# Additional factories: - -# Create a random animal -def random_animal(name: str) -> Pet: - """Let's be dynamic!""" - return random.choice([Dog, Cat])(name) - - # Show pets with various factories def main() -> None: """ @@ -95,27 +85,14 @@ def main() -> None: Here is your lovely Cat >>> pet.speak() meow - - # A shop that sells random animals - >>> shop = PetShop(random_animal) - >>> for name in ["Max", "Jack", "Buddy"]: - ... pet = shop.buy_pet(name) - ... pet.speak() - ... print("=" * 20) - Here is your lovely Cat - meow - ==================== - Here is your lovely Dog - woof - ==================== - Here is your lovely Dog - woof - ==================== """ if __name__ == "__main__": - random.seed(1234) # for deterministic doctest outputs + animals = [Dog, Cat] + random_animal: type[Pet] = random.choice(animals) + + shop = PetShop(random_animal) import doctest doctest.testmod() diff --git a/patterns/creational/borg.py b/patterns/creational/borg.py index 3ddc8c1d5..7a8073e69 100644 --- a/patterns/creational/borg.py +++ b/patterns/creational/borg.py @@ -13,7 +13,7 @@ its own dictionary, but the Borg pattern modifies this so that all instances have the same dictionary. In this example, the __shared_state attribute will be the dictionary -shared between all instances, and this is ensured by assigining +shared between all instances, and this is ensured by assigning __shared_state to the __dict__ variable when initializing a new instance (i.e., in the __init__ method). Other attributes are usually added to the instance's attribute dictionary, but, since the attribute @@ -32,18 +32,17 @@ *TL;DR Provides singleton-like behavior sharing state between instances. """ -from typing import Dict class Borg: - _shared_state: Dict[str, str] = {} + _shared_state: dict[str, str] = {} - def __init__(self): + def __init__(self) -> None: self.__dict__ = self._shared_state class YourBorg(Borg): - def __init__(self, state=None): + def __init__(self, state: str | None = None) -> None: super().__init__() if state: self.state = state @@ -52,7 +51,7 @@ def __init__(self, state=None): if not hasattr(self, "state"): self.state = "Init" - def __str__(self): + def __str__(self) -> str: return self.state diff --git a/patterns/creational/builder.py b/patterns/creational/builder.py index b1f463ee8..c5253ad30 100644 --- a/patterns/creational/builder.py +++ b/patterns/creational/builder.py @@ -1,13 +1,12 @@ """ -*What is this pattern about? +What is this pattern about? It decouples the creation of a complex object and its representation, so that the same process can be reused to build objects from the same family. This is useful when you must separate the specification of an object from its actual representation (generally for abstraction). -*What does this example do? - +What does this example do? The first example achieves this by using an abstract base class for a building, where the initializer (__init__ method) specifies the steps needed, and the concrete subclasses implement these steps. @@ -22,19 +21,20 @@ class for a building, where the initializer (__init__ method) specifies the In general, in Python this won't be necessary, but a second example showing this kind of arrangement is also included. -*Where is the pattern used practically? - -*References: -https://sourcemaking.com/design_patterns/builder +Where is the pattern used practically? +See: https://sourcemaking.com/design_patterns/builder -*TL;DR +TL;DR Decouples the creation of a complex object and its representation. """ # Abstract Building class Building: - def __init__(self): + floor: str + size: str + + def __init__(self) -> None: self.build_floor() self.build_size() @@ -44,24 +44,24 @@ def build_floor(self): def build_size(self): raise NotImplementedError - def __repr__(self): + def __repr__(self) -> str: return "Floor: {0.floor} | Size: {0.size}".format(self) # Concrete Buildings class House(Building): - def build_floor(self): + def build_floor(self) -> None: self.floor = "One" - def build_size(self): + def build_size(self) -> None: self.size = "Big" class Flat(Building): - def build_floor(self): + def build_floor(self) -> None: self.floor = "More than One" - def build_size(self): + def build_size(self) -> None: self.size = "Small" @@ -72,19 +72,22 @@ def build_size(self): class ComplexBuilding: - def __repr__(self): + floor: str + size: str + + def __repr__(self) -> str: return "Floor: {0.floor} | Size: {0.size}".format(self) class ComplexHouse(ComplexBuilding): - def build_floor(self): + def build_floor(self) -> None: self.floor = "One" - def build_size(self): + def build_size(self) -> None: self.size = "Big and fancy" -def construct_building(cls): +def construct_building(cls) -> Building: building = cls() building.build_floor() building.build_size() diff --git a/patterns/creational/factory.py b/patterns/creational/factory.py index 8dba488d4..801575bdf 100644 --- a/patterns/creational/factory.py +++ b/patterns/creational/factory.py @@ -22,6 +22,12 @@ 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""" @@ -41,15 +47,14 @@ def localize(self, msg: str) -> str: return msg -def get_localizer(language: str = "English") -> object: - +def get_localizer(language: str = "English") -> Localizer: """Factory""" - localizers = { + localizers: dict[str, type[Localizer]] = { "English": EnglishLocalizer, "Greek": GreekLocalizer, } - return localizers[language]() + return localizers.get(language, EnglishLocalizer)() def main(): diff --git a/patterns/creational/lazy_evaluation.py b/patterns/creational/lazy_evaluation.py index b56daf0c1..b9945d998 100644 --- a/patterns/creational/lazy_evaluation.py +++ b/patterns/creational/lazy_evaluation.py @@ -20,14 +20,24 @@ """ import functools +from collections.abc import Callable +from typing import Any, overload class lazy_property: - def __init__(self, function): + def __init__(self, function: Callable[["Person"], str]) -> None: self.function = function - functools.update_wrapper(self, function) + functools.update_wrapper(self, function) # type: ignore[arg-type] - def __get__(self, obj, 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) @@ -35,7 +45,7 @@ def __get__(self, obj, type_): return val -def lazy_property2(fn): +def lazy_property2(fn: Callable[[Any], str]) -> property: """ A lazy property decorator. @@ -44,29 +54,28 @@ def lazy_property2(fn): """ attr = "_lazy__" + fn.__name__ - @property - def _lazy_property(self): + def _lazy_property(self: Any) -> str: if not hasattr(self, attr): setattr(self, attr, fn(self)) return getattr(self, attr) - return _lazy_property + return property(_lazy_property) class Person: - def __init__(self, name, occupation): + def __init__(self, name: str, occupation: str) -> None: self.name = name self.occupation = occupation self.call_count2 = 0 @lazy_property - def relatives(self): + def relatives(self) -> str: # Get all relatives, let's assume that it costs much time. relatives = "Many relatives." return relatives @lazy_property2 - def parents(self): + def parents(self) -> str: self.call_count2 += 1 return "Father and mother" diff --git a/patterns/creational/pool.py b/patterns/creational/pool.py index 1d70ea690..684b11857 100644 --- a/patterns/creational/pool.py +++ b/patterns/creational/pool.py @@ -28,23 +28,31 @@ Stores a set of initialized objects kept ready to use. """ +from queue import Queue +from types import TracebackType + class ObjectPool: - def __init__(self, queue, auto_get=False): + 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 diff --git a/patterns/creational/prototype.py b/patterns/creational/prototype.py index 4151ffbf6..4c2dd7ed1 100644 --- a/patterns/creational/prototype.py +++ b/patterns/creational/prototype.py @@ -20,6 +20,7 @@ *TL;DR Creates new object instances by cloning prototype. """ + from __future__ import annotations from typing import Any diff --git a/patterns/dependency_injection.py b/patterns/dependency_injection.py index 2979f763c..4246d11a2 100644 --- a/patterns/dependency_injection.py +++ b/patterns/dependency_injection.py @@ -24,7 +24,7 @@ def get_current_time_as_html_fragment(self): """ import datetime -from typing import Callable +from collections.abc import Callable class ConstructorInjection: @@ -102,7 +102,7 @@ def main(): >>> time_with_si.get_current_time_as_html_fragment() Traceback (most recent call last): ... - AttributeError: 'SetterInjection' object has no attribute 'time_provider' + AttributeError: ... >>> time_with_si.set_time_provider(midnight_time_provider) >>> time_with_si.get_current_time_as_html_fragment() diff --git a/patterns/fundamental/delegation_pattern.py b/patterns/fundamental/delegation_pattern.py index bdcefc9d0..c035da202 100644 --- a/patterns/fundamental/delegation_pattern.py +++ b/patterns/fundamental/delegation_pattern.py @@ -8,7 +8,8 @@ from __future__ import annotations -from typing import Any, Callable +from collections.abc import Callable +from typing import Any class Delegator: @@ -19,16 +20,18 @@ class Delegator: >>> delegator.p2 Traceback (most recent call last): ... - AttributeError: 'Delegate' object has no attribute 'p2' + AttributeError: ... >>> delegator.do_something("nothing") 'Doing nothing' + >>> delegator.do_something("something", kw=", faif!") + 'Doing something, faif!' >>> delegator.do_anything() Traceback (most recent call last): ... - AttributeError: 'Delegate' object has no attribute 'do_anything' + AttributeError: ... """ - def __init__(self, delegate: Delegate): + def __init__(self, delegate: Delegate) -> None: self.delegate = delegate def __getattr__(self, name: str) -> Any | Callable: @@ -44,11 +47,11 @@ def wrapper(*args, **kwargs): class Delegate: - def __init__(self): + def __init__(self) -> None: self.p1 = 123 - def do_something(self, something: str) -> str: - return f"Doing {something}" + def do_something(self, something: str, kw=None) -> str: + return f"Doing {something}{kw or ''}" if __name__ == "__main__": diff --git a/patterns/other/blackboard.py b/patterns/other/blackboard.py index 49f8775f2..e69582ec3 100644 --- a/patterns/other/blackboard.py +++ b/patterns/other/blackboard.py @@ -9,13 +9,32 @@ https://en.wikipedia.org/wiki/Blackboard_system """ -import abc import random +from abc import ABC, abstractmethod + + +class AbstractExpert(ABC): + """Abstract class for experts in the blackboard system.""" + + @abstractmethod + def __init__(self, blackboard) -> None: + self.blackboard = blackboard + + @property + @abstractmethod + def is_eager_to_contribute(self) -> int: + raise NotImplementedError("Must provide implementation in subclass.") + + @abstractmethod + def contribute(self) -> None: + raise NotImplementedError("Must provide implementation in subclass.") class Blackboard: - def __init__(self): - self.experts = [] + """The blackboard system that holds the common state.""" + + def __init__(self) -> None: + self.experts: list = [] self.common_state = { "problems": 0, "suggestions": 0, @@ -23,12 +42,14 @@ def __init__(self): "progress": 0, # percentage, if 100 -> task is finished } - def add_expert(self, expert): + def add_expert(self, expert: AbstractExpert) -> None: self.experts.append(expert) class Controller: - def __init__(self, blackboard): + """The controller that manages the blackboard system.""" + + def __init__(self, blackboard: Blackboard) -> None: self.blackboard = blackboard def run_loop(self): @@ -43,26 +64,17 @@ def run_loop(self): return self.blackboard.common_state["contributions"] -class AbstractExpert(metaclass=abc.ABCMeta): - def __init__(self, blackboard): - self.blackboard = blackboard - - @property - @abc.abstractmethod - def is_eager_to_contribute(self): - raise NotImplementedError("Must provide implementation in subclass.") - - @abc.abstractmethod - def contribute(self): - raise NotImplementedError("Must provide implementation in subclass.") +class Student(AbstractExpert): + """Concrete class for a student expert.""" + def __init__(self, blackboard) -> None: + super().__init__(blackboard) -class Student(AbstractExpert): @property - def is_eager_to_contribute(self): + def is_eager_to_contribute(self) -> bool: return True - def contribute(self): + 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__] @@ -70,11 +82,16 @@ def contribute(self): class Scientist(AbstractExpert): + """Concrete class for a scientist expert.""" + + def __init__(self, blackboard) -> None: + super().__init__(blackboard) + @property - def is_eager_to_contribute(self): + def is_eager_to_contribute(self) -> int: return random.randint(0, 1) - def contribute(self): + 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__] @@ -82,11 +99,14 @@ def contribute(self): class Professor(AbstractExpert): + def __init__(self, blackboard) -> None: + super().__init__(blackboard) + @property - def is_eager_to_contribute(self): + def is_eager_to_contribute(self) -> bool: return True if self.blackboard.common_state["problems"] > 100 else False - def contribute(self): + 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__] @@ -95,6 +115,7 @@ def contribute(self): def main(): """ + >>> random.seed(1234) >>> blackboard = Blackboard() >>> blackboard.add_expert(Student(blackboard)) >>> blackboard.add_expert(Scientist(blackboard)) @@ -103,23 +124,10 @@ def main(): >>> c = Controller(blackboard) >>> contributions = c.run_loop() - >>> from pprint import pprint - >>> pprint(contributions) - ['Student', - 'Student', - 'Student', - 'Student', - 'Scientist', - 'Student', - 'Student', - 'Student', - 'Scientist', - 'Student', - 'Scientist', - 'Student', - 'Student', - 'Scientist', - 'Professor'] + >>> contributions[0], contributions[-1] + ('Student', 'Professor') + >>> set(contributions) == {"Student", "Scientist", "Professor"} + True """ diff --git a/patterns/other/graph_search.py b/patterns/other/graph_search.py index ad224db35..e3c921409 100644 --- a/patterns/other/graph_search.py +++ b/patterns/other/graph_search.py @@ -1,15 +1,16 @@ 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): + def __init__(self, graph: dict[str, list[str]]) -> None: self.graph = graph - def find_path_dfs(self, start, end, path=None): + def find_path_dfs( + self, start: str, end: str, path: list[str] | None = None + ) -> list[str] | None: path = path or [] path.append(start) @@ -20,20 +21,25 @@ def find_path_dfs(self, start, end, path=None): newpath = self.find_path_dfs(node, end, path[:]) if newpath: return newpath + return None - def find_all_paths_dfs(self, start, end, path=None): + def find_all_paths_dfs( + self, start: str, end: str, path: list[str] | None = None + ) -> list[list[str]]: path = path or [] path.append(start) if start == end: return [path] - paths = [] + paths: list[list[str]] = [] for node in self.graph.get(start, []): if node not in path: newpaths = self.find_all_paths_dfs(node, end, path[:]) paths.extend(newpaths) return paths - def find_shortest_path_dfs(self, start, end, path=None): + def find_shortest_path_dfs( + self, start: str, end: str, path: list[str] | None = None + ) -> list[str] | None: path = path or [] path.append(start) @@ -48,7 +54,7 @@ def find_shortest_path_dfs(self, start, end, path=None): shortest = newpath return shortest - def find_shortest_path_bfs(self, start, end): + 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. @@ -63,9 +69,9 @@ def find_shortest_path_bfs(self, start, end): (in terms of hops). If no such path exists, returns an empty list and an empty dictionary instead. """ - queue = [start] - dist_to = {start: 0} - edge_to = {} + queue: list[str] = [start] + dist_to: dict[str, int] = {start: 0} + edge_to: dict[str, str] = {} if start == end: return queue @@ -78,13 +84,14 @@ def find_shortest_path_bfs(self, start, end): dist_to[node] = dist_to[value] + 1 queue.append(node) if end in edge_to.keys(): - path = [] + 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(): diff --git a/patterns/structural/3-tier.py b/patterns/structural/3-tier.py index ecc042430..3c266f514 100644 --- a/patterns/structural/3-tier.py +++ b/patterns/structural/3-tier.py @@ -3,7 +3,7 @@ Separates presentation, application processing, and data management functions. """ -from typing import Dict, KeysView, Optional, Union +from collections.abc import KeysView class Data: @@ -16,7 +16,6 @@ class Data: } def __get__(self, obj, klas): - print("(Fetching from Data Store)") return {"products": self.products} @@ -29,9 +28,7 @@ class BusinessLogic: def product_list(self) -> KeysView[str]: return self.data["products"].keys() - def product_information( - self, product: str - ) -> Optional[Dict[str, Union[int, float]]]: + def product_information(self, product: str) -> dict[str, int | float] | None: return self.data["products"].get(product, None) diff --git a/patterns/structural/adapter.py b/patterns/structural/adapter.py index 433369ee7..5afe42faf 100644 --- a/patterns/structural/adapter.py +++ b/patterns/structural/adapter.py @@ -28,7 +28,8 @@ Allows the interface of an existing class to be used as another interface. """ -from typing import Callable, TypeVar +from collections.abc import Callable +from typing import Any, TypeVar T = TypeVar("T") @@ -74,16 +75,16 @@ class Adapter: dog = Adapter(dog, make_noise=dog.bark) """ - def __init__(self, obj: T, **adapted_methods: Callable): + 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): + def __getattr__(self, attr: str) -> Any: """All non-adapted calls are passed to the object.""" return getattr(self.obj, attr) - def original_dict(self): + def original_dict(self) -> dict[str, Any]: """Print original object dict.""" return self.obj.__dict__ diff --git a/patterns/structural/bridge.py b/patterns/structural/bridge.py index feddb6759..0990efc38 100644 --- a/patterns/structural/bridge.py +++ b/patterns/structural/bridge.py @@ -9,30 +9,32 @@ # ConcreteImplementor 1/2 class DrawingAPI1: - def draw_circle(self, x, y, radius): + 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, y, radius): + 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, y, radius, drawing_api): + def __init__( + self, x: int, y: int, radius: float, drawing_api: DrawingAPI2 | DrawingAPI1 + ) -> None: self._x = x self._y = y - self._radius = radius + self._radius: float = radius self._drawing_api = drawing_api # low-level i.e. Implementation specific - def draw(self): + def draw(self) -> None: self._drawing_api.draw_circle(self._x, self._y, self._radius) # high-level i.e. Abstraction specific - def scale(self, pct): + def scale(self, pct: float) -> None: self._radius *= pct diff --git a/patterns/structural/composite.py b/patterns/structural/composite.py index a4bedc1d4..ee0520c6b 100644 --- a/patterns/structural/composite.py +++ b/patterns/structural/composite.py @@ -27,7 +27,6 @@ """ from abc import ABC, abstractmethod -from typing import List class Graphic(ABC): @@ -38,7 +37,7 @@ def render(self) -> None: class CompositeGraphic(Graphic): def __init__(self) -> None: - self.graphics: List[Graphic] = [] + self.graphics: list[Graphic] = [] def render(self) -> None: for graphic in self.graphics: diff --git a/patterns/structural/decorator.py b/patterns/structural/decorator.py index 01c91b001..a32e2b06d 100644 --- a/patterns/structural/decorator.py +++ b/patterns/structural/decorator.py @@ -28,30 +28,30 @@ 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): + 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): + def render(self) -> str: return f"{self._wrapped.render()}" diff --git a/patterns/structural/facade.py b/patterns/structural/facade.py index 6561c6dce..f7b00be32 100644 --- a/patterns/structural/facade.py +++ b/patterns/structural/facade.py @@ -35,13 +35,13 @@ class CPU: Simple CPU representation. """ - def freeze(self): + def freeze(self) -> None: print("Freezing processor.") - def jump(self, position): + def jump(self, position: str) -> None: print("Jumping to:", position) - def execute(self): + def execute(self) -> None: print("Executing.") @@ -50,7 +50,7 @@ class Memory: Simple memory representation. """ - def load(self, position, data): + def load(self, position: str, data: str) -> None: print(f"Loading from {position} data: '{data}'.") @@ -59,7 +59,7 @@ class SolidStateDrive: Simple solid state drive representation. """ - def read(self, lba, size): + def read(self, lba: str, size: str) -> str: return f"Some data from sector {lba} with size {size}" diff --git a/patterns/structural/flyweight.py b/patterns/structural/flyweight.py index fad17a8b4..148a5bc05 100644 --- a/patterns/structural/flyweight.py +++ b/patterns/structural/flyweight.py @@ -31,12 +31,15 @@ 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 = weakref.WeakValueDictionary() + _pool: weakref.WeakValueDictionary[str, "Card"] = weakref.WeakValueDictionary() - def __new__(cls, value, suit): + 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) @@ -52,7 +55,7 @@ def __new__(cls, value, suit): # def __init__(self, value, suit): # self.value, self.suit = value, suit - def __repr__(self): + def __repr__(self) -> str: return f"" diff --git a/patterns/structural/front_controller.py b/patterns/structural/front_controller.py index 4852208dc..92f58b213 100644 --- a/patterns/structural/front_controller.py +++ b/patterns/structural/front_controller.py @@ -5,23 +5,27 @@ 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): + def show_index_page(self) -> None: print("Displaying mobile index page") class TabletView: - def show_index_page(self): + def show_index_page(self) -> None: print("Displaying tablet index page") class Dispatcher: - def __init__(self): + def __init__(self) -> None: self.mobile_view = MobileView() self.tablet_view = TabletView() - def dispatch(self, request): + def dispatch(self, request: Request) -> None: """ This function is used to dispatch the request based on the type of device. If it is a mobile, then mobile view will be called and if it is a tablet, @@ -39,10 +43,10 @@ def dispatch(self, request): class RequestController: """front controller""" - def __init__(self): + def __init__(self) -> None: self.dispatcher = Dispatcher() - def dispatch_request(self, request): + def dispatch_request(self, request: Any) -> None: """ This function takes a request object and sends it to the dispatcher. """ diff --git a/patterns/structural/mvc.py b/patterns/structural/mvc.py index 3f7dc3151..5726b0897 100644 --- a/patterns/structural/mvc.py +++ b/patterns/structural/mvc.py @@ -4,31 +4,38 @@ """ 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): + def __iter__(self) -> Any: pass @abstractmethod - def get(self, item): + 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): + 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): + def __str__(self) -> str: return f"{self:.2f}" products = { @@ -39,10 +46,10 @@ def __str__(self): item_type = "product" - def __iter__(self): + def __iter__(self) -> Any: yield from self.products - def get(self, product): + def get(self, product: str) -> dict: try: return self.products[product] except KeyError as e: @@ -50,33 +57,43 @@ def get(self, product): class View(ABC): + """The View is the presentation layer of the application.""" + @abstractmethod - def show_item_list(self, item_type, item_list): + def show_item_list(self, item_type: str, item_list: list) -> None: pass @abstractmethod - def show_item_information(self, item_type, item_name, item_info): + 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, item_name): + def item_not_found(self, item_type: str, item_name: str) -> None: pass class ConsoleView(View): - def show_item_list(self, item_type, item_list): + """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): + def capitalizer(string: str) -> str: + """Capitalizes the first letter of a string and lowercases the rest.""" return string[0].upper() + string[1:].lower() - def show_item_information(self, item_type, item_name, item_info): + 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(): @@ -84,35 +101,61 @@ def show_item_information(self, item_type, item_name, item_info): printout += "\n" print(printout) - def item_not_found(self, item_type, item_name): + def item_not_found(self, item_type: str, item_name: str) -> None: print(f'That {item_type} "{item_name}" does not exist in the records') class Controller: - def __init__(self, model, view): - self.model = model - self.view = view + """The Controller is the intermediary between the Model and the View.""" - def show_items(self): + 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): + def show_item_information(self, item_name: str) -> None: """ Show information about a {item_type} item. :param str item_name: the name of the {item_type} item to show information about """ + item_type: str = self.model.item_type try: - item_info = self.model.get(item_name) + item_info: dict = self.model.get(item_name) except Exception: - item_type = self.model.item_type self.view.item_not_found(item_type, item_name) else: - item_type = self.model.item_type self.view.show_item_information(item_type, item_name, item_info) +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() @@ -147,6 +190,27 @@ def main(): if __name__ == "__main__": + router = Router() + router.register("products", Controller, ProductModel, ConsoleView) + controller: Controller = router.resolve(argv[1]) + + action: str = str(argv[2]) if len(argv) > 2 else "" + args: str = " ".join(map(str, argv[3:])) if len(argv) > 3 else "" + + if hasattr(controller, action): + command = getattr(controller, action) + sig = signature(command) + + if len(sig.parameters) > 0: + if args: + command(args) + else: + print("Command requires arguments.") + else: + command() + else: + print(f"Command {action} not found in the controller.") + import doctest doctest.testmod() diff --git a/patterns/structural/proxy.py b/patterns/structural/proxy.py index 3ef74ec03..c12fb051c 100644 --- a/patterns/structural/proxy.py +++ b/patterns/structural/proxy.py @@ -15,8 +15,6 @@ without changing its interface. """ -from typing import Union - class Subject: """ @@ -59,7 +57,7 @@ def do_the_job(self, user: str) -> None: print("[log] I can do the job just for `admins`.") -def client(job_doer: Union[RealSubject, Proxy], user: str) -> None: +def client(job_doer: RealSubject | Proxy, user: str) -> None: job_doer.do_the_job(user) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..1e4740463 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,112 @@ +[build-system] +requires = ["setuptools >= 77.0.3"] +build-backend = "setuptools.build_meta" + +[project] +name = "python-patterns" +description = "A collection of design patterns and idioms in Python." +version = "0.1.0" +readme = "README.md" +requires-python = ">=3.10" +classifiers = [ + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", +] +dependencies= [ +] + +maintainers=[ + { name="faif" } +] + +[project.urls] +Homepage = "https://github.com/faif/python-patterns" +Repository = "https://github.com/faif/python-patterns" +"Bug Tracker" = "https://github.com/faif/python-patterns/issues" +Contributors = "https://github.com/faif/python-patterns/graphs/contributors" + +[project.optional-dependencies] +dev = [ + "codecov", + "codespell", + "mypy", + "pipx>=1.7.1", + "pipenv", + "pyupgrade", + "pytest>=6.2.0", + "pytest-cov>=2.11.0", + "pytest-randomly>=3.1.0", + "black>=25.1.0", + "build>=1.2.2", + "isort>=5.7.0", + "flake8>=7.1.0", + "tox>=4.25.0" +] + +[tool.setuptools] +packages = ["patterns"] + +[tool.pytest.ini_options] +filterwarnings = [ + "ignore::Warning:.*test class 'TestRunner'.*" +] +# Adding settings from tox.ini for pytest +testpaths = ["tests"] +#testpaths = ["tests", "patterns"] +python_files = ["test_*.py", "*_test.py"] +# Enable doctest discovery in patterns directory +addopts = "--doctest-modules --randomly-seed=1234 --cov=patterns --cov-report=term-missing" +doctest_optionflags = ["ELLIPSIS", "NORMALIZE_WHITESPACE"] +log_level = "INFO" + +[tool.coverage.run] +branch = true +source = ["./"] +#source = ["patterns"] +# Ensure coverage data is collected properly +relative_files = true +parallel = true +data_file = ".coverage" + +[tool.coverage.report] +# Regexes for lines to exclude from consideration +exclude_lines = [ + "def __repr__", + "if self\\.debug", + "raise AssertionError", + "raise NotImplementedError", + "if 0:", + "if __name__ == .__main__.:", + "@(abc\\.)?abstractmethod" +] +ignore_errors = true + +[tool.coverage.html] +directory = "coverage_html_report" + +[tool.mypy] +python_version = "3.12" +ignore_missing_imports = true + +[tool.flake8] +max-line-length = 120 +ignore = ["E266", "E731", "W503"] +exclude = ["venv*"] + +[tool.black] +target-version = ["py310"] + +[tool.tox] +legacy_tox_ini = """ +[tox] +envlist = py + +[testenv] +package = editable +deps = pipenv +commands_pre = pipenv sync --dev --system +commands = pytest {posargs:tests patterns} +""" diff --git a/pytest_local.ini b/pytest_local.ini new file mode 100644 index 000000000..154db6e67 --- /dev/null +++ b/pytest_local.ini @@ -0,0 +1,3 @@ +[pytest] +addopts = -q +testpaths = tests diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index 0de4748b0..000000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,8 +0,0 @@ --e . - -pytest~=6.2.0 -pytest-cov~=2.11.0 -pytest-randomly~=3.1.0 -black>=20.8b1 -isort~=5.7.0 -flake8~=3.8.0 \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index b4d2cdf1c..000000000 --- a/setup.py +++ /dev/null @@ -1,16 +0,0 @@ -from setuptools import find_packages, setup - -setup( - name="patterns", - packages=find_packages(), - description="A collection of design patterns and idioms in Python.", - classifiers=[ - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - ], -) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb 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_publish_subscribe.py b/tests/behavioral/test_publish_subscribe.py index c153da5b5..6f142855d 100644 --- a/tests/behavioral/test_publish_subscribe.py +++ b/tests/behavioral/test_publish_subscribe.py @@ -1,10 +1,9 @@ -import unittest from unittest.mock import call, patch from patterns.behavioral.publish_subscribe import Provider, Publisher, Subscriber -class TestProvider(unittest.TestCase): +class TestProvider: """ Integration tests ~ provider class with as little mocking as possible. """ @@ -12,19 +11,19 @@ class TestProvider(unittest.TestCase): def test_subscriber_shall_be_attachable_to_subscriptions(cls): subscription = "sub msg" pro = Provider() - cls.assertEqual(len(pro.subscribers), 0) + assert len(pro.subscribers) == 0 sub = Subscriber("sub name", pro) sub.subscribe(subscription) - cls.assertEqual(len(pro.subscribers[subscription]), 1) + assert len(pro.subscribers[subscription]) == 1 def test_subscriber_shall_be_detachable_from_subscriptions(cls): subscription = "sub msg" pro = Provider() sub = Subscriber("sub name", pro) sub.subscribe(subscription) - cls.assertEqual(len(pro.subscribers[subscription]), 1) + assert len(pro.subscribers[subscription]) == 1 sub.unsubscribe(subscription) - cls.assertEqual(len(pro.subscribers[subscription]), 0) + assert len(pro.subscribers[subscription]) == 0 def test_publisher_shall_append_subscription_message_to_queue(cls): """msg_queue ~ Provider.notify(msg) ~ Publisher.publish(msg)""" @@ -32,10 +31,10 @@ def test_publisher_shall_append_subscription_message_to_queue(cls): pro = Provider() pub = Publisher(pro) Subscriber("sub name", pro) - cls.assertEqual(len(pro.msg_queue), 0) + assert len(pro.msg_queue) == 0 pub.publish(expected_msg) - cls.assertEqual(len(pro.msg_queue), 1) - cls.assertEqual(pro.msg_queue[0], expected_msg) + assert len(pro.msg_queue) == 1 + assert pro.msg_queue[0] == expected_msg def test_provider_shall_update_affected_subscribers_with_published_subscription( cls, @@ -48,19 +47,21 @@ def test_provider_shall_update_affected_subscribers_with_published_subscription( 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: + 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) + 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: + 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) 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_strategy.py b/tests/behavioral/test_strategy.py index 86018a44d..53976f389 100644 --- a/tests/behavioral/test_strategy.py +++ b/tests/behavioral/test_strategy.py @@ -1,6 +1,6 @@ import pytest -from patterns.behavioral.strategy import Order, ten_percent_discount, on_sale_discount +from patterns.behavioral.strategy import Order, on_sale_discount, ten_percent_discount @pytest.fixture diff --git a/tests/behavioral/test_visitor.py b/tests/behavioral/test_visitor.py new file mode 100644 index 000000000..6826679b4 --- /dev/null +++ b/tests/behavioral/test_visitor.py @@ -0,0 +1,26 @@ +import pytest + +from patterns.behavioral.visitor import A, B, C, Visitor + + +@pytest.fixture +def visitor(): + return Visitor() + + +def test_visiting_generic_node(visitor): + a = A() + token = visitor.visit(a) + assert token == "generic_visit A", "The expected generic object was not called" + + +def test_visiting_specific_nodes(visitor): + b = B() + token = visitor.visit(b) + assert token == "visit_B B", "The expected specific object was not called" + + +def test_visiting_inherited_nodes(visitor): + c = C() + token = visitor.visit(c) + assert token == "visit_B C", "The expected inherited object was not called" diff --git a/tests/creational/test_abstract_factory.py b/tests/creational/test_abstract_factory.py index 1676e59d1..e717fafcd 100644 --- a/tests/creational/test_abstract_factory.py +++ b/tests/creational/test_abstract_factory.py @@ -1,13 +1,12 @@ -import unittest from unittest.mock import patch from patterns.creational.abstract_factory import Dog, PetShop -class TestPetShop(unittest.TestCase): +class TestPetShop: def test_dog_pet_shop_shall_show_dog_instance(self): dog_pet_shop = PetShop(Dog) with patch.object(Dog, "speak") as mock_Dog_speak: pet = dog_pet_shop.buy_pet("") pet.speak() - self.assertEqual(mock_Dog_speak.call_count, 1) + assert mock_Dog_speak.call_count == 1 diff --git a/tests/creational/test_borg.py b/tests/creational/test_borg.py index 182611c37..61785020a 100644 --- a/tests/creational/test_borg.py +++ b/tests/creational/test_borg.py @@ -1,28 +1,26 @@ -import unittest - from patterns.creational.borg import Borg, YourBorg -class BorgTest(unittest.TestCase): - def setUp(self): +class TestBorg: + def setup_method(self): self.b1 = Borg() self.b2 = Borg() # creating YourBorg instance implicitly sets the state attribute # for all borg instances. self.ib1 = YourBorg() - def tearDown(self): + def teardown_method(self): self.ib1.state = "Init" def test_initial_borg_state_shall_be_init(self): b = Borg() - self.assertEqual(b.state, "Init") + assert b.state == "Init" def test_changing_instance_attribute_shall_change_borg_state(self): self.b1.state = "Running" - self.assertEqual(self.b1.state, "Running") - self.assertEqual(self.b2.state, "Running") - self.assertEqual(self.ib1.state, "Running") + assert self.b1.state == "Running" + assert self.b2.state == "Running" + assert self.ib1.state == "Running" def test_instances_shall_have_own_ids(self): - self.assertNotEqual(id(self.b1), id(self.b2), id(self.ib1)) + assert id(self.b1) != id(self.b2), id(self.ib1) diff --git a/tests/creational/test_builder.py b/tests/creational/test_builder.py index 923bc4a5c..539d7994b 100644 --- a/tests/creational/test_builder.py +++ b/tests/creational/test_builder.py @@ -1,22 +1,20 @@ -import unittest - from patterns.creational.builder import ComplexHouse, Flat, House, construct_building -class TestSimple(unittest.TestCase): +class TestSimple: def test_house(self): house = House() - self.assertEqual(house.size, "Big") - self.assertEqual(house.floor, "One") + assert house.size == "Big" + assert house.floor == "One" def test_flat(self): flat = Flat() - self.assertEqual(flat.size, "Small") - self.assertEqual(flat.floor, "More than One") + assert flat.size == "Small" + assert flat.floor == "More than One" -class TestComplex(unittest.TestCase): +class TestComplex: def test_house(self): house = construct_building(ComplexHouse) - self.assertEqual(house.size, "Big and fancy") - self.assertEqual(house.floor, "One") + assert house.size == "Big and fancy" + assert house.floor == "One" diff --git a/tests/creational/test_factory.py b/tests/creational/test_factory.py new file mode 100644 index 000000000..7d18211d8 --- /dev/null +++ b/tests/creational/test_factory.py @@ -0,0 +1,30 @@ +from patterns.creational.factory import EnglishLocalizer, GreekLocalizer, get_localizer + + +class TestFactory: + def test_get_localizer_greek(self): + localizer = get_localizer("Greek") + assert isinstance(localizer, GreekLocalizer) + assert localizer.localize("dog") == "σκύλος" + assert localizer.localize("cat") == "γάτα" + # Test unknown word returns the word itself + assert localizer.localize("monkey") == "monkey" + + def test_get_localizer_english(self): + localizer = get_localizer("English") + assert isinstance(localizer, EnglishLocalizer) + assert localizer.localize("dog") == "dog" + assert localizer.localize("cat") == "cat" + + def test_get_localizer_default(self): + # Test default argument + localizer = get_localizer() + assert isinstance(localizer, EnglishLocalizer) + + def test_get_localizer_unknown_language(self): + # Test fallback for unknown language if applicable, + # or just verify what happens. + # Based on implementation: localizers.get(language, EnglishLocalizer)() + # It defaults to EnglishLocalizer for unknown keys. + localizer = get_localizer("Spanish") + assert isinstance(localizer, EnglishLocalizer) diff --git a/tests/creational/test_lazy.py b/tests/creational/test_lazy.py index 1b815b609..d1d02a1ba 100644 --- a/tests/creational/test_lazy.py +++ b/tests/creational/test_lazy.py @@ -1,38 +1,34 @@ -import unittest - from patterns.creational.lazy_evaluation import Person -class TestDynamicExpanding(unittest.TestCase): - def setUp(self): +class TestDynamicExpanding: + def setup_method(self): self.John = Person("John", "Coder") def test_innate_properties(self): - self.assertDictEqual( - {"name": "John", "occupation": "Coder", "call_count2": 0}, - self.John.__dict__, - ) + assert { + "name": "John", + "occupation": "Coder", + "call_count2": 0, + } == self.John.__dict__ def test_relatives_not_in_properties(self): - self.assertNotIn("relatives", self.John.__dict__) + assert "relatives" not in self.John.__dict__ def test_extended_properties(self): print(f"John's relatives: {self.John.relatives}") - self.assertDictEqual( - { - "name": "John", - "occupation": "Coder", - "relatives": "Many relatives.", - "call_count2": 0, - }, - self.John.__dict__, - ) + 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}") - self.assertIn("relatives", self.John.__dict__) + assert "relatives" in self.John.__dict__ def test_parents(self): for _ in range(2): - self.assertEqual(self.John.parents, "Father and mother") - self.assertEqual(self.John.call_count2, 1) + assert self.John.parents == "Father and mother" + assert self.John.call_count2 == 1 diff --git a/tests/creational/test_pool.py b/tests/creational/test_pool.py index 38476eb7a..97b9d269e 100644 --- a/tests/creational/test_pool.py +++ b/tests/creational/test_pool.py @@ -1,35 +1,33 @@ import queue -import unittest from patterns.creational.pool import ObjectPool -class TestPool(unittest.TestCase): - def setUp(self): +class TestPool: + def setup_method(self): self.sample_queue = queue.Queue() self.sample_queue.put("first") self.sample_queue.put("second") def test_items_recoil(self): with ObjectPool(self.sample_queue, True) as pool: - self.assertEqual(pool, "first") - self.assertTrue(self.sample_queue.get() == "second") - self.assertFalse(self.sample_queue.empty()) - self.assertTrue(self.sample_queue.get() == "first") - self.assertTrue(self.sample_queue.empty()) + assert pool == "first" + assert self.sample_queue.get() == "second" + assert not self.sample_queue.empty() + assert self.sample_queue.get() == "first" + assert self.sample_queue.empty() def test_frozen_pool(self): with ObjectPool(self.sample_queue) as pool: - self.assertEqual(pool, "first") - self.assertEqual(pool, "first") - self.assertTrue(self.sample_queue.get() == "second") - self.assertFalse(self.sample_queue.empty()) - self.assertTrue(self.sample_queue.get() == "first") - self.assertTrue(self.sample_queue.empty()) + assert pool == "first" + assert pool == "first" + assert self.sample_queue.get() == "second" + assert not self.sample_queue.empty() + assert self.sample_queue.get() == "first" + assert self.sample_queue.empty() -class TestNaitivePool(unittest.TestCase): - +class TestNaitivePool: """def test_object(queue): queue_object = QueueObject(queue, True) print('Inside func: {}'.format(queue_object.object))""" @@ -39,10 +37,10 @@ def test_pool_behavior_with_single_object_inside(self): sample_queue.put("yam") with ObjectPool(sample_queue) as obj: # print('Inside with: {}'.format(obj)) - self.assertEqual(obj, "yam") - self.assertFalse(sample_queue.empty()) - self.assertTrue(sample_queue.get() == "yam") - self.assertTrue(sample_queue.empty()) + assert obj == "yam" + assert not sample_queue.empty() + assert sample_queue.get() == "yam" + assert sample_queue.empty() # sample_queue.put('sam') # test_object(sample_queue) diff --git a/tests/creational/test_prototype.py b/tests/creational/test_prototype.py index 758ac8722..ca5d77724 100644 --- a/tests/creational/test_prototype.py +++ b/tests/creational/test_prototype.py @@ -1,31 +1,31 @@ -import unittest +import pytest from patterns.creational.prototype import Prototype, PrototypeDispatcher -class TestPrototypeFeatures(unittest.TestCase): - def setUp(self): +class TestPrototypeFeatures: + def setup_method(self): self.prototype = Prototype() def test_cloning_propperty_innate_values(self): sample_object_1 = self.prototype.clone() sample_object_2 = self.prototype.clone() - self.assertEqual(sample_object_1.value, sample_object_2.value) + assert sample_object_1.value == sample_object_2.value def test_extended_property_values_cloning(self): sample_object_1 = self.prototype.clone() sample_object_1.some_value = "test string" sample_object_2 = self.prototype.clone() - self.assertRaises(AttributeError, lambda: sample_object_2.some_value) + pytest.raises(AttributeError, lambda: sample_object_2.some_value) def test_cloning_propperty_assigned_values(self): sample_object_1 = self.prototype.clone() sample_object_2 = self.prototype.clone(value="re-assigned") - self.assertNotEqual(sample_object_1.value, sample_object_2.value) + assert sample_object_1.value != sample_object_2.value -class TestDispatcherFeatures(unittest.TestCase): - def setUp(self): +class TestDispatcherFeatures: + def setup_method(self): self.dispatcher = PrototypeDispatcher() self.prototype = Prototype() c = self.prototype.clone() @@ -36,13 +36,13 @@ def setUp(self): self.dispatcher.register_object("C", c) def test_batch_retrieving(self): - self.assertEqual(len(self.dispatcher.get_objects()), 3) + assert len(self.dispatcher.get_objects()) == 3 def test_particular_properties_retrieving(self): - self.assertEqual(self.dispatcher.get_objects()["A"].value, "a-value") - self.assertEqual(self.dispatcher.get_objects()["B"].value, "b-value") - self.assertEqual(self.dispatcher.get_objects()["C"].value, "default") + assert self.dispatcher.get_objects()["A"].value == "a-value" + assert self.dispatcher.get_objects()["B"].value == "b-value" + assert self.dispatcher.get_objects()["C"].value == "default" def test_extended_properties_retrieving(self): - self.assertEqual(self.dispatcher.get_objects()["A"].ext_value, "E") - self.assertTrue(self.dispatcher.get_objects()["B"].diff) + assert self.dispatcher.get_objects()["A"].ext_value == "E" + assert self.dispatcher.get_objects()["B"].diff diff --git a/tests/fundamental/test_delegation.py b/tests/fundamental/test_delegation.py new file mode 100644 index 000000000..f7124b14a --- /dev/null +++ b/tests/fundamental/test_delegation.py @@ -0,0 +1,16 @@ +import pytest + +from patterns.fundamental.delegation_pattern import Delegate, Delegator + + +def test_delegator_delegates_attribute_and_call(): + d = Delegator(Delegate()) + assert d.p1 == 123 + assert d.do_something("something") == "Doing something" + assert d.do_something("something", kw=", hi") == "Doing something, hi" + + +def test_delegator_missing_attribute_raises(): + d = Delegator(Delegate()) + with pytest.raises(AttributeError): + _ = d.p2 diff --git a/tests/structural/test_adapter.py b/tests/structural/test_adapter.py index 013230752..042a32f26 100644 --- a/tests/structural/test_adapter.py +++ b/tests/structural/test_adapter.py @@ -1,10 +1,8 @@ -import unittest - 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() @@ -13,57 +11,57 @@ def setUp(self): def test_dog_shall_bark(self): noise = self.dog.bark() expected_noise = "woof!" - self.assertEqual(noise, expected_noise) + assert noise == expected_noise def test_cat_shall_meow(self): noise = self.cat.meow() expected_noise = "meow!" - self.assertEqual(noise, expected_noise) + assert noise == expected_noise def test_human_shall_speak(self): noise = self.human.speak() expected_noise = "'hello'" - self.assertEqual(noise, expected_noise) + assert noise == expected_noise def test_car_shall_make_loud_noise(self): noise = self.car.make_noise(1) expected_noise = "vroom!" - self.assertEqual(noise, expected_noise) + assert noise == expected_noise def test_car_shall_make_very_loud_noise(self): noise = self.car.make_noise(10) expected_noise = "vroom!!!!!!!!!!" - self.assertEqual(noise, expected_noise) + assert noise == expected_noise -class AdapterTest(unittest.TestCase): +class TestAdapter: def test_dog_adapter_shall_make_noise(self): dog = Dog() dog_adapter = Adapter(dog, make_noise=dog.bark) noise = dog_adapter.make_noise() expected_noise = "woof!" - self.assertEqual(noise, expected_noise) + assert noise == expected_noise def test_cat_adapter_shall_make_noise(self): cat = Cat() cat_adapter = Adapter(cat, make_noise=cat.meow) noise = cat_adapter.make_noise() expected_noise = "meow!" - self.assertEqual(noise, expected_noise) + assert noise == expected_noise def test_human_adapter_shall_make_noise(self): human = Human() human_adapter = Adapter(human, make_noise=human.speak) noise = human_adapter.make_noise() expected_noise = "'hello'" - self.assertEqual(noise, expected_noise) + assert noise == expected_noise def test_car_adapter_shall_make_loud_noise(self): car = Car() car_adapter = Adapter(car, make_noise=car.make_noise) noise = car_adapter.make_noise(1) expected_noise = "vroom!" - self.assertEqual(noise, expected_noise) + assert noise == expected_noise def test_car_adapter_shall_make_very_loud_noise(self): car = Car() @@ -71,4 +69,4 @@ def test_car_adapter_shall_make_very_loud_noise(self): noise = car_adapter.make_noise(10) expected_noise = "vroom!!!!!!!!!!" - self.assertEqual(noise, expected_noise) + assert noise == expected_noise diff --git a/tests/structural/test_bridge.py b/tests/structural/test_bridge.py index 7fa8a2789..bcf2a46d6 100644 --- a/tests/structural/test_bridge.py +++ b/tests/structural/test_bridge.py @@ -1,22 +1,22 @@ -import unittest from unittest.mock import patch from patterns.structural.bridge import CircleShape, DrawingAPI1, DrawingAPI2 -class BridgeTest(unittest.TestCase): +class TestBridge: def test_bridge_shall_draw_with_concrete_api_implementation(cls): ci1 = DrawingAPI1() ci2 = DrawingAPI2() - with patch.object(ci1, "draw_circle") as mock_ci1_draw_circle, patch.object( - ci2, "draw_circle" - ) as mock_ci2_draw_circle: + with ( + patch.object(ci1, "draw_circle") as mock_ci1_draw_circle, + patch.object(ci2, "draw_circle") as mock_ci2_draw_circle, + ): sh1 = CircleShape(1, 2, 3, ci1) sh1.draw() - cls.assertEqual(mock_ci1_draw_circle.call_count, 1) + assert mock_ci1_draw_circle.call_count == 1 sh2 = CircleShape(1, 2, 3, ci2) sh2.draw() - cls.assertEqual(mock_ci2_draw_circle.call_count, 1) + assert mock_ci2_draw_circle.call_count == 1 def test_bridge_shall_scale_both_api_circles_with_own_implementation(cls): SCALE_FACTOR = 2 @@ -31,12 +31,13 @@ def test_bridge_shall_scale_both_api_circles_with_own_implementation(cls): sh2 = CircleShape(1, 2, CIRCLE2_RADIUS, ci2) sh1.scale(SCALE_FACTOR) sh2.scale(SCALE_FACTOR) - cls.assertEqual(sh1._radius, EXPECTED_CIRCLE1_RADIUS) - cls.assertEqual(sh2._radius, EXPECTED_CIRCLE2_RADIUS) - with patch.object(sh1, "scale") as mock_sh1_scale_circle, patch.object( - sh2, "scale" - ) as mock_sh2_scale_circle: + assert sh1._radius == EXPECTED_CIRCLE1_RADIUS + assert sh2._radius == EXPECTED_CIRCLE2_RADIUS + with ( + patch.object(sh1, "scale") as mock_sh1_scale_circle, + patch.object(sh2, "scale") as mock_sh2_scale_circle, + ): sh1.scale(2) sh2.scale(2) - cls.assertEqual(mock_sh1_scale_circle.call_count, 1) - cls.assertEqual(mock_sh2_scale_circle.call_count, 1) + assert mock_sh1_scale_circle.call_count == 1 + assert mock_sh2_scale_circle.call_count == 1 diff --git a/tests/structural/test_decorator.py b/tests/structural/test_decorator.py index 8a4154a91..04a7a112e 100644 --- a/tests/structural/test_decorator.py +++ b/tests/structural/test_decorator.py @@ -1,24 +1,18 @@ -import unittest - from patterns.structural.decorator import BoldWrapper, ItalicWrapper, TextTag -class TestTextWrapping(unittest.TestCase): - def setUp(self): +class TestTextWrapping: + def setup_method(self): self.raw_string = TextTag("raw but not cruel") def test_italic(self): - self.assertEqual( - ItalicWrapper(self.raw_string).render(), "raw but not cruel" - ) + assert ItalicWrapper(self.raw_string).render() == "raw but not cruel" def test_bold(self): - self.assertEqual( - BoldWrapper(self.raw_string).render(), "raw but not cruel" - ) + assert BoldWrapper(self.raw_string).render() == "raw but not cruel" def test_mixed_bold_and_italic(self): - self.assertEqual( - BoldWrapper(ItalicWrapper(self.raw_string)).render(), - "raw but not cruel", + assert ( + BoldWrapper(ItalicWrapper(self.raw_string)).render() + == "raw but not cruel" ) diff --git a/tests/structural/test_facade.py b/tests/structural/test_facade.py 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 index 3409bf0b5..a8965e2cf 100644 --- a/tests/structural/test_proxy.py +++ b/tests/structural/test_proxy.py @@ -1,37 +1,17 @@ -import sys -import unittest -from io import StringIO - from patterns.structural.proxy import Proxy, client -class ProxyTest(unittest.TestCase): - @classmethod - def setUpClass(cls): - """Class scope setup.""" - cls.proxy = Proxy() - - def setUp(cls): - """Function/test case scope setup.""" - cls.output = StringIO() - cls.saved_stdout = sys.stdout - sys.stdout = cls.output - - def tearDown(cls): - """Function/test case scope teardown.""" - cls.output.close() - sys.stdout = cls.saved_stdout - - def test_do_the_job_for_admin_shall_pass(self): - client(self.proxy, "admin") - assert self.output.getvalue() == ( +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): - client(self.proxy, "anonymous") - assert self.output.getvalue() == ( + def test_do_the_job_for_anonymous_shall_reject(self, capsys): + client(Proxy(), "anonymous") + assert capsys.readouterr().out == ( "[log] Doing the job for anonymous is requested.\n" "[log] I can do the job just for `admins`.\n" ) diff --git a/tests/test_hsm.py b/tests/test_hsm.py index f42323a92..4ddd4d331 100644 --- a/tests/test_hsm.py +++ b/tests/test_hsm.py @@ -1,6 +1,7 @@ -import unittest from unittest.mock import patch +import pytest + from patterns.other.hsm.hsm import ( Active, HierachicalStateMachine, @@ -12,26 +13,26 @@ ) -class HsmMethodTest(unittest.TestCase): +class TestHsmMethod: @classmethod - def setUpClass(cls): + def setup_class(cls): cls.hsm = HierachicalStateMachine() def test_initial_state_shall_be_standby(cls): - cls.assertEqual(isinstance(cls.hsm._current_state, Standby), True) + assert isinstance(cls.hsm._current_state, Standby) def test_unsupported_state_shall_raise_exception(cls): - with cls.assertRaises(UnsupportedState): + with pytest.raises(UnsupportedState): cls.hsm._next_state("missing") def test_unsupported_message_type_shall_raise_exception(cls): - with cls.assertRaises(UnsupportedMessageType): + 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) + 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): @@ -39,61 +40,60 @@ def test_method_perform_switchover_shall_return_specifically(cls): (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) + assert return_value == expected_return_value -class StandbyStateTest(unittest.TestCase): +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) + 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: + 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) + 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) + assert isinstance(cls.hsm._current_state, Suspect) def test_given_standby_on_message_diagnostics_failed_shall_raise_exception_and_keep_in_state( cls, ): - with cls.assertRaises(UnsupportedTransition): + with pytest.raises(UnsupportedTransition): cls.hsm.on_message("diagnostics failed") - cls.assertEqual(isinstance(cls.hsm._current_state, Standby), True) + assert isinstance(cls.hsm._current_state, Standby) def test_given_standby_on_message_diagnostics_passed_shall_raise_exception_and_keep_in_state( cls, ): - with cls.assertRaises(UnsupportedTransition): + with pytest.raises(UnsupportedTransition): cls.hsm.on_message("diagnostics passed") - cls.assertEqual(isinstance(cls.hsm._current_state, Standby), True) + assert isinstance(cls.hsm._current_state, Standby) def test_given_standby_on_message_operator_inservice_shall_raise_exception_and_keep_in_state( cls, ): - with cls.assertRaises(UnsupportedTransition): + with pytest.raises(UnsupportedTransition): cls.hsm.on_message("operator inservice") - cls.assertEqual(isinstance(cls.hsm._current_state, Standby), True) + assert isinstance(cls.hsm._current_state, Standby)