From 1938a6cc588423beef4c91789240ee0d99166cd5 Mon Sep 17 00:00:00 2001 From: Nikhil Rao Date: Fri, 15 Sep 2023 17:19:26 -0700 Subject: [PATCH] Add serializers for different var types (#1816) --- poetry.lock | 234 ++++++++++++------ pyproject.toml | 5 +- reflex/components/datadisplay/datatable.py | 73 ++++-- reflex/components/disclosure/tabs.pyi | 1 + reflex/components/forms/select.pyi | 3 +- reflex/components/graphing/plotly.py | 40 +-- reflex/components/graphing/plotly.pyi | 4 +- reflex/components/media/image.py | 35 ++- reflex/components/media/image.pyi | 2 +- reflex/model.py | 1 + reflex/utils/format.py | 135 +--------- reflex/utils/serializers.py | 193 +++++++++++++++ reflex/utils/types.py | 56 +---- reflex/vars.py | 18 +- scripts/pyi_generator.py | 11 +- .../components/datadisplay/test_datatable.py | 27 +- tests/components/graphing/test_plotly.py | 34 +++ tests/components/media/test_image.py | 65 +++++ ...t_prerequites.py => test_prerequisites.py} | 0 tests/test_var.py | 7 +- tests/utils/__init__.py | 0 tests/utils/test_serializers.py | 102 ++++++++ tests/{ => utils}/test_utils.py | 3 +- 23 files changed, 735 insertions(+), 314 deletions(-) create mode 100644 reflex/utils/serializers.py create mode 100644 tests/components/graphing/test_plotly.py create mode 100644 tests/components/media/test_image.py rename tests/{test_prerequites.py => test_prerequisites.py} (100%) create mode 100644 tests/utils/__init__.py create mode 100644 tests/utils/test_serializers.py rename tests/{ => utils}/test_utils.py (99%) diff --git a/poetry.lock b/poetry.lock index dd6d2efaa..7b7d68f5c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,14 +1,14 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. [[package]] name = "alembic" -version = "1.11.1" +version = "1.12.0" description = "A database migration tool for SQLAlchemy." optional = false python-versions = ">=3.7" files = [ - {file = "alembic-1.11.1-py3-none-any.whl", hash = "sha256:dc871798a601fab38332e38d6ddb38d5e734f60034baeb8e2db5b642fccd8ab8"}, - {file = "alembic-1.11.1.tar.gz", hash = "sha256:6a810a6b012c88b33458fceb869aef09ac75d6ace5291915ba7fae44de372c01"}, + {file = "alembic-1.12.0-py3-none-any.whl", hash = "sha256:03226222f1cf943deee6c85d9464261a6c710cd19b4fe867a3ad1f25afda610f"}, + {file = "alembic-1.12.0.tar.gz", hash = "sha256:8e7645c32e4f200675e69f0745415335eb59a3663f5feb487abfa0b30c45888b"}, ] [package.dependencies] @@ -45,13 +45,13 @@ trio = ["trio (<0.22)"] [[package]] name = "async-timeout" -version = "4.0.2" +version = "4.0.3" description = "Timeout context manager for asyncio programs" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, - {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, + {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, + {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, ] [package.dependencies] @@ -230,24 +230,24 @@ pycparser = "*" [[package]] name = "cfgv" -version = "3.3.1" +version = "3.4.0" description = "Validate configuration and produce human readable error messages." optional = false -python-versions = ">=3.6.1" +python-versions = ">=3.8" files = [ - {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, - {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, ] [[package]] name = "click" -version = "8.1.6" +version = "8.1.7" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" files = [ - {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"}, - {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"}, + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, ] [package.dependencies] @@ -386,13 +386,13 @@ files = [ [[package]] name = "exceptiongroup" -version = "1.1.2" +version = "1.1.3" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.1.2-py3-none-any.whl", hash = "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"}, - {file = "exceptiongroup-1.1.2.tar.gz", hash = "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5"}, + {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, + {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, ] [package.extras] @@ -421,18 +421,19 @@ test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==23.1.0)", "coverage[toml] (>=6 [[package]] name = "filelock" -version = "3.12.2" +version = "3.12.4" description = "A platform independent file lock." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"}, - {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"}, + {file = "filelock-3.12.4-py3-none-any.whl", hash = "sha256:08c21d87ded6e2b9da6728c3dff51baf1dcecf973b768ef35bcbc3447edb9ad4"}, + {file = "filelock-3.12.4.tar.gz", hash = "sha256:2e6f249f1f3654291606e046b09f1fd5eac39b360664c27f5aad072012f8bcbd"}, ] [package.extras] -docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] +docs = ["furo (>=2023.7.26)", "sphinx (>=7.1.2)", "sphinx-autodoc-typehints (>=1.24)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3)", "diff-cover (>=7.7)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-timeout (>=2.1)"] +typing = ["typing-extensions (>=4.7.1)"] [[package]] name = "greenlet" @@ -587,13 +588,13 @@ socks = ["socksio (==1.*)"] [[package]] name = "identify" -version = "2.5.26" +version = "2.5.29" description = "File identification library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "identify-2.5.26-py2.py3-none-any.whl", hash = "sha256:c22a8ead0d4ca11f1edd6c9418c3220669b3b7533ada0a0ffa6cc0ef85cf9b54"}, - {file = "identify-2.5.26.tar.gz", hash = "sha256:7243800bce2f58404ed41b7c002e53d4d22bcf3ae1b7900c2d7aefd95394bf7f"}, + {file = "identify-2.5.29-py2.py3-none-any.whl", hash = "sha256:24437fbf6f4d3fe6efd0eb9d67e24dd9106db99af5ceb27996a5f7895f24bf1b"}, + {file = "identify-2.5.29.tar.gz", hash = "sha256:d43d52b86b15918c137e3a74fff5224f60385cd0e9c38e99d07c257f02f151a5"}, ] [package.extras] @@ -1032,8 +1033,8 @@ files = [ [package.dependencies] numpy = [ {version = ">=1.20.3", markers = "python_version < \"3.10\""}, - {version = ">=1.21.0", markers = "python_version >= \"3.10\""}, {version = ">=1.23.2", markers = "python_version >= \"3.11\""}, + {version = ">=1.21.0", markers = "python_version >= \"3.10\" and python_version < \"3.11\""}, ] python-dateutil = ">=2.8.1" pytz = ">=2020.1" @@ -1052,6 +1053,73 @@ files = [ {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, ] +[[package]] +name = "pillow" +version = "10.0.1" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "Pillow-10.0.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:8f06be50669087250f319b706decf69ca71fdecd829091a37cc89398ca4dc17a"}, + {file = "Pillow-10.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:50bd5f1ebafe9362ad622072a1d2f5850ecfa44303531ff14353a4059113b12d"}, + {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6a90167bcca1216606223a05e2cf991bb25b14695c518bc65639463d7db722d"}, + {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f11c9102c56ffb9ca87134bd025a43d2aba3f1155f508eff88f694b33a9c6d19"}, + {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:186f7e04248103482ea6354af6d5bcedb62941ee08f7f788a1c7707bc720c66f"}, + {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0462b1496505a3462d0f35dc1c4d7b54069747d65d00ef48e736acda2c8cbdff"}, + {file = "Pillow-10.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d889b53ae2f030f756e61a7bff13684dcd77e9af8b10c6048fb2c559d6ed6eaf"}, + {file = "Pillow-10.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:552912dbca585b74d75279a7570dd29fa43b6d93594abb494ebb31ac19ace6bd"}, + {file = "Pillow-10.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:787bb0169d2385a798888e1122c980c6eff26bf941a8ea79747d35d8f9210ca0"}, + {file = "Pillow-10.0.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:fd2a5403a75b54661182b75ec6132437a181209b901446ee5724b589af8edef1"}, + {file = "Pillow-10.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2d7e91b4379f7a76b31c2dda84ab9e20c6220488e50f7822e59dac36b0cd92b1"}, + {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19e9adb3f22d4c416e7cd79b01375b17159d6990003633ff1d8377e21b7f1b21"}, + {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93139acd8109edcdeffd85e3af8ae7d88b258b3a1e13a038f542b79b6d255c54"}, + {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:92a23b0431941a33242b1f0ce6c88a952e09feeea9af4e8be48236a68ffe2205"}, + {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:cbe68deb8580462ca0d9eb56a81912f59eb4542e1ef8f987405e35a0179f4ea2"}, + {file = "Pillow-10.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:522ff4ac3aaf839242c6f4e5b406634bfea002469656ae8358644fc6c4856a3b"}, + {file = "Pillow-10.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:84efb46e8d881bb06b35d1d541aa87f574b58e87f781cbba8d200daa835b42e1"}, + {file = "Pillow-10.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:898f1d306298ff40dc1b9ca24824f0488f6f039bc0e25cfb549d3195ffa17088"}, + {file = "Pillow-10.0.1-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:bcf1207e2f2385a576832af02702de104be71301c2696d0012b1b93fe34aaa5b"}, + {file = "Pillow-10.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5d6c9049c6274c1bb565021367431ad04481ebb54872edecfcd6088d27edd6ed"}, + {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28444cb6ad49726127d6b340217f0627abc8732f1194fd5352dec5e6a0105635"}, + {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de596695a75496deb3b499c8c4f8e60376e0516e1a774e7bc046f0f48cd620ad"}, + {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:2872f2d7846cf39b3dbff64bc1104cc48c76145854256451d33c5faa55c04d1a"}, + {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4ce90f8a24e1c15465048959f1e94309dfef93af272633e8f37361b824532e91"}, + {file = "Pillow-10.0.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ee7810cf7c83fa227ba9125de6084e5e8b08c59038a7b2c9045ef4dde61663b4"}, + {file = "Pillow-10.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b1be1c872b9b5fcc229adeadbeb51422a9633abd847c0ff87dc4ef9bb184ae08"}, + {file = "Pillow-10.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:98533fd7fa764e5f85eebe56c8e4094db912ccbe6fbf3a58778d543cadd0db08"}, + {file = "Pillow-10.0.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:764d2c0daf9c4d40ad12fbc0abd5da3af7f8aa11daf87e4fa1b834000f4b6b0a"}, + {file = "Pillow-10.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fcb59711009b0168d6ee0bd8fb5eb259c4ab1717b2f538bbf36bacf207ef7a68"}, + {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:697a06bdcedd473b35e50a7e7506b1d8ceb832dc238a336bd6f4f5aa91a4b500"}, + {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f665d1e6474af9f9da5e86c2a3a2d2d6204e04d5af9c06b9d42afa6ebde3f21"}, + {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:2fa6dd2661838c66f1a5473f3b49ab610c98a128fc08afbe81b91a1f0bf8c51d"}, + {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:3a04359f308ebee571a3127fdb1bd01f88ba6f6fb6d087f8dd2e0d9bff43f2a7"}, + {file = "Pillow-10.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:723bd25051454cea9990203405fa6b74e043ea76d4968166dfd2569b0210886a"}, + {file = "Pillow-10.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:71671503e3015da1b50bd18951e2f9daf5b6ffe36d16f1eb2c45711a301521a7"}, + {file = "Pillow-10.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:44e7e4587392953e5e251190a964675f61e4dae88d1e6edbe9f36d6243547ff3"}, + {file = "Pillow-10.0.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:3855447d98cced8670aaa63683808df905e956f00348732448b5a6df67ee5849"}, + {file = "Pillow-10.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ed2d9c0704f2dc4fa980b99d565c0c9a543fe5101c25b3d60488b8ba80f0cce1"}, + {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5bb289bb835f9fe1a1e9300d011eef4d69661bb9b34d5e196e5e82c4cb09b37"}, + {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a0d3e54ab1df9df51b914b2233cf779a5a10dfd1ce339d0421748232cea9876"}, + {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:2cc6b86ece42a11f16f55fe8903595eff2b25e0358dec635d0a701ac9586588f"}, + {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:ca26ba5767888c84bf5a0c1a32f069e8204ce8c21d00a49c90dabeba00ce0145"}, + {file = "Pillow-10.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f0b4b06da13275bc02adfeb82643c4a6385bd08d26f03068c2796f60d125f6f2"}, + {file = "Pillow-10.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bc2e3069569ea9dbe88d6b8ea38f439a6aad8f6e7a6283a38edf61ddefb3a9bf"}, + {file = "Pillow-10.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:8b451d6ead6e3500b6ce5c7916a43d8d8d25ad74b9102a629baccc0808c54971"}, + {file = "Pillow-10.0.1-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:32bec7423cdf25c9038fef614a853c9d25c07590e1a870ed471f47fb80b244db"}, + {file = "Pillow-10.0.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7cf63d2c6928b51d35dfdbda6f2c1fddbe51a6bc4a9d4ee6ea0e11670dd981e"}, + {file = "Pillow-10.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f6d3d4c905e26354e8f9d82548475c46d8e0889538cb0657aa9c6f0872a37aa4"}, + {file = "Pillow-10.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:847e8d1017c741c735d3cd1883fa7b03ded4f825a6e5fcb9378fd813edee995f"}, + {file = "Pillow-10.0.1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:7f771e7219ff04b79e231d099c0a28ed83aa82af91fd5fa9fdb28f5b8d5addaf"}, + {file = "Pillow-10.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:459307cacdd4138edee3875bbe22a2492519e060660eaf378ba3b405d1c66317"}, + {file = "Pillow-10.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b059ac2c4c7a97daafa7dc850b43b2d3667def858a4f112d1aa082e5c3d6cf7d"}, + {file = "Pillow-10.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6caf3cd38449ec3cd8a68b375e0c6fe4b6fd04edb6c9766b55ef84a6e8ddf2d"}, + {file = "Pillow-10.0.1.tar.gz", hash = "sha256:d72967b06be9300fed5cfbc8b5bafceec48bf7cdc7dab66b1d2549035287191d"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] + [[package]] name = "platformdirs" version = "3.10.0" @@ -1072,13 +1140,13 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-co [[package]] name = "plotly" -version = "5.15.0" +version = "5.17.0" description = "An open-source, interactive data visualization library for Python" optional = false python-versions = ">=3.6" files = [ - {file = "plotly-5.15.0-py2.py3-none-any.whl", hash = "sha256:3508876bbd6aefb8a692c21a7128ca87ce42498dd041efa5c933ee44b55aab24"}, - {file = "plotly-5.15.0.tar.gz", hash = "sha256:822eabe53997d5ebf23c77e1d1fcbf3bb6aa745eb05d532afd4b6f9a2e2ab02f"}, + {file = "plotly-5.17.0-py2.py3-none-any.whl", hash = "sha256:7c84cdf11da162423da957bb093287134f2d6f170eb9a74f1459f825892247c3"}, + {file = "plotly-5.17.0.tar.gz", hash = "sha256:290d796bf7bab87aad184fe24b86096234c4c95dcca6ecbca02d02bdf17d3d97"}, ] [package.dependencies] @@ -1105,13 +1173,13 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "3.3.3" +version = "3.4.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.8" files = [ - {file = "pre_commit-3.3.3-py2.py3-none-any.whl", hash = "sha256:10badb65d6a38caff29703362271d7dca483d01da88f9d7e05d0b97171c136cb"}, - {file = "pre_commit-3.3.3.tar.gz", hash = "sha256:a2256f489cd913d575c145132ae196fe335da32d91a8294b7afe6622335dd023"}, + {file = "pre_commit-3.4.0-py2.py3-none-any.whl", hash = "sha256:96d529a951f8b677f730a7212442027e8ba53f9b04d217c4c67dc56c393ad945"}, + {file = "pre_commit-3.4.0.tar.gz", hash = "sha256:6bbd5129a64cad4c0dfaeeb12cd8f7ea7e15b77028d985341478c8af3c759522"}, ] [package.dependencies] @@ -1212,13 +1280,13 @@ email = ["email-validator (>=1.0.3)"] [[package]] name = "pygments" -version = "2.15.1" +version = "2.16.1" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.7" files = [ - {file = "Pygments-2.15.1-py3-none-any.whl", hash = "sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1"}, - {file = "Pygments-2.15.1.tar.gz", hash = "sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c"}, + {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, + {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, ] [package.extras] @@ -1226,13 +1294,13 @@ plugins = ["importlib-metadata"] [[package]] name = "pyright" -version = "1.1.318" +version = "1.1.327" description = "Command line wrapper for pyright" optional = false python-versions = ">=3.7" files = [ - {file = "pyright-1.1.318-py3-none-any.whl", hash = "sha256:056c1b2e711c3526e32919de1684ae599d34b7ec27e94398858a43f56ac9ba9b"}, - {file = "pyright-1.1.318.tar.gz", hash = "sha256:69dcf9c32d5be27d531750de627e76a7cadc741d333b547c09044278b508db7b"}, + {file = "pyright-1.1.327-py3-none-any.whl", hash = "sha256:3462cda239e9140276238bbdbd0b59d77406f1c2e14d8cb8c20c8e25639c6b3c"}, + {file = "pyright-1.1.327.tar.gz", hash = "sha256:ba74148ad64f22020dbbed6781c4bdb38ecb8a7ca90dc3c87a4f08d1c0e11592"}, ] [package.dependencies] @@ -1257,13 +1325,13 @@ files = [ [[package]] name = "pytest" -version = "7.4.0" +version = "7.4.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, - {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, + {file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"}, + {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"}, ] [package.dependencies] @@ -1348,15 +1416,18 @@ six = ">=1.5" [[package]] name = "python-engineio" -version = "4.5.1" +version = "4.7.1" description = "Engine.IO server and client for Python" optional = false python-versions = ">=3.6" files = [ - {file = "python-engineio-4.5.1.tar.gz", hash = "sha256:b167a1b208fcdce5dbe96a61a6ca22391cfa6715d796c22de93e3adf9c07ae0c"}, - {file = "python_engineio-4.5.1-py3-none-any.whl", hash = "sha256:67a675569f3e9bb274a8077f3c2068a8fe79cbfcb111cf31ca27b968484fe6c7"}, + {file = "python-engineio-4.7.1.tar.gz", hash = "sha256:a8422e345cd9a21451303380b160742ff02197975b1c3a02cef115febe2b1b20"}, + {file = "python_engineio-4.7.1-py3-none-any.whl", hash = "sha256:52499e8ab94fea1a6525ffe872fe7028d04b575799c5fa8e2cf7880e032de42e"}, ] +[package.dependencies] +simple-websocket = ">=0.10.0" + [package.extras] asyncio-client = ["aiohttp (>=3.4)"] client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"] @@ -1377,32 +1448,33 @@ six = ">=1.4.0" [[package]] name = "python-socketio" -version = "5.8.0" +version = "5.9.0" description = "Socket.IO server and client for Python" optional = false python-versions = ">=3.6" files = [ - {file = "python-socketio-5.8.0.tar.gz", hash = "sha256:e714f4dddfaaa0cb0e37a1e2deef2bb60590a5b9fea9c343dd8ca5e688416fd9"}, - {file = "python_socketio-5.8.0-py3-none-any.whl", hash = "sha256:7adb8867aac1c2929b9c1429f1c02e12ca4c36b67c807967393e367dfbb01441"}, + {file = "python-socketio-5.9.0.tar.gz", hash = "sha256:dc42735f65534187f381fde291ebf620216a4960001370f32de940229b2e7f8f"}, + {file = "python_socketio-5.9.0-py3-none-any.whl", hash = "sha256:c20f12e4ed0cba57581af26bbeea9998bc2eeebb3b952fa92493a1e051cfe9dc"}, ] [package.dependencies] bidict = ">=0.21.0" -python-engineio = ">=4.3.0" +python-engineio = ">=4.7.0" [package.extras] asyncio-client = ["aiohttp (>=3.4)"] client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"] +docs = ["sphinx"] [[package]] name = "pytz" -version = "2023.3" +version = "2023.3.post1" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" files = [ - {file = "pytz-2023.3-py2.py3-none-any.whl", hash = "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb"}, - {file = "pytz-2023.3.tar.gz", hash = "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588"}, + {file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"}, + {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, ] [[package]] @@ -1417,6 +1489,7 @@ 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"}, @@ -1424,8 +1497,15 @@ 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"}, @@ -1442,6 +1522,7 @@ 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"}, @@ -1449,6 +1530,7 @@ 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"}, @@ -1476,13 +1558,13 @@ ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)" [[package]] name = "rich" -version = "13.5.1" +version = "13.5.2" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.7.0" files = [ - {file = "rich-13.5.1-py3-none-any.whl", hash = "sha256:b97381b204a206e1be618f5e1215a57174a1a7732490b3bf6668cf41d30bc72d"}, - {file = "rich-13.5.1.tar.gz", hash = "sha256:881653ee7037803559d8eae98f145e0a4c4b0ec3ff0300d2cc8d479c71fc6819"}, + {file = "rich-13.5.2-py3-none-any.whl", hash = "sha256:146a90b3b6b47cac4a73c12866a499e9817426423f57c5a66949c086191a8808"}, + {file = "rich-13.5.2.tar.gz", hash = "sha256:fb9d6c0a0f643c99eed3875b5377a184132ba9be4d61516a55273d3554d75a39"}, ] [package.dependencies] @@ -1551,6 +1633,20 @@ docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-g testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +[[package]] +name = "simple-websocket" +version = "0.10.1" +description = "Simple WebSocket server and client for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "simple-websocket-0.10.1.tar.gz", hash = "sha256:0ab46c8ffa51a46dc95eed94608b3b722841c0bf849def71d465c5c356679c82"}, + {file = "simple_websocket-0.10.1-py3-none-any.whl", hash = "sha256:62c36bacfd75cc867927bb39d91951342a7234bdfe20f41dd969a3b8bb1413b7"}, +] + +[package.dependencies] +wsproto = "*" + [[package]] name = "six" version = "1.16.0" @@ -1635,7 +1731,7 @@ files = [ ] [package.dependencies] -greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\")"} +greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [package.extras] @@ -1731,13 +1827,13 @@ test = ["aiomysql (>=0.1.1,<0.2.0)", "aiosqlite (>=0.17.0,<0.20.0)", "arrow (>=1 [[package]] name = "tenacity" -version = "8.2.2" +version = "8.2.3" description = "Retry code until it succeeds" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "tenacity-8.2.2-py3-none-any.whl", hash = "sha256:2f277afb21b851637e8f52e6a613ff08734c347dc19ade928e519d7d2d8569b0"}, - {file = "tenacity-8.2.2.tar.gz", hash = "sha256:43af037822bd0029025877f3b2d97cc4d7bb0c2991000a3d59d71517c5c969e0"}, + {file = "tenacity-8.2.3-py3-none-any.whl", hash = "sha256:ce510e327a630c9e1beaf17d42e6ffacc88185044ad85cf74c0a8887c6a0f88c"}, + {file = "tenacity-8.2.3.tar.gz", hash = "sha256:5398ef0d78e63f40007c1fb4c0bff96e1911394d2fa8d194f77619c05ff6cc8a"}, ] [package.extras] @@ -1787,13 +1883,13 @@ sortedcontainers = "*" [[package]] name = "trio-websocket" -version = "0.10.3" +version = "0.10.4" description = "WebSocket library for Trio" optional = false python-versions = ">=3.7" files = [ - {file = "trio-websocket-0.10.3.tar.gz", hash = "sha256:1a748604ad906a7dcab9a43c6eb5681e37de4793ba0847ef0bc9486933ed027b"}, - {file = "trio_websocket-0.10.3-py3-none-any.whl", hash = "sha256:a9937d48e8132ebf833019efde2a52ca82d223a30a7ea3e8d60a7d28f75a4e3a"}, + {file = "trio-websocket-0.10.4.tar.gz", hash = "sha256:e66b3db3e2453017431dfbd352081006654e1241c2a6800dc2f43d7df54d55c5"}, + {file = "trio_websocket-0.10.4-py3-none-any.whl", hash = "sha256:c7a620c4013c34b7e4477d89fe76695da1e455e4510a8d7ae13f81c632bdce1d"}, ] [package.dependencies] @@ -1923,13 +2019,13 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", [[package]] name = "virtualenv" -version = "20.24.2" +version = "20.24.5" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.24.2-py3-none-any.whl", hash = "sha256:43a3052be36080548bdee0b42919c88072037d50d56c28bd3f853cbe92b953ff"}, - {file = "virtualenv-20.24.2.tar.gz", hash = "sha256:fd8a78f46f6b99a67b7ec5cf73f92357891a7b3a40fd97637c27f854aae3b9e0"}, + {file = "virtualenv-20.24.5-py3-none-any.whl", hash = "sha256:b80039f280f4919c77b30f1c23294ae357c4c8701042086e3fc005963e4e537b"}, + {file = "virtualenv-20.24.5.tar.gz", hash = "sha256:e8361967f6da6fbdf1426483bfe9fca8287c242ac0bc30429905721cefbff752"}, ] [package.dependencies] @@ -1938,7 +2034,7 @@ filelock = ">=3.12.2,<4" platformdirs = ">=3.9.1,<4" [package.extras] -docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] [[package]] @@ -2125,4 +2221,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = "^3.7" -content-hash = "44cce3d4423be203bf6b1ddc046cbdd9061924523b86baea8a42cd954dc86b36" +content-hash = "0dd6230851cc4f43e192e45431d1c1dcb451b7946ae7cd169e220e7f7a072aa2" diff --git a/pyproject.toml b/pyproject.toml index ea2dcf431..727984196 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,6 @@ fastapi = "^0.96.0" gunicorn = "^20.1.0" httpx = "^0.24.0" jinja2 = "^3.1.2" -plotly = "^5.13.0" psutil = "^5.9.4" pydantic = "^1.10.2" python-multipart = "^0.0.5" @@ -63,6 +62,10 @@ pandas = [ {version = "^1.5.3", python = ">=3.8,<4.0"}, {version = "^1.1", python = ">=3.7, <3.8"} ] +pillow = [ + {version = "^10.0.0", python = ">=3.8,<4.0"} +] +plotly = "^5.13.0" asynctest = "^0.13.0" pre-commit = {version = "^3.2.1", python = ">=3.8,<4.0"} selenium = "^4.11.0" diff --git a/reflex/components/datadisplay/datatable.py b/reflex/components/datadisplay/datatable.py index 0347fca39..a06a389d7 100644 --- a/reflex/components/datadisplay/datatable.py +++ b/reflex/components/datadisplay/datatable.py @@ -1,10 +1,13 @@ """Table components.""" +from __future__ import annotations + from typing import Any, Dict, List, Union from reflex.components.component import Component from reflex.components.tags import Tag -from reflex.utils import format, imports, types +from reflex.utils import imports, types +from reflex.utils.serializers import serialize, serializer from reflex.vars import BaseVar, ComputedVar, ImportVar, Var @@ -106,23 +109,59 @@ class DataTable(Gridjs): ) def _render(self) -> Tag: - if isinstance(self.data, Var): - if types.is_dataframe(self.data.type_): - self.columns = BaseVar( - name=f"{self.data.name}.columns", - type_=List[Any], - state=self.data.state, - ) - self.data = BaseVar( - name=f"{self.data.name}.data", - type_=List[List[Any]], - state=self.data.state, - ) - else: + if isinstance(self.data, Var) and types.is_dataframe(self.data.type_): + self.columns = BaseVar( + name=f"{self.data.name}.columns", + type_=List[Any], + state=self.data.state, + ) + self.data = BaseVar( + name=f"{self.data.name}.data", + type_=List[List[Any]], + state=self.data.state, + ) + if types.is_dataframe(type(self.data)): # If given a pandas df break up the data and columns - if types.is_dataframe(type(self.data)): - self.columns = Var.create(list(self.data.columns.values.tolist())) # type: ignore - self.data = Var.create(format.format_dataframe_values(self.data)) # type: ignore + data = serialize(self.data) + assert isinstance(data, dict), "Serialized dataframe should be a dict." + self.columns = Var.create_safe(data["columns"]) + self.data = Var.create_safe(data["data"]) # Render the table. return super()._render() + + +try: + from pandas import DataFrame + + def format_dataframe_values(df: DataFrame) -> List[List[Any]]: + """Format dataframe values to a list of lists. + + Args: + df: The dataframe to format. + + Returns: + The dataframe as a list of lists. + """ + return [ + [str(d) if isinstance(d, (list, tuple)) else d for d in data] + for data in list(df.values.tolist()) + ] + + @serializer + def serialize_dataframe(df: DataFrame) -> dict: + """Serialize a pandas dataframe. + + Args: + df: The dataframe to serialize. + + Returns: + The serialized dataframe. + """ + return { + "columns": df.columns.tolist(), + "data": format_dataframe_values(df), + } + +except ImportError: + pass diff --git a/reflex/components/disclosure/tabs.pyi b/reflex/components/disclosure/tabs.pyi index 2af1719e3..dde3ebd7d 100644 --- a/reflex/components/disclosure/tabs.pyi +++ b/reflex/components/disclosure/tabs.pyi @@ -25,6 +25,7 @@ class Tabs(ChakraComponent): is_manual: If true, the tabs will be manually activated and display its panel by pressing Space or Enter. If false, the tabs will be automatically activated and their panel is displayed when they receive focus. orientation: The orientation of the tab list. variant: "line" | "enclosed" | "enclosed-colored" | "soft-rounded" | "solid-rounded" | "unstyled" + color_scheme: The color scheme of the tabs. items: The items for the tabs component, a list of tuple (label, panel) **props: The properties of the component. diff --git a/reflex/components/forms/select.pyi b/reflex/components/forms/select.pyi index 97a7d35a5..c28f7bea0 100644 --- a/reflex/components/forms/select.pyi +++ b/reflex/components/forms/select.pyi @@ -13,7 +13,7 @@ from reflex.event import EventHandler, EventChain, EventSpec class Select(ChakraComponent): @overload @classmethod - def create(cls, *children, value: Optional[Union[Var[str], str]] = None, default_value: Optional[Union[Var[str], str]] = None, placeholder: Optional[Union[Var[str], str]] = None, error_border_color: Optional[Union[Var[str], str]] = None, focus_border_color: Optional[Union[Var[str], str]] = None, is_disabled: Optional[Union[Var[bool], bool]] = None, is_invalid: Optional[Union[Var[bool], bool]] = None, is_read_only: Optional[Union[Var[bool], bool]] = None, is_required: Optional[Union[Var[bool], bool]] = None, variant: Optional[Union[Var[str], str]] = None, size: Optional[Union[Var[str], str]] = None, on_blur: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_change: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_click: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_context_menu: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_double_click: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_focus: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mount: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_down: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_enter: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_leave: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_move: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_out: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_over: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_up: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_scroll: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_unmount: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, **props) -> "Select": # type: ignore + def create(cls, *children, value: Optional[Union[Var[str], str]] = None, default_value: Optional[Union[Var[str], str]] = None, placeholder: Optional[Union[Var[str], str]] = None, error_border_color: Optional[Union[Var[str], str]] = None, focus_border_color: Optional[Union[Var[str], str]] = None, is_disabled: Optional[Union[Var[bool], bool]] = None, is_invalid: Optional[Union[Var[bool], bool]] = None, is_required: Optional[Union[Var[bool], bool]] = None, variant: Optional[Union[Var[str], str]] = None, size: Optional[Union[Var[str], str]] = None, on_blur: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_change: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_click: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_context_menu: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_double_click: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_focus: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mount: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_down: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_enter: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_leave: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_move: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_out: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_over: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_up: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_scroll: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_unmount: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, **props) -> "Select": # type: ignore """Create a select component. If a list is provided as the first children, a default component @@ -28,7 +28,6 @@ class Select(ChakraComponent): focus_border_color: The border color when the select is focused. is_disabled: If true, the select will be disabled. is_invalid: If true, the form control will be invalid. This has 2 side effects: - The FormLabel and FormErrorIcon will have `data-invalid` set to true - The form element (e.g, Input) will have `aria-invalid` set to true - is_read_only: If true, the form control will be readonly is_required: If true, the form control will be required. This has 2 side effects: - The FormLabel will show a required indicator - The form element (e.g, Input) will have `aria-required` set to true variant: "outline" | "filled" | "flushed" | "unstyled" size: The size of the select. diff --git a/reflex/components/graphing/plotly.py b/reflex/components/graphing/plotly.py index 3a0d46965..8618da14c 100644 --- a/reflex/components/graphing/plotly.py +++ b/reflex/components/graphing/plotly.py @@ -1,13 +1,17 @@ """Component for displaying a plotly graph.""" -from typing import Dict, List - -from plotly.graph_objects import Figure +import json +from typing import Any, Dict, List from reflex.components.component import NoSSRComponent -from reflex.components.tags import Tag +from reflex.utils.serializers import serializer from reflex.vars import Var +try: + from plotly.graph_objects import Figure +except ImportError: + Figure = Any + class PlotlyLib(NoSSRComponent): """A component that wraps a plotly lib.""" @@ -39,14 +43,22 @@ class Plotly(PlotlyLib): # If true, the graph will resize when the window is resized. use_resize_handler: Var[bool] - def _render(self) -> Tag: - if ( - isinstance(self.data, Figure) - and self.layout is None - and self.width is not None - ): - layout = Var.create({"width": self.width, "height": self.height}) - assert layout is not None - self.layout = layout - return super()._render() +try: + from plotly.graph_objects import Figure + from plotly.io import to_json + + @serializer + def serialize_figure(figure: Figure) -> list: + """Serialize a plotly figure. + + Args: + figure: The figure to serialize. + + Returns: + The serialized figure. + """ + return json.loads(str(to_json(figure)))["data"] + +except ImportError: + pass diff --git a/reflex/components/graphing/plotly.pyi b/reflex/components/graphing/plotly.pyi index 15009f610..7ee75c73f 100644 --- a/reflex/components/graphing/plotly.pyi +++ b/reflex/components/graphing/plotly.pyi @@ -3,7 +3,7 @@ # This file was generated by `scripts/pyi_generator.py`! # ------------------------------------------------------ -from typing import Dict, List, Optional, Union, overload +from typing import Any, Dict, List, Optional, Union, overload from reflex.components.component import Component from reflex.components.component import NoSSRComponent from reflex.vars import Var, BaseVar, ComputedVar @@ -31,7 +31,7 @@ class PlotlyLib(NoSSRComponent): class Plotly(PlotlyLib): @overload @classmethod - def create(cls, *children, data: Optional[Union[Var[Figure], Figure]] = None, layout: Optional[Union[Var[Dict], Dict]] = None, width: Optional[Union[Var[str], str]] = None, height: Optional[Union[Var[str], str]] = None, use_resize_handler: Optional[Union[Var[bool], bool]] = None, on_blur: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_click: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_context_menu: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_double_click: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_focus: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mount: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_down: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_enter: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_leave: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_move: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_out: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_over: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_up: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_scroll: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_unmount: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, **props) -> "Plotly": # type: ignore + def create(cls, *children, data: Optional[Union[Var[Any], Any]] = None, layout: Optional[Union[Var[Dict], Dict]] = None, width: Optional[Union[Var[str], str]] = None, height: Optional[Union[Var[str], str]] = None, use_resize_handler: Optional[Union[Var[bool], bool]] = None, on_blur: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_click: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_context_menu: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_double_click: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_focus: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mount: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_down: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_enter: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_leave: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_move: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_out: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_over: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_up: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_scroll: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_unmount: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, **props) -> "Plotly": # type: ignore """Create the component. Args: diff --git a/reflex/components/media/image.py b/reflex/components/media/image.py index e1ed4454e..0f25801e1 100644 --- a/reflex/components/media/image.py +++ b/reflex/components/media/image.py @@ -1,12 +1,14 @@ """An image component.""" from __future__ import annotations -from typing import Any, Optional, Set +import base64 +import io +from typing import Any, Optional from reflex.components.component import Component from reflex.components.libs.chakra import ChakraComponent from reflex.components.tags import Tag -from reflex.utils import format, types +from reflex.utils.serializers import serializer from reflex.vars import Var @@ -51,7 +53,7 @@ class Image(ChakraComponent): # Learn more _[here](https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images)_ src_set: Var[str] - def get_triggers(self) -> Set[str]: + def get_triggers(self) -> set[str]: """Get the event triggers for the component. Returns: @@ -60,9 +62,30 @@ class Image(ChakraComponent): return super().get_triggers() | {"on_error", "on_load"} def _render(self) -> Tag: - # If the src is an image, convert it to a base64 string. - if types.is_image(type(self.src)): - self.src = Var.create(format.format_image_data(self.src)) # type: ignore + self.src.is_string = True # Render the table. return super()._render() + + +try: + from PIL.Image import Image as Img + + @serializer + def serialize_image(image: Img) -> str: + """Serialize a plotly figure. + + Args: + image: The image to serialize. + + Returns: + The serialized image. + """ + buff = io.BytesIO() + image.save(buff, format="PNG") + image_bytes = buff.getvalue() + base64_image = base64.b64encode(image_bytes).decode("utf-8") + return f"data:image/png;base64,{base64_image}" + +except ImportError: + pass diff --git a/reflex/components/media/image.pyi b/reflex/components/media/image.pyi index 965546381..b48b0442d 100644 --- a/reflex/components/media/image.pyi +++ b/reflex/components/media/image.pyi @@ -3,7 +3,7 @@ # This file was generated by `scripts/pyi_generator.py`! # ------------------------------------------------------ -from typing import Any, Optional, Set, Union, overload +from typing import Any, Optional, Union, overload from reflex.components.libs.chakra import ChakraComponent from reflex.components.component import Component from reflex.vars import Var, BaseVar, ComputedVar diff --git a/reflex/model.py b/reflex/model.py index f579784d4..12703e7be 100644 --- a/reflex/model.py +++ b/reflex/model.py @@ -197,6 +197,7 @@ class Model(Base, sqlmodel.SQLModel): target_metadata=sqlmodel.SQLModel.metadata, render_item=cls._alembic_render_item, process_revision_directives=writer, # type: ignore + compare_type=False, ) env.run_migrations() changes_detected = False diff --git a/reflex/utils/format.py b/reflex/utils/format.py index 2e68de82a..21c08cb95 100644 --- a/reflex/utils/format.py +++ b/reflex/utils/format.py @@ -2,22 +2,16 @@ from __future__ import annotations -import base64 -import io import json import os import os.path as op import re import sys -import types as builtin_types -from typing import TYPE_CHECKING, Any, Callable, Type, Union - -import plotly.graph_objects as go -from plotly.graph_objects import Figure -from plotly.io import to_json +from typing import TYPE_CHECKING, Any, Union from reflex import constants -from reflex.utils import exceptions, types +from reflex.utils import exceptions, serializers, types +from reflex.utils.serializers import serialize from reflex.vars import Var if TYPE_CHECKING: @@ -316,12 +310,9 @@ def format_prop( return prop return json_dumps(prop) - elif isinstance(prop, Figure): - prop = json.loads(to_json(prop))["data"] # type: ignore - # For dictionaries, convert any properties to strings. elif isinstance(prop, dict): - prop = format_dict(prop) + prop = serializers.serialize_dict(prop) # type: ignore else: # Dump the prop as JSON. @@ -461,44 +452,6 @@ def format_query_params(router_data: dict[str, Any]) -> dict[str, str]: return {k.replace("-", "_"): v for k, v in params.items()} -def format_dataframe_values(value: Type) -> list[Any]: - """Format dataframe values. - - Args: - value: The value to format. - - Returns: - Format data - """ - if not types.is_dataframe(type(value)): - return value - - format_data = [] - for data in list(value.values.tolist()): - element = [] - for d in data: - element.append(str(d) if isinstance(d, (list, tuple)) else d) - format_data.append(element) - - return format_data - - -def format_image_data(value: Type) -> str: - """Format image data. - - Args: - value: The value to format. - - Returns: - Format data - """ - buff = io.BytesIO() - value.save(buff, format="PNG") - image_bytes = buff.getvalue() - base64_image = base64.b64encode(image_bytes).decode("utf-8") - return f"data:image/png;base64,{base64_image}" - - def format_state(value: Any) -> Any: """Recursively format values in the given state. @@ -523,30 +476,12 @@ def format_state(value: Any) -> Any: if isinstance(value, types.StateBases): return value - # Convert plotly figures to JSON. - if isinstance(value, go.Figure): - return json.loads(to_json(value))["data"] # type: ignore + # Serialize the value. + serialized = serialize(value) + if serialized is not None: + return serialized - # Convert pandas dataframes to JSON. - if types.is_dataframe(type(value)): - return { - "columns": value.columns.tolist(), - "data": format_dataframe_values(value), - } - - # Convert datetime objects to str. - if types.is_datetime(type(value)): - return str(value) - - # Convert Image objects to base64. - if types.is_image(type(value)): - return format_image_data(value) # type: ignore - - raise TypeError( - "State vars must be primitive Python types, " - "or subclasses of rx.Base. " - f"Got var of type {type(value)}." - ) + raise TypeError(f"No JSON serializer found for var {value} of type {type(value)}.") def format_ref(ref: str) -> str: @@ -580,58 +515,6 @@ def format_array_ref(refs: str, idx: Var | None) -> str: return f"refs_{clean_ref}" -def format_dict(prop: ComponentStyle) -> str: - """Format a dict with vars potentially as values. - - Args: - prop: The dict to format. - - Returns: - The formatted dict. - - Raises: - InvalidStylePropError: If a style prop has a callable value - """ - # Import here to avoid circular imports. - from reflex.event import EventHandler - from reflex.vars import Var - - prop_dict = {} - - # Convert any var keys to strings. - for key, value in prop.items(): - if issubclass(type(value), Callable): - raise exceptions.InvalidStylePropError( - f"The style prop `{to_snake_case(key)}` cannot have " # type: ignore - f"`{value.fn.__qualname__ if isinstance(value, EventHandler) else value.__qualname__ if isinstance(value, builtin_types.FunctionType) else value}`, " - f"an event handler or callable as its value" - ) - prop_dict[key] = str(value) if isinstance(value, Var) else value - - # Dump the dict to a string. - fprop = json_dumps(prop_dict) - - def unescape_double_quotes_in_var(m: re.Match) -> str: - # Since the outer quotes are removed, the inner escaped quotes must be unescaped. - return re.sub('\\\\"', '"', m.group(1)) - - # This substitution is necessary to unwrap var values. - fprop = re.sub( - pattern=r""" - (? list[tuple[str, str]]: """Take a route and return a list of tuple for use in breadcrumb. diff --git a/reflex/utils/serializers.py b/reflex/utils/serializers.py new file mode 100644 index 000000000..2fe4a1cb0 --- /dev/null +++ b/reflex/utils/serializers.py @@ -0,0 +1,193 @@ +"""Serializers used to convert Var types to JSON strings.""" + +from __future__ import annotations + +import re +import types as builtin_types +from datetime import date, datetime, time, timedelta +from typing import Any, Callable, Dict, Type, Union, get_type_hints + +from reflex.utils import exceptions, types + +# Mapping from type to a serializer. +# The serializer should convert the type to a JSON object. +SerializedType = Union[str, bool, int, float, list, dict] +Serializer = Callable[[Type], SerializedType] +SERIALIZERS: dict[Type, Serializer] = {} + + +def serializer(fn: Serializer) -> Serializer: + """Decorator to add a serializer for a given type. + + Args: + fn: The function to decorate. + + Returns: + The decorated function. + + Raises: + ValueError: If the function does not take a single argument. + """ + # Get the global serializers. + global SERIALIZERS + + # Check the type hints to get the type of the argument. + type_hints = get_type_hints(fn) + args = [arg for arg in type_hints if arg != "return"] + + # Make sure the function takes a single argument. + if len(args) != 1: + raise ValueError("Serializer must take a single argument.") + + # Get the type of the argument. + type_ = type_hints[args[0]] + + # Make sure the type is not already registered. + registered_fn = SERIALIZERS.get(type_) + if registered_fn is not None and registered_fn != fn: + raise ValueError( + f"Serializer for type {type_} is already registered as {registered_fn.__qualname__}." + ) + + # Register the serializer. + SERIALIZERS[type_] = fn + + # Return the function. + return fn + + +def serialize(value: Any) -> SerializedType | None: + """Serialize the value to a JSON string. + + Args: + value: The value to serialize. + + Returns: + The serialized value, or None if a serializer is not found. + """ + # Get the serializer for the type. + serializer = get_serializer(type(value)) + + # If there is no serializer, return None. + if serializer is None: + return None + + # Serialize the value. + return serializer(value) + + +def get_serializer(type_: Type) -> Serializer | None: + """Get the serializer for the type. + + Args: + type_: The type to get the serializer for. + + Returns: + The serializer for the type, or None if there is no serializer. + """ + global SERIALIZERS + + # First, check if the type is registered. + serializer = SERIALIZERS.get(type_) + if serializer is not None: + return serializer + + # If the type is not registered, check if it is a subclass of a registered type. + for registered_type, serializer in SERIALIZERS.items(): + if types._issubclass(type_, registered_type): + return serializer + + # If there is no serializer, return None. + return None + + +def has_serializer(type_: Type) -> bool: + """Check if there is a serializer for the type. + + Args: + type_: The type to check. + + Returns: + Whether there is a serializer for the type. + """ + return get_serializer(type_) is not None + + +@serializer +def serialize_str(value: str) -> str: + """Serialize a string. + + Args: + value: The string to serialize. + + Returns: + The serialized string. + """ + return value + + +@serializer +def serialize_dict(prop: Dict[str, Any]) -> str: + """Serialize a dictionary to a JSON string. + + Args: + prop: The dictionary to serialize. + + Returns: + The serialized dictionary. + + Raises: + InvalidStylePropError: If the style prop is invalid. + """ + # Import here to avoid circular imports. + from reflex.event import EventHandler + from reflex.utils.format import json_dumps, to_snake_case + from reflex.vars import Var + + prop_dict = {} + + # Convert any var keys to strings. + for key, value in prop.items(): + if types._issubclass(type(value), Callable): + raise exceptions.InvalidStylePropError( + f"The style prop `{to_snake_case(key)}` cannot have " # type: ignore + f"`{value.fn.__qualname__ if isinstance(value, EventHandler) else value.__qualname__ if isinstance(value, builtin_types.FunctionType) else value}`, " + f"an event handler or callable as its value" + ) + prop_dict[key] = str(value) if isinstance(value, Var) else value + + # Dump the dict to a string. + fprop = json_dumps(prop_dict) + + def unescape_double_quotes_in_var(m: re.Match) -> str: + # Since the outer quotes are removed, the inner escaped quotes must be unescaped. + return re.sub('\\\\"', '"', m.group(1)) + + # This substitution is necessary to unwrap var values. + fprop = re.sub( + pattern=r""" + (? str: + """Serialize a datetime to a JSON string. + + Args: + dt: The datetime to serialize. + + Returns: + The serialized datetime. + """ + return str(dt) diff --git a/reflex/utils/types.py b/reflex/utils/types.py index 98703039d..bfa3e5cd5 100644 --- a/reflex/utils/types.py +++ b/reflex/utils/types.py @@ -4,10 +4,10 @@ from __future__ import annotations import contextlib import typing -from datetime import date, datetime, time, timedelta from typing import Any, Callable, Type, Union, _GenericAlias # type: ignore from reflex.base import Base +from reflex.utils import serializers # Union of generic types. GenericType = Union[Type, _GenericAlias] @@ -143,60 +143,16 @@ def is_dataframe(value: Type) -> bool: return value.__name__ == "DataFrame" -def is_image(value: Type) -> bool: - """Check if the given value is a pillow image. By checking if the value subclasses PIL. +def is_valid_var_type(type_: Type) -> bool: + """Check if the given type is a valid prop type. Args: - value: The value to check. + type_: The type to check. Returns: - Whether the value is a pillow image. + Whether the type is a valid prop type. """ - if is_generic_alias(value) or value == typing.Any: - return False - return "PIL" in value.__module__ - - -def is_figure(value: Type) -> bool: - """Check if the given value is a figure. - - Args: - value: The value to check. - - Returns: - Whether the value is a figure. - """ - return value.__name__ == "Figure" - - -def is_datetime(value: Type) -> bool: - """Check if the given value is a datetime object. - - Args: - value: The value to check. - - Returns: - Whether the value is a date, datetime, time, or timedelta. - """ - return issubclass(value, (date, datetime, time, timedelta)) - - -def is_valid_var_type(var: Type) -> bool: - """Check if the given value is a valid prop type. - - Args: - var: The value to check. - - Returns: - Whether the value is a valid prop type. - """ - return ( - _issubclass(var, StateVar) - or is_dataframe(var) - or is_figure(var) - or is_image(var) - or is_datetime(var) - ) + return _issubclass(type_, StateVar) or serializers.has_serializer(type_) def is_backend_variable(name: str) -> bool: diff --git a/reflex/vars.py b/reflex/vars.py index b6ed08e1d..c21edf353 100644 --- a/reflex/vars.py +++ b/reflex/vars.py @@ -24,13 +24,12 @@ from typing import ( get_type_hints, ) -from plotly.graph_objects import Figure -from plotly.io import to_json from pydantic.fields import ModelField from reflex import constants from reflex.base import Base from reflex.utils import console, format, types +from reflex.utils.serializers import serialize if TYPE_CHECKING: from reflex.state import State @@ -126,19 +125,16 @@ class Var(ABC): type_ = type(value) - # Special case for plotly figures. - if isinstance(value, Figure): - value = json.loads(to_json(value))["data"] # type: ignore - type_ = Figure - - if isinstance(value, dict): - value = format.format_dict(value) + # Try to serialize the value. + serialized = serialize(value) + if serialized is not None: + value = serialized try: name = value if isinstance(value, str) else json.dumps(value) except TypeError as e: raise TypeError( - f"To create a Var must be Var or JSON-serializable. Got {value} of type {type(value)}." + f"No JSON serializer found for var {value} of type {type_}." ) from e return BaseVar(name=name, type_=type_, is_local=is_local, is_string=is_string) @@ -184,7 +180,7 @@ class Var(ABC): """ if self.state: return self.full_name - if self.is_string or self.type_ is Figure: + if self.is_string: return self.name try: return json.loads(self.name) diff --git a/scripts/pyi_generator.py b/scripts/pyi_generator.py index c603abd0f..b8f3c9b70 100644 --- a/scripts/pyi_generator.py +++ b/scripts/pyi_generator.py @@ -7,7 +7,7 @@ import re import sys from inspect import getfullargspec from pathlib import Path -from typing import Any, Dict, List, Optional, get_args +from typing import Any, Dict, List, Optional, Union, get_args # NOQA import black @@ -181,7 +181,14 @@ class PyiGenerator: return _get_var_definition(self.current_module, _name) def _generate_function(self, _name, _func): - definition = "".join(inspect.getsource(_func).split(":\n")[0].split("\n")) + import textwrap + + # Don't generate indented functions. + source = inspect.getsource(_func) + if textwrap.dedent(source) != source: + return [] + + definition = "".join([line for line in source.split(":\n")[0].split("\n")]) return [f"{definition}:", " ..."] def _write_pyi_file(self, variables, functions, classes): diff --git a/tests/components/datadisplay/test_datatable.py b/tests/components/datadisplay/test_datatable.py index 94446c6bb..f6a154b0a 100644 --- a/tests/components/datadisplay/test_datatable.py +++ b/tests/components/datadisplay/test_datatable.py @@ -2,8 +2,12 @@ import pandas as pd import pytest import reflex as rx -from reflex.components import data_table +from reflex.components.datadisplay.datatable import ( + DataTable, + serialize_dataframe, # type: ignore +) from reflex.utils import types +from reflex.utils.serializers import serialize @pytest.mark.parametrize( @@ -31,11 +35,11 @@ def test_validate_data_table(data_table_state: rx.Var, expected): """ if not types.is_dataframe(data_table_state.data.type_): - data_table_component = data_table( + data_table_component = DataTable.create( data=data_table_state.data, columns=data_table_state.columns ) else: - data_table_component = data_table(data=data_table_state.data) + data_table_component = DataTable.create(data=data_table_state.data) data_table_dict = data_table_component.render() @@ -62,7 +66,7 @@ def test_invalid_props(props): props: props to pass in component. """ with pytest.raises(ValueError): - data_table(**props) + DataTable.create(**props) @pytest.mark.parametrize( @@ -96,10 +100,21 @@ def test_computed_var_without_annotation(fixture, request, err_msg, is_data_fram """ with pytest.raises(ValueError) as err: if is_data_frame: - data_table(data=request.getfixturevalue(fixture).data) + DataTable.create(data=request.getfixturevalue(fixture).data) else: - data_table( + DataTable.create( data=request.getfixturevalue(fixture).data, columns=request.getfixturevalue(fixture).columns, ) assert err.value.args[0] == err_msg + + +def test_serialize_dataframe(): + """Test if dataframe is serialized correctly.""" + df = pd.DataFrame( + [["foo", "bar"], ["foo1", "bar1"]], columns=["column1", "column2"] + ) + value = serialize(df) + assert value == serialize_dataframe(df) + assert isinstance(value, dict) + assert list(value.keys()) == ["columns", "data"] diff --git a/tests/components/graphing/test_plotly.py b/tests/components/graphing/test_plotly.py new file mode 100644 index 000000000..75935811c --- /dev/null +++ b/tests/components/graphing/test_plotly.py @@ -0,0 +1,34 @@ +import numpy as np +import plotly.graph_objects as go +import pytest + +from reflex.components.graphing.plotly import serialize_figure # type: ignore +from reflex.utils.serializers import serialize + + +@pytest.fixture +def plotly_fig() -> go.Figure: + """Get a plotly figure. + + Returns: + A random plotly figure. + """ + # Generate random data. + data = np.random.randint(0, 10, size=(10, 4)) + trace = go.Scatter( + x=list(range(len(data))), y=data[:, 0], mode="lines", name="Trace 1" + ) + + # Create a graph. + return go.Figure(data=[trace]) + + +def test_serialize_plotly(plotly_fig: go.Figure): + """Test that serializing a plotly figure works. + + Args: + plotly_fig: The figure to serialize. + """ + value = serialize(plotly_fig) + assert isinstance(value, list) + assert value == serialize_figure(plotly_fig) diff --git a/tests/components/media/test_image.py b/tests/components/media/test_image.py new file mode 100644 index 000000000..781a1a5b3 --- /dev/null +++ b/tests/components/media/test_image.py @@ -0,0 +1,65 @@ +import pytest + +try: + # PIL is only available in python 3.8+ + import numpy as np + import PIL + from PIL.Image import Image as Img + + import reflex as rx + from reflex.components.media.image import Image, serialize_image # type: ignore + from reflex.utils.serializers import serialize + + @pytest.fixture + def pil_image() -> Img: + """Get an image. + + Returns: + A random PIL image. + """ + imarray = np.random.rand(100, 100, 3) * 255 + return PIL.Image.fromarray(imarray.astype("uint8")).convert("RGBA") # type: ignore + + def test_serialize_image(pil_image: Img): + """Test that serializing an image works. + + Args: + pil_image: The image to serialize. + """ + data = serialize(pil_image) + assert isinstance(data, str) + assert data == serialize_image(pil_image) + assert data.startswith("data:image/png;base64,") + + def test_set_src_str(): + """Test that setting the src works.""" + image = rx.image(src="pic2.jpeg") + assert str(image.src) == "pic2.jpeg" # type: ignore + + def test_set_src_img(pil_image: Img): + """Test that setting the src works. + + Args: + pil_image: The image to serialize. + """ + image = Image.create(src=pil_image) + assert str(image.src) == serialize_image(pil_image) # type: ignore + + def test_render(pil_image: Img): + """Test that rendering an image works. + + Args: + pil_image: The image to serialize. + """ + image = Image.create(src=pil_image) + assert not image.src.is_string # type: ignore + image._render() + assert image.src.is_string # type: ignore + +except ImportError: + + def test_pillow_import(): + """Make sure the Python version is less than 3.8.""" + import sys + + assert sys.version_info < (3, 8) diff --git a/tests/test_prerequites.py b/tests/test_prerequisites.py similarity index 100% rename from tests/test_prerequites.py rename to tests/test_prerequisites.py diff --git a/tests/test_var.py b/tests/test_var.py index f654a4d9b..3289afed8 100644 --- a/tests/test_var.py +++ b/tests/test_var.py @@ -230,14 +230,9 @@ def test_create_type_error(): value = ErrorType() - with pytest.raises(TypeError) as exception: + with pytest.raises(TypeError): Var.create(value) - assert ( - exception.value.args[0] - == f"To create a Var must be Var or JSON-serializable. Got {value} of type {type(value)}." - ) - def v(value) -> Var: val = ( diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/utils/test_serializers.py b/tests/utils/test_serializers.py new file mode 100644 index 000000000..983b14981 --- /dev/null +++ b/tests/utils/test_serializers.py @@ -0,0 +1,102 @@ +import datetime +from typing import Any, Dict, Type + +import pytest + +from reflex.utils import serializers + + +@pytest.mark.parametrize( + "type_,expected", + [ + (str, True), + (dict, True), + (Dict[int, int], True), + ], +) +def test_has_serializer(type_: Type, expected: bool): + """Test that has_serializer returns the correct value. + + + Args: + type_: The type to check. + expected: The expected result. + """ + assert serializers.has_serializer(type_) == expected + + +@pytest.mark.parametrize( + "type_,expected", + [ + (str, serializers.serialize_str), + (dict, serializers.serialize_dict), + (Dict[int, int], serializers.serialize_dict), + (datetime.datetime, serializers.serialize_datetime), + (datetime.date, serializers.serialize_datetime), + (datetime.time, serializers.serialize_datetime), + (datetime.timedelta, serializers.serialize_datetime), + ], +) +def test_get_serializer(type_: Type, expected: serializers.Serializer): + """Test that get_serializer returns the correct value. + + + Args: + type_: The type to check. + expected: The expected result. + """ + assert serializers.get_serializer(type_) == expected + + +def test_add_serializer(): + """Test that adding a serializer works.""" + + def serialize_test(value: int) -> str: + """Serialize an int to a string. + + Args: + value: The value to serialize. + + Returns: + The serialized value. + """ + return str(value) + + # Initially there should be no serializer for int. + assert not serializers.has_serializer(int) + assert serializers.serialize(5) is None + + # Register the serializer. + assert serializers.serializer(serialize_test) == serialize_test + + # There should now be a serializer for int. + assert serializers.has_serializer(int) + assert serializers.get_serializer(int) == serialize_test + assert serializers.serialize(5) == "5" + + # Remove the serializer. + serializers.SERIALIZERS.pop(int) + + +@pytest.mark.parametrize( + "value,expected", + [ + ("test", "test"), + (datetime.datetime(2021, 1, 1, 1, 1, 1, 1), "2021-01-01 01:01:01.000001"), + (datetime.date(2021, 1, 1), "2021-01-01"), + (datetime.time(1, 1, 1, 1), "01:01:01.000001"), + (datetime.timedelta(1, 1, 1), "1 day, 0:00:01.000001"), + (5, None), + (None, None), + ([], None), + ], +) +def test_serialize(value: Any, expected: str): + """Test that serialize returns the correct value. + + + Args: + value: The value to serialize. + expected: The expected result. + """ + assert serializers.serialize(value) == expected diff --git a/tests/test_utils.py b/tests/utils/test_utils.py similarity index 99% rename from tests/test_utils.py rename to tests/utils/test_utils.py index 8dd61a650..09ca3abdf 100644 --- a/tests/test_utils.py +++ b/tests/utils/test_utils.py @@ -21,6 +21,7 @@ from reflex.utils import ( types, ) from reflex.utils import exec as utils_exec +from reflex.utils.serializers import serialize from reflex.vars import BaseVar, Var @@ -777,4 +778,4 @@ def test_style_prop_with_event_handler_value(callable): } with pytest.raises(TypeError): - format.format_dict(style) # type: ignore + serialize(style) # type: ignore