From 1430075bdc1dbfa23e85e8b13d65440d5bac9666 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Mon, 18 Sep 2023 13:52:10 -0700 Subject: [PATCH] Reassign state Var when fields on a Base instance change (#1748) --- poetry.lock | 96 ++++++++++++++-- pyproject.toml | 1 + reflex/state.py | 268 +++++++++++++++++++++++++++---------------- reflex/vars.py | 272 -------------------------------------------- reflex/vars.pyi | 36 ------ tests/conftest.py | 12 ++ tests/test_state.py | 238 +++++++++++++++++++++++++++++++++++--- tests/test_var.py | 34 ------ 8 files changed, 490 insertions(+), 467 deletions(-) diff --git a/poetry.lock b/poetry.lock index 7b7d68f5c..68f14c6b4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1489,7 +1489,6 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -1497,15 +1496,8 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -1522,7 +1514,6 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -1530,7 +1521,6 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -2189,6 +2179,90 @@ files = [ {file = "websockets-10.4.tar.gz", hash = "sha256:eef610b23933c54d5d921c92578ae5f89813438fded840c2e9809d378dc765d3"}, ] +[[package]] +name = "wrapt" +version = "1.15.0" +description = "Module for decorators, wrappers and monkey patching." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +files = [ + {file = "wrapt-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a"}, + {file = "wrapt-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923"}, + {file = "wrapt-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee"}, + {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727"}, + {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7"}, + {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0"}, + {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec"}, + {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90"}, + {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975"}, + {file = "wrapt-1.15.0-cp310-cp310-win32.whl", hash = "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1"}, + {file = "wrapt-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e"}, + {file = "wrapt-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7"}, + {file = "wrapt-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72"}, + {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb"}, + {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e"}, + {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c"}, + {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3"}, + {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92"}, + {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98"}, + {file = "wrapt-1.15.0-cp311-cp311-win32.whl", hash = "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416"}, + {file = "wrapt-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248"}, + {file = "wrapt-1.15.0-cp35-cp35m-win32.whl", hash = "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559"}, + {file = "wrapt-1.15.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"}, + {file = "wrapt-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba"}, + {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752"}, + {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364"}, + {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475"}, + {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8"}, + {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418"}, + {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2"}, + {file = "wrapt-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1"}, + {file = "wrapt-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420"}, + {file = "wrapt-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317"}, + {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e"}, + {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e"}, + {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0"}, + {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019"}, + {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034"}, + {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653"}, + {file = "wrapt-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0"}, + {file = "wrapt-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e"}, + {file = "wrapt-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145"}, + {file = "wrapt-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f"}, + {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd"}, + {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b"}, + {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f"}, + {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6"}, + {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094"}, + {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7"}, + {file = "wrapt-1.15.0-cp38-cp38-win32.whl", hash = "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b"}, + {file = "wrapt-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1"}, + {file = "wrapt-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86"}, + {file = "wrapt-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c"}, + {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d"}, + {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc"}, + {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29"}, + {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a"}, + {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8"}, + {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9"}, + {file = "wrapt-1.15.0-cp39-cp39-win32.whl", hash = "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff"}, + {file = "wrapt-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6"}, + {file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"}, + {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"}, +] + [[package]] name = "wsproto" version = "1.2.0" @@ -2221,4 +2295,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = "^3.7" -content-hash = "0dd6230851cc4f43e192e45431d1c1dcb451b7946ae7cd169e220e7f7a072aa2" +content-hash = "091bbeb36378731e9016db10ac0fcd19dda01947515fcfdc29303b2b3a2b37d6" diff --git a/pyproject.toml b/pyproject.toml index 727984196..542bdc2af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,7 @@ alembic = "^1.11.1" platformdirs = "^3.10.0" distro = {version = "^1.8.0", platform = "linux"} python-engineio = "!=4.6.0" +wrapt = "^1.15.0" [tool.poetry.group.dev.dependencies] pytest = "^7.1.2" diff --git a/reflex/state.py b/reflex/state.py index 5a623c49c..5116069ab 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -22,18 +22,18 @@ from typing import ( Sequence, Set, Type, - Union, ) import cloudpickle import pydantic +import wrapt from redis import Redis from reflex import constants from reflex.base import Base from reflex.event import Event, EventHandler, EventSpec, fix_events, window_alert from reflex.utils import format, prerequisites, types -from reflex.vars import BaseVar, ComputedVar, ReflexDict, ReflexList, ReflexSet, Var +from reflex.vars import BaseVar, ComputedVar, Var Delta = Dict[str, Any] @@ -129,32 +129,6 @@ class State(Base, ABC, extra=pydantic.Extra.allow): # Create a fresh copy of the backend variables for this instance self._backend_vars = copy.deepcopy(self.backend_vars) - # Initialize the mutable fields. - self._init_mutable_fields() - - def _init_mutable_fields(self): - """Initialize mutable fields. - - Allow mutation to dict, list, and set to be detected by the app. - """ - for field in self.base_vars.values(): - value = getattr(self, field.name) - - if types._issubclass(field.type_, Union[List, Dict, Set]): - value_in_rx_data = _convert_mutable_datatypes( - value, self._reassign_field, field.name - ) - setattr(self, field.name, value_in_rx_data) - - for field_name, value in self._backend_vars.items(): - if isinstance(value, (list, dict, set)): - value_in_rx_data = _convert_mutable_datatypes( - value, self._reassign_field, field_name - ) - self._backend_vars[field_name] = value_in_rx_data - - self._clean() - def _init_event_handlers(self, state: State | None = None): """Initialize event handlers. @@ -178,20 +152,6 @@ class State(Base, ABC, extra=pydantic.Extra.allow): if state.parent_state is not None: self._init_event_handlers(state.parent_state) - def _reassign_field(self, field_name: str): - """Reassign the given field. - - Primarily for mutation in fields of mutable data types. - - Args: - field_name: The name of the field we want to reassign - """ - setattr( - self, - field_name, - getattr(self, field_name), - ) - def __repr__(self) -> str: """Get the string representation of the state. @@ -636,9 +596,20 @@ class State(Base, ABC, extra=pydantic.Extra.allow): } if name in inherited_vars: return getattr(super().__getattribute__("parent_state"), name) - elif name in super().__getattribute__("_backend_vars"): - return super().__getattribute__("_backend_vars").__getitem__(name) - return super().__getattribute__(name) + + backend_vars = super().__getattribute__("_backend_vars") + if name in backend_vars: + value = backend_vars[name] + else: + value = super().__getattribute__(name) + + if isinstance(value, MutableProxy.__mutable_types__) and ( + name in super().__getattribute__("base_vars") or name in backend_vars + ): + # track changes in mutable containers (list, dict, set, etc) + return MutableProxy(wrapped=value, state=self, field_name=name) + + return value def __setattr__(self, name: str, value: Any): """Set the attribute. @@ -649,18 +620,16 @@ class State(Base, ABC, extra=pydantic.Extra.allow): name: The name of the attribute. value: The value of the attribute. """ + if isinstance(value, MutableProxy): + # unwrap proxy objects when assigning back to the state + value = value.__wrapped__ + # Set the var on the parent state. inherited_vars = {**self.inherited_vars, **self.inherited_backend_vars} if name in inherited_vars: setattr(self.parent_state, name, value) return - # Make sure lists and dicts are converted to ReflexList, ReflexDict and ReflexSet. - if name in (*self.base_vars, *self.backend_vars) and types._isinstance( - value, Union[List, Dict, Set] - ): - value = _convert_mutable_datatypes(value, self._reassign_field, name) - if types.is_backend_variable(name) and name != "_backend_vars": self._backend_vars.__setitem__(name, value) self.dirty_vars.add(name) @@ -1087,54 +1056,6 @@ class StateManager(Base): self.redis.set(token, cloudpickle.dumps(state), ex=self.token_expiration) -def _convert_mutable_datatypes( - field_value: Any, reassign_field: Callable, field_name: str -) -> Any: - """Recursively convert mutable data to the Rx data types. - - Note: right now only list, dict and set would be handled recursively. - - Args: - field_value: The target field_value. - reassign_field: - The function to reassign the field in the parent state. - field_name: the name of the field in the parent state - - Returns: - The converted field_value - """ - if isinstance(field_value, list): - field_value = [ - _convert_mutable_datatypes(value, reassign_field, field_name) - for value in field_value - ] - - field_value = ReflexList( - field_value, reassign_field=reassign_field, field_name=field_name - ) - - if isinstance(field_value, dict): - field_value = { - key: _convert_mutable_datatypes(value, reassign_field, field_name) - for key, value in field_value.items() - } - field_value = ReflexDict( - field_value, reassign_field=reassign_field, field_name=field_name - ) - - if isinstance(field_value, set): - field_value = [ - _convert_mutable_datatypes(value, reassign_field, field_name) - for value in field_value - ] - - field_value = ReflexSet( - field_value, reassign_field=reassign_field, field_name=field_name - ) - - return field_value - - class ClientStorageBase: """Base class for client-side storage.""" @@ -1234,3 +1155,152 @@ class LocalStorage(ClientStorageBase, str): inst = super().__new__(cls, object) inst.name = name return inst + + +class MutableProxy(wrapt.ObjectProxy): + """A proxy for a mutable object that tracks changes.""" + + # Methods on wrapped objects which should mark the state as dirty. + __mark_dirty_attrs__ = set( + [ + "add", + "append", + "clear", + "difference_update", + "discard", + "extend", + "insert", + "intersection_update", + "pop", + "popitem", + "remove", + "reverse", + "setdefault", + "sort", + "symmetric_difference_update", + "update", + ] + ) + + __mutable_types__ = (list, dict, set, Base) + + def __init__(self, wrapped: Any, state: State, field_name: str): + """Create a proxy for a mutable object that tracks changes. + + Args: + wrapped: The object to proxy. + state: The state to mark dirty when the object is changed. + field_name: The name of the field on the state associated with the + wrapped object. + """ + super().__init__(wrapped) + self._self_state = state + self._self_field_name = field_name + + def _mark_dirty(self, wrapped=None, instance=None, args=tuple(), kwargs=None): + """Mark the state as dirty, then call a wrapped function. + + Intended for use with `FunctionWrapper` from the `wrapt` library. + + Args: + wrapped: The wrapped function. + instance: The instance of the wrapped function. + args: The args for the wrapped function. + kwargs: The kwargs for the wrapped function. + """ + self._self_state.dirty_vars.add(self._self_field_name) + self._self_state._mark_dirty() + if wrapped is not None: + wrapped(*args, **(kwargs or {})) + + def __getattribute__(self, __name: str) -> Any: + """Get the attribute on the proxied object and return a proxy if mutable. + + Args: + __name: The name of the attribute. + + Returns: + The attribute value. + """ + value = super().__getattribute__(__name) + + if callable(value) and __name in super().__getattribute__( + "__mark_dirty_attrs__" + ): + # Wrap special callables, like "append", which should mark state dirty. + return wrapt.FunctionWrapper( + value, + super().__getattribute__("_mark_dirty"), + ) + + if isinstance( + value, super().__getattribute__("__mutable_types__") + ) and __name not in ("__wrapped__", "_self_state"): + # Recursively wrap mutable attribute values retrieved through this proxy. + return MutableProxy( + wrapped=value, + state=self._self_state, + field_name=self._self_field_name, + ) + + return value + + def __getitem__(self, key) -> Any: + """Get the item on the proxied object and return a proxy if mutable. + + Args: + key: The key of the item. + + Returns: + The item value. + """ + value = super().__getitem__(key) + if isinstance(value, self.__mutable_types__): + # Recursively wrap mutable items retrieved through this proxy. + return MutableProxy( + wrapped=value, + state=self._self_state, + field_name=self._self_field_name, + ) + return value + + def __delattr__(self, name): + """Delete the attribute on the proxied object and mark state dirty. + + Args: + name: The name of the attribute. + """ + self._mark_dirty(super().__delattr__, args=(name,)) + + def __delitem__(self, key): + """Delete the item on the proxied object and mark state dirty. + + Args: + key: The key of the item. + """ + self._mark_dirty(super().__delitem__, args=(key,)) + + def __setitem__(self, key, value): + """Set the item on the proxied object and mark state dirty. + + Args: + key: The key of the item. + value: The value of the item. + """ + self._mark_dirty(super().__setitem__, args=(key, value)) + + def __setattr__(self, name, value): + """Set the attribute on the proxied object and mark state dirty. + + If the attribute starts with "_self_", then the state is NOT marked + dirty as these are internal proxy attributes. + + Args: + name: The name of the attribute. + value: The value of the attribute. + """ + if name.startswith("_self_"): + # Special case attributes of the proxy itself, not applied to the wrapped object. + super().__setattr__(name, value) + return + self._mark_dirty(super().__setattr__, args=(name, value)) diff --git a/reflex/vars.py b/reflex/vars.py index c21edf353..36fe0e0e0 100644 --- a/reflex/vars.py +++ b/reflex/vars.py @@ -15,7 +15,6 @@ from typing import ( Dict, List, Optional, - Set, Tuple, Type, Union, @@ -1321,277 +1320,6 @@ def cached_var(fget: Callable[[Any], Any]) -> ComputedVar: return cvar -class ReflexList(list): - """A custom list that reflex can detect its mutation.""" - - def __init__( - self, - original_list: List, - reassign_field: Callable = lambda _field_name: None, - field_name: str = "", - ): - """Initialize ReflexList. - - Args: - original_list (List): The original list - reassign_field (Callable): - The method in the parent state to reassign the field. - Default to be a no-op function - field_name (str): the name of field in the parent state - """ - self._reassign_field = lambda: reassign_field(field_name) - - super().__init__(original_list) - - def append(self, *args, **kwargs): - """Append. - - Args: - args: The args passed. - kwargs: The kwargs passed. - """ - super().append(*args, **kwargs) - self._reassign_field() - - def insert(self, *args, **kwargs): - """Insert. - - Args: - args: The args passed. - kwargs: The kwargs passed. - """ - super().insert(*args, **kwargs) - self._reassign_field() - - def __setitem__(self, *args, **kwargs): - """Set item. - - Args: - args: The args passed. - kwargs: The kwargs passed. - """ - super().__setitem__(*args, **kwargs) - self._reassign_field() - - def __delitem__(self, *args, **kwargs): - """Delete item. - - Args: - args: The args passed. - kwargs: The kwargs passed. - """ - super().__delitem__(*args, **kwargs) - self._reassign_field() - - def clear(self, *args, **kwargs): - """Remove all item from the list. - - Args: - args: The args passed. - kwargs: The kwargs passed. - """ - super().clear(*args, **kwargs) - self._reassign_field() - - def extend(self, *args, **kwargs): - """Add all item of a list to the end of the list. - - Args: - args: The args passed. - kwargs: The kwargs passed. - """ - super().extend(*args, **kwargs) - self._reassign_field() if hasattr(self, "_reassign_field") else None - - def pop(self, *args, **kwargs): - """Remove an element. - - Args: - args: The args passed. - kwargs: The kwargs passed. - """ - super().pop(*args, **kwargs) - self._reassign_field() - - def remove(self, *args, **kwargs): - """Remove an element. - - Args: - args: The args passed. - kwargs: The kwargs passed. - """ - super().remove(*args, **kwargs) - self._reassign_field() - - -class ReflexDict(dict): - """A custom dict that reflex can detect its mutation.""" - - def __init__( - self, - original_dict: Dict, - reassign_field: Callable = lambda _field_name: None, - field_name: str = "", - ): - """Initialize ReflexDict. - - Args: - original_dict: The original dict - reassign_field: - The method in the parent state to reassign the field. - Default to be a no-op function - field_name: the name of field in the parent state - """ - super().__init__(original_dict) - self._reassign_field = lambda: reassign_field(field_name) - - def clear(self): - """Remove all item from the list.""" - super().clear() - - self._reassign_field() - - def setdefault(self, *args, **kwargs): - """Return value of key if or set default. - - Args: - args: The args passed. - kwargs: The kwargs passed. - """ - super().setdefault(*args, **kwargs) - self._reassign_field() - - def popitem(self): - """Pop last item.""" - super().popitem() - self._reassign_field() - - def pop(self, k, d=None): - """Remove an element. - - Args: - k: The args passed. - d: The kwargs passed. - """ - super().pop(k, d) - self._reassign_field() - - def update(self, *args, **kwargs): - """Update the dict with another dict. - - Args: - args: The args passed. - kwargs: The kwargs passed. - """ - super().update(*args, **kwargs) - self._reassign_field() - - def __setitem__(self, *args, **kwargs): - """Set an item in the dict. - - Args: - args: The args passed. - kwargs: The kwargs passed. - """ - super().__setitem__(*args, **kwargs) - self._reassign_field() if hasattr(self, "_reassign_field") else None - - def __delitem__(self, *args, **kwargs): - """Delete an item in the dict. - - Args: - args: The args passed. - kwargs: The kwargs passed. - """ - super().__delitem__(*args, **kwargs) - self._reassign_field() - - -class ReflexSet(set): - """A custom set that reflex can detect its mutation.""" - - def __init__( - self, - original_set: Set, - reassign_field: Callable = lambda _field_name: None, - field_name: str = "", - ): - """Initialize ReflexSet. - - Args: - original_set (Set): The original set - reassign_field (Callable): - The method in the parent state to reassign the field. - Default to be a no-op function - field_name (str): the name of field in the parent state - """ - self._reassign_field = lambda: reassign_field(field_name) - - super().__init__(original_set) - - def add(self, *args, **kwargs): - """Add an element to set. - - Args: - args: The args passed. - kwargs: The kwargs passed. - """ - super().add(*args, **kwargs) - self._reassign_field() - - def remove(self, *args, **kwargs): - """Remove an element. - Raise key error if element not found. - - Args: - args: The args passed. - kwargs: The kwargs passed. - """ - super().remove(*args, **kwargs) - self._reassign_field() - - def discard(self, *args, **kwargs): - """Remove an element. - Does not raise key error if element not found. - - Args: - args: The args passed. - kwargs: The kwargs passed. - """ - super().discard(*args, **kwargs) - self._reassign_field() - - def pop(self, *args, **kwargs): - """Remove an element. - - Args: - args: The args passed. - kwargs: The kwargs passed. - """ - super().pop(*args, **kwargs) - self._reassign_field() - - def clear(self, *args, **kwargs): - """Remove all elements from the set. - - Args: - args: The args passed. - kwargs: The kwargs passed. - """ - super().clear(*args, **kwargs) - self._reassign_field() - - def update(self, *args, **kwargs): - """Adds elements from an iterable to the set. - - Args: - args: The args passed. - kwargs: The kwargs passed. - """ - super().update(*args, **kwargs) - self._reassign_field() - - class ImportVar(Base): """An import var.""" diff --git a/reflex/vars.pyi b/reflex/vars.pyi index 7bb9810cd..e4c84324d 100644 --- a/reflex/vars.pyi +++ b/reflex/vars.pyi @@ -116,42 +116,6 @@ class ComputedVar(Var): def cached_var(fget: Callable[[Any], Any]) -> ComputedVar: ... -class ReflexList(list): - def __init__( - self, original_list: List, reassign_field: Callable = ..., field_name: str = ... - ) -> None: ... - def append(self, *args, **kwargs) -> None: ... - def insert(self, *args, **kwargs) -> None: ... - def __setitem__(self, *args, **kwargs) -> None: ... - def __delitem__(self, *args, **kwargs) -> None: ... - def clear(self, *args, **kwargs) -> None: ... - def extend(self, *args, **kwargs) -> None: ... - def pop(self, *args, **kwargs) -> None: ... - def remove(self, *args, **kwargs) -> None: ... - -class ReflexDict(dict): - def __init__( - self, original_dict: Dict, reassign_field: Callable = ..., field_name: str = ... - ) -> None: ... - def clear(self) -> None: ... - def setdefault(self, *args, **kwargs) -> None: ... - def popitem(self) -> None: ... - def pop(self, k, d: Incomplete | None = ...) -> None: ... - def update(self, *args, **kwargs) -> None: ... - def __setitem__(self, *args, **kwargs) -> None: ... - def __delitem__(self, *args, **kwargs) -> None: ... - -class ReflexSet(set): - def __init__( - self, original_set: Set, reassign_field: Callable = ..., field_name: str = ... - ) -> None: ... - def add(self, *args, **kwargs) -> None: ... - def remove(self, *args, **kwargs) -> None: ... - def discard(self, *args, **kwargs) -> None: ... - def pop(self, *args, **kwargs) -> None: ... - def clear(self, *args, **kwargs) -> None: ... - def update(self, *args, **kwargs) -> None: ... - class ImportVar(Base): tag: Optional[str] is_default: Optional[bool] = False diff --git a/tests/conftest.py b/tests/conftest.py index 5b9bab450..026ae47bc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -547,6 +547,16 @@ def mutable_state(): A state object. """ + class OtherBase(rx.Base): + bar: str = "" + + class CustomVar(rx.Base): + foo: str = "" + array: List[str] = [] + hashmap: Dict[str, str] = {} + test_set: Set[str] = set() + custom: OtherBase = OtherBase() + class MutableTestState(rx.State): """A test state.""" @@ -561,6 +571,8 @@ def mutable_state(): "third_key": {"key": "value"}, } test_set: Set[Union[str, int]] = {1, 2, 3, 4, "five"} + custom: CustomVar = CustomVar() + _be_custom: CustomVar = CustomVar() def reassign_mutables(self): self.array = ["modified_value", [1, 2, 3], {"mod_key": "mod_value"}] diff --git a/tests/test_state.py b/tests/test_state.py index e7f51c117..8240c961c 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -2,6 +2,7 @@ from __future__ import annotations import datetime import functools +import sys from typing import Dict, List import pytest @@ -11,9 +12,9 @@ import reflex as rx from reflex.base import Base from reflex.constants import IS_HYDRATED, RouteVar from reflex.event import Event, EventHandler -from reflex.state import State +from reflex.state import MutableProxy, State from reflex.utils import format -from reflex.vars import BaseVar, ComputedVar, ReflexDict, ReflexList, ReflexSet +from reflex.vars import BaseVar, ComputedVar class Object(Base): @@ -1310,31 +1311,54 @@ def test_setattr_of_mutable_types(mutable_state): hashmap = mutable_state.hashmap test_set = mutable_state.test_set - assert isinstance(array, ReflexList) - assert isinstance(array[1], ReflexList) - assert isinstance(array[2], ReflexDict) + assert isinstance(array, MutableProxy) + assert isinstance(array, list) + assert isinstance(array[1], MutableProxy) + assert isinstance(array[1], list) + assert isinstance(array[2], MutableProxy) + assert isinstance(array[2], dict) - assert isinstance(hashmap, ReflexDict) - assert isinstance(hashmap["key"], ReflexList) - assert isinstance(hashmap["third_key"], ReflexDict) + assert isinstance(hashmap, MutableProxy) + assert isinstance(hashmap, dict) + assert isinstance(hashmap["key"], MutableProxy) + assert isinstance(hashmap["key"], list) + assert isinstance(hashmap["third_key"], MutableProxy) + assert isinstance(hashmap["third_key"], dict) + assert isinstance(test_set, MutableProxy) assert isinstance(test_set, set) + assert isinstance(mutable_state.custom, MutableProxy) + assert isinstance(mutable_state.custom.array, MutableProxy) + assert isinstance(mutable_state.custom.array, list) + assert isinstance(mutable_state.custom.hashmap, MutableProxy) + assert isinstance(mutable_state.custom.hashmap, dict) + assert isinstance(mutable_state.custom.test_set, MutableProxy) + assert isinstance(mutable_state.custom.test_set, set) + assert isinstance(mutable_state.custom.custom, MutableProxy) + mutable_state.reassign_mutables() array = mutable_state.array hashmap = mutable_state.hashmap test_set = mutable_state.test_set - assert isinstance(array, ReflexList) - assert isinstance(array[1], ReflexList) - assert isinstance(array[2], ReflexDict) + assert isinstance(array, MutableProxy) + assert isinstance(array, list) + assert isinstance(array[1], MutableProxy) + assert isinstance(array[1], list) + assert isinstance(array[2], MutableProxy) + assert isinstance(array[2], dict) - assert isinstance(hashmap, ReflexDict) - assert isinstance(hashmap["mod_key"], ReflexList) - assert isinstance(hashmap["mod_third_key"], ReflexDict) + assert isinstance(hashmap, MutableProxy) + assert isinstance(hashmap, dict) + assert isinstance(hashmap["mod_key"], MutableProxy) + assert isinstance(hashmap["mod_key"], list) + assert isinstance(hashmap["mod_third_key"], MutableProxy) + assert isinstance(hashmap["mod_third_key"], dict) - assert isinstance(test_set, ReflexSet) + assert isinstance(test_set, MutableProxy) + assert isinstance(test_set, set) def test_error_on_state_method_shadow(): @@ -1375,3 +1399,187 @@ def test_state_with_invalid_yield(): "must only return/yield: None, Events or other EventHandlers" in err.value.args[0] ) + + +def test_mutable_list(mutable_state): + """Test that mutable lists are tracked correctly. + + Args: + mutable_state: A test state. + """ + assert not mutable_state.dirty_vars + + def assert_array_dirty(): + assert mutable_state.dirty_vars == {"array"} + mutable_state._clean() + assert not mutable_state.dirty_vars + + # Test all list operations + mutable_state.array.append(42) + assert_array_dirty() + mutable_state.array.extend([1, 2, 3]) + assert_array_dirty() + mutable_state.array.insert(0, 0) + assert_array_dirty() + mutable_state.array.pop() + assert_array_dirty() + mutable_state.array.remove(42) + assert_array_dirty() + mutable_state.array.clear() + assert_array_dirty() + mutable_state.array += [1, 2, 3] + assert_array_dirty() + mutable_state.array.reverse() + assert_array_dirty() + mutable_state.array.sort() + assert_array_dirty() + mutable_state.array[0] = 666 + assert_array_dirty() + del mutable_state.array[0] + assert_array_dirty() + + # Test nested list operations + mutable_state.array[0] = [1, 2, 3] + assert_array_dirty() + mutable_state.array[0].append(4) + assert_array_dirty() + assert isinstance(mutable_state.array[0], MutableProxy) + + +def test_mutable_dict(mutable_state): + """Test that mutable dicts are tracked correctly. + + Args: + mutable_state: A test state. + """ + assert not mutable_state.dirty_vars + + def assert_hashmap_dirty(): + assert mutable_state.dirty_vars == {"hashmap"} + mutable_state._clean() + assert not mutable_state.dirty_vars + + # Test all dict operations + mutable_state.hashmap.update({"new_key": 43}) + assert_hashmap_dirty() + mutable_state.hashmap.setdefault("another_key", 66) + assert_hashmap_dirty() + mutable_state.hashmap.pop("new_key") + assert_hashmap_dirty() + mutable_state.hashmap.popitem() + assert_hashmap_dirty() + mutable_state.hashmap.clear() + assert_hashmap_dirty() + mutable_state.hashmap["new_key"] = 42 + assert_hashmap_dirty() + del mutable_state.hashmap["new_key"] + assert_hashmap_dirty() + if sys.version_info >= (3, 9): + mutable_state.hashmap |= {"new_key": 44} + assert_hashmap_dirty() + + # Test nested dict operations + mutable_state.hashmap["array"] = [] + assert_hashmap_dirty() + mutable_state.hashmap["array"].append(1) + assert_hashmap_dirty() + mutable_state.hashmap["dict"] = {} + assert_hashmap_dirty() + mutable_state.hashmap["dict"]["key"] = 42 + assert_hashmap_dirty() + mutable_state.hashmap["dict"]["dict"] = {} + assert_hashmap_dirty() + mutable_state.hashmap["dict"]["dict"]["key"] = 43 + assert_hashmap_dirty() + + +def test_mutable_set(mutable_state): + """Test that mutable sets are tracked correctly. + + Args: + mutable_state: A test state. + """ + assert not mutable_state.dirty_vars + + def assert_set_dirty(): + assert mutable_state.dirty_vars == {"test_set"} + mutable_state._clean() + assert not mutable_state.dirty_vars + + # Test all set operations + mutable_state.test_set.add(42) + assert_set_dirty() + mutable_state.test_set.update([1, 2, 3]) + assert_set_dirty() + mutable_state.test_set.remove(42) + assert_set_dirty() + mutable_state.test_set.discard(3) + assert_set_dirty() + mutable_state.test_set.pop() + assert_set_dirty() + mutable_state.test_set.intersection_update([1, 2, 3]) + assert_set_dirty() + mutable_state.test_set.difference_update([99]) + assert_set_dirty() + mutable_state.test_set.symmetric_difference_update([102, 99]) + assert_set_dirty() + mutable_state.test_set |= {1, 2, 3} + assert_set_dirty() + mutable_state.test_set &= {2, 3, 4} + assert_set_dirty() + mutable_state.test_set -= {2} + assert_set_dirty() + mutable_state.test_set ^= {42} + assert_set_dirty() + mutable_state.test_set.clear() + assert_set_dirty() + + +def test_mutable_custom(mutable_state): + """Test that mutable custom types derived from Base are tracked correctly. + + Args: + mutable_state: A test state. + """ + assert not mutable_state.dirty_vars + + def assert_custom_dirty(): + assert mutable_state.dirty_vars == {"custom"} + mutable_state._clean() + assert not mutable_state.dirty_vars + + mutable_state.custom.foo = "bar" + assert_custom_dirty() + mutable_state.custom.array.append(42) + assert_custom_dirty() + mutable_state.custom.hashmap["key"] = 68 + assert_custom_dirty() + mutable_state.custom.test_set.add(42) + assert_custom_dirty() + mutable_state.custom.custom.bar = "baz" + assert_custom_dirty() + + +def test_mutable_backend(mutable_state): + """Test that mutable backend vars are tracked correctly. + + Args: + mutable_state: A test state. + """ + assert not mutable_state.dirty_vars + + def assert_custom_dirty(): + assert mutable_state.dirty_vars == {"_be_custom"} + mutable_state._clean() + assert not mutable_state.dirty_vars + + mutable_state._be_custom.foo = "bar" + assert_custom_dirty() + mutable_state._be_custom.array.append(42) + assert_custom_dirty() + mutable_state._be_custom.hashmap["key"] = 68 + assert_custom_dirty() + mutable_state._be_custom.test_set.add(42) + assert_custom_dirty() + mutable_state._be_custom.custom.bar = "baz" + assert_custom_dirty() diff --git a/tests/test_var.py b/tests/test_var.py index 3289afed8..3d9d212a2 100644 --- a/tests/test_var.py +++ b/tests/test_var.py @@ -2,7 +2,6 @@ import json import typing from typing import Dict, List, Set, Tuple -import cloudpickle import pytest from pandas import DataFrame @@ -12,9 +11,6 @@ from reflex.vars import ( BaseVar, ComputedVar, ImportVar, - ReflexDict, - ReflexList, - ReflexSet, Var, get_local_storage, ) @@ -586,36 +582,6 @@ def test_computed_var_with_annotation_error(request, fixture, full_name): ) -def test_pickleable_rx_list(): - """Test that ReflexList is pickleable.""" - rx_list = ReflexList( - original_list=[1, 2, 3], reassign_field=lambda x: x, field_name="random" - ) - - pickled_list = cloudpickle.dumps(rx_list) - assert cloudpickle.loads(pickled_list) == rx_list - - -def test_pickleable_rx_dict(): - """Test that ReflexDict is pickleable.""" - rx_dict = ReflexDict( - original_dict={1: 2, 3: 4}, reassign_field=lambda x: x, field_name="random" - ) - - pickled_dict = cloudpickle.dumps(rx_dict) - assert cloudpickle.loads(pickled_dict) == rx_dict - - -def test_pickleable_rx_set(): - """Test that ReflexSet is pickleable.""" - rx_set = ReflexSet( - original_set={1, 2, 3}, reassign_field=lambda x: x, field_name="random" - ) - - pickled_set = cloudpickle.dumps(rx_set) - assert cloudpickle.loads(pickled_set) == rx_set - - @pytest.mark.parametrize( "import_var,expected", zip(