From 90d803d3360a23c85b95e83e1db15791f5d59183 Mon Sep 17 00:00:00 2001 From: Sassan Haradji Date: Tue, 7 May 2024 01:09:22 +0400 Subject: [PATCH] refactor(autorun): improve type-hints so that its final return value has the correct type, regardless `default_value` is provided or not refactor(view): improve type-hints so that its final return value has the correct type, regardless `default_value` is provided or not refactor(combine_reducers): use `make_immutable` instead of `make_dataclass` test(view): write tests for `store.view` --- .github/workflows/integration_delivery.yml | 40 +++--- CHANGELOG.md | 9 ++ poetry.lock | 148 ++++++++++----------- pyproject.toml | 4 +- redux/__init__.py | 2 + redux/autorun.py | 99 ++++++++++---- redux/basic_types.py | 104 ++++++++++++--- redux/combine_reducers.py | 24 +--- redux/main.py | 94 +++++++++++-- redux/py.typed | 1 + tests/test_async.py | 114 +++++++++++++++- tests/test_autorun.py | 12 +- tests/test_views.py | 137 +++++++++++++++++++ 13 files changed, 601 insertions(+), 187 deletions(-) create mode 100644 redux/py.typed create mode 100644 tests/test_views.py diff --git a/.github/workflows/integration_delivery.yml b/.github/workflows/integration_delivery.yml index 72685f8..b38cd40 100644 --- a/.github/workflows/integration_delivery.yml +++ b/.github/workflows/integration_delivery.yml @@ -23,7 +23,7 @@ jobs: path: | ~/.cache ~/.local - key: poetry-${{ hashFiles('poetry.lock') }} + key: poetry-${{ hashFiles('poetry.lock') }}-${{ env.PYTHON_VERSION}} - uses: actions/setup-python@v5 name: Setup Python @@ -61,7 +61,7 @@ jobs: path: | ~/.cache ~/.local - key: poetry-${{ hashFiles('poetry.lock') }} + key: poetry-${{ hashFiles('poetry.lock') }}-${{ env.PYTHON_VERSION}} - name: Type Check run: poetry run poe typecheck @@ -75,8 +75,8 @@ jobs: - uses: actions/checkout@v4 name: Checkout - - uses: actions/setup-python@v5 - name: Setup Python + - name: Setup Python + uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} architecture: x64 @@ -88,7 +88,7 @@ jobs: path: | ~/.cache ~/.local - key: poetry-${{ hashFiles('poetry.lock') }} + key: poetry-${{ hashFiles('poetry.lock') }}-${{ env.PYTHON_VERSION}} - name: Lint run: poetry run poe lint @@ -105,8 +105,8 @@ jobs: - uses: actions/checkout@v4 name: Checkout - - uses: actions/setup-python@v5 - name: Setup Python + - name: Setup Python + uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} architecture: x64 @@ -118,7 +118,7 @@ jobs: path: | ~/.cache ~/.local - key: poetry-${{ hashFiles('poetry.lock') }} + key: poetry-${{ hashFiles('poetry.lock') }}-${{ env.PYTHON_VERSION}} - name: Run Tests run: poetry run poe test @@ -141,7 +141,7 @@ jobs: with: file: ./coverage.xml flags: integration - fail_ci_if_error: true + fail_ci_if_error: false token: ${{ secrets.CODECOV_TOKEN }} build: @@ -150,8 +150,8 @@ jobs: - dependencies runs-on: ubuntu-latest outputs: - version: ${{ steps.extract_version.outputs.VERSION }} - name: ${{ steps.extract_version.outputs.NAME }} + version: ${{ steps.extract-version.outputs.VERSION }} + name: ${{ steps.extract-version.outputs.NAME }} steps: - uses: actions/checkout@v4 name: Checkout @@ -169,22 +169,21 @@ jobs: path: | ~/.cache ~/.local - key: poetry-${{ hashFiles('poetry.lock') }} + key: poetry-${{ hashFiles('poetry.lock') }}-${{ env.PYTHON_VERSION}} - name: Extract Version - id: extract_version + id: extract-version run: | echo "VERSION=$(poetry version --short)" >> "$GITHUB_OUTPUT" - echo "NAME=$(poetry version | cut -d' ' -f1)" >> "$GITHUB_OUTPUT" echo "VERSION=$(poetry version --short)" + echo "NAME=$(poetry version | cut -d' ' -f1)" >> "$GITHUB_OUTPUT" echo "NAME=$(poetry version | cut -d' ' -f1)" - name: Extract Version from CHANGELOG.md - id: extract_changelog_version run: | VERSION_CHANGELOG=$(sed -n '3 s/## Version //p' CHANGELOG.md) echo "VERSION_CHANGELOG=$VERSION_CHANGELOG" - if [ "${{ steps.extract_version.outputs.VERSION }}" != "$VERSION_CHANGELOG" ]; then + if [ "${{ steps.extract-version.outputs.VERSION }}" != "$VERSION_CHANGELOG" ]; then echo "Error: Version extracted from CHANGELOG.md does not match the version in pyproject.toml" exit 1 else @@ -193,11 +192,10 @@ jobs: - name: Extract Version from Tag if: startsWith(github.ref, 'refs/tags/v') - id: extract_tag_version run: | VERSION_TAG=$(sed 's/^v//' <<< ${{ github.ref_name }}) echo "VERSION_TAG=$VERSION_TAG" - if [ "${{ steps.extract_version.outputs.VERSION }}" != "$VERSION_TAG" ]; then + if [ "${{ steps.extract-version.outputs.VERSION }}" != "$VERSION_TAG" ]; then echo "Error: Version extracted from tag does not match the version in pyproject.toml" exit 1 else @@ -232,7 +230,7 @@ jobs: - build runs-on: ubuntu-latest environment: - name: release + name: pypi url: https://pypi.org/p/${{ needs.build.outputs.name }} permissions: id-token: write @@ -265,7 +263,9 @@ jobs: runs-on: ubuntu-latest environment: name: release - url: https://pypi.org/p/${{ needs.build.outputs.name }} + url: + https://github.com/${{ github.repository }}/releases/tag/v${{ + needs.build.outputs.version }} permissions: contents: write steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 30dabf6..db1c7ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## Version 0.15.2 + +- refactor(autorun): improve type-hints so that its final return value has the correct + type, regardless `default_value` is provided or not +- refactor(view): improve type-hints so that its final return value has the correct + type, regardless `default_value` is provided or not +- refactor(combine_reducers): use `make_immutable` instead of `make_dataclass` +- test(view): write tests for `store.view` + ## Version 0.15.1 - feat(core): add `view` method to `Store` to allow computing a derived value from diff --git a/poetry.lock b/poetry.lock index d51ad8f..bf1fdff 100644 --- a/poetry.lock +++ b/poetry.lock @@ -13,63 +13,63 @@ files = [ [[package]] name = "coverage" -version = "7.4.4" +version = "7.5.1" 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"}, + {file = "coverage-7.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0884920835a033b78d1c73b6d3bbcda8161a900f38a488829a83982925f6c2e"}, + {file = "coverage-7.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:39afcd3d4339329c5f58de48a52f6e4e50f6578dd6099961cf22228feb25f38f"}, + {file = "coverage-7.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7b0ceee8147444347da6a66be737c9d78f3353b0681715b668b72e79203e4a"}, + {file = "coverage-7.5.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a9ca3f2fae0088c3c71d743d85404cec8df9be818a005ea065495bedc33da35"}, + {file = "coverage-7.5.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd215c0c7d7aab005221608a3c2b46f58c0285a819565887ee0b718c052aa4e"}, + {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4bf0655ab60d754491004a5efd7f9cccefcc1081a74c9ef2da4735d6ee4a6223"}, + {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61c4bf1ba021817de12b813338c9be9f0ad5b1e781b9b340a6d29fc13e7c1b5e"}, + {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:db66fc317a046556a96b453a58eced5024af4582a8dbdc0c23ca4dbc0d5b3146"}, + {file = "coverage-7.5.1-cp310-cp310-win32.whl", hash = "sha256:b016ea6b959d3b9556cb401c55a37547135a587db0115635a443b2ce8f1c7228"}, + {file = "coverage-7.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:df4e745a81c110e7446b1cc8131bf986157770fa405fe90e15e850aaf7619bc8"}, + {file = "coverage-7.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:796a79f63eca8814ca3317a1ea443645c9ff0d18b188de470ed7ccd45ae79428"}, + {file = "coverage-7.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fc84a37bfd98db31beae3c2748811a3fa72bf2007ff7902f68746d9757f3746"}, + {file = "coverage-7.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6175d1a0559986c6ee3f7fccfc4a90ecd12ba0a383dcc2da30c2b9918d67d8a3"}, + {file = "coverage-7.5.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fc81d5878cd6274ce971e0a3a18a8803c3fe25457165314271cf78e3aae3aa2"}, + {file = "coverage-7.5.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:556cf1a7cbc8028cb60e1ff0be806be2eded2daf8129b8811c63e2b9a6c43bca"}, + {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9981706d300c18d8b220995ad22627647be11a4276721c10911e0e9fa44c83e8"}, + {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d7fed867ee50edf1a0b4a11e8e5d0895150e572af1cd6d315d557758bfa9c057"}, + {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef48e2707fb320c8f139424a596f5b69955a85b178f15af261bab871873bb987"}, + {file = "coverage-7.5.1-cp311-cp311-win32.whl", hash = "sha256:9314d5678dcc665330df5b69c1e726a0e49b27df0461c08ca12674bcc19ef136"}, + {file = "coverage-7.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:5fa567e99765fe98f4e7d7394ce623e794d7cabb170f2ca2ac5a4174437e90dd"}, + {file = "coverage-7.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b6cf3764c030e5338e7f61f95bd21147963cf6aa16e09d2f74f1fa52013c1206"}, + {file = "coverage-7.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ec92012fefebee89a6b9c79bc39051a6cb3891d562b9270ab10ecfdadbc0c34"}, + {file = "coverage-7.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16db7f26000a07efcf6aea00316f6ac57e7d9a96501e990a36f40c965ec7a95d"}, + {file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beccf7b8a10b09c4ae543582c1319c6df47d78fd732f854ac68d518ee1fb97fa"}, + {file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8748731ad392d736cc9ccac03c9845b13bb07d020a33423fa5b3a36521ac6e4e"}, + {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7352b9161b33fd0b643ccd1f21f3a3908daaddf414f1c6cb9d3a2fd618bf2572"}, + {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:7a588d39e0925f6a2bff87154752481273cdb1736270642aeb3635cb9b4cad07"}, + {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:68f962d9b72ce69ea8621f57551b2fa9c70509af757ee3b8105d4f51b92b41a7"}, + {file = "coverage-7.5.1-cp312-cp312-win32.whl", hash = "sha256:f152cbf5b88aaeb836127d920dd0f5e7edff5a66f10c079157306c4343d86c19"}, + {file = "coverage-7.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:5a5740d1fb60ddf268a3811bcd353de34eb56dc24e8f52a7f05ee513b2d4f596"}, + {file = "coverage-7.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e2213def81a50519d7cc56ed643c9e93e0247f5bbe0d1247d15fa520814a7cd7"}, + {file = "coverage-7.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5037f8fcc2a95b1f0e80585bd9d1ec31068a9bcb157d9750a172836e98bc7a90"}, + {file = "coverage-7.5.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3721c2c9e4c4953a41a26c14f4cef64330392a6d2d675c8b1db3b645e31f0e"}, + {file = "coverage-7.5.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca498687ca46a62ae590253fba634a1fe9836bc56f626852fb2720f334c9e4e5"}, + {file = "coverage-7.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cdcbc320b14c3e5877ee79e649677cb7d89ef588852e9583e6b24c2e5072661"}, + {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:57e0204b5b745594e5bc14b9b50006da722827f0b8c776949f1135677e88d0b8"}, + {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fe7502616b67b234482c3ce276ff26f39ffe88adca2acf0261df4b8454668b4"}, + {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9e78295f4144f9dacfed4f92935fbe1780021247c2fabf73a819b17f0ccfff8d"}, + {file = "coverage-7.5.1-cp38-cp38-win32.whl", hash = "sha256:1434e088b41594baa71188a17533083eabf5609e8e72f16ce8c186001e6b8c41"}, + {file = "coverage-7.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:0646599e9b139988b63704d704af8e8df7fa4cbc4a1f33df69d97f36cb0a38de"}, + {file = "coverage-7.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4cc37def103a2725bc672f84bd939a6fe4522310503207aae4d56351644682f1"}, + {file = "coverage-7.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc0b4d8bfeabd25ea75e94632f5b6e047eef8adaed0c2161ada1e922e7f7cece"}, + {file = "coverage-7.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d0a0f5e06881ecedfe6f3dd2f56dcb057b6dbeb3327fd32d4b12854df36bf26"}, + {file = "coverage-7.5.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9735317685ba6ec7e3754798c8871c2f49aa5e687cc794a0b1d284b2389d1bd5"}, + {file = "coverage-7.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d21918e9ef11edf36764b93101e2ae8cc82aa5efdc7c5a4e9c6c35a48496d601"}, + {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c3e757949f268364b96ca894b4c342b41dc6f8f8b66c37878aacef5930db61be"}, + {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:79afb6197e2f7f60c4824dd4b2d4c2ec5801ceb6ba9ce5d2c3080e5660d51a4f"}, + {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d1d0d98d95dd18fe29dc66808e1accf59f037d5716f86a501fc0256455219668"}, + {file = "coverage-7.5.1-cp39-cp39-win32.whl", hash = "sha256:1cc0fe9b0b3a8364093c53b0b4c0c2dd4bb23acbec4c9240b5f284095ccf7981"}, + {file = "coverage-7.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:dde0070c40ea8bb3641e811c1cfbf18e265d024deff6de52c5950677a8fb1e0f"}, + {file = "coverage-7.5.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:6537e7c10cc47c595828b8a8be04c72144725c383c4702703ff4e42e44577312"}, + {file = "coverage-7.5.1.tar.gz", hash = "sha256:54de9ef3a9da981f7af93eafde4ede199e0846cd819eb27c88e2b712aae9708c"}, ] [package.extras] @@ -124,13 +124,13 @@ files = [ [[package]] name = "pluggy" -version = "1.4.0" +version = "1.5.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"}, + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] [package.extras] @@ -175,23 +175,23 @@ dev = ["twine (>=3.4.1)"] [[package]] name = "pytest" -version = "8.1.1" +version = "8.2.0" 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"}, + {file = "pytest-8.2.0-py3-none-any.whl", hash = "sha256:1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233"}, + {file = "pytest-8.2.0.tar.gz", hash = "sha256:d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f"}, ] [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} iniconfig = "*" packaging = "*" -pluggy = ">=1.4,<2.0" +pluggy = ">=1.5,<2.0" [package.extras] -testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-cov" @@ -244,13 +244,13 @@ pytest = ">=7.0.0" [[package]] name = "python-immutable" -version = "1.0.5" +version = "1.1.1" description = "Immutable implementation for Python using dataclasses" optional = false -python-versions = ">=3.9,<4.0" +python-versions = "<4.0,>=3.9" files = [ - {file = "python_immutable-1.0.5-py3-none-any.whl", hash = "sha256:ea1309539a954ff2b527e4d175261ceddd96dd97a4bec436f5825ebb2d4f2818"}, - {file = "python_immutable-1.0.5.tar.gz", hash = "sha256:e2a30d8b4b1fe8dfe3aedc02392b0e567915b3aabbb6f57abb1689780e2ec1a4"}, + {file = "python_immutable-1.1.1-py3-none-any.whl", hash = "sha256:56feffc7c628c404b6a49c08e35b53690aa36ec6284ff105233a1b34c4a04048"}, + {file = "python_immutable-1.1.1.tar.gz", hash = "sha256:c7aa209f69c02793b8cdb8b645d186beb159ba37f3a30ffdbc66474d06db96a0"}, ] [package.dependencies] @@ -284,18 +284,18 @@ files = [ [[package]] name = "setuptools" -version = "69.2.0" +version = "69.5.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"}, - {file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"}, + {file = "setuptools-69.5.1-py3-none-any.whl", hash = "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32"}, + {file = "setuptools-69.5.1.tar.gz", hash = "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987"}, ] [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)", "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"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "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)", "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,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "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]] @@ -325,16 +325,16 @@ files = [ [[package]] name = "typing-extensions" -version = "4.10.0" +version = "4.11.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, - {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, + {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, + {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, ] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "2bed3b9c22fb7750df3ff6adeb44835bc683e519d7214358bcac9e5bf70a34a7" +content-hash = "e4b1be6c9bed46fde33d4bba9eda00717f8f35f7b5f6426a5d61c4ba29315e35" diff --git a/pyproject.toml b/pyproject.toml index 65c7279..38a936e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "python-redux" -version = "0.15.1" +version = "0.15.2" description = "Redux implementation for Python" authors = ["Sassan Haradji "] license = "Apache-2.0" @@ -9,7 +9,7 @@ packages = [{ include = "redux" }, { include = "redux_pytest" }] [tool.poetry.dependencies] python = "^3.11" -python-immutable = "^1.0.5" +python-immutable = "^1.1.1" [tool.poetry.group.dev] optional = true diff --git a/redux/__init__.py b/redux/__init__.py index 5a66436..e0b7cc1 100644 --- a/redux/__init__.py +++ b/redux/__init__.py @@ -24,6 +24,7 @@ ReducerType, Scheduler, ViewDecorator, + ViewOptions, ViewReturnType, is_complete_reducer_result, is_state_reducer_result, @@ -55,6 +56,7 @@ 'ReducerType', 'Scheduler', 'ViewDecorator', + 'ViewOptions', 'ViewReturnType', 'is_complete_reducer_result', 'is_state_reducer_result', diff --git a/redux/autorun.py b/redux/autorun.py index 3ba1372..ac7534e 100644 --- a/redux/autorun.py +++ b/redux/autorun.py @@ -1,9 +1,10 @@ # ruff: noqa: D100, D101, D102, D103, D104, D105, D107 from __future__ import annotations +import functools import inspect import weakref -from asyncio import Task, iscoroutine +from asyncio import Future, Task, iscoroutine, iscoroutinefunction from typing import TYPE_CHECKING, Any, Callable, Concatenate, Generic, cast from redux.basic_types import ( @@ -64,17 +65,24 @@ def __init__( # noqa: PLR0913 ComparatorOutput, object(), ) - self._latest_value: AutorunOriginalReturnType = options.default_value + if iscoroutinefunction(func): + self._latest_value = Future() + self._latest_value.set_result(options.default_value) + else: + self._latest_value: AutorunOriginalReturnType = options.default_value self._subscriptions: set[ Callable[[AutorunOriginalReturnType], Any] | weakref.ref[Callable[[AutorunOriginalReturnType], Any]] ] = set() - self._check_and_call(store._state, self._options.initial_call) # noqa: SLF001 + if self._check(store._state) and self._options.initial_call: # noqa: SLF001 + self._call() if self._options.reactive: self._unsubscribe = store.subscribe( - lambda state: self._check_and_call(state, self._options.auto_call), + lambda state: self._call() + if self._check(state) and self._options.auto_call + else None, ) else: self._unsubscribe = None @@ -116,11 +124,17 @@ def _task_callback( AutorunArgs, ], task: Task, + *, + future: Future | None, ) -> None: - task.add_done_callback(lambda _: self.inform_subscribers()) - self._latest_value = cast(AutorunOriginalReturnType, task) + task.add_done_callback( + lambda result: ( + future.set_result(result.result()) if future else None, + self.inform_subscribers(), + ), + ) - def _check_and_call( + def _check( self: Autorun[ State, Action, @@ -130,36 +144,63 @@ def _check_and_call( AutorunOriginalReturnType, AutorunArgs, ], - state: State, - _call: bool, # noqa: FBT001 - *args: AutorunArgs.args, - **kwargs: AutorunArgs.kwargs, - ) -> None: + state: State | None, + ) -> bool: + if state is None: + return False try: selector_result = self._selector(state) except AttributeError: - return + return False if self._comparator is None: comparator_result = cast(ComparatorOutput, selector_result) else: try: comparator_result = self._comparator(state) except AttributeError: - return - if self._should_be_called or comparator_result != self._last_comparator_result: - self._last_selector_result = selector_result - self._last_comparator_result = comparator_result - self._should_be_called = not _call - if _call: - func = ( - self._func() if isinstance(self._func, weakref.ref) else self._func + return False + self._should_be_called = ( + self._should_be_called or comparator_result != self._last_comparator_result + ) + self._last_selector_result = selector_result + self._last_comparator_result = comparator_result + return self._should_be_called + + def _call( + self: Autorun[ + State, + Action, + Event, + SelectorOutput, + ComparatorOutput, + AutorunOriginalReturnType, + AutorunArgs, + ], + *args: AutorunArgs.args, + **kwargs: AutorunArgs.kwargs, + ) -> None: + self._should_be_called = False + func = self._func() if isinstance(self._func, weakref.ref) else self._func + if func: + value: AutorunOriginalReturnType = func( + self._last_selector_result, + *args, + **kwargs, + ) + create_task = self._store._create_task # noqa: SLF001 + if iscoroutine(value) and create_task: + future = Future() + self._latest_value = cast(AutorunOriginalReturnType, future) + create_task( + value, + callback=functools.partial( + self._task_callback, + future=future, + ), ) - if func: - self._latest_value = func(selector_result, *args, **kwargs) - create_task = self._store._create_task # noqa: SLF001 - if iscoroutine(self._latest_value) and create_task: - create_task(self._latest_value, callback=self._task_callback) - self.inform_subscribers() + else: + self._latest_value = value + self.inform_subscribers() def __call__( self: Autorun[ @@ -175,8 +216,8 @@ def __call__( **kwargs: AutorunArgs.kwargs, ) -> AutorunOriginalReturnType: state = self._store._state # noqa: SLF001 - if state is not None: - self._check_and_call(state, True, *args, **kwargs) # noqa: FBT003 + if self._check(state) or self._should_be_called or args or kwargs: + self._call(*args, **kwargs) return cast(AutorunOriginalReturnType, self._latest_value) def __repr__( diff --git a/redux/basic_types.py b/redux/basic_types.py index 6e47e1d..c08f1a2 100644 --- a/redux/basic_types.py +++ b/redux/basic_types.py @@ -6,15 +6,18 @@ from typing import ( TYPE_CHECKING, Any, + Awaitable, Callable, Concatenate, Coroutine, Generic, + Never, ParamSpec, Protocol, Sequence, TypeAlias, TypeGuard, + overload, ) from immutable import Immutable @@ -24,6 +27,11 @@ from asyncio import Task +T = TypeVar('T') + +AwaitableOrNot = Awaitable[T] | T + + class BaseAction(Immutable): ... @@ -120,6 +128,9 @@ class CreateStoreOptions(Immutable, Generic[Action, Event]): grace_time_in_seconds: float = 1 +# Autorun + + class AutorunOptions(Immutable, Generic[AutorunOriginalReturnType]): default_value: AutorunOriginalReturnType | None = None initial_call: bool = True @@ -130,11 +141,8 @@ class AutorunOptions(Immutable, Generic[AutorunOriginalReturnType]): subscribers_keep_ref: bool = True -class ViewOptions(Immutable, Generic[ViewOriginalReturnType]): - default_value: ViewOriginalReturnType | None = None - keep_ref: bool = True - subscribers_initial_run: bool = True - subscribers_keep_ref: bool = True +AutorunOptionsWithDefault = AutorunOptions[AutorunOriginalReturnType] +AutorunOptionsWithoutDefault = AutorunOptions[Never] class AutorunReturnType( @@ -161,26 +169,51 @@ def subscribe( def unsubscribe(self: AutorunReturnType) -> None: ... -AutorunDecorator = Callable[ - [ - Callable[ +class AutorunDecorator( + Protocol, + Generic[SelectorOutput, AutorunOriginalReturnType], +): + @overload + def __call__( + self: AutorunDecorator, + func: Callable[ Concatenate[SelectorOutput, AutorunArgs], AutorunOriginalReturnType, ], - ], - AutorunReturnType[AutorunOriginalReturnType, AutorunArgs], -] + ) -> AutorunReturnType[AutorunOriginalReturnType, AutorunArgs]: ... + @overload + def __call__( + self: AutorunDecorator, + func: Callable[ + Concatenate[SelectorOutput, AutorunArgs], + Awaitable[AutorunOriginalReturnType], + ], + ) -> AutorunReturnType[Awaitable[AutorunOriginalReturnType], AutorunArgs]: ... -ViewDecorator = Callable[ - [ - Callable[ - Concatenate[SelectorOutput, ViewArgs], - ViewOriginalReturnType, + +class UnknownAutorunDecorator(Protocol, Generic[SelectorOutput]): + def __call__( + self: UnknownAutorunDecorator, + func: Callable[ + Concatenate[SelectorOutput, AutorunArgs], + AutorunOriginalReturnType, ], - ], - Callable[ViewArgs, ViewOriginalReturnType], -] + ) -> AutorunReturnType[AutorunOriginalReturnType, AutorunArgs]: ... + + +# View + + +class ViewOptions(Immutable, Generic[ViewOriginalReturnType]): + default_value: ViewOriginalReturnType | None = None + keep_ref: bool = True + subscribers_initial_run: bool = True + subscribers_keep_ref: bool = True + + +ViewOptionsWithDefault = ViewOptions[ViewOriginalReturnType] +ViewOptionsWithoutDefault = ViewOptions[Never] class ViewReturnType( @@ -207,6 +240,39 @@ def subscribe( def unsubscribe(self: ViewReturnType) -> None: ... +class ViewDecorator( + Protocol, + Generic[SelectorOutput, ViewOriginalReturnType], +): + @overload + def __call__( + self: ViewDecorator, + func: Callable[ + Concatenate[SelectorOutput, ViewArgs], + ViewOriginalReturnType, + ], + ) -> ViewReturnType[ViewOriginalReturnType, ViewArgs]: ... + + @overload + def __call__( + self: ViewDecorator, + func: Callable[ + Concatenate[SelectorOutput, ViewArgs], + Awaitable[ViewOriginalReturnType], + ], + ) -> ViewReturnType[Awaitable[ViewOriginalReturnType], ViewArgs]: ... + + +class UnknownViewDecorator(Protocol, Generic[SelectorOutput]): + def __call__( + self: UnknownViewDecorator, + func: Callable[ + Concatenate[SelectorOutput, ViewArgs], + ViewOriginalReturnType, + ], + ) -> ViewReturnType[ViewOriginalReturnType, ViewArgs]: ... + + class EventSubscriber(Protocol): def __call__( self: EventSubscriber, diff --git a/redux/combine_reducers.py b/redux/combine_reducers.py index 3d4286c..b2e70e7 100644 --- a/redux/combine_reducers.py +++ b/redux/combine_reducers.py @@ -5,9 +5,11 @@ import functools import operator import uuid -from dataclasses import asdict, fields, make_dataclass +from dataclasses import asdict, fields from typing import TYPE_CHECKING, Any, TypeVar, cast +from immutable import make_immutable + from .basic_types import ( Action, BaseAction, @@ -46,12 +48,7 @@ def combine_reducers( state_class = cast( type[state_type], - make_dataclass( - state_type.__name__, - ('_id', *reducers.keys()), - frozen=True, - kw_only=True, - ), + make_immutable(state_type.__name__, (('_id', str), *reducers.keys())), ) def combined_reducer( @@ -66,11 +63,9 @@ def combined_reducer( key = action.key reducer = action.reducer reducers[key] = reducer - state_class = make_dataclass( + state_class = make_immutable( state_type.__name__, - ('_id', *reducers.keys()), - frozen=True, - kw_only=True, + (('_id', str), *reducers.keys()), ) reducer_result = reducer( None, @@ -111,12 +106,7 @@ def combined_reducer( annotations_copy = copy.deepcopy(state_class.__annotations__) del fields_copy[key] del annotations_copy[key] - state_class = make_dataclass( - state_type.__name__, - annotations_copy, - frozen=True, - kw_only=True, - ) + state_class = make_immutable(state_type.__name__, annotations_copy) cast(Any, state_class).__dataclass_fields__ = fields_copy state = state_class( diff --git a/redux/main.py b/redux/main.py index 081ba62..896ca37 100644 --- a/redux/main.py +++ b/redux/main.py @@ -8,7 +8,15 @@ import weakref from collections import defaultdict from threading import Lock, Thread -from typing import Any, Callable, Concatenate, Generic, cast +from typing import ( + Any, + Awaitable, + Callable, + Concatenate, + Generic, + cast, + overload, +) from redux.autorun import Autorun from redux.basic_types import ( @@ -17,8 +25,11 @@ AutorunArgs, AutorunDecorator, AutorunOptions, + AutorunOptionsWithDefault, + AutorunOptionsWithoutDefault, AutorunOriginalReturnType, AutorunReturnType, + AwaitableOrNot, BaseAction, BaseEvent, ComparatorOutput, @@ -35,9 +46,13 @@ SelectorOutput, SnapshotAtom, State, + UnknownAutorunDecorator, + UnknownViewDecorator, ViewArgs, ViewDecorator, ViewOptions, + ViewOptionsWithDefault, + ViewOptionsWithoutDefault, ViewOriginalReturnType, ViewReturnType, is_complete_reducer_result, @@ -257,25 +272,54 @@ def wait_for_store_to_finish(self: Store[State, Action, Event]) -> None: def _handle_finish_event(self: Store[State, Action, Event]) -> None: Thread(target=self.wait_for_store_to_finish).start() + @overload + def autorun( + self: Store[State, Action, Event], + selector: Callable[[State], SelectorOutput], + comparator: Callable[[State], ComparatorOutput] | None = None, + *, + options: AutorunOptionsWithoutDefault | None = None, + ) -> UnknownAutorunDecorator[SelectorOutput]: ... + @overload + def autorun( + self: Store[State, Action, Event], + selector: Callable[[State], SelectorOutput], + comparator: Callable[[State], ComparatorOutput] | None = None, + *, + options: AutorunOptionsWithDefault[AutorunOriginalReturnType], + ) -> AutorunDecorator[SelectorOutput, AutorunOriginalReturnType]: ... def autorun( self: Store[State, Action, Event], selector: Callable[[State], SelectorOutput], comparator: Callable[[State], ComparatorOutput] | None = None, *, options: AutorunOptions[AutorunOriginalReturnType] | None = None, - ) -> AutorunDecorator[ - SelectorOutput, - AutorunArgs, - AutorunOriginalReturnType, - ]: + ) -> ( + AutorunDecorator[SelectorOutput, AutorunOriginalReturnType] + | UnknownAutorunDecorator[SelectorOutput] + ): """Create a new autorun, reflecting on state changes.""" + @overload def decorator( func: Callable[ Concatenate[SelectorOutput, AutorunArgs], AutorunOriginalReturnType, ], - ) -> AutorunReturnType[AutorunOriginalReturnType, AutorunArgs]: + ) -> AutorunReturnType[AutorunOriginalReturnType, AutorunArgs]: ... + @overload + def decorator( + func: Callable[ + Concatenate[SelectorOutput, AutorunArgs], + Awaitable[AutorunOriginalReturnType], + ], + ) -> AutorunReturnType[Awaitable[AutorunOriginalReturnType], AutorunArgs]: ... + def decorator( + func: Callable[ + Concatenate[SelectorOutput, AutorunArgs], + AwaitableOrNot[AutorunOriginalReturnType], + ], + ) -> AutorunReturnType[AwaitableOrNot[AutorunOriginalReturnType], AutorunArgs]: return Autorun( store=self, selector=selector, @@ -286,20 +330,52 @@ def decorator( return decorator + @overload + def view( + self: Store[State, Action, Event], + selector: Callable[[State], SelectorOutput], + *, + options: ViewOptionsWithoutDefault | None = None, + ) -> UnknownViewDecorator[SelectorOutput]: ... + @overload + def view( + self: Store[State, Action, Event], + selector: Callable[[State], SelectorOutput], + *, + options: ViewOptionsWithDefault[ViewOriginalReturnType], + ) -> ViewDecorator[SelectorOutput, ViewOriginalReturnType]: ... def view( self: Store[State, Action, Event], selector: Callable[[State], SelectorOutput], *, options: ViewOptions[ViewOriginalReturnType] | None = None, - ) -> ViewDecorator[SelectorOutput, ViewArgs, ViewOriginalReturnType]: + ) -> ( + ViewDecorator[SelectorOutput, ViewOriginalReturnType] + | UnknownViewDecorator[SelectorOutput] + ): """Create a new view, throttling calls for unchanged selector results.""" + @overload def decorator( func: Callable[ Concatenate[SelectorOutput, ViewArgs], ViewOriginalReturnType, ], - ) -> ViewReturnType[ViewOriginalReturnType, ViewArgs]: + ) -> ViewReturnType[ViewOriginalReturnType, ViewArgs]: ... + @overload + def decorator( + func: Callable[ + Concatenate[SelectorOutput, ViewArgs], + Awaitable[ViewOriginalReturnType], + ], + ) -> ViewReturnType[Awaitable[ViewOriginalReturnType], ViewArgs]: ... + + def decorator( + func: Callable[ + Concatenate[SelectorOutput, ViewArgs], + AwaitableOrNot[ViewOriginalReturnType], + ], + ) -> ViewReturnType[AwaitableOrNot[ViewOriginalReturnType], ViewArgs]: _options = options or ViewOptions() return Autorun( store=self, diff --git a/redux/py.typed b/redux/py.typed new file mode 100644 index 0000000..1556979 --- /dev/null +++ b/redux/py.typed @@ -0,0 +1 @@ +# Marker file for PEP 561. The redux package uses inline types. diff --git a/tests/test_async.py b/tests/test_async.py index b991c28..2076ae4 100644 --- a/tests/test_async.py +++ b/tests/test_async.py @@ -3,12 +3,13 @@ import asyncio from dataclasses import replace -from typing import TYPE_CHECKING, Callable, Coroutine, Generator +from typing import TYPE_CHECKING, Callable, Coroutine import pytest from immutable import Immutable from redux.basic_types import ( + AutorunOptions, BaseAction, CompleteReducerResult, CreateStoreOptions, @@ -16,6 +17,7 @@ FinishEvent, InitAction, InitializationActionError, + ViewOptions, ) from redux.main import Store @@ -58,7 +60,7 @@ def reducer( @pytest.fixture() -def store(event_loop: LoopThread) -> Generator[StoreType, None, None]: +def store(event_loop: LoopThread) -> StoreType: def _create_task_with_callback( coro: Coroutine, callback: Callable[[asyncio.Task], None] | None = None, @@ -70,16 +72,17 @@ def create_task_with_callback() -> None: event_loop.loop.call_soon_threadsafe(create_task_with_callback) - store = Store( + return Store( reducer, options=CreateStoreOptions( auto_init=True, task_creator=_create_task_with_callback, ), ) - yield store - for i in range(INCREMENTS): - _ = i + + +def dispatch_actions(store: StoreType) -> None: + for _ in range(INCREMENTS): store.dispatch(IncrementAction()) @@ -103,6 +106,101 @@ async def _(mirrored_value: int) -> None: event_loop.stop() store.dispatch(FinishAction()) + dispatch_actions(store) + + +def test_autorun_default_value( + store: StoreType, + event_loop: LoopThread, +) -> None: + @store.autorun(lambda state: state.value, options=AutorunOptions(default_value=5)) + async def _(value: int) -> int: + store.dispatch(SetMirroredValueAction(value=value)) + return value + + @store.autorun( + lambda state: state.mirrored_value, + lambda state: state.mirrored_value >= INCREMENTS, + ) + async def _(mirrored_value: int) -> None: + if mirrored_value < INCREMENTS: + return + event_loop.stop() + store.dispatch(FinishAction()) + + dispatch_actions(store) + + +def test_view( + store: StoreType, + event_loop: LoopThread, +) -> None: + calls = [] + + @store.view(lambda state: state.value) + async def doubled(value: int) -> int: + calls.append(value) + return value * 2 + + @store.autorun(lambda state: state.value) + async def _(value: int) -> None: + assert await doubled() == value * 2 + for _ in range(10): + await doubled() + if value < INCREMENTS: + store.dispatch(IncrementAction()) + else: + event_loop.stop() + store.dispatch(FinishAction()) + assert calls == list(range(INCREMENTS + 1)) + + +def test_view_with_args( + store: StoreType, + event_loop: LoopThread, +) -> None: + calls = [] + + @store.view(lambda state: state.value) + async def multiplied(value: int, factor: int) -> int: + calls.append(value) + return value * factor + + @store.autorun(lambda state: state.value) + async def _(value: int) -> None: + assert await multiplied(factor=2) == value * 2 + assert await multiplied(factor=3) == value * 3 + if value < INCREMENTS: + store.dispatch(IncrementAction()) + else: + event_loop.stop() + store.dispatch(FinishAction()) + assert calls == [j for i in list(range(INCREMENTS + 1)) for j in [i] * 2] + + +def test_view_with_default_value( + store: StoreType, + event_loop: LoopThread, +) -> None: + calls = [] + + @store.view(lambda state: state.value, options=ViewOptions(default_value=5)) + async def doubled(value: int) -> int: + calls.append(value) + return value * 2 + + @store.autorun(lambda state: state.value) + async def _(value: int) -> None: + assert await doubled() == value * 2 + if value < INCREMENTS: + store.dispatch(IncrementAction()) + else: + event_loop.stop() + store.dispatch(FinishAction()) + assert calls == list(range(INCREMENTS + 1)) + + store.dispatch(InitAction()) + def test_subscription( store: StoreType, @@ -116,6 +214,8 @@ async def render(state: StateType) -> None: unsubscribe = store.subscribe(render) + dispatch_actions(store) + def test_event_subscription( store: StoreType, @@ -128,6 +228,8 @@ async def finish() -> None: store.subscribe_event(FinishEvent, finish) store.dispatch(FinishAction()) + dispatch_actions(store) + def test_event_subscription_with_no_task_creator(event_loop: LoopThread) -> None: store = Store( diff --git a/tests/test_autorun.py b/tests/test_autorun.py index 8576946..3eabe99 100644 --- a/tests/test_autorun.py +++ b/tests/test_autorun.py @@ -292,7 +292,7 @@ def render(_: int) -> None: ... ] -def test_view_mode_autorun( +def test_view_mode_with_arguments_autorun( store: StoreType, ) -> None: @store.autorun( @@ -308,13 +308,3 @@ def render(_: int, *, some_other_value: int) -> int: return some_other_value assert render(some_other_value=12345) == 12345 - - -def test_view( - store: StoreType, -) -> None: - @store.view(lambda state: state.value) - def render(_: int, *, some_other_value: int) -> int: - return some_other_value - - assert render(some_other_value=12345) == 12345 diff --git a/tests/test_views.py b/tests/test_views.py new file mode 100644 index 0000000..289aa49 --- /dev/null +++ b/tests/test_views.py @@ -0,0 +1,137 @@ +# ruff: noqa: D100, D101, D102, D103, D104, D107 +from __future__ import annotations + +from dataclasses import replace +from typing import Generator + +import pytest +from immutable import Immutable + +from redux.basic_types import ( + BaseAction, + CompleteReducerResult, + CreateStoreOptions, + FinishAction, + FinishEvent, + InitAction, + InitializationActionError, + ViewOptions, +) +from redux.main import Store + + +class StateType(Immutable): + value: int + + +class IncrementAction(BaseAction): ... + + +class DecrementAction(BaseAction): ... + + +class IncrementByTwoAction(BaseAction): ... + + +Action = ( + IncrementAction | DecrementAction | IncrementByTwoAction | InitAction | FinishAction +) + + +def reducer( + state: StateType | None, + action: Action, +) -> StateType | CompleteReducerResult[StateType, Action, FinishEvent]: + if state is None: + if isinstance(action, InitAction): + return StateType(value=0) + raise InitializationActionError(action) + + if isinstance(action, IncrementAction): + return replace(state, value=state.value + 1) + + if isinstance(action, DecrementAction): + return replace(state, value=state.value - 1) + + if isinstance(action, IncrementByTwoAction): + return replace(state, value=state.value + 2) + + return state + + +StoreType = Store[StateType, Action, FinishEvent] + + +@pytest.fixture() +def store() -> Generator[StoreType, None, None]: + store = Store(reducer, options=CreateStoreOptions(auto_init=True)) + yield store + + store.dispatch(IncrementAction()) + store.dispatch(IncrementByTwoAction()) + store.dispatch(IncrementAction()) + store.dispatch(FinishAction()) + + +def test_general( + store: StoreType, +) -> None: + @store.view(lambda state: state.value) + def render(value: int) -> int: + return value + + store.dispatch(IncrementAction()) + + assert render() == 1 + + +def test_uninitialized_store( + store: StoreType, +) -> None: + store = Store(reducer, options=CreateStoreOptions(auto_init=False)) + + @store.view(lambda state: state.value) + def render(value: int) -> int: + return value + + assert render() is None + + store.dispatch(InitAction()) + assert render() == 0 + + store.dispatch(IncrementAction()) + assert render() == 1 + + store.dispatch(FinishAction()) + + +def test_with_default_value_and_uninitialized_store( + store: StoreType, +) -> None: + store = Store(reducer, options=CreateStoreOptions(auto_init=False)) + + @store.view(lambda state: state.value, options=ViewOptions(default_value=5)) + def render(value: int) -> int: + return value + + assert render() == 5 + + store.dispatch(InitAction()) + assert render() == 0 + + store.dispatch(IncrementAction()) + assert render() == 1 + + store.dispatch(FinishAction()) + + +def test_with_arguments( + store: StoreType, +) -> None: + @store.view(lambda state: state.value) + def render(_: int, *, some_other_value: int) -> int: + return some_other_value + + store.dispatch(IncrementAction()) + + assert render(some_other_value=12345) == 12345