From a17c6524c36ff7d398a1bb1ef8e120ff1e8563e2 Mon Sep 17 00:00:00 2001 From: Sassan Haradji Date: Sat, 16 Mar 2024 03:01:27 +0400 Subject: [PATCH] refactor: improve creating new state classes in `combine_reducers` upon registering/unregistering sub-reducers feat: add test fixture for snapshot testing the store chore(test): add test infrastructure for snapshot testing the store test: move demo files to test files and update the to use snapshot fixture --- .github/workflows/integration_delivery.yml | 67 ++++++- .gitignore | 1 + CHANGELOG.md | 8 + README.md | 2 + poetry.lock | 213 ++++++++++++++++++--- pyproject.toml | 14 +- redux/__init__.py | 14 +- redux/basic_types.py | 21 ++ redux/combine_reducers.py | 43 ++--- redux/test.py | 128 +++++++++++++ tests/conftest.py | 44 +++++ tests/results/store:test_general-000.json | 9 + tests/results/store:test_general-001.json | 9 + tests/results/store:test_general-002.json | 9 + tests/results/store:test_general-003.json | 9 + tests/results/store:test_general-004.json | 9 + tests/results/store:test_general-005.json | 12 ++ tests/results/store:test_general-006.json | 12 ++ tests/results/store:test_general-007.json | 12 ++ tests/results/store:test_general-008.json | 12 ++ tests/results/store:test_general-009.json | 9 + tests/results/store:test_general-010.json | 9 + tests/results/store:test_general-011.json | 9 + tests/results/store:test_general-012.json | 9 + tests/results/store:test_general-013.json | 9 + tests/results/store:test_general-014.json | 9 + tests/results/store:test_todo-000.json | 9 + tests/results/store:test_todo-001.json | 14 ++ tests/results/store:test_todo-002.json | 14 ++ demo.py => tests/test_features.py | 69 ++++--- tests/test_todo.py | 115 +++++++++++ todo_demo.py | 108 ----------- 32 files changed, 823 insertions(+), 208 deletions(-) create mode 100644 redux/test.py create mode 100644 tests/conftest.py create mode 100644 tests/results/store:test_general-000.json create mode 100644 tests/results/store:test_general-001.json create mode 100644 tests/results/store:test_general-002.json create mode 100644 tests/results/store:test_general-003.json create mode 100644 tests/results/store:test_general-004.json create mode 100644 tests/results/store:test_general-005.json create mode 100644 tests/results/store:test_general-006.json create mode 100644 tests/results/store:test_general-007.json create mode 100644 tests/results/store:test_general-008.json create mode 100644 tests/results/store:test_general-009.json create mode 100644 tests/results/store:test_general-010.json create mode 100644 tests/results/store:test_general-011.json create mode 100644 tests/results/store:test_general-012.json create mode 100644 tests/results/store:test_general-013.json create mode 100644 tests/results/store:test_general-014.json create mode 100644 tests/results/store:test_todo-000.json create mode 100644 tests/results/store:test_todo-001.json create mode 100644 tests/results/store:test_todo-002.json rename demo.py => tests/test_features.py (75%) create mode 100644 tests/test_todo.py delete mode 100644 todo_demo.py diff --git a/.github/workflows/integration_delivery.yml b/.github/workflows/integration_delivery.yml index c0d7ec3..93dba12 100644 --- a/.github/workflows/integration_delivery.yml +++ b/.github/workflows/integration_delivery.yml @@ -93,6 +93,70 @@ jobs: - name: Lint run: poetry run poe lint + test: + name: Test + needs: + - dependencies + runs-on: ubuntu-latest + environment: + name: test + url: https://app.codecov.io/gh/${{ github.repository }}/ + steps: + - uses: actions/checkout@v4 + name: Checkout + + - uses: actions/setup-python@v5 + name: Setup Python + with: + python-version: ${{ env.PYTHON_VERSION }} + architecture: x64 + + - name: Load Cached Poetry + id: cached-poetry + uses: actions/cache/restore@v4 + with: + path: | + ~/.cache + ~/.local + key: poetry-${{ hashFiles('poetry.lock') }} + + - name: Test + run: poetry run poe test --cov-report=xml --cov-report=html + + - name: Prepare list of JSON files with mismatching pairs + if: failure() + run: | + mkdir -p artifacts + for file in $(find tests/ -name "*.mismatch.json"); do + base=${file%.mismatch.json}.json + if [[ -f "$base" ]]; then + echo "$file" >> artifacts/files_to_upload.txt + echo "$base" >> artifacts/files_to_upload.txt + fi + done + + - name: Collect Mismatching Store Snapshots + if: failure() + uses: actions/upload-artifact@v4 + with: + name: mismatching-snapshots + path: | + @artifacts/files_to_upload.txt + + - name: Collect HTML Coverage Report + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: htmlcov + + - name: Upload Coverage to Codecov + uses: codecov/codecov-action@v4 + with: + file: ./coverage.xml + flags: integration + fail_ci_if_error: true + token: ${{ secrets.CODECOV_TOKEN }} + build: name: Build needs: @@ -150,6 +214,7 @@ jobs: needs: - type-check - lint + - test - build runs-on: ubuntu-latest environment: @@ -179,11 +244,11 @@ jobs: needs: - type-check - lint + - test - build - pypi-publish environment: name: release - url: https://pypi.org/p/${{ needs.build.outputs.name }} runs-on: ubuntu-latest permissions: contents: write diff --git a/.gitignore b/.gitignore index df5355e..c9e4c76 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,7 @@ nosetests.xml coverage.xml *.cover .hypothesis/ +tests/**/results/*mismatch.json # Translations *.mo diff --git a/CHANGELOG.md b/CHANGELOG.md index fe88af5..8e674a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## Version 0.12.0 + +- refactor: improve creating new state classes in `combine_reducers` upon registering/unregistering + sub-reducers +- feat: add test fixture for snapshot testing the store +- chore(test): add test infrastructure for snapshot testing the store +- test: move demo files to test files and update the to use snapshot fixture + ## Version 0.11.0 - feat: add `keep_ref` parameter to subscriptions and autoruns, defaulting to `True`, diff --git a/README.md b/README.md index c70152e..0f2391a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # 🚀 Python Redux +[![codecov](https://codecov.io/gh/sassanh/python-redux/graph/badge.svg?token=4F3EWZRLCL)](https://codecov.io/gh/sassanh/python-redux) + ## 🌟 Overview Python Redux is a Redux implementation for Python, bringing Redux's state management diff --git a/poetry.lock b/poetry.lock index 2780791..cf37e82 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,91 @@ # This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.4.4" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "coverage-7.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2"}, + {file = "coverage-7.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c"}, + {file = "coverage-7.4.4-cp310-cp310-win32.whl", hash = "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d"}, + {file = "coverage-7.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f"}, + {file = "coverage-7.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf"}, + {file = "coverage-7.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b"}, + {file = "coverage-7.4.4-cp311-cp311-win32.whl", hash = "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286"}, + {file = "coverage-7.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec"}, + {file = "coverage-7.4.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76"}, + {file = "coverage-7.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9"}, + {file = "coverage-7.4.4-cp312-cp312-win32.whl", hash = "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0"}, + {file = "coverage-7.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e"}, + {file = "coverage-7.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384"}, + {file = "coverage-7.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1"}, + {file = "coverage-7.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a"}, + {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409"}, + {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e"}, + {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd"}, + {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7"}, + {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c"}, + {file = "coverage-7.4.4-cp38-cp38-win32.whl", hash = "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e"}, + {file = "coverage-7.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8"}, + {file = "coverage-7.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d"}, + {file = "coverage-7.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357"}, + {file = "coverage-7.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e"}, + {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e"}, + {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4"}, + {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec"}, + {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd"}, + {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade"}, + {file = "coverage-7.4.4-cp39-cp39-win32.whl", hash = "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57"}, + {file = "coverage-7.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c"}, + {file = "coverage-7.4.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677"}, + {file = "coverage-7.4.4.tar.gz", hash = "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49"}, +] + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + [[package]] name = "nodeenv" version = "1.8.0" @@ -14,6 +100,17 @@ files = [ [package.dependencies] setuptools = "*" +[[package]] +name = "packaging" +version = "24.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, +] + [[package]] name = "pastel" version = "0.2.1" @@ -25,6 +122,21 @@ files = [ {file = "pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d"}, ] +[[package]] +name = "pluggy" +version = "1.4.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + [[package]] name = "poethepoet" version = "0.24.4" @@ -61,58 +173,99 @@ nodeenv = ">=1.6.0" all = ["twine (>=3.4.1)"] dev = ["twine (>=3.4.1)"] +[[package]] +name = "pytest" +version = "8.1.1" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"}, + {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.4,<2.0" + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + [[package]] name = "python-immutable" -version = "1.0.2" +version = "1.0.5" description = "Immutable implementation for Python using dataclasses" optional = false python-versions = ">=3.9,<4.0" files = [ - {file = "python_immutable-1.0.2-py3-none-any.whl", hash = "sha256:603e1275ea79969106d999e553465364b284c90cd4fbae89dadfd1ca597d2d2f"}, - {file = "python_immutable-1.0.2.tar.gz", hash = "sha256:42de875494a5eefd0288ee9a4e18d8767a2cc4fdd3aa34a3e5b49b72f3bcc7fb"}, + {file = "python_immutable-1.0.5-py3-none-any.whl", hash = "sha256:ea1309539a954ff2b527e4d175261ceddd96dd97a4bec436f5825ebb2d4f2818"}, + {file = "python_immutable-1.0.5.tar.gz", hash = "sha256:e2a30d8b4b1fe8dfe3aedc02392b0e567915b3aabbb6f57abb1689780e2ec1a4"}, ] +[package.dependencies] +typing-extensions = ">=4.10.0,<5.0.0" + [[package]] name = "ruff" -version = "0.3.2" +version = "0.3.3" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.3.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77f2612752e25f730da7421ca5e3147b213dca4f9a0f7e0b534e9562c5441f01"}, - {file = "ruff-0.3.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9966b964b2dd1107797be9ca7195002b874424d1d5472097701ae8f43eadef5d"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b83d17ff166aa0659d1e1deaf9f2f14cbe387293a906de09bc4860717eb2e2da"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb875c6cc87b3703aeda85f01c9aebdce3d217aeaca3c2e52e38077383f7268a"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be75e468a6a86426430373d81c041b7605137a28f7014a72d2fc749e47f572aa"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:967978ac2d4506255e2f52afe70dda023fc602b283e97685c8447d036863a302"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1231eacd4510f73222940727ac927bc5d07667a86b0cbe822024dd00343e77e9"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c6d613b19e9a8021be2ee1d0e27710208d1603b56f47203d0abbde906929a9b"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8439338a6303585d27b66b4626cbde89bb3e50fa3cae86ce52c1db7449330a7"}, - {file = "ruff-0.3.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:de8b480d8379620cbb5ea466a9e53bb467d2fb07c7eca54a4aa8576483c35d36"}, - {file = "ruff-0.3.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b74c3de9103bd35df2bb05d8b2899bf2dbe4efda6474ea9681280648ec4d237d"}, - {file = "ruff-0.3.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f380be9fc15a99765c9cf316b40b9da1f6ad2ab9639e551703e581a5e6da6745"}, - {file = "ruff-0.3.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0ac06a3759c3ab9ef86bbeca665d31ad3aa9a4b1c17684aadb7e61c10baa0df4"}, - {file = "ruff-0.3.2-py3-none-win32.whl", hash = "sha256:9bd640a8f7dd07a0b6901fcebccedadeb1a705a50350fb86b4003b805c81385a"}, - {file = "ruff-0.3.2-py3-none-win_amd64.whl", hash = "sha256:0c1bdd9920cab5707c26c8b3bf33a064a4ca7842d91a99ec0634fec68f9f4037"}, - {file = "ruff-0.3.2-py3-none-win_arm64.whl", hash = "sha256:5f65103b1d76e0d600cabd577b04179ff592064eaa451a70a81085930e907d0b"}, - {file = "ruff-0.3.2.tar.gz", hash = "sha256:fa78ec9418eb1ca3db392811df3376b46471ae93792a81af2d1cbb0e5dcb5142"}, + {file = "ruff-0.3.3-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:973a0e388b7bc2e9148c7f9be8b8c6ae7471b9be37e1cc732f8f44a6f6d7720d"}, + {file = "ruff-0.3.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cfa60d23269d6e2031129b053fdb4e5a7b0637fc6c9c0586737b962b2f834493"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1eca7ff7a47043cf6ce5c7f45f603b09121a7cc047447744b029d1b719278eb5"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7d3f6762217c1da954de24b4a1a70515630d29f71e268ec5000afe81377642d"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b24c19e8598916d9c6f5a5437671f55ee93c212a2c4c569605dc3842b6820386"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5a6cbf216b69c7090f0fe4669501a27326c34e119068c1494f35aaf4cc683778"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:352e95ead6964974b234e16ba8a66dad102ec7bf8ac064a23f95371d8b198aab"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d6ab88c81c4040a817aa432484e838aaddf8bfd7ca70e4e615482757acb64f8"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79bca3a03a759cc773fca69e0bdeac8abd1c13c31b798d5bb3c9da4a03144a9f"}, + {file = "ruff-0.3.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2700a804d5336bcffe063fd789ca2c7b02b552d2e323a336700abb8ae9e6a3f8"}, + {file = "ruff-0.3.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fd66469f1a18fdb9d32e22b79f486223052ddf057dc56dea0caaf1a47bdfaf4e"}, + {file = "ruff-0.3.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:45817af234605525cdf6317005923bf532514e1ea3d9270acf61ca2440691376"}, + {file = "ruff-0.3.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0da458989ce0159555ef224d5b7c24d3d2e4bf4c300b85467b08c3261c6bc6a8"}, + {file = "ruff-0.3.3-py3-none-win32.whl", hash = "sha256:f2831ec6a580a97f1ea82ea1eda0401c3cdf512cf2045fa3c85e8ef109e87de0"}, + {file = "ruff-0.3.3-py3-none-win_amd64.whl", hash = "sha256:be90bcae57c24d9f9d023b12d627e958eb55f595428bafcb7fec0791ad25ddfc"}, + {file = "ruff-0.3.3-py3-none-win_arm64.whl", hash = "sha256:0171aab5fecdc54383993389710a3d1227f2da124d76a2784a7098e818f92d61"}, + {file = "ruff-0.3.3.tar.gz", hash = "sha256:38671be06f57a2f8aba957d9f701ea889aa5736be806f18c0cd03d6ff0cbca8d"}, ] [[package]] name = "setuptools" -version = "69.1.0" +version = "69.2.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-69.1.0-py3-none-any.whl", hash = "sha256:c054629b81b946d63a9c6e732bc8b2513a7c3ea645f11d0139a2191d735c60c6"}, - {file = "setuptools-69.1.0.tar.gz", hash = "sha256:850894c4195f09c4ed30dba56213bf7c3f21d86ed6bdaafb5df5972593bfc401"}, + {file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"}, + {file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"}, ] [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "tomli" @@ -127,16 +280,16 @@ files = [ [[package]] name = "typing-extensions" -version = "4.9.0" +version = "4.10.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, - {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, + {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, + {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, ] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "b119d5c58b44d6c35d2dd2d5287052e3d9a84fd182c9d2160edd9126182d526c" +content-hash = "d133175be95d6fa0edb98478c528d11fda8bedc0b30377cbc0f9f9a52a9c5525" diff --git a/pyproject.toml b/pyproject.toml index 743c8da..e26ab32 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "python-redux" -version = "0.11.0" +version = "0.12.0" description = "Redux implementation for Python" authors = ["Sassan Haradji "] license = "Apache-2.0" @@ -9,7 +9,7 @@ packages = [{ include = "redux" }] [tool.poetry.dependencies] python = "^3.11" -python-immutable = "^1.0.2" +python-immutable = "^1.0.5" typing-extensions = "^4.9.0" [tool.poetry.group.dev] @@ -18,7 +18,9 @@ optional = true [tool.poetry.group.dev.dependencies] poethepoet = "^0.24.4" pyright = "^1.1.354" -ruff = "^0.3.2" +ruff = "^0.3.3" +pytest = "^8.1.1" +pytest-cov = "^4.1.0" [build-system] requires = ["poetry-core"] @@ -31,7 +33,8 @@ todo_demo = "todo_demo:main" [tool.poe.tasks] lint = "ruff check . --unsafe-fixes" typecheck = "pyright -p pyproject.toml ." -sanity = ["typecheck", "lint"] +test = "pytest --cov=redux --cov-report=term-missing" +sanity = ["typecheck", "lint", "test"] [tool.ruff] lint.select = ['ALL'] @@ -44,6 +47,9 @@ docstring-quotes = "double" inline-quotes = "single" multiline-quotes = "double" +[tool.ruff.lint.per-file-ignores] +"tests/*" = ["S101"] + [tool.ruff.format] quote-style = 'single' diff --git a/redux/__init__.py b/redux/__init__.py index 032bf89..1aa3d51 100644 --- a/redux/__init__.py +++ b/redux/__init__.py @@ -5,7 +5,12 @@ AutorunReturnType, AutorunType, BaseAction, + BaseCombineReducerState, BaseEvent, + CombineReducerAction, + CombineReducerInitAction, + CombineReducerRegisterAction, + CombineReducerUnregisterAction, CompleteReducerResult, CreateStoreOptions, Dispatch, @@ -22,14 +27,7 @@ is_complete_reducer_result, is_state_reducer_result, ) -from .combine_reducers import ( - BaseCombineReducerState, - CombineReducerAction, - CombineReducerInitAction, - CombineReducerRegisterAction, - CombineReducerUnregisterAction, - combine_reducers, -) +from .combine_reducers import combine_reducers from .main import Store __all__ = ( diff --git a/redux/basic_types.py b/redux/basic_types.py index 49956f9..77974c6 100644 --- a/redux/basic_types.py +++ b/redux/basic_types.py @@ -170,3 +170,24 @@ def __call__( | None = None, ) -> None: ... + + +class BaseCombineReducerState(Immutable): + _id: str + + +class CombineReducerAction(BaseAction): + _id: str + + +class CombineReducerInitAction(CombineReducerAction, InitAction): + key: str + + +class CombineReducerRegisterAction(CombineReducerAction): + key: str + reducer: ReducerType + + +class CombineReducerUnregisterAction(CombineReducerAction): + key: str diff --git a/redux/combine_reducers.py b/redux/combine_reducers.py index 6be126f..3d4286c 100644 --- a/redux/combine_reducers.py +++ b/redux/combine_reducers.py @@ -5,16 +5,20 @@ import functools import operator import uuid -from dataclasses import asdict, make_dataclass +from dataclasses import asdict, fields, make_dataclass from typing import TYPE_CHECKING, Any, TypeVar, cast from .basic_types import ( Action, BaseAction, + BaseCombineReducerState, BaseEvent, + CombineReducerAction, + CombineReducerInitAction, + CombineReducerRegisterAction, + CombineReducerUnregisterAction, CompleteReducerResult, Event, - Immutable, InitAction, is_complete_reducer_result, ) @@ -23,27 +27,6 @@ from redux import ReducerType -class BaseCombineReducerState(Immutable): - _id: str - - -class CombineReducerAction(BaseAction): - _id: str - - -class CombineReducerInitAction(CombineReducerAction, InitAction): - key: str - - -class CombineReducerRegisterAction(CombineReducerAction): - key: str - reducer: ReducerType - - -class CombineReducerUnregisterAction(CombineReducerAction): - key: str - - CombineReducerState = TypeVar( 'CombineReducerState', bound=BaseCombineReducerState, @@ -64,7 +47,7 @@ def combine_reducers( state_class = cast( type[state_type], make_dataclass( - 'combined_reducer', + state_type.__name__, ('_id', *reducers.keys()), frozen=True, kw_only=True, @@ -84,9 +67,10 @@ def combined_reducer( reducer = action.reducer reducers[key] = reducer state_class = make_dataclass( - 'combined_reducer', + state_type.__name__, ('_id', *reducers.keys()), frozen=True, + kw_only=True, ) reducer_result = reducer( None, @@ -123,11 +107,16 @@ def combined_reducer( key = action.key del reducers[key] - fields_copy = copy.copy(cast(Any, state_class).__dataclass_fields__) + fields_copy = {field.name: field for field in fields(state_class)} annotations_copy = copy.deepcopy(state_class.__annotations__) del fields_copy[key] del annotations_copy[key] - state_class = make_dataclass('combined_reducer', annotations_copy) + state_class = make_dataclass( + state_type.__name__, + annotations_copy, + frozen=True, + kw_only=True, + ) cast(Any, state_class).__dataclass_fields__ = fields_copy state = state_class( diff --git a/redux/test.py b/redux/test.py new file mode 100644 index 0000000..0ed0808 --- /dev/null +++ b/redux/test.py @@ -0,0 +1,128 @@ +# ruff: noqa: S101 +"""Let the test check snapshots of the window during execution.""" +from __future__ import annotations + +import dataclasses +import json +import os +from enum import IntEnum, StrEnum +from types import NoneType +from typing import TYPE_CHECKING, Any + +import pytest +from immutable import Immutable, is_immutable + +if TYPE_CHECKING: + from logging import Logger + from pathlib import Path + + from _pytest.fixtures import SubRequest + + from redux.main import Store + + +override_store_snapshots = os.environ.get('REDUX_TEST_OVERRIDE_SNAPSHOTS', '0') == '1' + + +Atom = int | float | str | bool | NoneType | dict[str, 'Atom'] | list['Atom'] + + +class StoreSnapshotContext: + """Context object for tests taking snapshots of the store.""" + + def __init__( + self: StoreSnapshotContext, + test_id: str, + path: Path, + logger: Logger, + ) -> None: + """Create a new store snapshot context.""" + self.test_counter = 0 + self.id = test_id + self.results_dir = path.parent / 'results' + self.logger = logger + self.results_dir.mkdir(exist_ok=True) + + def _convert_value(self: StoreSnapshotContext, obj: object | type) -> Atom: + import sys + from pathlib import Path + + if is_immutable(obj): + return self._convert_dataclass_to_dict(obj) + if isinstance(obj, (list, tuple)): + return [self._convert_value(i) for i in obj] + if isinstance(obj, type): + file_path = sys.modules[obj.__module__].__file__ + if file_path: + return f"""{Path(file_path).relative_to(Path().absolute()).as_posix()}:{ + obj.__name__}""" + return f'{obj.__module__}:{obj.__name__}' + if callable(obj): + return self._convert_value(obj()) + if isinstance(obj, StrEnum): + return str(obj) + if isinstance(obj, IntEnum): + return int(obj) + if isinstance(obj, (int, float, str, bool, NoneType)): + return obj + self.logger.warning( + 'Unable to serialize', + extra={'type': type(obj), 'value': obj}, + ) + return None + + def _convert_dataclass_to_dict( + self: StoreSnapshotContext, + obj: Immutable, + ) -> dict[str, Any]: + result = {} + for field in dataclasses.fields(obj): + value = self._convert_value(getattr(obj, field.name)) + result[field.name] = value + return result + + def set_store(self: StoreSnapshotContext, store: Store) -> None: + """Set the store to take snapshots of.""" + self.store = store + + @property + def snapshot(self: StoreSnapshotContext) -> str: + """Return the snapshot of the current state of the store.""" + return ( + json.dumps(self._convert_value(self.store._state), indent=2) # noqa: SLF001 + if self.store._state # noqa: SLF001 + else '' + ) + + def take(self: StoreSnapshotContext, title: str | None = None) -> None: + """Take a snapshot of the current window.""" + if title: + filename = f"""store:{"_".join(self.id.split(":")[-1:])}:{title}-{ + self.test_counter:03d}""" + else: + filename = ( + f'store:{"_".join(self.id.split(":")[-1:])}-{self.test_counter:03d}' + ) + + path = self.results_dir / filename + json_path = path.with_suffix('.json') + + new_snapshot = self.snapshot + if json_path.exists() and not override_store_snapshots: + old_snapshot = json_path.read_text() + if old_snapshot != new_snapshot: + path.with_suffix('.mismatch.json').write_text(new_snapshot) + assert old_snapshot == new_snapshot + json_path.write_text(new_snapshot) + + self.test_counter += 1 + + +@pytest.fixture() +def snapshot_store(request: SubRequest, logger: Logger) -> StoreSnapshotContext: + """Take a snapshot of the current state of the store.""" + return StoreSnapshotContext( + request.node.nodeid, + request.node.path, + logger, + ) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..99b79e5 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,44 @@ +"""Pytest configuration file for the tests.""" +from __future__ import annotations + +import datetime +import random +import time +import uuid +from typing import TYPE_CHECKING + +import pytest + +from redux.test import snapshot_store + +if TYPE_CHECKING: + from logging import Logger + +__all__ = ['snapshot_store'] + + +@pytest.fixture() +def logger() -> Logger: + import logging + + return logging.getLogger('test') + + +@pytest.fixture(autouse=True) +def _(monkeypatch: pytest.MonkeyPatch) -> None: + """Mock external resources.""" + random.seed(0) + + class DateTime(datetime.datetime): + @classmethod + def now(cls: type[DateTime], tz: datetime.tzinfo | None = None) -> DateTime: + _ = tz + return DateTime(2023, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc) + + monkeypatch.setattr(datetime, 'datetime', DateTime) + monkeypatch.setattr( + time, + 'time', + lambda: datetime.datetime.now(tz=datetime.timezone.utc).timestamp(), + ) + monkeypatch.setattr(uuid, 'uuid4', lambda: uuid.UUID(int=random.getrandbits(128))) diff --git a/tests/results/store:test_general-000.json b/tests/results/store:test_general-000.json new file mode 100644 index 0000000..fc51f07 --- /dev/null +++ b/tests/results/store:test_general-000.json @@ -0,0 +1,9 @@ +{ + "_id": "e3e70682c2094cac629f6fbed82c07cd", + "straight": { + "count": 0 + }, + "base10": { + "count": 10 + } +} \ No newline at end of file diff --git a/tests/results/store:test_general-001.json b/tests/results/store:test_general-001.json new file mode 100644 index 0000000..fc51f07 --- /dev/null +++ b/tests/results/store:test_general-001.json @@ -0,0 +1,9 @@ +{ + "_id": "e3e70682c2094cac629f6fbed82c07cd", + "straight": { + "count": 0 + }, + "base10": { + "count": 10 + } +} \ No newline at end of file diff --git a/tests/results/store:test_general-002.json b/tests/results/store:test_general-002.json new file mode 100644 index 0000000..28bc30a --- /dev/null +++ b/tests/results/store:test_general-002.json @@ -0,0 +1,9 @@ +{ + "_id": "e3e70682c2094cac629f6fbed82c07cd", + "straight": { + "count": 1 + }, + "base10": { + "count": 11 + } +} \ No newline at end of file diff --git a/tests/results/store:test_general-003.json b/tests/results/store:test_general-003.json new file mode 100644 index 0000000..28bc30a --- /dev/null +++ b/tests/results/store:test_general-003.json @@ -0,0 +1,9 @@ +{ + "_id": "e3e70682c2094cac629f6fbed82c07cd", + "straight": { + "count": 1 + }, + "base10": { + "count": 11 + } +} \ No newline at end of file diff --git a/tests/results/store:test_general-004.json b/tests/results/store:test_general-004.json new file mode 100644 index 0000000..28bc30a --- /dev/null +++ b/tests/results/store:test_general-004.json @@ -0,0 +1,9 @@ +{ + "_id": "e3e70682c2094cac629f6fbed82c07cd", + "straight": { + "count": 1 + }, + "base10": { + "count": 11 + } +} \ No newline at end of file diff --git a/tests/results/store:test_general-005.json b/tests/results/store:test_general-005.json new file mode 100644 index 0000000..1538a23 --- /dev/null +++ b/tests/results/store:test_general-005.json @@ -0,0 +1,12 @@ +{ + "_id": "e3e70682c2094cac629f6fbed82c07cd", + "straight": { + "count": 1 + }, + "base10": { + "count": 11 + }, + "inverse": { + "count": 0 + } +} \ No newline at end of file diff --git a/tests/results/store:test_general-006.json b/tests/results/store:test_general-006.json new file mode 100644 index 0000000..e5e257e --- /dev/null +++ b/tests/results/store:test_general-006.json @@ -0,0 +1,12 @@ +{ + "_id": "e3e70682c2094cac629f6fbed82c07cd", + "straight": { + "count": 2 + }, + "base10": { + "count": 12 + }, + "inverse": { + "count": -1 + } +} \ No newline at end of file diff --git a/tests/results/store:test_general-007.json b/tests/results/store:test_general-007.json new file mode 100644 index 0000000..e5e257e --- /dev/null +++ b/tests/results/store:test_general-007.json @@ -0,0 +1,12 @@ +{ + "_id": "e3e70682c2094cac629f6fbed82c07cd", + "straight": { + "count": 2 + }, + "base10": { + "count": 12 + }, + "inverse": { + "count": -1 + } +} \ No newline at end of file diff --git a/tests/results/store:test_general-008.json b/tests/results/store:test_general-008.json new file mode 100644 index 0000000..e5e257e --- /dev/null +++ b/tests/results/store:test_general-008.json @@ -0,0 +1,12 @@ +{ + "_id": "e3e70682c2094cac629f6fbed82c07cd", + "straight": { + "count": 2 + }, + "base10": { + "count": 12 + }, + "inverse": { + "count": -1 + } +} \ No newline at end of file diff --git a/tests/results/store:test_general-009.json b/tests/results/store:test_general-009.json new file mode 100644 index 0000000..fd43740 --- /dev/null +++ b/tests/results/store:test_general-009.json @@ -0,0 +1,9 @@ +{ + "_id": "e3e70682c2094cac629f6fbed82c07cd", + "base10": { + "count": 12 + }, + "inverse": { + "count": -1 + } +} \ No newline at end of file diff --git a/tests/results/store:test_general-010.json b/tests/results/store:test_general-010.json new file mode 100644 index 0000000..fd43740 --- /dev/null +++ b/tests/results/store:test_general-010.json @@ -0,0 +1,9 @@ +{ + "_id": "e3e70682c2094cac629f6fbed82c07cd", + "base10": { + "count": 12 + }, + "inverse": { + "count": -1 + } +} \ No newline at end of file diff --git a/tests/results/store:test_general-011.json b/tests/results/store:test_general-011.json new file mode 100644 index 0000000..b599301 --- /dev/null +++ b/tests/results/store:test_general-011.json @@ -0,0 +1,9 @@ +{ + "_id": "e3e70682c2094cac629f6fbed82c07cd", + "base10": { + "count": 11 + }, + "inverse": { + "count": 0 + } +} \ No newline at end of file diff --git a/tests/results/store:test_general-012.json b/tests/results/store:test_general-012.json new file mode 100644 index 0000000..b599301 --- /dev/null +++ b/tests/results/store:test_general-012.json @@ -0,0 +1,9 @@ +{ + "_id": "e3e70682c2094cac629f6fbed82c07cd", + "base10": { + "count": 11 + }, + "inverse": { + "count": 0 + } +} \ No newline at end of file diff --git a/tests/results/store:test_general-013.json b/tests/results/store:test_general-013.json new file mode 100644 index 0000000..b599301 --- /dev/null +++ b/tests/results/store:test_general-013.json @@ -0,0 +1,9 @@ +{ + "_id": "e3e70682c2094cac629f6fbed82c07cd", + "base10": { + "count": 11 + }, + "inverse": { + "count": 0 + } +} \ No newline at end of file diff --git a/tests/results/store:test_general-014.json b/tests/results/store:test_general-014.json new file mode 100644 index 0000000..b599301 --- /dev/null +++ b/tests/results/store:test_general-014.json @@ -0,0 +1,9 @@ +{ + "_id": "e3e70682c2094cac629f6fbed82c07cd", + "base10": { + "count": 11 + }, + "inverse": { + "count": 0 + } +} \ No newline at end of file diff --git a/tests/results/store:test_todo-000.json b/tests/results/store:test_todo-000.json new file mode 100644 index 0000000..48f5970 --- /dev/null +++ b/tests/results/store:test_todo-000.json @@ -0,0 +1,9 @@ +{ + "items": [ + { + "id": "e3e70682c2094cac629f6fbed82c07cd", + "content": "Initial Item", + "timestamp": 1672531200.0 + } + ] +} \ No newline at end of file diff --git a/tests/results/store:test_todo-001.json b/tests/results/store:test_todo-001.json new file mode 100644 index 0000000..c3e1d4a --- /dev/null +++ b/tests/results/store:test_todo-001.json @@ -0,0 +1,14 @@ +{ + "items": [ + { + "id": "e3e70682c2094cac629f6fbed82c07cd", + "content": "Initial Item", + "timestamp": 1672531200.0 + }, + { + "id": "f728b4fa42485e3a0a5d2f346baa9455", + "content": "New Item", + "timestamp": 1672531200.0 + } + ] +} \ No newline at end of file diff --git a/tests/results/store:test_todo-002.json b/tests/results/store:test_todo-002.json new file mode 100644 index 0000000..c3e1d4a --- /dev/null +++ b/tests/results/store:test_todo-002.json @@ -0,0 +1,14 @@ +{ + "items": [ + { + "id": "e3e70682c2094cac629f6fbed82c07cd", + "content": "Initial Item", + "timestamp": 1672531200.0 + }, + { + "id": "f728b4fa42485e3a0a5d2f346baa9455", + "content": "New Item", + "timestamp": 1672531200.0 + } + ] +} \ No newline at end of file diff --git a/demo.py b/tests/test_features.py similarity index 75% rename from demo.py rename to tests/test_features.py index dc01baf..f60f04a 100644 --- a/demo.py +++ b/tests/test_features.py @@ -2,27 +2,26 @@ from __future__ import annotations import time +from typing import TYPE_CHECKING, TypeAlias from immutable import Immutable -from redux import ( +if TYPE_CHECKING: + from logging import Logger + + from redux.test import StoreSnapshotContext + +from redux.basic_types import ( BaseAction, BaseCombineReducerState, - CombineReducerAction, - CombineReducerRegisterAction, - CombineReducerUnregisterAction, - InitAction, - InitializationActionError, - Store, - combine_reducers, -) -from redux.basic_types import ( BaseEvent, + CombineReducerAction, CompleteReducerResult, FinishAction, + InitAction, + InitializationActionError, ReducerResult, ) -from redux.main import CreateStoreOptions class CountAction(BaseAction): @@ -41,9 +40,6 @@ class DoNothingAction(CountAction): ... -ActionType = InitAction | FinishAction | CountAction | CombineReducerAction - - class CountStateType(Immutable): count: int @@ -54,6 +50,9 @@ class StateType(BaseCombineReducerState): inverse: CountStateType +ActionType: TypeAlias = InitAction | FinishAction | CountAction | CombineReducerAction + + # Reducers < def straight_reducer( state: CountStateType | None, @@ -114,22 +113,32 @@ def inverse_reducer( return state -reducer, reducer_id = combine_reducers( - state_type=StateType, - action_type=ActionType, # pyright: ignore [reportArgumentType] - event_type=SleepEvent | PrintEvent, # pyright: ignore [reportArgumentType] - straight=straight_reducer, - base10=base10_reducer, -) # > -def main() -> None: +def test_general(snapshot_store: StoreSnapshotContext, logger: Logger) -> None: + from redux import ( + CombineReducerRegisterAction, + CombineReducerUnregisterAction, + Store, + ) + from redux.combine_reducers import combine_reducers + from redux.main import CreateStoreOptions + + reducer, reducer_id = combine_reducers( + state_type=StateType, + action_type=ActionType, # pyright: ignore [reportArgumentType] + event_type=SleepEvent | PrintEvent, # pyright: ignore [reportArgumentType] + straight=straight_reducer, + base10=base10_reducer, + ) + # Initialization < store = Store( reducer, CreateStoreOptions(auto_init=True, threads=2), ) + snapshot_store.set_store(store) def event_handler(event: SleepEvent) -> None: time.sleep(event.duration) @@ -140,7 +149,7 @@ def event_handler(event: SleepEvent) -> None: # ----- # Subscription < - store.subscribe(lambda state: print('Subscription state:', state)) + store.subscribe(lambda _: snapshot_store.take()) # > # ----- @@ -148,15 +157,15 @@ def event_handler(event: SleepEvent) -> None: # Autorun < @store.autorun(lambda state: state.base10) def render(base10_value: CountStateType) -> int: - print('Autorun:', base10_value) + snapshot_store.take() return base10_value.count - render.subscribe(lambda a: print(a)) + render.subscribe(lambda a: logger.info(a)) - print(f'Render output {render()}') + snapshot_store.take() store.dispatch(IncrementAction()) - print(f'Render output {render()}') + snapshot_store.take() store.dispatch( CombineReducerRegisterAction( @@ -167,7 +176,7 @@ def render(base10_value: CountStateType) -> int: ) store.dispatch(DoNothingAction()) - print(f'Render output {render()}') + snapshot_store.take() store.dispatch( CombineReducerUnregisterAction( @@ -175,10 +184,10 @@ def render(base10_value: CountStateType) -> int: key='straight', ), ) - print(f'Render output {render()}') + snapshot_store.take() store.dispatch(DecrementAction()) - print(f'Render output {render()}') + snapshot_store.take() store.dispatch(FinishAction()) # > diff --git a/tests/test_todo.py b/tests/test_todo.py new file mode 100644 index 0000000..2f3a657 --- /dev/null +++ b/tests/test_todo.py @@ -0,0 +1,115 @@ +# ruff: noqa: A003, D100, D101, D102, D103, D104, D105, D107, T201 +from __future__ import annotations + +import time +import uuid +from dataclasses import replace +from typing import TYPE_CHECKING, Sequence + +from immutable import Immutable + +if TYPE_CHECKING: + from logging import Logger + + from redux.test import StoreSnapshotContext + + +def test_todo(snapshot_store: StoreSnapshotContext, logger: Logger) -> None: + from redux import BaseAction, Store + from redux.basic_types import ( + BaseEvent, + CompleteReducerResult, + CreateStoreOptions, + FinishAction, + InitAction, + InitializationActionError, + ReducerResult, + ) + + # state: + class ToDoItem(Immutable): + id: str + content: str + timestamp: float + + class ToDoState(Immutable): + items: Sequence[ToDoItem] + + # actions: + class AddTodoItemAction(BaseAction): + content: str + timestamp: float + + class RemoveTodoItemAction(BaseAction): + id: str + + # events: + class CallApi(BaseEvent): + parameters: object + + # reducer: + def reducer( + state: ToDoState | None, + action: BaseAction, + ) -> ReducerResult[ToDoState, BaseAction, BaseEvent]: + if state is None: + if isinstance(action, InitAction): + return ToDoState( + items=[ + ToDoItem( + id=uuid.uuid4().hex, + content='Initial Item', + timestamp=time.time(), + ), + ], + ) + raise InitializationActionError(action) + if isinstance(action, AddTodoItemAction): + return replace( + state, + items=[ + *state.items, + ToDoItem( + id=uuid.uuid4().hex, + content=action.content, + timestamp=action.timestamp, + ), + ], + ) + if isinstance(action, RemoveTodoItemAction): + return CompleteReducerResult( + state=replace( + state, + actions=[item for item in state.items if item.id != action.id], + ), + events=[CallApi(parameters={})], + ) + return state + + store = Store(reducer, options=CreateStoreOptions(auto_init=True)) + snapshot_store.set_store(store) + + # subscription: + dummy_render = logger.info + store.subscribe(dummy_render) + + # autorun: + @store.autorun( + lambda state: state.items[0].content if len(state.items) > 0 else None, + ) + def reaction(_: str | None) -> None: + snapshot_store.take() + + _ = reaction + + # event listener, note that this will run async in a separate thread, so it can + # include async operations like API calls, etc: + dummy_api_call = logger.info + store.subscribe_event(CallApi, lambda event: dummy_api_call(event.parameters)) + + # dispatch: + store.dispatch(AddTodoItemAction(content='New Item', timestamp=time.time())) + snapshot_store.take() + + store.dispatch(FinishAction()) + snapshot_store.take() diff --git a/todo_demo.py b/todo_demo.py deleted file mode 100644 index 2260b72..0000000 --- a/todo_demo.py +++ /dev/null @@ -1,108 +0,0 @@ -# ruff: noqa: A003, D100, D101, D102, D103, D104, D105, D107, T201 -from __future__ import annotations - -import time -import uuid -from dataclasses import replace -from typing import Sequence - -from immutable import Immutable - -from redux import BaseAction, Store -from redux.basic_types import ( - BaseEvent, - CompleteReducerResult, - FinishAction, - ReducerResult, -) - - -# state: -class ToDoItem(Immutable): - id: str - content: str - timestamp: float - - -class ToDoState(Immutable): - items: Sequence[ToDoItem] - - -# actions: -class AddTodoItemAction(BaseAction): - content: str - timestamp: float - - -class RemoveTodoItemAction(BaseAction): - id: str - - -# events: -class CallApi(BaseEvent): - parameters: object - - -# reducer: -def reducer( - state: ToDoState | None, - action: BaseAction, -) -> ReducerResult[ToDoState, BaseAction, BaseEvent]: - if state is None: - return ToDoState( - items=[ - ToDoItem( - id=uuid.uuid4().hex, - content='Initial Item', - timestamp=time.time(), - ), - ], - ) - if isinstance(action, AddTodoItemAction): - return replace( - state, - items=[ - *state.items, - ToDoItem( - id=uuid.uuid4().hex, - content=action.content, - timestamp=action.timestamp, - ), - ], - ) - if isinstance(action, RemoveTodoItemAction): - return CompleteReducerResult( - state=replace( - state, - actions=[item for item in state.items if item.id != action.id], - ), - events=[CallApi(parameters={})], - ) - return state - - -def main() -> None: - store = Store(reducer) - - # subscription: - dummy_render = print - store.subscribe(dummy_render) - - # autorun: - @store.autorun( - lambda state: state.items[0].content if len(state.items) > 0 else None, - ) - def reaction(content: str | None) -> None: - print(content) - - _ = reaction - - # event listener, note that this will run async in a separate thread, so it can - # include async operations like API calls, etc: - dummy_api_call = print - store.subscribe_event(CallApi, lambda event: dummy_api_call(event.parameters)) - - # dispatch: - store.dispatch(AddTodoItemAction(content='New Item', timestamp=time.time())) - - store.dispatch(FinishAction())