From 43656f95c2ba9472304368df187d5269de694a70 Mon Sep 17 00:00:00 2001 From: Stephen Tierney Date: Fri, 25 Oct 2019 21:12:21 +1100 Subject: [PATCH 1/4] Swapped dash to quart --- dash-renderer/package-lock.json | 153 +++++++++++++++----------------- dash/dash.py | 107 +++++++++++----------- requires-install.txt | 7 +- 3 files changed, 128 insertions(+), 139 deletions(-) diff --git a/dash-renderer/package-lock.json b/dash-renderer/package-lock.json index 5bcf0a2927..52dc61b2da 100644 --- a/dash-renderer/package-lock.json +++ b/dash-renderer/package-lock.json @@ -911,9 +911,9 @@ "dev": true }, "@hapi/hoek": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-8.3.1.tgz", - "integrity": "sha512-75ocgnI7HG/I01iGA3/rs0y6PXydUA/kxhFZM0HoT8NLSTnt/J8Gq03iKl4a4B/2A3iMG0ctXtxr5Hg9SGr1gw==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-8.3.2.tgz", + "integrity": "sha512-NP5SG4bzix+EtSMtcudp8TvI0lB46mXNo8uFpTDw6tqxGx4z5yx+giIunEFA0Z7oUO4DuWrOJV9xqR2tJVEdyA==", "dev": true }, "@hapi/joi": { @@ -1397,9 +1397,9 @@ "dev": true }, "@types/node": { - "version": "12.11.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.11.1.tgz", - "integrity": "sha512-TJtwsqZ39pqcljJpajeoofYRfeZ7/I/OMUQ5pR4q5wOKf2ocrUvBAZUMhWsOvKx3dVc/aaV5GluBivt0sWqA5A==", + "version": "12.11.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.11.7.tgz", + "integrity": "sha512-JNbGaHFCLwgHn/iCckiGSOZ1XYHsKFwREtzPwSGCVld1SGhOlmZw2D4ZI94HQCrBHbADzW9m4LER/8olJTRGHA==", "dev": true }, "@types/normalize-package-data": { @@ -2566,20 +2566,20 @@ } }, "browserslist": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.7.0.tgz", - "integrity": "sha512-9rGNDtnj+HaahxiVV38Gn8n8Lr8REKsel68v1sPFfIGEK6uSXTY3h9acgiT1dZVtOOUtifo/Dn8daDQ5dUgVsA==", + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.7.2.tgz", + "integrity": "sha512-uZavT/gZXJd2UTi9Ov7/Z340WOSQ3+m1iBVRUknf+okKxonL9P83S3ctiBDtuRmRu8PiCHjqyueqQ9HYlJhxiw==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30000989", - "electron-to-chromium": "^1.3.247", - "node-releases": "^1.1.29" + "caniuse-lite": "^1.0.30001004", + "electron-to-chromium": "^1.3.295", + "node-releases": "^1.1.38" } }, "bser": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.0.tgz", - "integrity": "sha512-8zsjWrQkkBoLK6uxASk1nJ2SKv97ltiGDo6A3wA0/yRPz+CwmEyDo0hUrhIuukG2JHpAl3bvFIixw2/3Hi0DOg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", "dev": true, "requires": { "node-int64": "^0.4.0" @@ -2743,9 +2743,9 @@ } }, "caniuse-lite": { - "version": "1.0.30000999", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000999.tgz", - "integrity": "sha512-1CUyKyecPeksKwXZvYw0tEoaMCo/RwBlXmEtN5vVnabvO0KPd9RQLcaAuR9/1F+KDMv6esmOFWlsXuzDk+8rxg==", + "version": "1.0.30001004", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001004.tgz", + "integrity": "sha512-3nfOR4O8Wa2RWoYfJkMtwRVOsK96TQ+eq57wd0iKaEWl8dwG4hKZ/g0MVBfCvysFvMLi9fQGR/DvozMdkEPl3g==", "dev": true }, "capture-exit": { @@ -3312,12 +3312,12 @@ "dev": true }, "core-js-compat": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.3.2.tgz", - "integrity": "sha512-gfiK4QnNXhnnHVOIZst2XHdFfdMTPxtR0EGs0TdILMlGIft+087oH6/Sw2xTTIjpWXC9vEwsJA8VG3XTGcmO5g==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.3.3.tgz", + "integrity": "sha512-GNZkENsx5pMnS7Inwv7ZO/s3B68a9WU5kIjxqrD/tkNR8mtfXJRk8fAKRlbvWZSGPc59/TkiOBDYl5Cb65pTVA==", "dev": true, "requires": { - "browserslist": "^4.7.0", + "browserslist": "^4.7.1", "semver": "^6.3.0" }, "dependencies": { @@ -3594,9 +3594,9 @@ }, "dependencies": { "whatwg-url": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.0.0.tgz", - "integrity": "sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", "dev": true, "requires": { "lodash.sortby": "^4.7.0", @@ -4000,9 +4000,9 @@ "dev": true }, "electron-to-chromium": { - "version": "1.3.284", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.284.tgz", - "integrity": "sha512-duOA4IWKH4R8ttiE8q/7xfg6eheRvMKlGqOOcGlDukdHEDJ26Wf7cMrCiK9Am11mswR6E/a23jXVA4UPDthTIw==", + "version": "1.3.296", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.296.tgz", + "integrity": "sha512-s5hv+TSJSVRsxH190De66YHb50pBGTweT9XGWYu/LMR20KX6TsjFzObo36CjVAzM+PUeeKSBRtm/mISlCzeojQ==", "dev": true }, "elliptic": { @@ -4109,9 +4109,9 @@ "dev": true }, "es-abstract": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.15.0.tgz", - "integrity": "sha512-bhkEqWJ2t2lMeaJDuk7okMkJWI/yqgH/EoGwpcvv0XW9RWQsRspI4wt6xuyuvMvvQE3gg/D9HXppgk21w78GyQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.16.0.tgz", + "integrity": "sha512-xdQnfykZ9JMEiasTAJZJdMWCQ1Vm00NBw79/AWi7ELfZuuPCSOMDZbT9mkOfSctVtfhb+sAAzrm+j//GjjLHLg==", "dev": true, "requires": { "es-to-primitive": "^1.2.0", @@ -4487,12 +4487,12 @@ } }, "eslint-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.2.tgz", - "integrity": "sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", "dev": true, "requires": { - "eslint-visitor-keys": "^1.0.0" + "eslint-visitor-keys": "^1.1.0" } }, "eslint-visitor-keys": { @@ -5950,9 +5950,9 @@ } }, "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.5.tgz", + "integrity": "sha512-J9dlskqUXK1OeTOYBEn5s8aMukWMwWfs+rPTn/jn50Ux4MNXVhubL1wu/j2t+H4NVI+cXEcCaYellqaPVGXNqQ==", "dev": true, "requires": { "fs.realpath": "^1.0.0", @@ -6110,9 +6110,9 @@ } }, "graceful-fs": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz", - "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", + "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", "dev": true }, "growly": { @@ -6128,9 +6128,9 @@ "dev": true }, "handlebars": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.4.3.tgz", - "integrity": "sha512-B0W4A2U1ww3q7VVthTKfh+epHx+q4mCt6iK+zEAzbMBpWQAwxCeKxEGpj/1oQTpzPXDNSOG7hmG14TsISH50yw==", + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.4.5.tgz", + "integrity": "sha512-0Ce31oWVB7YidkaTq33ZxEbN+UDxMMgThvCe8ptgQViymL5DPis9uLdTA13MiRPhgvqyxIegugrP97iK3JeBHg==", "dev": true, "requires": { "neo-async": "^2.6.0", @@ -7749,9 +7749,9 @@ } }, "jsx-ast-utils": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.2.1.tgz", - "integrity": "sha512-v3FxCcAf20DayI+uxnCuw795+oOIkVu6EnJ1+kSzhqqTZHNkTZ7B66ZgLp4oLJ/gbA64cI0B7WRoHZMSRdyVRQ==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.2.3.tgz", + "integrity": "sha512-EdIHFMm+1BPynpKOpdPqiOsvnIrInRGJD7bzPZdPkjitQEqpdpUuFpq4T0npZFKTiB3RhWFdGN+oqOJIdhDhQA==", "dev": true, "requires": { "array-includes": "^3.0.3", @@ -8844,9 +8844,9 @@ "optional": true }, "nanoid": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.4.tgz", - "integrity": "sha512-PijW88Ry+swMFfArOrm7uRAdVmJilLbej7WwVY6L5QwLDckqxSOinGGMV596yp5C8+MH3VvCXCSZ6AodGtKrYQ==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.6.tgz", + "integrity": "sha512-2NDzpiuEy3+H0AVtdt8LoFi7PnqkOnIzYmJQp7xsEU6VexLluHQwKREuiz57XaQC5006seIadPrIZJhyS2n7aw==", "dev": true }, "nanomatch": { @@ -8980,9 +8980,9 @@ } }, "node-releases": { - "version": "1.1.36", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.36.tgz", - "integrity": "sha512-ggXhX6QGyJSjj3r+6ml2LqqC28XOWmKtpb+a15/Zpr9V3yoNazxJNlcQDS9bYaid5FReEWHEgToH1mwoUceWwg==", + "version": "1.1.39", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.39.tgz", + "integrity": "sha512-8MRC/ErwNCHOlAFycy9OPca46fQYUjbJRDcZTHVWIGXIjYLM73k70vv3WkYutVnM4cCo4hE0MqBVVZjP6vjISA==", "dev": true, "requires": { "semver": "^6.3.0" @@ -9759,9 +9759,9 @@ "dev": true }, "postcss": { - "version": "7.0.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.18.tgz", - "integrity": "sha512-/7g1QXXgegpF+9GJj4iN7ChGF40sYuGYJ8WZu8DZWnmhQ/G36hfdk3q9LBJmoK+lZ+yzZ5KYpOoxq7LF1BxE8g==", + "version": "7.0.20", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.20.tgz", + "integrity": "sha512-VOdO3a5nHVftPSEbG1zaG320b4mH5KAflH+pIeVAF5/hlw6YumELSgHZQBekjg29Oj4qw7XAyp9tIEBpeNWcyg==", "dev": true, "requires": { "chalk": "^2.4.2", @@ -11360,9 +11360,9 @@ } }, "react-is": { - "version": "16.10.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.10.2.tgz", - "integrity": "sha512-INBT1QEgtcCCgvccr5/86CfD71fw9EPmDxgiJX4I2Ddr6ZsV6iFXsuby+qWJPtmNuMY0zByTsG4468P7nHuNWA==" + "version": "16.11.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.11.0.tgz", + "integrity": "sha512-gbBVYR2p8mnriqAwWx9LbuUrShnAuSCNnuPGyc7GJrMVQtPDAh8iLpv7FRuMPFb56KkaVZIYSz1PrjI9q0QPCw==" }, "react-redux": { "version": "4.4.10", @@ -11636,9 +11636,9 @@ } }, "regjsgen": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.0.tgz", - "integrity": "sha512-RnIrLhrXCX5ow/E5/Mh2O4e/oa1/jW0eaBKTSy3LaCj+M3Bqvm97GWDp2yUtzIs4LEn65zR2yiYGFqb2ApnzDA==", + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.1.tgz", + "integrity": "sha512-5qxzGZjDs9w4tzT3TPhCJqWdCc3RLYwy9J2NB0nm5Lz+S273lvWcpjaTGHsT1dc6Hhfq41uSEOw8wBmxrKOuyg==", "dev": true }, "regjsparser": { @@ -13982,23 +13982,16 @@ "integrity": "sha512-8OaIKfzL5cpx8eCMAhhvTlft8GYF8b2eQr6JkCyVdrgjcytyOmPCXrqXFcUnhonRpLlh5yxEZVohm6mzaowUOw==" }, "uglify-js": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.2.tgz", - "integrity": "sha512-+gh/xFte41GPrgSMJ/oJVq15zYmqr74pY9VoM69UzMzq9NFk4YDylclb1/bhEzZSaUQjbW5RvniHeq1cdtRYjw==", + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.4.tgz", + "integrity": "sha512-9Yc2i881pF4BPGhjteCXQNaXx1DCwm3dtOyBaG2hitHjLWOczw/ki8vD1bqyT3u6K0Ms/FpCShkmfg+FtlOfYA==", "dev": true, "optional": true, "requires": { - "commander": "2.20.0", + "commander": "~2.20.3", "source-map": "~0.6.1" }, "dependencies": { - "commander": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", - "dev": true, - "optional": true - }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -14738,9 +14731,9 @@ } }, "webpack-dev-server": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.8.2.tgz", - "integrity": "sha512-0xxogS7n5jHDQWy0WST0q6Ykp7UGj4YvWh+HVN71JoE7BwPxMZrwgraBvmdEMbDVMBzF0u+mEzn8TQzBm5NYJQ==", + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.9.0.tgz", + "integrity": "sha512-E6uQ4kRrTX9URN9s/lIbqTAztwEPdvzVrcmHE8EQ9YnuT9J8Es5Wrd8n9BKg1a0oZ5EgEke/EQFgUsp18dSTBw==", "dev": true, "requires": { "ansi-html": "0.0.7", @@ -14761,7 +14754,7 @@ "loglevel": "^1.6.4", "opn": "^5.5.0", "p-retry": "^3.0.1", - "portfinder": "^1.0.24", + "portfinder": "^1.0.25", "schema-utils": "^1.0.0", "selfsigned": "^1.10.7", "semver": "^6.3.0", @@ -15309,9 +15302,9 @@ "dev": true }, "ws": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.1.2.tgz", - "integrity": "sha512-gftXq3XI81cJCgkUiAVixA0raD9IVmXqsylCrjRygw4+UOOGzPoxnQ6r/CnVL9i+mDncJo94tSkyrtuuQVBmrg==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.0.tgz", + "integrity": "sha512-+SqNqFbwTm/0DC18KYzIsMTnEWpLwJsiasW/O17la4iDRRIO9uaHbvKiAS3AHgTiuuWerK/brj4O6MYZkei9xg==", "dev": true, "requires": { "async-limiter": "^1.0.0" diff --git a/dash/dash.py b/dash/dash.py index 0c402b2108..7741e9432b 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -16,27 +16,33 @@ from functools import wraps from textwrap import dedent -import flask -from flask_compress import Compress +from quart_compress import Compress from werkzeug.debug.tbtools import get_current_traceback +import flask +import quart + import plotly import dash_renderer -from .dependencies import Input, Output, State -from .resources import Scripts, Css -from .development.base_component import Component, ComponentRegistry -from . import exceptions -from ._utils import AttributeDict as _AttributeDict -from ._utils import interpolate_str as _interpolate -from ._utils import format_tag as _format_tag -from ._utils import generate_hash as _generate_hash -from ._utils import patch_collections_abc as _patch_collections_abc -from . import _watch -from ._utils import get_asset_path as _get_asset_path -from ._utils import create_callback_id as _create_callback_id -from ._configs import get_combined_config, pathname_configs -from .version import __version__ +from dash.dependencies import Input, Output, State +from dash.resources import Scripts, Css +from dash.development.base_component import Component, ComponentRegistry +from dash import exceptions +from dash._utils import AttributeDict as _AttributeDict +from dash._utils import interpolate_str as _interpolate +from dash._utils import format_tag as _format_tag +from dash._utils import generate_hash as _generate_hash +from dash._utils import patch_collections_abc as _patch_collections_abc +from dash import _watch +from dash._utils import get_asset_path as _get_asset_path +from dash._utils import create_callback_id as _create_callback_id +from dash._configs import get_combined_config, pathname_configs +from dash.version import __version__ + +from asgiref.sync import sync_to_async + +import inspect _default_index = """ @@ -212,7 +218,6 @@ def __init__( assets_url_path="assets", assets_ignore="", assets_external_path=None, - eager_loading=False, include_assets_files=True, url_base_pathname=None, requests_pathname_prefix=None, @@ -228,11 +233,6 @@ def __init__( plugins=None, **obsolete ): - # Apply _force_eager_loading overrides from modules - for module_name in ComponentRegistry.registry: - module = sys.modules[module_name] - eager = getattr(module, '_force_eager_loading', False) - eager_loading = eager_loading or eager for key in obsolete: if key in ["components_cache_max_age", "static_folder"]: @@ -247,13 +247,13 @@ def __init__( # We have 3 cases: server is either True (we create the server), False # (defer server creation) or a Flask app instance (we use their server) - if isinstance(server, flask.Flask): + if isinstance(server, quart.Quart): self.server = server if name is None: name = getattr(server, "name", "__main__") elif isinstance(server, bool): name = name if name else "__main__" - self.server = flask.Flask(name) if server else None + self.server = quart.Quart(name) if server else None else: raise ValueError("server must be a Flask app or a boolean") @@ -294,7 +294,6 @@ def __init__( "name", "assets_folder", "assets_url_path", - "eager_loading", "url_base_pathname", "routes_pathname_prefix", "requests_pathname_prefix", @@ -321,7 +320,7 @@ def __init__( # static files from the packages self.css = Css(serve_locally) - self.scripts = Scripts(serve_locally, eager_loading) + self.scripts = Scripts(serve_locally) self.registered_paths = collections.defaultdict(set) @@ -364,7 +363,7 @@ def init_app(self, app=None): ) self.server.register_blueprint( - flask.Blueprint( + quart.Blueprint( assets_blueprint_name, config.name, static_folder=self.config.assets_folder, @@ -481,7 +480,7 @@ def serve_layout(self): layout = self._layout_value() # TODO - Set browser cache limit - pass hash into frontend - return flask.Response( + return quart.Response( json.dumps(layout, cls=plotly.utils.PlotlyJSONEncoder), mimetype="application/json", ) @@ -512,7 +511,7 @@ def serve_reload_hash(self): _reload.hard = False _reload.changed_assets = [] - return flask.jsonify( + return quart.jsonify( { "reloadHash": _hash, "hard": hard, @@ -522,7 +521,7 @@ def serve_reload_hash(self): ) def serve_routes(self): - return flask.Response( + return quart.Response( json.dumps(self.routes, cls=plotly.utils.PlotlyJSONEncoder), mimetype="application/json", ) @@ -619,9 +618,7 @@ def _generate_scripts_html(self): dev = self._dev_tools.serve_dev_bundles srcs = ( self._collect_and_register_resources( - self.scripts._resources._filter_resources( - deps, dev_bundles=dev - ) + self.scripts._resources._filter_resources(deps, dev_bundles=dev) ) + self.config.external_scripts + self._collect_and_register_resources( @@ -664,9 +661,7 @@ def _generate_meta_html(self): tags = [] if not has_ie_compat: - tags.append( - '' - ) + tags.append('') if not has_charset: tags.append('') @@ -711,7 +706,7 @@ def serve_component_suites(self, package_name, path_in_package_dist): package.__path__, ) - return flask.Response( + return quart.Response( pkgutil.get_data(package_name, path_in_package_dist), mimetype=mimetype, ) @@ -830,7 +825,7 @@ def interpolate_index(self, **kwargs): ) def dependencies(self): - return flask.jsonify( + return quart.jsonify( [ { "output": k, @@ -942,9 +937,7 @@ def _validate_callback(self, output, inputs, state): {2} """ ).format( - arg_prop, - arg_id, - component.available_properties, + arg_prop, arg_id, component.available_properties ) ) @@ -1084,9 +1077,8 @@ def _raise_invalid( location_header=( "The value in question is located at" if not toplevel - else "The value in question is either the only value " - "returned,\nor is in the top level of the returned " - "list," + else "The value in question is either the only value returned," + "\nor is in the top level of the returned list," ), location=( "\n" @@ -1260,9 +1252,12 @@ def callback(self, output, inputs=[], state=[]): def wrap_func(func): @wraps(func) - def add_context(*args, **kwargs): + async def add_context(*args, **kwargs): # don't touch the comment on the next line - used by debugger - output_value = func(*args, **kwargs) # %% callback invoked %% + if not inspect.iscoroutinefunction(func): + output_value = await sync_to_async(func)(*args, **kwargs) # %% callback invoked %% + else: + output_value = await func(*args, **kwargs) # %% callback invoked %% if multi: if not isinstance(output_value, (list, tuple)): raise exceptions.InvalidCallbackReturnValue( @@ -1335,30 +1330,30 @@ def add_context(*args, **kwargs): return wrap_func - def dispatch(self): - body = flask.request.get_json() + async def dispatch(self): + body = await quart.request.get_json() inputs = body.get("inputs", []) state = body.get("state", []) output = body["output"] args = [] - flask.g.input_values = input_values = { + quart.g.input_values = input_values = { "{}.{}".format(x["id"], x["property"]): x.get("value") for x in inputs } - flask.g.state_values = { + quart.g.state_values = { "{}.{}".format(x["id"], x["property"]): x.get("value") for x in state } changed_props = body.get("changedPropIds") - flask.g.triggered_inputs = ( + quart.g.triggered_inputs = ( [{"prop_id": x, "value": input_values[x]} for x in changed_props] if changed_props else [] ) - response = flask.g.dash_response = flask.Response( + response = quart.g.dash_response = quart.Response(None, mimetype="application/json" ) @@ -1382,7 +1377,8 @@ def dispatch(self): ][0] ) - response.set_data(self.callback_map[output]["callback"](*args)) + response_data = await self.callback_map[output]["callback"](*args) + response.set_data(response_data) return response def _validate_layout(self): @@ -1469,9 +1465,8 @@ def _invalid_resources_handler(err): @staticmethod def _serve_default_favicon(): - return flask.Response( - pkgutil.get_data("dash", "favicon.ico"), - content_type="image/x-icon", + return quart.Response( + pkgutil.get_data("dash", "favicon.ico"), content_type="image/x-icon" ) def get_asset_url(self, path): diff --git a/requires-install.txt b/requires-install.txt index 50485284bb..e3d7b726d4 100644 --- a/requires-install.txt +++ b/requires-install.txt @@ -1,8 +1,9 @@ -Flask>=1.0.2 -flask-compress +asgiref +quart +quart-compress plotly dash_renderer==1.1.2 dash-core-components==1.3.1 dash-html-components==1.0.1 dash-table==4.4.1 -future \ No newline at end of file +future From 689b7ff1df79a25696c3aa836f58a179d330074a Mon Sep 17 00:00:00 2001 From: Stephen Tierney Date: Fri, 25 Oct 2019 21:37:36 +1100 Subject: [PATCH 2/4] Updated dash.py to latest version --- dash/dash.py | 82 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 47 insertions(+), 35 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index 7741e9432b..8244b9d754 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -16,33 +16,30 @@ from functools import wraps from textwrap import dedent -from quart_compress import Compress -from werkzeug.debug.tbtools import get_current_traceback - import flask import quart +from quart_compress import Compress +from asgiref.sync import sync_to_async +import inspect +from werkzeug.debug.tbtools import get_current_traceback import plotly import dash_renderer -from dash.dependencies import Input, Output, State -from dash.resources import Scripts, Css -from dash.development.base_component import Component, ComponentRegistry -from dash import exceptions -from dash._utils import AttributeDict as _AttributeDict -from dash._utils import interpolate_str as _interpolate -from dash._utils import format_tag as _format_tag -from dash._utils import generate_hash as _generate_hash -from dash._utils import patch_collections_abc as _patch_collections_abc -from dash import _watch -from dash._utils import get_asset_path as _get_asset_path -from dash._utils import create_callback_id as _create_callback_id -from dash._configs import get_combined_config, pathname_configs -from dash.version import __version__ - -from asgiref.sync import sync_to_async - -import inspect +from .dependencies import Input, Output, State +from .resources import Scripts, Css +from .development.base_component import Component, ComponentRegistry +from . import exceptions +from ._utils import AttributeDict as _AttributeDict +from ._utils import interpolate_str as _interpolate +from ._utils import format_tag as _format_tag +from ._utils import generate_hash as _generate_hash +from ._utils import patch_collections_abc as _patch_collections_abc +from . import _watch +from ._utils import get_asset_path as _get_asset_path +from ._utils import create_callback_id as _create_callback_id +from ._configs import get_combined_config, pathname_configs +from .version import __version__ _default_index = """ @@ -218,6 +215,7 @@ def __init__( assets_url_path="assets", assets_ignore="", assets_external_path=None, + eager_loading=False, include_assets_files=True, url_base_pathname=None, requests_pathname_prefix=None, @@ -233,6 +231,11 @@ def __init__( plugins=None, **obsolete ): + # Apply _force_eager_loading overrides from modules + for module_name in ComponentRegistry.registry: + module = sys.modules[module_name] + eager = getattr(module, '_force_eager_loading', False) + eager_loading = eager_loading or eager for key in obsolete: if key in ["components_cache_max_age", "static_folder"]: @@ -294,6 +297,7 @@ def __init__( "name", "assets_folder", "assets_url_path", + "eager_loading", "url_base_pathname", "routes_pathname_prefix", "requests_pathname_prefix", @@ -320,7 +324,7 @@ def __init__( # static files from the packages self.css = Css(serve_locally) - self.scripts = Scripts(serve_locally) + self.scripts = Scripts(serve_locally, eager_loading) self.registered_paths = collections.defaultdict(set) @@ -618,7 +622,9 @@ def _generate_scripts_html(self): dev = self._dev_tools.serve_dev_bundles srcs = ( self._collect_and_register_resources( - self.scripts._resources._filter_resources(deps, dev_bundles=dev) + self.scripts._resources._filter_resources( + deps, dev_bundles=dev + ) ) + self.config.external_scripts + self._collect_and_register_resources( @@ -661,7 +667,9 @@ def _generate_meta_html(self): tags = [] if not has_ie_compat: - tags.append('') + tags.append( + '' + ) if not has_charset: tags.append('') @@ -937,7 +945,9 @@ def _validate_callback(self, output, inputs, state): {2} """ ).format( - arg_prop, arg_id, component.available_properties + arg_prop, + arg_id, + component.available_properties, ) ) @@ -1077,8 +1087,9 @@ def _raise_invalid( location_header=( "The value in question is located at" if not toplevel - else "The value in question is either the only value returned," - "\nor is in the top level of the returned list," + else "The value in question is either the only value " + "returned,\nor is in the top level of the returned " + "list," ), location=( "\n" @@ -1254,10 +1265,11 @@ def wrap_func(func): @wraps(func) async def add_context(*args, **kwargs): # don't touch the comment on the next line - used by debugger - if not inspect.iscoroutinefunction(func): - output_value = await sync_to_async(func)(*args, **kwargs) # %% callback invoked %% + if inspect.iscoroutinefunction(func): + output_value = await func(*args, **kwargs) # %% callback invoked else: - output_value = await func(*args, **kwargs) # %% callback invoked %% + output_value = await sync_to_async(func)(*args, **kwargs) # %% callback invoked + if multi: if not isinstance(output_value, (list, tuple)): raise exceptions.InvalidCallbackReturnValue( @@ -1331,7 +1343,7 @@ async def add_context(*args, **kwargs): return wrap_func async def dispatch(self): - body = await quart.request.get_json() + body = quart.request.get_json() inputs = body.get("inputs", []) state = body.get("state", []) output = body["output"] @@ -1353,7 +1365,7 @@ async def dispatch(self): else [] ) - response = quart.g.dash_response = quart.Response(None, + response = quart.g.dash_response = quart.Response( mimetype="application/json" ) @@ -1377,8 +1389,7 @@ async def dispatch(self): ][0] ) - response_data = await self.callback_map[output]["callback"](*args) - response.set_data(response_data) + response.set_data(await self.callback_map[output]["callback"](*args)) return response def _validate_layout(self): @@ -1466,7 +1477,8 @@ def _invalid_resources_handler(err): @staticmethod def _serve_default_favicon(): return quart.Response( - pkgutil.get_data("dash", "favicon.ico"), content_type="image/x-icon" + pkgutil.get_data("dash", "favicon.ico"), + content_type="image/x-icon", ) def get_asset_url(self, path): From 209010a28eb5a6f9f8901b3e20f82f9077ce7c8b Mon Sep 17 00:00:00 2001 From: Stephen Tierney Date: Fri, 25 Oct 2019 21:48:45 +1100 Subject: [PATCH 3/4] Formatted using black black --config pyproject.toml ./ --- dash/_callback_context.py | 15 +- dash/_configs.py | 95 +- dash/_watch.py | 2 +- dash/dash.py | 10 +- dash/dependencies.py | 20 +- dash/development/_py_components_generation.py | 390 +++--- dash/development/_r_components_generation.py | 279 +++-- dash/development/build_process.py | 2 +- dash/resources.py | 75 +- dash/testing/application_runners.py | 5 +- dash/testing/browser.py | 2 +- dash/testing/plugin.py | 4 +- dash/version.py | 2 +- pyproject.toml | 22 + tests/assets/simple_app.py | 4 +- tests/integration/IntegrationTests.py | 58 +- .../integration/clientside/test_clientside.py | 62 +- .../devtools/test_devtools_error_handling.py | 48 +- .../integration/devtools/test_props_check.py | 58 +- .../integration/renderer/test_dependencies.py | 5 +- .../integration/renderer/test_multi_output.py | 95 +- .../integration/renderer/test_persistence.py | 363 +++--- tests/integration/test_integration.py | 25 +- tests/integration/test_race_conditions.py | 40 +- tests/integration/test_render.py | 1079 +++++++++-------- tests/integration/test_scripts.py | 4 +- tests/integration/utils.py | 10 +- tests/unit/dash/test_async_resources.py | 8 +- tests/unit/dash/test_utils.py | 39 +- tests/unit/development/conftest.py | 4 +- tests/unit/development/metadata_test.py | 90 +- tests/unit/development/test_base_component.py | 28 +- .../unit/development/test_component_loader.py | 8 +- .../test_flow_metadata_conversions.py | 14 +- tests/unit/development/test_generate_class.py | 10 +- .../development/test_generate_class_file.py | 6 +- .../development/test_metadata_conversions.py | 19 +- .../unit/development/test_r_component_gen.py | 22 +- tests/unit/test_app_runners.py | 2 +- tests/unit/test_configs.py | 14 +- 40 files changed, 1681 insertions(+), 1357 deletions(-) create mode 100644 pyproject.toml diff --git a/dash/_callback_context.py b/dash/_callback_context.py index 04284cc6b7..035859784f 100644 --- a/dash/_callback_context.py +++ b/dash/_callback_context.py @@ -10,11 +10,12 @@ def assert_context(*args, **kwargs): if not flask.has_request_context(): raise exceptions.MissingCallbackContextException( ( - 'dash.callback_context.{} ' - 'is only available from a callback!' - ).format(getattr(func, '__name__')) + "dash.callback_context.{} " + "is only available from a callback!" + ).format(getattr(func, "__name__")) ) return func(*args, **kwargs) + return assert_context @@ -23,19 +24,19 @@ class CallbackContext: @property @has_context def inputs(self): - return getattr(flask.g, 'input_values', {}) + return getattr(flask.g, "input_values", {}) @property @has_context def states(self): - return getattr(flask.g, 'state_values', {}) + return getattr(flask.g, "state_values", {}) @property @has_context def triggered(self): - return getattr(flask.g, 'triggered_inputs', []) + return getattr(flask.g, "triggered_inputs", []) @property @has_context def response(self): - return getattr(flask.g, 'dash_response') + return getattr(flask.g, "dash_response") diff --git a/dash/_configs.py b/dash/_configs.py index c93fca5eba..a92c0220df 100644 --- a/dash/_configs.py +++ b/dash/_configs.py @@ -10,25 +10,25 @@ def load_dash_env_vars(): { var: os.getenv(var, os.getenv(var.lower())) for var in ( - 'DASH_APP_NAME', - 'DASH_URL_BASE_PATHNAME', - 'DASH_ROUTES_PATHNAME_PREFIX', - 'DASH_REQUESTS_PATHNAME_PREFIX', - 'DASH_SUPPRESS_CALLBACK_EXCEPTIONS', - 'DASH_ASSETS_EXTERNAL_PATH', - 'DASH_INCLUDE_ASSETS_FILES', - 'DASH_COMPONENTS_CACHE_MAX_AGE', - 'DASH_INCLUDE_ASSETS_FILES', - 'DASH_SERVE_DEV_BUNDLES', - 'DASH_DEBUG', - 'DASH_UI', - 'DASH_PROPS_CHECK', - 'DASH_HOT_RELOAD', - 'DASH_HOT_RELOAD_INTERVAL', - 'DASH_HOT_RELOAD_WATCH_INTERVAL', - 'DASH_HOT_RELOAD_MAX_RETRY', - 'DASH_SILENCE_ROUTES_LOGGING', - 'DASH_PRUNE_ERRORS', + "DASH_APP_NAME", + "DASH_URL_BASE_PATHNAME", + "DASH_ROUTES_PATHNAME_PREFIX", + "DASH_REQUESTS_PATHNAME_PREFIX", + "DASH_SUPPRESS_CALLBACK_EXCEPTIONS", + "DASH_ASSETS_EXTERNAL_PATH", + "DASH_INCLUDE_ASSETS_FILES", + "DASH_COMPONENTS_CACHE_MAX_AGE", + "DASH_INCLUDE_ASSETS_FILES", + "DASH_SERVE_DEV_BUNDLES", + "DASH_DEBUG", + "DASH_UI", + "DASH_PROPS_CHECK", + "DASH_HOT_RELOAD", + "DASH_HOT_RELOAD_INTERVAL", + "DASH_HOT_RELOAD_WATCH_INTERVAL", + "DASH_HOT_RELOAD_MAX_RETRY", + "DASH_SILENCE_ROUTES_LOGGING", + "DASH_PRUNE_ERRORS", ) } ) @@ -44,19 +44,19 @@ def get_combined_config(name, val, default=None): if val is not None: return val - env = load_dash_env_vars().get('DASH_{}'.format(name.upper())) + env = load_dash_env_vars().get("DASH_{}".format(name.upper())) if env is None: return default - return env.lower() == 'true' if env.lower() in {'true', 'false'} \ - else env + return env.lower() == "true" if env.lower() in {"true", "false"} else env def pathname_configs( - url_base_pathname=None, - routes_pathname_prefix=None, - requests_pathname_prefix=None): - _pathname_config_error_message = ''' + url_base_pathname=None, + routes_pathname_prefix=None, + requests_pathname_prefix=None, +): + _pathname_config_error_message = """ {} This is ambiguous. To fix this, set `routes_pathname_prefix` instead of `url_base_pathname`. @@ -68,57 +68,64 @@ def pathname_configs( If you need these to be different values then you should set `requests_pathname_prefix` and `routes_pathname_prefix`, not `url_base_pathname`. - ''' + """ url_base_pathname = get_combined_config( - 'url_base_pathname', url_base_pathname) + "url_base_pathname", url_base_pathname + ) routes_pathname_prefix = get_combined_config( - 'routes_pathname_prefix', routes_pathname_prefix) + "routes_pathname_prefix", routes_pathname_prefix + ) requests_pathname_prefix = get_combined_config( - 'requests_pathname_prefix', requests_pathname_prefix) + "requests_pathname_prefix", requests_pathname_prefix + ) if url_base_pathname is not None and requests_pathname_prefix is not None: raise exceptions.InvalidConfig( _pathname_config_error_message.format( - 'You supplied `url_base_pathname` and ' - '`requests_pathname_prefix`.' + "You supplied `url_base_pathname` and " + "`requests_pathname_prefix`." ) ) if url_base_pathname is not None and routes_pathname_prefix is not None: raise exceptions.InvalidConfig( _pathname_config_error_message.format( - 'You supplied `url_base_pathname` and ' - '`routes_pathname_prefix`.') + "You supplied `url_base_pathname` and " + "`routes_pathname_prefix`." + ) ) if url_base_pathname is not None and routes_pathname_prefix is None: routes_pathname_prefix = url_base_pathname elif routes_pathname_prefix is None: - routes_pathname_prefix = '/' + routes_pathname_prefix = "/" - if not routes_pathname_prefix.startswith('/'): + if not routes_pathname_prefix.startswith("/"): raise exceptions.InvalidConfig( - '`routes_pathname_prefix` needs to start with `/`') - if not routes_pathname_prefix.endswith('/'): + "`routes_pathname_prefix` needs to start with `/`" + ) + if not routes_pathname_prefix.endswith("/"): raise exceptions.InvalidConfig( - '`routes_pathname_prefix` needs to end with `/`') + "`routes_pathname_prefix` needs to end with `/`" + ) app_name = load_dash_env_vars().DASH_APP_NAME if not requests_pathname_prefix and app_name: - requests_pathname_prefix = '/' + app_name + routes_pathname_prefix + requests_pathname_prefix = "/" + app_name + routes_pathname_prefix elif requests_pathname_prefix is None: requests_pathname_prefix = routes_pathname_prefix - if not requests_pathname_prefix.startswith('/'): + if not requests_pathname_prefix.startswith("/"): raise exceptions.InvalidConfig( - '`requests_pathname_prefix` needs to start with `/`') + "`requests_pathname_prefix` needs to start with `/`" + ) if not requests_pathname_prefix.endswith(routes_pathname_prefix): raise exceptions.InvalidConfig( - '`requests_pathname_prefix` needs to ends with ' - '`routes_pathname_prefix`.' + "`requests_pathname_prefix` needs to ends with " + "`routes_pathname_prefix`." ) return url_base_pathname, routes_pathname_prefix, requests_pathname_prefix diff --git a/dash/_watch.py b/dash/_watch.py index 34c523478c..65c87e284a 100644 --- a/dash/_watch.py +++ b/dash/_watch.py @@ -11,7 +11,7 @@ def watch(folders, on_change, pattern=None, sleep_time=0.1): def walk(): walked = [] for folder in folders: - for current, _, files, in os.walk(folder): + for current, _, files in os.walk(folder): for f in files: if pattern and not pattern.search(f): continue diff --git a/dash/dash.py b/dash/dash.py index 8244b9d754..f8ce69060b 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -234,7 +234,7 @@ def __init__( # Apply _force_eager_loading overrides from modules for module_name in ComponentRegistry.registry: module = sys.modules[module_name] - eager = getattr(module, '_force_eager_loading', False) + eager = getattr(module, "_force_eager_loading", False) eager_loading = eager_loading or eager for key in obsolete: @@ -1266,9 +1266,13 @@ def wrap_func(func): async def add_context(*args, **kwargs): # don't touch the comment on the next line - used by debugger if inspect.iscoroutinefunction(func): - output_value = await func(*args, **kwargs) # %% callback invoked + output_value = await func( + *args, **kwargs + ) # %% callback invoked else: - output_value = await sync_to_async(func)(*args, **kwargs) # %% callback invoked + output_value = await sync_to_async(func)( + *args, **kwargs + ) # %% callback invoked if multi: if not isinstance(output_value, (list, tuple)): diff --git a/dash/dependencies.py b/dash/dependencies.py index 314cf7db75..24cf3fa043 100644 --- a/dash/dependencies.py +++ b/dash/dependencies.py @@ -5,13 +5,10 @@ def __init__(self, component_id, component_property): self.component_property = component_property def __str__(self): - return '{}.{}'.format( - self.component_id, - self.component_property - ) + return "{}.{}".format(self.component_id, self.component_property) def __repr__(self): - return '<{} `{}`>'.format(self.__class__.__name__, self) + return "<{} `{}`>".format(self.__class__.__name__, self) def __eq__(self, other): return isinstance(other, DashDependency) and str(self) == str(other) @@ -36,15 +33,16 @@ class ClientsideFunction: # pylint: disable=too-few-public-methods def __init__(self, namespace=None, function_name=None): - if namespace in ['PreventUpdate', 'no_update']: - raise ValueError('"{}" is a forbidden namespace in' - ' dash_clientside.'.format(namespace)) + if namespace in ["PreventUpdate", "no_update"]: + raise ValueError( + '"{}" is a forbidden namespace in' + " dash_clientside.".format(namespace) + ) self.namespace = namespace self.function_name = function_name def __repr__(self): - return 'ClientsideFunction({}, {})'.format( - self.namespace, - self.function_name + return "ClientsideFunction({}, {})".format( + self.namespace, self.function_name ) diff --git a/dash/development/_py_components_generation.py b/dash/development/_py_components_generation.py index ac5179adea..f11db98dd6 100644 --- a/dash/development/_py_components_generation.py +++ b/dash/development/_py_components_generation.py @@ -70,29 +70,33 @@ def __init__(self, {default_argtext}): wildcard_prefixes = repr(parse_wildcards(props)) list_of_valid_keys = repr(list(map(str, filtered_props.keys()))) docstring = create_docstring( - component_name=typename, - props=filtered_props, - description=description).replace('\r\n', '\n') + component_name=typename, props=filtered_props, description=description + ).replace("\r\n", "\n") prohibit_events(props) # pylint: disable=unused-variable prop_keys = list(props.keys()) - if 'children' in props: - prop_keys.remove('children') + if "children" in props: + prop_keys.remove("children") default_argtext = "children=None, " - argtext = 'children=children, **args' + argtext = "children=children, **args" else: default_argtext = "" - argtext = '**args' + argtext = "**args" default_argtext += ", ".join( - [('{:s}=Component.REQUIRED'.format(p) - if props[p]['required'] else - '{:s}=Component.UNDEFINED'.format(p)) - for p in prop_keys - if not p.endswith("-*") and - p not in python_keywords and - p != 'setProps'] + ["**kwargs"] + [ + ( + "{:s}=Component.REQUIRED".format(p) + if props[p]["required"] + else "{:s}=Component.UNDEFINED".format(p) + ) + for p in prop_keys + if not p.endswith("-*") + and p not in python_keywords + and p != "setProps" + ] + + ["**kwargs"] ) required_args = required_props(props) return c.format( @@ -104,7 +108,7 @@ def __init__(self, {default_argtext}): docstring=docstring, default_argtext=default_argtext, argtext=argtext, - required_props=required_args + required_props=required_args, ) @@ -121,33 +125,31 @@ def generate_class_file(typename, props, description, namespace): Returns ------- """ - import_string =\ - "# AUTO GENERATED FILE - DO NOT EDIT\n\n" + \ - "from dash.development.base_component import " + \ - "Component, _explicitize_args\n\n\n" + import_string = ( + "# AUTO GENERATED FILE - DO NOT EDIT\n\n" + + "from dash.development.base_component import " + + "Component, _explicitize_args\n\n\n" + ) class_string = generate_class_string( - typename, - props, - description, - namespace + typename, props, description, namespace ) file_name = "{:s}.py".format(typename) file_path = os.path.join(namespace, file_name) - with open(file_path, 'w') as f: + with open(file_path, "w") as f: f.write(import_string) f.write(class_string) - print('Generated {}'.format(file_name)) + print("Generated {}".format(file_name)) def generate_imports(project_shortname, components): - with open(os.path.join(project_shortname, '_imports_.py'), 'w') as f: - imports_string = '{}\n\n{}'.format( - '\n'.join( - 'from .{0} import {0}'.format(x) for x in components), - '__all__ = [\n{}\n]'.format( - ',\n'.join(' "{}"'.format(x) for x in components)) + with open(os.path.join(project_shortname, "_imports_.py"), "w") as f: + imports_string = "{}\n\n{}".format( + "\n".join("from .{0} import {0}".format(x) for x in components), + "__all__ = [\n{}\n]".format( + ",\n".join(' "{}"'.format(x) for x in components) + ), ) f.write(imports_string) @@ -156,15 +158,15 @@ def generate_imports(project_shortname, components): def generate_classes_files(project_shortname, metadata, *component_generators): components = [] for component_path, component_data in metadata.items(): - component_name = component_path.split('/')[-1].split('.')[0] + component_name = component_path.split("/")[-1].split(".")[0] components.append(component_name) for generator in component_generators: generator( component_name, - component_data['props'], - component_data['description'], - project_shortname + component_data["props"], + component_data["description"], + project_shortname, ) return components @@ -184,7 +186,7 @@ def generate_class(typename, props, description, namespace): ------- """ string = generate_class_string(typename, props, description, namespace) - scope = {'Component': Component, '_explicitize_args': _explicitize_args} + scope = {"Component": Component, "_explicitize_args": _explicitize_args} # pylint: disable=exec-used exec(string, scope) result = scope[typename] @@ -203,8 +205,11 @@ def required_props(props): list List of prop names (str) that are required for the Component """ - return [prop_name for prop_name, prop in list(props.items()) - if prop['required']] + return [ + prop_name + for prop_name, prop in list(props.items()) + if prop["required"] + ] def create_docstring(component_name, props, description): @@ -232,21 +237,26 @@ def create_docstring(component_name, props, description): Keyword arguments:\n{args}""" ).format( - n='n' if component_name[0].lower() in ['a', 'e', 'i', 'o', 'u'] - else '', + n="n" + if component_name[0].lower() in ["a", "e", "i", "o", "u"] + else "", name=component_name, description=description, - args='\n'.join( + args="\n".join( create_prop_docstring( prop_name=p, - type_object=prop['type'] if 'type' in prop - else prop['flowType'], - required=prop['required'], - description=prop['description'], - default=prop.get('defaultValue'), + type_object=prop["type"] + if "type" in prop + else prop["flowType"], + required=prop["required"], + description=prop["description"], + default=prop.get("defaultValue"), indent_num=0, - is_flow_type='flowType' in prop and 'type' not in prop) - for p, prop in list(filter_props(props).items()))) + is_flow_type="flowType" in prop and "type" not in prop, + ) + for p, prop in list(filter_props(props).items()) + ), + ) def prohibit_events(props): @@ -262,10 +272,11 @@ def prohibit_events(props): ------- ? """ - if 'dashEvents' in props or 'fireEvents' in props: + if "dashEvents" in props or "fireEvents" in props: raise NonExistentEventException( - 'Events are no longer supported by dash. Use properties instead, ' - 'eg `n_clicks` instead of a `click` event.') + "Events are no longer supported by dash. Use properties instead, " + "eg `n_clicks` instead of a `click` event." + ) def parse_wildcards(props): @@ -302,11 +313,11 @@ def reorder_props(props): dict Dictionary with {propName: propMetadata} structure """ - if 'children' in props: + if "children" in props: # Constructing an OrderedDict with duplicate keys, you get the order # from the first one but the value from the last. # Doing this to avoid mutating props, which can cause confusion. - props = OrderedDict([('children', '')] + list(props.items())) + props = OrderedDict([("children", "")] + list(props.items())) return props @@ -360,23 +371,25 @@ def filter_props(props): filtered_props = copy.deepcopy(props) for arg_name, arg in list(filtered_props.items()): - if 'type' not in arg and 'flowType' not in arg: + if "type" not in arg and "flowType" not in arg: filtered_props.pop(arg_name) continue # Filter out functions and instances -- # these cannot be passed from Python - if 'type' in arg: # These come from PropTypes - arg_type = arg['type']['name'] - if arg_type in {'func', 'symbol', 'instanceOf'}: + if "type" in arg: # These come from PropTypes + arg_type = arg["type"]["name"] + if arg_type in {"func", "symbol", "instanceOf"}: filtered_props.pop(arg_name) - elif 'flowType' in arg: # These come from Flow & handled differently - arg_type_name = arg['flowType']['name'] - if arg_type_name == 'signature': + elif "flowType" in arg: # These come from Flow & handled differently + arg_type_name = arg["flowType"]["name"] + if arg_type_name == "signature": # This does the same as the PropTypes filter above, but "func" # is under "type" if "name" is "signature" vs just in "name" - if 'type' not in arg['flowType'] \ - or arg['flowType']['type'] != 'object': + if ( + "type" not in arg["flowType"] + or arg["flowType"]["type"] != "object" + ): filtered_props.pop(arg_name) else: raise ValueError @@ -385,8 +398,15 @@ def filter_props(props): # pylint: disable=too-many-arguments -def create_prop_docstring(prop_name, type_object, required, description, - default, indent_num, is_flow_type=False): +def create_prop_docstring( + prop_name, + type_object, + required, + description, + default, + indent_num, + is_flow_type=False, +): """Create the Dash component prop docstring. Parameters @@ -417,114 +437,124 @@ def create_prop_docstring(prop_name, type_object, required, description, py_type_name = js_to_py_type( type_object=type_object, is_flow_type=is_flow_type, - indent_num=indent_num + 1) - indent_spacing = ' ' * indent_num + indent_num=indent_num + 1, + ) + indent_spacing = " " * indent_num if default is None: - default = '' + default = "" else: - default = default['value'] + default = default["value"] - if default in ['true', 'false']: + if default in ["true", "false"]: default = default.title() - is_required = 'optional' + is_required = "optional" if required: - is_required = 'required' - elif default and default not in ['null', '{}', '[]']: - is_required = 'default {}'.format( - default.replace('\n', '\n' + indent_spacing) + is_required = "required" + elif default and default not in ["null", "{}", "[]"]: + is_required = "default {}".format( + default.replace("\n", "\n" + indent_spacing) ) - if '\n' in py_type_name: - return '{indent_spacing}- {name} (dict; {is_required}): ' \ - '{description}{period}' \ - '{name} has the following type: {type}'.format( + if "\n" in py_type_name: + return ( + "{indent_spacing}- {name} (dict; {is_required}): " + "{description}{period}" + "{name} has the following type: {type}".format( indent_spacing=indent_spacing, name=prop_name, type=py_type_name, - description=description.strip().strip('.'), - period='. ' if description else '', - is_required=is_required) - return '{indent_spacing}- {name} ({type}' \ - '{is_required}){description}'.format( + description=description.strip().strip("."), + period=". " if description else "", + is_required=is_required, + ) + ) + return ( + "{indent_spacing}- {name} ({type}" + "{is_required}){description}".format( indent_spacing=indent_spacing, name=prop_name, - type='{}; '.format(py_type_name) if py_type_name else '', + type="{}; ".format(py_type_name) if py_type_name else "", description=( - ': {}'.format(description) if description != '' else '' + ": {}".format(description) if description != "" else "" ), - is_required=is_required) + is_required=is_required, + ) + ) def map_js_to_py_types_prop_types(type_object): """Mapping from the PropTypes js type object to the Python type.""" def shape_or_exact(): - return 'dict containing keys {}.\n{}'.format( - ', '.join( - "'{}'".format(t) for t in list(type_object['value'].keys()) + return "dict containing keys {}.\n{}".format( + ", ".join( + "'{}'".format(t) for t in list(type_object["value"].keys()) ), - 'Those keys have the following types:\n{}'.format( - '\n'.join( + "Those keys have the following types:\n{}".format( + "\n".join( create_prop_docstring( prop_name=prop_name, type_object=prop, - required=prop['required'], - description=prop.get('description', ''), - default=prop.get('defaultValue'), - indent_num=1 - ) for prop_name, prop in - list(type_object['value'].items()))) - ) + required=prop["required"], + description=prop.get("description", ""), + default=prop.get("defaultValue"), + indent_num=1, + ) + for prop_name, prop in list(type_object["value"].items()) + ) + ), + ) return dict( - array=lambda: 'list', - bool=lambda: 'boolean', - number=lambda: 'number', - string=lambda: 'string', - object=lambda: 'dict', - any=lambda: 'boolean | number | string | dict | list', - element=lambda: 'dash component', - node=lambda: 'a list of or a singular dash ' - 'component, string or number', - + array=lambda: "list", + bool=lambda: "boolean", + number=lambda: "number", + string=lambda: "string", + object=lambda: "dict", + any=lambda: "boolean | number | string | dict | list", + element=lambda: "dash component", + node=lambda: "a list of or a singular dash " + "component, string or number", # React's PropTypes.oneOf - enum=lambda: 'a value equal to: {}'.format( - ', '.join( - '{}'.format(str(t['value'])) - for t in type_object['value'])), - + enum=lambda: "a value equal to: {}".format( + ", ".join( + "{}".format(str(t["value"])) for t in type_object["value"] + ) + ), # React's PropTypes.oneOfType - union=lambda: '{}'.format( - ' | '.join( - '{}'.format(js_to_py_type(subType)) - for subType in type_object['value'] - if js_to_py_type(subType) != '')), - + union=lambda: "{}".format( + " | ".join( + "{}".format(js_to_py_type(subType)) + for subType in type_object["value"] + if js_to_py_type(subType) != "" + ) + ), # React's PropTypes.arrayOf arrayOf=lambda: ( - "list" + (" of {}".format( - js_to_py_type(type_object["value"]) + 's' - if js_to_py_type(type_object["value"]).split(' ')[0] != 'dict' - else js_to_py_type(type_object["value"]).replace( - 'dict', 'dicts', 1 + "list" + + ( + " of {}".format( + js_to_py_type(type_object["value"]) + "s" + if js_to_py_type(type_object["value"]).split(" ")[0] + != "dict" + else js_to_py_type(type_object["value"]).replace( + "dict", "dicts", 1 + ) ) + if js_to_py_type(type_object["value"]) != "" + else "" ) - if js_to_py_type(type_object["value"]) != "" - else "") ), - # React's PropTypes.objectOf objectOf=lambda: ( - 'dict with strings as keys and values of type {}' - ).format( - js_to_py_type(type_object['value'])), - + "dict with strings as keys and values of type {}" + ).format(js_to_py_type(type_object["value"])), # React's PropTypes.shape shape=shape_or_exact, # React's PropTypes.exact - exact=shape_or_exact + exact=shape_or_exact, ) @@ -532,46 +562,51 @@ def map_js_to_py_types_flow_types(type_object): """Mapping from the Flow js types to the Python type.""" return dict( - array=lambda: 'list', - boolean=lambda: 'boolean', - number=lambda: 'number', - string=lambda: 'string', - Object=lambda: 'dict', - any=lambda: 'bool | number | str | dict | list', - Element=lambda: 'dash component', - Node=lambda: 'a list of or a singular dash ' - 'component, string or number', - + array=lambda: "list", + boolean=lambda: "boolean", + number=lambda: "number", + string=lambda: "string", + Object=lambda: "dict", + any=lambda: "bool | number | str | dict | list", + Element=lambda: "dash component", + Node=lambda: "a list of or a singular dash " + "component, string or number", # React's PropTypes.oneOfType - union=lambda: '{}'.format( - ' | '.join( - '{}'.format(js_to_py_type(subType)) - for subType in type_object['elements'] - if js_to_py_type(subType) != '')), - + union=lambda: "{}".format( + " | ".join( + "{}".format(js_to_py_type(subType)) + for subType in type_object["elements"] + if js_to_py_type(subType) != "" + ) + ), # Flow's Array type - Array=lambda: 'list{}'.format( - ' of {}s'.format( - js_to_py_type(type_object['elements'][0])) - if js_to_py_type(type_object['elements'][0]) != '' - else ''), - + Array=lambda: "list{}".format( + " of {}s".format(js_to_py_type(type_object["elements"][0])) + if js_to_py_type(type_object["elements"][0]) != "" + else "" + ), # React's PropTypes.shape - signature=lambda indent_num: 'dict containing keys {}.\n{}'.format( - ', '.join("'{}'".format(d['key']) - for d in type_object['signature']['properties']), - '{}Those keys have the following types:\n{}'.format( - ' ' * indent_num, - '\n'.join( + signature=lambda indent_num: "dict containing keys {}.\n{}".format( + ", ".join( + "'{}'".format(d["key"]) + for d in type_object["signature"]["properties"] + ), + "{}Those keys have the following types:\n{}".format( + " " * indent_num, + "\n".join( create_prop_docstring( - prop_name=prop['key'], - type_object=prop['value'], - required=prop['value']['required'], - description=prop['value'].get('description', ''), - default=prop.get('defaultValue'), + prop_name=prop["key"], + type_object=prop["value"], + required=prop["value"]["required"], + description=prop["value"].get("description", ""), + default=prop.get("defaultValue"), indent_num=indent_num, - is_flow_type=True) - for prop in type_object['signature']['properties']))), + is_flow_type=True, + ) + for prop in type_object["signature"]["properties"] + ), + ), + ), ) @@ -592,17 +627,22 @@ def js_to_py_type(type_object, is_flow_type=False, indent_num=0): str Python type string """ - js_type_name = type_object['name'] - js_to_py_types = map_js_to_py_types_flow_types(type_object=type_object) \ - if is_flow_type \ + js_type_name = type_object["name"] + js_to_py_types = ( + map_js_to_py_types_flow_types(type_object=type_object) + if is_flow_type else map_js_to_py_types_prop_types(type_object=type_object) + ) - if 'computed' in type_object and type_object['computed'] \ - or type_object.get('type', '') == 'function': - return '' + if ( + "computed" in type_object + and type_object["computed"] + or type_object.get("type", "") == "function" + ): + return "" if js_type_name in js_to_py_types: - if js_type_name == 'signature': # This is a Flow object w/ signature + if js_type_name == "signature": # This is a Flow object w/ signature return js_to_py_types[js_type_name](indent_num) # All other types return js_to_py_types[js_type_name]() - return '' + return "" diff --git a/dash/development/_r_components_generation.py b/dash/development/_r_components_generation.py index bb0b7c335a..245861414d 100644 --- a/dash/development/_r_components_generation.py +++ b/dash/development/_r_components_generation.py @@ -46,7 +46,7 @@ file = "deps"), meta = NULL, script = {script_name}, stylesheet = {css_name}, head = NULL, attachment = NULL, package = "{rpkgname}", -all_files = FALSE), class = "html_dependency")""" # noqa:E501 +all_files = FALSE), class = "html_dependency")""" # noqa:E501 frame_body_template = """`{project_shortname}` = structure(list(name = "{project_shortname}", version = "{project_ver}", src = list(href = NULL, @@ -214,10 +214,12 @@ def generate_class_string(name, props, project_shortname, prefix): prop_keys.remove(item) elif item in r_keywords: prop_keys.remove(item) - warnings.warn(( - 'WARNING: prop "{}" in component "{}" is an R keyword' - ' - REMOVED FROM THE R COMPONENT' - ).format(item, name)) + warnings.warn( + ( + 'WARNING: prop "{}" in component "{}" is an R keyword' + " - REMOVED FROM THE R COMPONENT" + ).format(item, name) + ) default_argtext += ", ".join("{}=NULL".format(p) for p in prop_keys) @@ -227,16 +229,18 @@ def generate_class_string(name, props, project_shortname, prefix): for p in prop_keys ) - return r_component_string.format(funcname=format_fn_name(prefix, name), - name=name, - default_argtext=default_argtext, - wildcards=wildcards, - wildcard_declaration=wildcard_declaration, - default_paramtext=default_paramtext, - project_shortname=project_shortname, - prop_names=prop_names, - wildcard_names=wildcard_names, - package_name=package_name) + return r_component_string.format( + funcname=format_fn_name(prefix, name), + name=name, + default_argtext=default_argtext, + wildcards=wildcards, + wildcard_declaration=wildcard_declaration, + default_paramtext=default_paramtext, + project_shortname=project_shortname, + prop_names=prop_names, + wildcard_names=wildcard_names, + package_name=package_name, + ) # pylint: disable=R0914 @@ -284,10 +288,10 @@ def generate_js_metadata(pkg_data, project_shortname): project_ver = str(dep) if "css" in rpp: css_name = "'{}'".format(rpp) - script_name = 'NULL' + script_name = "NULL" else: script_name = "'{}'".format(rpp) - css_name = 'NULL' + css_name = "NULL" function_frame += [ frame_element_template.format( dep_name=dep_name, @@ -327,7 +331,7 @@ def generate_js_metadata(pkg_data, project_shortname): def wrap(tag, code): if tag == "": return code - return '\\{}{{\n{}}}'.format(tag, code) + return "\\{}{{\n{}}}".format(tag, code) def write_help_file(name, props, description, prefix, rpkg_data): @@ -362,50 +366,50 @@ def write_help_file(name, props, description, prefix, rpkg_data): item_text += "\n\n".join( "\\item{{{}}}{{{}{}}}".format( - p, - print_r_type(props[p]["type"]), - props[p]["description"] + p, print_r_type(props[p]["type"]), props[p]["description"] ) for p in prop_keys ) if any(key.endswith("-*") for key in prop_keys): - default_argtext += ', ...' + default_argtext += ", ..." item_text += wildcard_help_template.format(get_wildcards_r(prop_keys)) # in R, the online help viewer does not properly wrap lines for # the usage string -- we will hard wrap at 80 characters using # textwrap.fill, starting from the beginning of the usage string - file_path = os.path.join('man', file_name) - with open(file_path, 'w') as f: - f.write(help_string.format( - funcname=funcname, - name=name, - default_argtext=textwrap.fill(default_argtext, - width=80, - break_long_words=False), - item_text=item_text, - description=description.replace('\n', ' ') - )) - if rpkg_data is not None and 'r_examples' in rpkg_data: - ex = rpkg_data.get('r_examples') + file_path = os.path.join("man", file_name) + with open(file_path, "w") as f: + f.write( + help_string.format( + funcname=funcname, + name=name, + default_argtext=textwrap.fill( + default_argtext, width=80, break_long_words=False + ), + item_text=item_text, + description=description.replace("\n", " "), + ) + ) + if rpkg_data is not None and "r_examples" in rpkg_data: + ex = rpkg_data.get("r_examples") the_ex = ([e for e in ex if e.get("name") == funcname] or [None])[0] result = "" if the_ex and "code" in the_ex.keys(): - result += wrap("examples", - wrap("dontrun" if the_ex.get("dontrun") else "", - the_ex["code"])) - with open(file_path, 'a+') as fa: - fa.write(result + '\n') - - -def write_class_file(name, - props, - description, - project_shortname, - prefix=None, - rpkg_data=None): + result += wrap( + "examples", + wrap( + "dontrun" if the_ex.get("dontrun") else "", the_ex["code"] + ), + ) + with open(file_path, "a+") as fa: + fa.write(result + "\n") + + +def write_class_file( + name, props, description, project_shortname, prefix=None, rpkg_data=None +): props = reorder_props(props=props) # generate the R help pages for each of the Dash components that we @@ -415,8 +419,7 @@ def write_class_file(name, # from within Python write_help_file(name, props, description, prefix, rpkg_data) - import_string =\ - "# AUTO GENERATED FILE - DO NOT EDIT\n\n" + import_string = "# AUTO GENERATED FILE - DO NOT EDIT\n\n" class_string = generate_class_string( name, props, project_shortname, prefix ) @@ -478,14 +481,14 @@ def write_js_metadata(pkg_data, project_shortname, has_wildcards): # pylint: disable=R0914, R0913, R0912, R0915 def generate_rpkg( - pkg_data, - rpkg_data, - project_shortname, - export_string, - package_depends, - package_imports, - package_suggests, - has_wildcards, + pkg_data, + rpkg_data, + project_shortname, + export_string, + package_depends, + package_imports, + package_suggests, + has_wildcards, ): """Generate documents for R package creation. @@ -556,9 +559,9 @@ def generate_rpkg( os.symlink("LICENSE.txt", "LICENSE") import_string = "# AUTO GENERATED FILE - DO NOT EDIT\n\n" - packages_string = '' + packages_string = "" - rpackage_list = package_depends.split(', ') + package_imports.split(', ') + rpackage_list = package_depends.split(", ") + package_imports.split(", ") rpackage_list = filter(bool, rpackage_list) if rpackage_list: @@ -631,23 +634,23 @@ def format_fn_name(prefix, name): # pylint: disable=unused-argument def generate_exports( - project_shortname, - components, - metadata, - pkg_data, - rpkg_data, - prefix, - package_depends, - package_imports, - package_suggests, - **kwargs + project_shortname, + components, + metadata, + pkg_data, + rpkg_data, + prefix, + package_depends, + package_imports, + package_suggests, + **kwargs ): export_string = make_namespace_exports(components, prefix) # Look for wildcards in the metadata has_wildcards = False for component_data in metadata.values(): - if any(key.endswith('-*') for key in component_data['props']): + if any(key.endswith("-*") for key in component_data["props"]): has_wildcards = True break @@ -670,9 +673,9 @@ def make_namespace_exports(components, prefix): export_string = "" for component in components: if ( - not component.endswith("-*") - and str(component) not in r_keywords - and str(component) not in ["setProps", "children"] + not component.endswith("-*") + and str(component) not in r_keywords + and str(component) not in ["setProps", "children"] ): export_string += "export({}{})\n".format(prefix, component) @@ -695,7 +698,7 @@ def make_namespace_exports(components, prefix): s = script.read() # remove comments - s = re.sub('#.*$', '', s, flags=re.M) + s = re.sub("#.*$", "", s, flags=re.M) # put the whole file on one line s = s.replace("\n", " ").replace("\r", " ") @@ -716,7 +719,8 @@ def make_namespace_exports(components, prefix): # now, in whatever is left, look for functions matches = re.findall( # in R, either = or <- may be used to create and assign objects - r"([^A-Za-z0-9._]|^)([A-Za-z0-9._]+)\s*(=|<-)\s*function", s + r"([^A-Za-z0-9._]|^)([A-Za-z0-9._]+)\s*(=|<-)\s*function", + s, ) for match in matches: fn = match[1] @@ -724,8 +728,9 @@ def make_namespace_exports(components, prefix): if fn[0] != "." and fn not in fnlist: fnlist.append(fn) - export_string += "\n".join("export({})".format(function) - for function in fnlist) + export_string += "\n".join( + "export({})".format(function) for function in fnlist + ) return export_string @@ -733,21 +738,23 @@ def get_r_prop_types(type_object): """Mapping from the PropTypes js type object to the R type.""" def shape_or_exact(): - return 'lists containing elements {}.\n{}'.format( - ', '.join( - "'{}'".format(t) for t in list(type_object['value'].keys()) + return "lists containing elements {}.\n{}".format( + ", ".join( + "'{}'".format(t) for t in list(type_object["value"].keys()) ), - 'Those elements have the following types:\n{}'.format( - '\n'.join( + "Those elements have the following types:\n{}".format( + "\n".join( create_prop_docstring_r( prop_name=prop_name, type_object=prop, - required=prop['required'], - description=prop.get('description', ''), - indent_num=1 - ) for prop_name, prop in - list(type_object['value'].items()))) - ) + required=prop["required"], + description=prop.get("description", ""), + indent_num=1, + ) + for prop_name, prop in list(type_object["value"].items()) + ) + ), + ) return dict( array=lambda: "unnamed list", @@ -756,14 +763,15 @@ def shape_or_exact(): string=lambda: "character", object=lambda: "named list", any=lambda: "logical | numeric | character | " - "named list | unnamed list", + "named list | unnamed list", element=lambda: "dash component", node=lambda: "a list of or a singular dash " - "component, string or number", + "component, string or number", # React's PropTypes.oneOf enum=lambda: "a value equal to: {}".format( - ", ".join("{}".format(str(t["value"])) - for t in type_object["value"]) + ", ".join( + "{}".format(str(t["value"])) for t in type_object["value"] + ) ), # React's PropTypes.oneOfType union=lambda: "{}".format( @@ -775,22 +783,21 @@ def shape_or_exact(): ), # React's PropTypes.arrayOf arrayOf=lambda: ( - "list" + (" of {}s".format( - get_r_type(type_object["value"])) - if get_r_type(type_object["value"]) != "" - else "") + "list" + + ( + " of {}s".format(get_r_type(type_object["value"])) + if get_r_type(type_object["value"]) != "" + else "" + ) ), # React's PropTypes.objectOf objectOf=lambda: ( "list with named elements and values of type {}" - ).format( - get_r_type(type_object["value"]) - ), - + ).format(get_r_type(type_object["value"])), # React's PropTypes.shape shape=shape_or_exact, # React's PropTypes.exact - exact=shape_or_exact + exact=shape_or_exact, ) @@ -812,9 +819,9 @@ def get_r_type(type_object, is_flow_type=False, indent_num=0): js_type_name = type_object["name"] js_to_r_types = get_r_prop_types(type_object=type_object) if ( - "computed" in type_object - and type_object["computed"] - or type_object.get("type", "") == "function" + "computed" in type_object + and type_object["computed"] + or type_object.get("type", "") == "function" ): return "" elif js_type_name in js_to_r_types: @@ -831,8 +838,14 @@ def print_r_type(typedata): # pylint: disable=too-many-arguments -def create_prop_docstring_r(prop_name, type_object, required, description, - indent_num, is_flow_type=False): +def create_prop_docstring_r( + prop_name, + type_object, + required, + description, + indent_num, + is_flow_type=False, +): """ Create the Dash component prop docstring Parameters @@ -858,32 +871,40 @@ def create_prop_docstring_r(prop_name, type_object, required, description, r_type_name = get_r_type( type_object=type_object, is_flow_type=is_flow_type, - indent_num=indent_num + 1) - - indent_spacing = ' ' * indent_num - if '\n' in r_type_name: - return '{indent_spacing}- {name} ({is_required}): {description}. ' \ - '{name} has the following type: {type}'.format( - indent_spacing=indent_spacing, - name=prop_name, - type=r_type_name, - description=description, - is_required='required' if required else 'optional') - return '{indent_spacing}- {name} ({type}' \ - '{is_required}){description}'.format( - indent_spacing=indent_spacing, - name=prop_name, - type='{}; '.format(r_type_name) if r_type_name else '', - description=( - ': {}'.format(description) if description != '' else '' - ), - is_required='required' if required else 'optional') + indent_num=indent_num + 1, + ) + + indent_spacing = " " * indent_num + if "\n" in r_type_name: + return ( + "{indent_spacing}- {name} ({is_required}): {description}. " + "{name} has the following type: {type}".format( + indent_spacing=indent_spacing, + name=prop_name, + type=r_type_name, + description=description, + is_required="required" if required else "optional", + ) + ) + return ( + "{indent_spacing}- {name} ({type}" + "{is_required}){description}".format( + indent_spacing=indent_spacing, + name=prop_name, + type="{}; ".format(r_type_name) if r_type_name else "", + description=( + ": {}".format(description) if description != "" else "" + ), + is_required="required" if required else "optional", + ) + ) def get_wildcards_r(prop_keys): wildcards = "" - wildcards += ", ".join("'{}'".format(p) - for p in prop_keys if p.endswith("-*")) + wildcards += ", ".join( + "'{}'".format(p) for p in prop_keys if p.endswith("-*") + ) if wildcards == "": wildcards = "NULL" diff --git a/dash/development/build_process.py b/dash/development/build_process.py index d40a5c6c73..226dd26e52 100644 --- a/dash/development/build_process.py +++ b/dash/development/build_process.py @@ -142,7 +142,7 @@ def bundles(self, build=None): self._concat(self.build_folder, target), ) - _script = 'build:dev' if build == 'local' else 'build:js' + _script = "build:dev" if build == "local" else "build:js" logger.info("run `npm run %s`", _script) os.chdir(self.main) run_command_with_process("npm run {}".format(_script)) diff --git a/dash/resources.py b/dash/resources.py index 7923edfc50..4126616180 100644 --- a/dash/resources.py +++ b/dash/resources.py @@ -19,10 +19,10 @@ def _filter_resources(self, all_resources, dev_bundles=False): filtered_resources = [] for s in all_resources: filtered_resource = {} - if 'dynamic' in s: - filtered_resource['dynamic'] = s['dynamic'] - if 'async' in s: - if 'dynamic' in s: + if "dynamic" in s: + filtered_resource["dynamic"] = s["dynamic"] + if "async" in s: + if "dynamic" in s: raise exceptions.ResourceException( "Can't have both 'dynamic' and 'async'. " "{}".format(json.dumps(filtered_resource)) @@ -35,51 +35,50 @@ def _filter_resources(self, all_resources, dev_bundles=False): # 'lazy' -> always dynamic # 'eager' -> dynamic if server is not eager # (to prevent ever loading it) - filtered_resource['dynamic'] = ( + filtered_resource["dynamic"] = ( not self.config.eager_loading - if s['async'] is True + if s["async"] is True else ( - s['async'] == 'eager' - and not self.config.eager_loading + s["async"] == "eager" and not self.config.eager_loading ) - or s['async'] == 'lazy' + or s["async"] == "lazy" ) - if 'namespace' in s: - filtered_resource['namespace'] = s['namespace'] - if 'external_url' in s and not self.config.serve_locally: - filtered_resource['external_url'] = s['external_url'] - elif 'dev_package_path' in s and dev_bundles: - filtered_resource['relative_package_path'] = s[ - 'dev_package_path' + if "namespace" in s: + filtered_resource["namespace"] = s["namespace"] + if "external_url" in s and not self.config.serve_locally: + filtered_resource["external_url"] = s["external_url"] + elif "dev_package_path" in s and dev_bundles: + filtered_resource["relative_package_path"] = s[ + "dev_package_path" ] - elif 'relative_package_path' in s: - filtered_resource['relative_package_path'] = s[ - 'relative_package_path' + elif "relative_package_path" in s: + filtered_resource["relative_package_path"] = s[ + "relative_package_path" ] - elif 'absolute_path' in s: - filtered_resource['absolute_path'] = s['absolute_path'] - elif 'asset_path' in s: - info = os.stat(s['filepath']) - filtered_resource['asset_path'] = s['asset_path'] - filtered_resource['ts'] = info.st_mtime + elif "absolute_path" in s: + filtered_resource["absolute_path"] = s["absolute_path"] + elif "asset_path" in s: + info = os.stat(s["filepath"]) + filtered_resource["asset_path"] = s["asset_path"] + filtered_resource["ts"] = info.st_mtime elif self.config.serve_locally: warnings.warn( ( - 'You have set your config to `serve_locally=True` but ' - 'A local version of {} is not available.\n' - 'If you added this file with ' - '`app.scripts.append_script` ' - 'or `app.css.append_css`, use `external_scripts` ' - 'or `external_stylesheets` instead.\n' - 'See https://dash.plot.ly/external-resources' - ).format(s['external_url']) + "You have set your config to `serve_locally=True` but " + "A local version of {} is not available.\n" + "If you added this file with " + "`app.scripts.append_script` " + "or `app.css.append_css`, use `external_scripts` " + "or `external_stylesheets` instead.\n" + "See https://dash.plot.ly/external-resources" + ).format(s["external_url"]) ) continue else: raise exceptions.ResourceException( - '{} does not have a ' - 'relative_package_path, absolute_path, or an ' - 'external_url.'.format(json.dumps(filtered_resource)) + "{} does not have a " + "relative_package_path, absolute_path, or an " + "external_url.".format(json.dumps(filtered_resource)) ) filtered_resources.append(filtered_resource) @@ -102,7 +101,7 @@ def __init__(self, serve_locally, eager_loading): class Css: def __init__(self, serve_locally): - self._resources = Resources('_css_dist') + self._resources = Resources("_css_dist") self._resources.config = self.config = _Config(serve_locally, True) def append_css(self, stylesheet): @@ -114,7 +113,7 @@ def get_all_css(self): class Scripts: def __init__(self, serve_locally, eager): - self._resources = Resources('_js_dist') + self._resources = Resources("_js_dist") self._resources.config = self.config = _Config(serve_locally, eager) def append_script(self, script): diff --git a/dash/testing/application_runners.py b/dash/testing/application_runners.py index fb2bae68ba..d521dcba0a 100644 --- a/dash/testing/application_runners.py +++ b/dash/testing/application_runners.py @@ -255,7 +255,7 @@ def start(self, app, start_timeout=2, cwd=None): """Start the server with subprocess and Rscript.""" # app is a R string chunk - if (os.path.isfile(app) and os.path.exists(app)): + if os.path.isfile(app) and os.path.exists(app): # app is already a file in a dir - use that as cwd if not cwd: cwd = os.path.dirname(app) @@ -286,8 +286,7 @@ def start(self, app, start_timeout=2, cwd=None): break if cwd: logger.info( - "RRunner inferred cwd from the Python call stack: %s", - cwd + "RRunner inferred cwd from the Python call stack: %s", cwd ) else: logger.warning( diff --git a/dash/testing/browser.py b/dash/testing/browser.py index 61c76c222f..e0bb18f242 100644 --- a/dash/testing/browser.py +++ b/dash/testing/browser.py @@ -445,7 +445,7 @@ def zoom_in_graph_by_ratio( elem_or_selector, start_fraction=0.5, zoom_box_fraction=0.2, - compare=True + compare=True, ): """Zoom out a graph with a zoom box fraction of component dimension default start at middle with a rectangle of 1/5 of the dimension use diff --git a/dash/testing/plugin.py b/dash/testing/plugin.py index 894cccad80..57cbd7b915 100644 --- a/dash/testing/plugin.py +++ b/dash/testing/plugin.py @@ -51,8 +51,8 @@ def pytest_addoption(parser): dash.addoption( "--percy-assets", action="store", - default='tests/assets', - help="configure how Percy will discover your app's assets" + default="tests/assets", + help="configure how Percy will discover your app's assets", ) dash.addoption( diff --git a/dash/version.py b/dash/version.py index 8e3c933cd0..bf2561596c 100644 --- a/dash/version.py +++ b/dash/version.py @@ -1 +1 @@ -__version__ = '1.4.1' +__version__ = "1.4.1" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..cc05a21313 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,22 @@ +[tool.black] +line-length = 79 +include = '\.pyi?$' +exclude = ''' +/( + \.eggs + | \.git + | \.hg + | \.mypy_cache + | \.tox + | venv* + | .circleci + | .github + | .pytest_cache + | @plotly + | dash.egg-info + | _build + | buck-out + | build + | dist +)/ +''' diff --git a/tests/assets/simple_app.py b/tests/assets/simple_app.py index 3e485c0890..1cb9f2bb59 100644 --- a/tests/assets/simple_app.py +++ b/tests/assets/simple_app.py @@ -26,7 +26,9 @@ def on_value(value): return value -@app.callback(Output("style-output", "style"), [Input("style-btn", "n_clicks")]) +@app.callback( + Output("style-output", "style"), [Input("style-btn", "n_clicks")] +) def on_style(value): if value is None: raise PreventUpdate diff --git a/tests/integration/IntegrationTests.py b/tests/integration/IntegrationTests.py index a450cb30cd..8c5892d4f0 100644 --- a/tests/integration/IntegrationTests.py +++ b/tests/integration/IntegrationTests.py @@ -28,23 +28,27 @@ def setUpClass(cls): super(IntegrationTests, cls).setUpClass() options = Options() - options.add_argument('--no-sandbox') + options.add_argument("--no-sandbox") capabilities = DesiredCapabilities.CHROME - capabilities['loggingPrefs'] = {'browser': 'SEVERE'} + capabilities["loggingPrefs"] = {"browser": "SEVERE"} - if 'DASH_TEST_CHROMEPATH' in os.environ: - options.binary_location = os.environ['DASH_TEST_CHROMEPATH'] + if "DASH_TEST_CHROMEPATH" in os.environ: + options.binary_location = os.environ["DASH_TEST_CHROMEPATH"] cls.driver = webdriver.Chrome( - options=options, desired_capabilities=capabilities, - service_args=["--verbose", "--log-path=chrome.log"] - ) + options=options, + desired_capabilities=capabilities, + service_args=["--verbose", "--log-path=chrome.log"], + ) cls.percy_runner = percy.Runner( loader=percy.ResourceLoader( webdriver=cls.driver, - base_url='/assets', root_dir='tests/assets')) + base_url="/assets", + root_dir="tests/assets", + ) + ) cls.percy_runner.initialize_build() @@ -72,12 +76,7 @@ def startServer(self, dash, **kwargs): def run(): dash.scripts.config.serve_locally = True dash.css.config.serve_locally = True - kws = dict( - port=8050, - debug=False, - processes=4, - threaded=False - ) + kws = dict(port=8050, debug=False, processes=4, threaded=False) kws.update(kwargs) dash.run_server(**kws) @@ -88,33 +87,33 @@ def run(): # Visit the dash page self.driver.implicitly_wait(2) - self.driver.get('http://localhost:8050') + self.driver.get("http://localhost:8050") - def percy_snapshot(self, name=''): - snapshot_name = '{} - py{}.{}'.format( - name, sys.version_info.major, sys.version_info.minor) - print(snapshot_name) - self.percy_runner.snapshot( - name=snapshot_name + def percy_snapshot(self, name=""): + snapshot_name = "{} - py{}.{}".format( + name, sys.version_info.major, sys.version_info.minor ) + print(snapshot_name) + self.percy_runner.snapshot(name=snapshot_name) def wait_for_element_by_css_selector(self, selector, timeout=TIMEOUT): return WebDriverWait(self.driver, timeout).until( EC.presence_of_element_located((By.CSS_SELECTOR, selector)), - 'Could not find element with selector "{}"'.format(selector) + 'Could not find element with selector "{}"'.format(selector), ) - def wait_for_text_to_equal(self, selector, assertion_text, timeout=TIMEOUT): + def wait_for_text_to_equal( + self, selector, assertion_text, timeout=TIMEOUT + ): el = self.wait_for_element_by_css_selector(selector) WebDriverWait(self.driver, timeout).until( lambda *args: ( - (str(el.text) == assertion_text) or - (str(el.get_attribute('value')) == assertion_text) + (str(el.text) == assertion_text) + or (str(el.get_attribute("value")) == assertion_text) ), "Element '{}' text was supposed to equal '{}' but it didn't".format( - selector, - assertion_text - ) + selector, assertion_text + ), ) def clear_log(self): @@ -140,7 +139,8 @@ def wait_until_get_log(self, timeout=10): cnt += 1 if cnt * poll >= timeout * 1000: raise SeleniumDriverTimeout( - 'cannot get log in {}'.format(timeout)) + "cannot get log in {}".format(timeout) + ) return logs diff --git a/tests/integration/clientside/test_clientside.py b/tests/integration/clientside/test_clientside.py index 83d8057ecf..860fad63cd 100644 --- a/tests/integration/clientside/test_clientside.py +++ b/tests/integration/clientside/test_clientside.py @@ -270,41 +270,45 @@ def test_clsd006_PreventUpdate(dash_duo): [ dcc.Input(id="first", value=1), dcc.Input(id="second", value=1), - dcc.Input(id="third", value=1) + dcc.Input(id="third", value=1), ] ) app.clientside_callback( - ClientsideFunction(namespace="clientside", function_name="add1_prevent_at_11"), + ClientsideFunction( + namespace="clientside", function_name="add1_prevent_at_11" + ), Output("second", "value"), [Input("first", "value")], - [State("second", "value")] + [State("second", "value")], ) app.clientside_callback( - ClientsideFunction(namespace="clientside", function_name="add1_prevent_at_11"), + ClientsideFunction( + namespace="clientside", function_name="add1_prevent_at_11" + ), Output("third", "value"), [Input("second", "value")], - [State("third", "value")] + [State("third", "value")], ) dash_duo.start_server(app) - dash_duo.wait_for_text_to_equal("#first", '1') - dash_duo.wait_for_text_to_equal("#second", '2') - dash_duo.wait_for_text_to_equal("#third", '2') + dash_duo.wait_for_text_to_equal("#first", "1") + dash_duo.wait_for_text_to_equal("#second", "2") + dash_duo.wait_for_text_to_equal("#third", "2") dash_duo.find_element("#first").send_keys("1") - dash_duo.wait_for_text_to_equal("#first", '11') - dash_duo.wait_for_text_to_equal("#second", '2') - dash_duo.wait_for_text_to_equal("#third", '2') + dash_duo.wait_for_text_to_equal("#first", "11") + dash_duo.wait_for_text_to_equal("#second", "2") + dash_duo.wait_for_text_to_equal("#third", "2") dash_duo.find_element("#first").send_keys("1") - dash_duo.wait_for_text_to_equal("#first", '111') - dash_duo.wait_for_text_to_equal("#second", '3') - dash_duo.wait_for_text_to_equal("#third", '3') + dash_duo.wait_for_text_to_equal("#first", "111") + dash_duo.wait_for_text_to_equal("#second", "3") + dash_duo.wait_for_text_to_equal("#third", "3") def test_clsd006_no_update(dash_duo): @@ -314,33 +318,33 @@ def test_clsd006_no_update(dash_duo): [ dcc.Input(id="first", value=1), dcc.Input(id="second", value=1), - dcc.Input(id="third", value=1) + dcc.Input(id="third", value=1), ] ) app.clientside_callback( - ClientsideFunction(namespace="clientside", function_name="add1_no_update_at_11"), - [Output("second", "value"), - Output("third", "value")], + ClientsideFunction( + namespace="clientside", function_name="add1_no_update_at_11" + ), + [Output("second", "value"), Output("third", "value")], [Input("first", "value")], - [State("second", "value"), - State("third", "value")] + [State("second", "value"), State("third", "value")], ) dash_duo.start_server(app) - dash_duo.wait_for_text_to_equal("#first", '1') - dash_duo.wait_for_text_to_equal("#second", '2') - dash_duo.wait_for_text_to_equal("#third", '2') + dash_duo.wait_for_text_to_equal("#first", "1") + dash_duo.wait_for_text_to_equal("#second", "2") + dash_duo.wait_for_text_to_equal("#third", "2") dash_duo.find_element("#first").send_keys("1") - dash_duo.wait_for_text_to_equal("#first", '11') - dash_duo.wait_for_text_to_equal("#second", '2') - dash_duo.wait_for_text_to_equal("#third", '3') + dash_duo.wait_for_text_to_equal("#first", "11") + dash_duo.wait_for_text_to_equal("#second", "2") + dash_duo.wait_for_text_to_equal("#third", "3") dash_duo.find_element("#first").send_keys("1") - dash_duo.wait_for_text_to_equal("#first", '111') - dash_duo.wait_for_text_to_equal("#second", '3') - dash_duo.wait_for_text_to_equal("#third", '4') + dash_duo.wait_for_text_to_equal("#first", "111") + dash_duo.wait_for_text_to_equal("#second", "3") + dash_duo.wait_for_text_to_equal("#third", "4") diff --git a/tests/integration/devtools/test_devtools_error_handling.py b/tests/integration/devtools/test_devtools_error_handling.py index e62fe31077..e27a5e5299 100644 --- a/tests/integration/devtools/test_devtools_error_handling.py +++ b/tests/integration/devtools/test_devtools_error_handling.py @@ -68,19 +68,19 @@ def test_dveh001_python_errors(dash_duo): # the top (first) error is the most recent one - ie from the second click error0 = get_error_html(dash_duo, 0) # user part of the traceback shown by default - assert 'in update_output' in error0 - assert 'Special 2 clicks exception' in error0 - assert 'in bad_sub' not in error0 + assert "in update_output" in error0 + assert "Special 2 clicks exception" in error0 + assert "in bad_sub" not in error0 # dash and flask part of the traceback not included - assert '%% callback invoked %%' not in error0 - assert 'self.wsgi_app' not in error0 + assert "%% callback invoked %%" not in error0 + assert "self.wsgi_app" not in error0 error1 = get_error_html(dash_duo, 1) - assert 'in update_output' in error1 - assert 'in bad_sub' in error1 - assert 'ZeroDivisionError' in error1 - assert '%% callback invoked %%' not in error1 - assert 'self.wsgi_app' not in error1 + assert "in update_output" in error1 + assert "in bad_sub" in error1 + assert "ZeroDivisionError" in error1 + assert "%% callback invoked %%" not in error1 + assert "self.wsgi_app" not in error1 def test_dveh006_long_python_errors(dash_duo): @@ -103,20 +103,20 @@ def test_dveh006_long_python_errors(dash_duo): dash_duo.find_element(".test-devtools-error-toggle").click() error0 = get_error_html(dash_duo, 0) - assert 'in update_output' in error0 - assert 'Special 2 clicks exception' in error0 - assert 'in bad_sub' not in error0 + assert "in update_output" in error0 + assert "Special 2 clicks exception" in error0 + assert "in bad_sub" not in error0 # dash and flask part of the traceback ARE included # since we set dev_tools_prune_errors=False - assert '%% callback invoked %%' in error0 - assert 'self.wsgi_app' in error0 + assert "%% callback invoked %%" in error0 + assert "self.wsgi_app" in error0 error1 = get_error_html(dash_duo, 1) - assert 'in update_output' in error1 - assert 'in bad_sub' in error1 - assert 'ZeroDivisionError' in error1 - assert '%% callback invoked %%' in error1 - assert 'self.wsgi_app' in error1 + assert "in update_output" in error1 + assert "in bad_sub" in error1 + assert "ZeroDivisionError" in error1 + assert "%% callback invoked %%" in error1 + assert "self.wsgi_app" in error1 def test_dveh002_prevent_update_not_in_error_msg(dash_duo): @@ -222,7 +222,9 @@ def update_output(n_clicks): dash_duo.wait_for_element("#button").click() dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "1") - dash_duo.percy_snapshot("devtools - validation creation exception - closed") + dash_duo.percy_snapshot( + "devtools - validation creation exception - closed" + ) dash_duo.find_element(".test-devtools-error-toggle").click() dash_duo.percy_snapshot("devtools - validation creation exception - open") @@ -265,7 +267,9 @@ def update_outputs(n_clicks): dash_duo.find_element("#multi-output").click() dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "1") - dash_duo.percy_snapshot("devtools - multi output python exception - closed") + dash_duo.percy_snapshot( + "devtools - multi output python exception - closed" + ) dash_duo.find_element(".test-devtools-error-toggle").click() dash_duo.percy_snapshot("devtools - multi output python exception - open") diff --git a/tests/integration/devtools/test_props_check.py b/tests/integration/devtools/test_props_check.py index 95d81349ac..d7549959dd 100644 --- a/tests/integration/devtools/test_props_check.py +++ b/tests/integration/devtools/test_props_check.py @@ -56,13 +56,9 @@ "name": "nested object with bad value", "component": DataTable, "props": { - "columns": [{ - "id": "id", - "name": "name", - "format": { - "locale": "asdf" - } - }] + "columns": [ + {"id": "id", "name": "name", "format": {"locale": "asdf"}} + ] }, }, "invalid-shape-3": { @@ -70,13 +66,9 @@ "name": "invalid oneOf within nested object", "component": DataTable, "props": { - "columns": [{ - "id": "id", - "name": "name", - "on_change": { - "action": "asdf" - } - }] + "columns": [ + {"id": "id", "name": "name", "on_change": {"action": "asdf"}} + ] }, }, "invalid-shape-4": { @@ -84,13 +76,9 @@ "name": "invalid key within deeply nested object", "component": DataTable, "props": { - "columns": [{ - "id": "id", - "name": "name", - "on_change": { - "asdf": "asdf" - } - }] + "columns": [ + {"id": "id", "name": "name", "on_change": {"asdf": "asdf"}} + ] }, }, "invalid-shape-5": { @@ -139,13 +127,9 @@ "name": "nested string instead of number/null", "component": DataTable, "props": { - "columns": [{ - "id": "id", - "name": "name", - "format": { - "prefix": "asdf" - } - }] + "columns": [ + {"id": "id", "name": "name", "format": {"prefix": "asdf"}} + ] }, }, "allow-null": { @@ -153,13 +137,9 @@ "name": "nested null", "component": DataTable, "props": { - "columns": [{ - "id": "id", - "name": "name", - "format": { - "prefix": None - } - }] + "columns": [ + {"id": "id", "name": "name", "format": {"prefix": None}} + ] }, }, "allow-null-2": { @@ -203,7 +183,9 @@ "name": "allow exact with optional and required keys", "component": dcc.Dropdown, "props": { - "options": [{"label": "new york", "value": "ny", "disabled": False}] + "options": [ + {"label": "new york", "value": "ny", "disabled": False} + ] }, }, "allow-exact-with-optional-and-required-2": { @@ -218,7 +200,9 @@ def test_dvpc001_prop_check_errors_with_path(dash_duo): app = dash.Dash(__name__) - app.layout = html.Div([html.Div(id="content"), dcc.Location(id="location")]) + app.layout = html.Div( + [html.Div(id="content"), dcc.Location(id="location")] + ) @app.callback( Output("content", "children"), [Input("location", "pathname")] diff --git a/tests/integration/renderer/test_dependencies.py b/tests/integration/renderer/test_dependencies.py index ef62c4406f..808ba6cb1c 100644 --- a/tests/integration/renderer/test_dependencies.py +++ b/tests/integration/renderer/test_dependencies.py @@ -42,6 +42,9 @@ def update_output_2(value): rqs = dash_duo.redux_state_rqs assert len(rqs) == 1 - assert rqs[0]["controllerId"] == "output-1.children" and not rqs[0]['rejected'] + assert ( + rqs[0]["controllerId"] == "output-1.children" + and not rqs[0]["rejected"] + ) assert dash_duo.get_logs() == [] diff --git a/tests/integration/renderer/test_multi_output.py b/tests/integration/renderer/test_multi_output.py index 6a97ccd0b6..52f193a678 100644 --- a/tests/integration/renderer/test_multi_output.py +++ b/tests/integration/renderer/test_multi_output.py @@ -16,13 +16,14 @@ def test_rdmo001_single_input_multi_outputs_on_multiple_components(dash_duo): N_OUTPUTS = 50 - app.layout = html.Div([ - html.Button("click me", id="btn"), - ] + [html.Div(id="output-{}".format(i)) for i in range(N_OUTPUTS)]) + app.layout = html.Div( + [html.Button("click me", id="btn")] + + [html.Div(id="output-{}".format(i)) for i in range(N_OUTPUTS)] + ) @app.callback( [Output("output-{}".format(i), "children") for i in range(N_OUTPUTS)], - [Input("btn", "n_clicks")] + [Input("btn", "n_clicks")], ) def update_output(n_clicks): if n_clicks is None: @@ -40,8 +41,7 @@ def update_output(n_clicks): for i in range(N_OUTPUTS): dash_duo.wait_for_text_to_equal( - "#output-{}".format(i), - "{}={}".format(i, i + click) + "#output-{}".format(i), "{}={}".format(i, i + click) ) assert call_count.value == click @@ -51,29 +51,31 @@ def test_rdmo002_multi_outputs_on_single_component(dash_duo): call_count = Value("i") app = dash.Dash(__name__) - app.layout = html.Div([ - dcc.Input(id="input", value="dash"), - html.Div(html.Div(id="output"), id="output-container"), - ]) + app.layout = html.Div( + [ + dcc.Input(id="input", value="dash"), + html.Div(html.Div(id="output"), id="output-container"), + ] + ) @app.callback( - [Output("output", "children"), - Output("output", "style"), - Output("output", "className")], - [Input("input", "value")] + [ + Output("output", "children"), + Output("output", "style"), + Output("output", "className"), + ], + [Input("input", "value")], ) def update_output(value): call_count.value += 1 - return [ - value, - {"fontFamily": value}, - value - ] + return [value, {"fontFamily": value}, value] dash_duo.start_server(app) dash_duo.wait_for_text_to_equal("#output-container", "dash") - _html = dash_duo.find_element("#output-container").get_property("innerHTML") + _html = dash_duo.find_element("#output-container").get_property( + "innerHTML" + ) assert _html == ( '
dash
' ) @@ -83,7 +85,9 @@ def update_output(value): dash_duo.find_element("#input").send_keys(" hello") dash_duo.wait_for_text_to_equal("#output-container", "dash hello") - _html = dash_duo.find_element("#output-container").get_property("innerHTML") + _html = dash_duo.find_element("#output-container").get_property( + "innerHTML" + ) assert _html == ( '
dash hello
' @@ -95,14 +99,14 @@ def update_output(value): def test_rdmo003_single_output_as_multi(dash_duo): app = dash.Dash(__name__) - app.layout = html.Div([ - dcc.Input(id="input", value=""), - html.Div(html.Div(id="output"), id="output-container"), - ]) + app.layout = html.Div( + [ + dcc.Input(id="input", value=""), + html.Div(html.Div(id="output"), id="output-container"), + ] + ) - @app.callback( - [Output("output", "children")], - [Input("input", "value")]) + @app.callback([Output("output", "children")], [Input("input", "value")]) def update_output(value): return ["out" + value] @@ -114,19 +118,17 @@ def update_output(value): def test_rdmo004_multi_output_circular_dependencies(dash_duo): app = dash.Dash(__name__) - app.layout = html.Div([ - dcc.Input(id="a"), - dcc.Input(id="b"), - html.P(id="c") - ]) + app.layout = html.Div( + [dcc.Input(id="a"), dcc.Input(id="b"), html.P(id="c")] + ) @app.callback(Output("a", "value"), [Input("b", "value")]) def set_a(b): return ((b or "") + "X")[:100] @app.callback( - [Output("b", "value"), Output("c", "children")], - [Input("a", "value")]) + [Output("b", "value"), Output("c", "children")], [Input("a", "value")] + ) def set_bc(a): return [a, a] @@ -135,7 +137,7 @@ def set_bc(a): debug=True, use_debugger=True, use_reloader=False, - dev_tools_hot_reload=False + dev_tools_hot_reload=False, ) # the UI still renders the output triggered by callback @@ -147,18 +149,17 @@ def set_bc(a): def test_rdmo005_set_props_behavior(dash_duo): app = dash.Dash(__name__) - app.layout = html.Div([ - dash_renderer_test_components.UncontrolledInput( - id="id", - value="" - ), - html.Div( - id="container", - children=dash_renderer_test_components.UncontrolledInput( - value="" + app.layout = html.Div( + [ + dash_renderer_test_components.UncontrolledInput(id="id", value=""), + html.Div( + id="container", + children=dash_renderer_test_components.UncontrolledInput( + value="" + ), ), - ) - ]) + ] + ) dash_duo.start_server( app, diff --git a/tests/integration/renderer/test_persistence.py b/tests/integration/renderer/test_persistence.py index 70c62d49cc..32f21279eb 100644 --- a/tests/integration/renderer/test_persistence.py +++ b/tests/integration/renderer/test_persistence.py @@ -17,21 +17,24 @@ def clear_storage(dash_duo): def table_columns(names, **extra_props): - return [dict( - id='c{}'.format(i), - name=n, - renamable=True, - hideable=True, - **extra_props - ) for i, n in enumerate(names)] + return [ + dict( + id="c{}".format(i), + name=n, + renamable=True, + hideable=True, + **extra_props + ) + for i, n in enumerate(names) + ] -def simple_table(names=('a', 'b'), **props_override): +def simple_table(names=("a", "b"), **props_override): props = dict( - id='table', + id="table", columns=table_columns(names), - data=[{'c0': 0, 'c1': 1}, {'c0': 2, 'c1': 3}], - persistence=True + data=[{"c0": 0, "c1": 1}, {"c0": 2, "c1": 3}], + persistence=True, ) props.update(props_override) return dt.DataTable(**props) @@ -39,54 +42,60 @@ def simple_table(names=('a', 'b'), **props_override): def reloadable_app(**props_override): app = dash.Dash(__name__) - app.persistence = Value('i', 1) + app.persistence = Value("i", 1) def layout(): - return html.Div([ - html.Div(id='out'), - simple_table(persistence=app.persistence.value, **props_override) - ]) + return html.Div( + [ + html.Div(id="out"), + simple_table( + persistence=app.persistence.value, **props_override + ), + ] + ) + app.layout = layout @app.callback( - Output('out', 'children'), - [Input('table', 'columns'), Input('table', 'hidden_columns')] + Output("out", "children"), + [Input("table", "columns"), Input("table", "hidden_columns")], ) def report_props(columns, hidden_columns): - return 'names: [{}]; hidden: [{}]'.format( - ', '.join([col['name'] for col in columns]), - ', '.join(hidden_columns or []) + return "names: [{}]; hidden: [{}]".format( + ", ".join([col["name"] for col in columns]), + ", ".join(hidden_columns or []), ) return app -NEW_NAME = 'mango' +NEW_NAME = "mango" def rename_and_hide(dash_duo, rename=0, new_name=NEW_NAME, hide=1): dash_duo.find_element( - '.dash-header.column-{} .column-header--edit'.format(rename) + ".dash-header.column-{} .column-header--edit".format(rename) ).click() prompt = dash_duo.driver.switch_to.alert prompt.send_keys(new_name) prompt.accept() dash_duo.find_element( - '.dash-header.column-{} .column-header--hide'.format(hide) + ".dash-header.column-{} .column-header--hide".format(hide) ).click() -def check_table_names(dash_duo, names, table_id='table'): +def check_table_names(dash_duo, names, table_id="table"): dash_duo.wait_for_text_to_equal( - '#{} .column-0 .column-header-name'.format(table_id), - names[0] + "#{} .column-0 .column-header-name".format(table_id), names[0] + ) + headers = dash_duo.find_elements( + "#{} .column-header-name".format(table_id) ) - headers = dash_duo.find_elements('#{} .column-header-name'.format(table_id)) assert len(headers) == len(names) for i, n in enumerate(names): name_el = dash_duo.find_element( - '#{} .column-{} .column-header-name'.format(table_id, i) + "#{} .column-{} .column-header-name".format(table_id, i) ) assert name_el.text == n @@ -94,372 +103,380 @@ def check_table_names(dash_duo, names, table_id='table'): def test_rdps001_local_reload(dash_duo): app = reloadable_app() dash_duo.start_server(app) - dash_duo.wait_for_text_to_equal('#out', 'names: [a, b]; hidden: []') - check_table_names(dash_duo, ['a', 'b']) + dash_duo.wait_for_text_to_equal("#out", "names: [a, b]; hidden: []") + check_table_names(dash_duo, ["a", "b"]) rename_and_hide(dash_duo) # callback output - dash_duo.wait_for_text_to_equal('#out', 'names: [{}, b]; hidden: [c1]'.format(NEW_NAME)) + dash_duo.wait_for_text_to_equal( + "#out", "names: [{}, b]; hidden: [c1]".format(NEW_NAME) + ) check_table_names(dash_duo, [NEW_NAME]) dash_duo.wait_for_page() # callback gets persisted values, not the values provided with the layout - dash_duo.wait_for_text_to_equal('#out', 'names: [{}, b]; hidden: [c1]'.format(NEW_NAME)) + dash_duo.wait_for_text_to_equal( + "#out", "names: [{}, b]; hidden: [c1]".format(NEW_NAME) + ) check_table_names(dash_duo, [NEW_NAME]) # new persistence reverts app.persistence.value = 2 dash_duo.wait_for_page() - check_table_names(dash_duo, ['a', 'b']) - rename_and_hide(dash_duo, 1, 'two', 0) - dash_duo.wait_for_text_to_equal('#out', 'names: [a, two]; hidden: [c0]') - check_table_names(dash_duo, ['two']) + check_table_names(dash_duo, ["a", "b"]) + rename_and_hide(dash_duo, 1, "two", 0) + dash_duo.wait_for_text_to_equal("#out", "names: [a, two]; hidden: [c0]") + check_table_names(dash_duo, ["two"]) # put back the old persistence, get the old values app.persistence.value = 1 dash_duo.wait_for_page() - dash_duo.wait_for_text_to_equal('#out', 'names: [{}, b]; hidden: [c1]'.format(NEW_NAME)) + dash_duo.wait_for_text_to_equal( + "#out", "names: [{}, b]; hidden: [c1]".format(NEW_NAME) + ) check_table_names(dash_duo, [NEW_NAME]) # falsy persistence disables it app.persistence.value = 0 dash_duo.wait_for_page() - check_table_names(dash_duo, ['a', 'b']) + check_table_names(dash_duo, ["a", "b"]) rename_and_hide(dash_duo) check_table_names(dash_duo, [NEW_NAME]) dash_duo.wait_for_page() - check_table_names(dash_duo, ['a', 'b']) + check_table_names(dash_duo, ["a", "b"]) # falsy to previous truthy also brings the values app.persistence.value = 2 dash_duo.wait_for_page() - dash_duo.wait_for_text_to_equal('#out', 'names: [a, two]; hidden: [c0]') - check_table_names(dash_duo, ['two']) + dash_duo.wait_for_text_to_equal("#out", "names: [a, two]; hidden: [c0]") + check_table_names(dash_duo, ["two"]) def test_rdps002_session_reload(dash_duo): - app = reloadable_app(persistence_type='session') + app = reloadable_app(persistence_type="session") dash_duo.start_server(app) - check_table_names(dash_duo, ['a', 'b']) + check_table_names(dash_duo, ["a", "b"]) rename_and_hide(dash_duo) check_table_names(dash_duo, [NEW_NAME]) dash_duo.wait_for_page() # callback gets persisted values, not the values provided with the layout - dash_duo.wait_for_text_to_equal('#out', 'names: [{}, b]; hidden: [c1]'.format(NEW_NAME)) + dash_duo.wait_for_text_to_equal( + "#out", "names: [{}, b]; hidden: [c1]".format(NEW_NAME) + ) check_table_names(dash_duo, [NEW_NAME]) def test_rdps003_memory_reload(dash_duo): - app = reloadable_app(persistence_type='memory') + app = reloadable_app(persistence_type="memory") dash_duo.start_server(app) - check_table_names(dash_duo, ['a', 'b']) + check_table_names(dash_duo, ["a", "b"]) rename_and_hide(dash_duo) check_table_names(dash_duo, [NEW_NAME]) dash_duo.wait_for_page() # no persistence after reload with persistence_type=memory - check_table_names(dash_duo, ['a', 'b']) + check_table_names(dash_duo, ["a", "b"]) def test_rdps004_show_hide(dash_duo): app = dash.Dash(__name__) - app.layout = html.Div([ - html.Button('Show/Hide', id='toggle-table'), - html.Div(id='container') - ]) + app.layout = html.Div( + [html.Button("Show/Hide", id="toggle-table"), html.Div(id="container")] + ) @app.callback( - Output('container', 'children'), - [Input('toggle-table', 'n_clicks')] + Output("container", "children"), [Input("toggle-table", "n_clicks")] ) def toggle_table(n): if (n or 0) % 2: - return 'nope' + return "nope" return simple_table( - persistence_type='memory', - persistence=1 if (n or 0) < 3 else 2 + persistence_type="memory", persistence=1 if (n or 0) < 3 else 2 ) dash_duo.start_server(app) - check_table_names(dash_duo, ['a', 'b']) + check_table_names(dash_duo, ["a", "b"]) rename_and_hide(dash_duo) check_table_names(dash_duo, [NEW_NAME]) - dash_duo.find_element('#toggle-table').click() + dash_duo.find_element("#toggle-table").click() # table is gone - dash_duo.wait_for_text_to_equal('#container', 'nope') + dash_duo.wait_for_text_to_equal("#container", "nope") - dash_duo.find_element('#toggle-table').click() + dash_duo.find_element("#toggle-table").click() # table is back, with persisted props check_table_names(dash_duo, [NEW_NAME]) - dash_duo.find_element('#toggle-table').click() + dash_duo.find_element("#toggle-table").click() # gone again - dash_duo.wait_for_text_to_equal('#container', 'nope') + dash_duo.wait_for_text_to_equal("#container", "nope") - dash_duo.find_element('#toggle-table').click() + dash_duo.find_element("#toggle-table").click() # table is back, new persistence val so props not persisted - check_table_names(dash_duo, ['a', 'b']) + check_table_names(dash_duo, ["a", "b"]) def test_rdps005_persisted_props(dash_duo): app = dash.Dash(__name__) - app.layout = html.Div([ - html.Button('toggle persisted_props', id='toggle-table'), - html.Div(id='container') - ]) + app.layout = html.Div( + [ + html.Button("toggle persisted_props", id="toggle-table"), + html.Div(id="container"), + ] + ) @app.callback( - Output('container', 'children'), - [Input('toggle-table', 'n_clicks')] + Output("container", "children"), [Input("toggle-table", "n_clicks")] ) def toggle_table(n): if (n or 0) % 2: - return simple_table(persisted_props=['data', 'columns.name']) + return simple_table(persisted_props=["data", "columns.name"]) return simple_table() dash_duo.start_server(app) - check_table_names(dash_duo, ['a', 'b']) + check_table_names(dash_duo, ["a", "b"]) rename_and_hide(dash_duo) check_table_names(dash_duo, [NEW_NAME]) - dash_duo.find_element('#toggle-table').click() + dash_duo.find_element("#toggle-table").click() # hidden_columns not persisted - check_table_names(dash_duo, [NEW_NAME, 'b']) + check_table_names(dash_duo, [NEW_NAME, "b"]) - dash_duo.find_element('#toggle-table').click() + dash_duo.find_element("#toggle-table").click() # back to original persisted_props hidden_columns returns check_table_names(dash_duo, [NEW_NAME]) def test_rdps006_move_on_page(dash_duo): app = dash.Dash(__name__) - app.layout = html.Div([ - html.Button('move table', id='move-table'), - html.Div(id='container') - ]) + app.layout = html.Div( + [html.Button("move table", id="move-table"), html.Div(id="container")] + ) @app.callback( - Output('container', 'children'), - [Input('move-table', 'n_clicks')] + Output("container", "children"), [Input("move-table", "n_clicks")] ) def move_table(n): - children = [html.Div('div 0', id='div0'), simple_table()] + children = [html.Div("div 0", id="div0"), simple_table()] for i in range(1, (n or 0) + 1): children = [ - html.Div('div {}'.format(i), id='div{}'.format(i)), - html.Div(children) + html.Div("div {}".format(i), id="div{}".format(i)), + html.Div(children), ] return children def find_last_div(n): - dash_duo.wait_for_text_to_equal('#div{}'.format(n), 'div {}'.format(n)) - assert len(dash_duo.find_elements('#div{}'.format(n + 1))) == 0 + dash_duo.wait_for_text_to_equal("#div{}".format(n), "div {}".format(n)) + assert len(dash_duo.find_elements("#div{}".format(n + 1))) == 0 dash_duo.start_server(app) find_last_div(0) - check_table_names(dash_duo, ['a', 'b']) + check_table_names(dash_duo, ["a", "b"]) rename_and_hide(dash_duo) check_table_names(dash_duo, [NEW_NAME]) for i in range(1, 5): - dash_duo.find_element('#move-table').click() + dash_duo.find_element("#move-table").click() find_last_div(i) check_table_names(dash_duo, [NEW_NAME]) def test_rdps007_one_prop_changed(dash_duo): app = dash.Dash(__name__) - app.layout = html.Div([ - html.Button('hide/show cols', id='hide-cols'), - html.Div(id='container') - ]) + app.layout = html.Div( + [ + html.Button("hide/show cols", id="hide-cols"), + html.Div(id="container"), + ] + ) @app.callback( - Output('container', 'children'), - [Input('hide-cols', 'n_clicks')] + Output("container", "children"), [Input("hide-cols", "n_clicks")] ) def hide_cols(n): - return simple_table(hidden_columns=['c0'] if (n or 0) % 2 else []) + return simple_table(hidden_columns=["c0"] if (n or 0) % 2 else []) dash_duo.start_server(app) - check_table_names(dash_duo, ['a', 'b']) + check_table_names(dash_duo, ["a", "b"]) rename_and_hide(dash_duo) check_table_names(dash_duo, [NEW_NAME]) - dash_duo.find_element('#hide-cols').click() + dash_duo.find_element("#hide-cols").click() # hidden_columns gets the new value - check_table_names(dash_duo, ['b']) + check_table_names(dash_duo, ["b"]) - dash_duo.find_element('#hide-cols').click() + dash_duo.find_element("#hide-cols").click() # back to original hidden_columns, but saved value won't come back - check_table_names(dash_duo, [NEW_NAME, 'b']) + check_table_names(dash_duo, [NEW_NAME, "b"]) def test_rdps008_unsaved_part_changed(dash_duo): app = dash.Dash(__name__) - app.layout = html.Div([ - html.Button('toggle deletable', id='deletable'), - html.Div(id='container') - ]) + app.layout = html.Div( + [ + html.Button("toggle deletable", id="deletable"), + html.Div(id="container"), + ] + ) @app.callback( - Output('container', 'children'), - [Input('deletable', 'n_clicks')] + Output("container", "children"), [Input("deletable", "n_clicks")] ) def toggle_deletable(n): if (n or 0) % 2: return simple_table( - columns=table_columns(('a', 'b'), deletable=True) + columns=table_columns(("a", "b"), deletable=True) ) return simple_table() dash_duo.start_server(app) - check_table_names(dash_duo, ['a', 'b']) + check_table_names(dash_duo, ["a", "b"]) rename_and_hide(dash_duo) check_table_names(dash_duo, [NEW_NAME]) - assert len(dash_duo.find_elements('.column-header--delete')) == 0 + assert len(dash_duo.find_elements(".column-header--delete")) == 0 - dash_duo.find_element('#deletable').click() + dash_duo.find_element("#deletable").click() # column names still persisted when columns.deletable changed # because extracted name list didn't change check_table_names(dash_duo, [NEW_NAME]) - assert len(dash_duo.find_elements('.column-header--delete')) == 1 + assert len(dash_duo.find_elements(".column-header--delete")) == 1 - dash_duo.find_element('#deletable').click() + dash_duo.find_element("#deletable").click() check_table_names(dash_duo, [NEW_NAME]) - assert len(dash_duo.find_elements('.column-header--delete')) == 0 + assert len(dash_duo.find_elements(".column-header--delete")) == 0 def test_rdps009_clear_prop_callback(dash_duo): app = dash.Dash(__name__) - app.layout = html.Div([ - html.Button('reset name edits', id='reset-names'), - simple_table() - ]) + app.layout = html.Div( + [html.Button("reset name edits", id="reset-names"), simple_table()] + ) @app.callback( - Output('table', 'columns'), - [Input('reset-names', 'n_clicks')] + Output("table", "columns"), [Input("reset-names", "n_clicks")] ) def reset_names(n): # callbacks that return the actual persisted prop, as opposed to # the whole component containing them, always clear persistence, even # if the value is identical to the original. no_update can prevent this. # if we had multiple inputs, would need to check triggered - return table_columns(('a', 'b')) if n else dash.no_update + return table_columns(("a", "b")) if n else dash.no_update dash_duo.start_server(app) - check_table_names(dash_duo, ['a', 'b']) + check_table_names(dash_duo, ["a", "b"]) rename_and_hide(dash_duo) check_table_names(dash_duo, [NEW_NAME]) - dash_duo.find_element('#reset-names').click() + dash_duo.find_element("#reset-names").click() # names are reset, but not hidden_columns - check_table_names(dash_duo, ['a']) + check_table_names(dash_duo, ["a"]) def test_rdps010_toggle_persistence(dash_duo): def make_input(persistence): - return dcc.Input(id='persisted', value='a', persistence=persistence) + return dcc.Input(id="persisted", value="a", persistence=persistence) app = dash.Dash(__name__) - app.layout = html.Div([ - dcc.Input(id='persistence-val', value=''), - html.Div(make_input(''), id='persisted-container'), - html.Div(id='out') - ]) + app.layout = html.Div( + [ + dcc.Input(id="persistence-val", value=""), + html.Div(make_input(""), id="persisted-container"), + html.Div(id="out"), + ] + ) @app.callback( - Output('persisted-container', 'children'), - [Input('persistence-val', 'value')] + Output("persisted-container", "children"), + [Input("persistence-val", "value")], ) def set_persistence(val): return make_input(val) - @app.callback(Output('out', 'children'), [Input('persisted', 'value')]) + @app.callback(Output("out", "children"), [Input("persisted", "value")]) def set_out(val): return val dash_duo.start_server(app) - dash_duo.wait_for_text_to_equal('#out', 'a') - dash_duo.find_element('#persisted').send_keys('lpaca') - dash_duo.wait_for_text_to_equal('#out', 'alpaca') + dash_duo.wait_for_text_to_equal("#out", "a") + dash_duo.find_element("#persisted").send_keys("lpaca") + dash_duo.wait_for_text_to_equal("#out", "alpaca") - dash_duo.find_element('#persistence-val').send_keys('s') - dash_duo.wait_for_text_to_equal('#out', 'a') - dash_duo.find_element('#persisted').send_keys('nchovies') - dash_duo.wait_for_text_to_equal('#out', 'anchovies') + dash_duo.find_element("#persistence-val").send_keys("s") + dash_duo.wait_for_text_to_equal("#out", "a") + dash_duo.find_element("#persisted").send_keys("nchovies") + dash_duo.wait_for_text_to_equal("#out", "anchovies") - dash_duo.find_element('#persistence-val').send_keys('2') - dash_duo.wait_for_text_to_equal('#out', 'a') - dash_duo.find_element('#persisted').send_keys('ardvark') - dash_duo.wait_for_text_to_equal('#out', 'aardvark') + dash_duo.find_element("#persistence-val").send_keys("2") + dash_duo.wait_for_text_to_equal("#out", "a") + dash_duo.find_element("#persisted").send_keys("ardvark") + dash_duo.wait_for_text_to_equal("#out", "aardvark") # alpaca not saved with falsy persistence - dash_duo.clear_input('#persistence-val') - dash_duo.wait_for_text_to_equal('#out', 'a') + dash_duo.clear_input("#persistence-val") + dash_duo.wait_for_text_to_equal("#out", "a") # anchovies and aardvark saved - dash_duo.find_element('#persistence-val').send_keys('s') - dash_duo.wait_for_text_to_equal('#out', 'anchovies') - dash_duo.find_element('#persistence-val').send_keys('2') - dash_duo.wait_for_text_to_equal('#out', 'aardvark') + dash_duo.find_element("#persistence-val").send_keys("s") + dash_duo.wait_for_text_to_equal("#out", "anchovies") + dash_duo.find_element("#persistence-val").send_keys("2") + dash_duo.wait_for_text_to_equal("#out", "aardvark") def test_rdps011_toggle_persistence2(dash_duo): app = dash.Dash(__name__) - app.layout = html.Div([ - dcc.Input(id='persistence-val', value=''), - dcc.Input(id='persisted2', value='a', persistence=''), - html.Div(id='out') - ]) + app.layout = html.Div( + [ + dcc.Input(id="persistence-val", value=""), + dcc.Input(id="persisted2", value="a", persistence=""), + html.Div(id="out"), + ] + ) # this is not a good way to set persistence, as it doesn't allow you to # get the right initial value. Much better is to update the whole component # as we do in the previous test case... but it shouldn't break this way. @app.callback( - Output('persisted2', 'persistence'), - [Input('persistence-val', 'value')] + Output("persisted2", "persistence"), + [Input("persistence-val", "value")], ) def set_persistence(val): return val - @app.callback(Output('out', 'children'), [Input('persisted2', 'value')]) + @app.callback(Output("out", "children"), [Input("persisted2", "value")]) def set_out(val): return val dash_duo.start_server(app) - dash_duo.wait_for_text_to_equal('#out', 'a') + dash_duo.wait_for_text_to_equal("#out", "a") - dash_duo.find_element('#persistence-val').send_keys('s') + dash_duo.find_element("#persistence-val").send_keys("s") time.sleep(0.2) assert not dash_duo.get_logs() - dash_duo.wait_for_text_to_equal('#out', 'a') - dash_duo.find_element('#persisted2').send_keys('pricot') - dash_duo.wait_for_text_to_equal('#out', 'apricot') + dash_duo.wait_for_text_to_equal("#out", "a") + dash_duo.find_element("#persisted2").send_keys("pricot") + dash_duo.wait_for_text_to_equal("#out", "apricot") - dash_duo.find_element('#persistence-val').send_keys('2') - dash_duo.wait_for_text_to_equal('#out', 'a') - dash_duo.find_element('#persisted2').send_keys('rtichoke') - dash_duo.wait_for_text_to_equal('#out', 'artichoke') + dash_duo.find_element("#persistence-val").send_keys("2") + dash_duo.wait_for_text_to_equal("#out", "a") + dash_duo.find_element("#persisted2").send_keys("rtichoke") + dash_duo.wait_for_text_to_equal("#out", "artichoke") # no persistence, still goes back to original value - dash_duo.clear_input('#persistence-val') - dash_duo.wait_for_text_to_equal('#out', 'a') + dash_duo.clear_input("#persistence-val") + dash_duo.wait_for_text_to_equal("#out", "a") # apricot and artichoke saved - dash_duo.find_element('#persistence-val').send_keys('s') - dash_duo.wait_for_text_to_equal('#out', 'apricot') - dash_duo.find_element('#persistence-val').send_keys('2') + dash_duo.find_element("#persistence-val").send_keys("s") + dash_duo.wait_for_text_to_equal("#out", "apricot") + dash_duo.find_element("#persistence-val").send_keys("2") assert not dash_duo.get_logs() - dash_duo.wait_for_text_to_equal('#out', 'artichoke') + dash_duo.wait_for_text_to_equal("#out", "artichoke") diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index 770738a39b..d4fef7e34a 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -713,10 +713,12 @@ def interpolate_index(self, **kwargs): {renderer} - """.format(app_entry=kwargs["app_entry"], - config=kwargs["config"], - scripts=kwargs["scripts"], - renderer=renderer) + """.format( + app_entry=kwargs["app_entry"], + config=kwargs["config"], + scripts=kwargs["scripts"], + renderer=renderer, + ) app = CustomDash() @@ -975,7 +977,8 @@ def test_inin023_wrong_callback_id(): app.layout = html.Div( [ html.Div( - [html.Div(id="inner-div"), dcc.Input(id="inner-input")], id="outer-div" + [html.Div(id="inner-div"), dcc.Input(id="inner-input")], + id="outer-div", ), dcc.Input(id="outer-input"), ], @@ -986,7 +989,9 @@ def test_inin023_wrong_callback_id(): with pytest.raises(NonExistentIdException) as err: - @app.callback(Output("nuh-uh", "children"), [Input("inner-input", "value")]) + @app.callback( + Output("nuh-uh", "children"), [Input("inner-input", "value")] + ) def f(a): return a @@ -996,7 +1001,9 @@ def f(a): with pytest.raises(NonExistentIdException) as err: - @app.callback(Output("inner-div", "children"), [Input("yeah-no", "value")]) + @app.callback( + Output("inner-div", "children"), [Input("yeah-no", "value")] + ) def g(a): return a @@ -1014,6 +1021,8 @@ def g2(a): return [a, a] # the right way - @app.callback(Output("inner-div", "children"), [Input("inner-input", "value")]) + @app.callback( + Output("inner-div", "children"), [Input("inner-input", "value")] + ) def h(a): return a diff --git a/tests/integration/test_race_conditions.py b/tests/integration/test_race_conditions.py index b597068e3c..83379ae33e 100644 --- a/tests/integration/test_race_conditions.py +++ b/tests/integration/test_race_conditions.py @@ -22,15 +22,15 @@ def setUp(self): def create_race_conditions_test(endpoints): def test(self): app = Dash() - app.layout = html.Div([ - html.Div('Hello world', id='output'), - dcc.Input(id='input', value='initial value') - ]) + app.layout = html.Div( + [ + html.Div("Hello world", id="output"), + dcc.Input(id="input", value="initial value"), + ] + ) app.scripts.config.serve_locally = True - @app.callback( - Output('output', 'children'), - [Input('input', 'value')]) + @app.callback(Output("output", "children"), [Input("input", "value")]) def update(value): return value @@ -43,7 +43,7 @@ def element_text(id): try: return self.driver.find_element_by_id(id).text except: - return '' + return "" app.server.before_request(delay) self.startServer(app) @@ -54,11 +54,10 @@ def element_text(id): time.sleep(total_delay + DELAY_TIME) wait_for( - lambda: element_text('output') == 'initial value', + lambda: element_text("output") == "initial value", lambda: '"{}" != "initial value"\nbody text: {}'.format( - element_text('output'), - element_text('react-entry-point') - ) + element_text("output"), element_text("react-entry-point") + ), ) self.assertTrue(self.is_console_clean()) @@ -67,10 +66,10 @@ def element_text(id): routes = [ - 'layout', - 'dependencies', - 'update-component', - '_config' + "layout", + "dependencies", + "update-component", + "_config" # routes and component-suites # are other endpoints but are excluded to speed up tests ] @@ -78,9 +77,8 @@ def element_text(id): for route_list in itertools.permutations(routes, len(routes)): setattr( Tests, - 'test_delayed_{}'.format( - '_'.join([ - r.replace('-', '_') for r in route_list - ])), - create_race_conditions_test(route_list) + "test_delayed_{}".format( + "_".join([r.replace("-", "_") for r in route_list]) + ), + create_race_conditions_test(route_list), ) diff --git a/tests/integration/test_render.py b/tests/integration/test_render.py index ea1915bc7f..38cc3fa5c1 100644 --- a/tests/integration/test_render.py +++ b/tests/integration/test_render.py @@ -29,63 +29,65 @@ def setUp(self): def wait_for_element_by_css_selector(self, selector, timeout=TIMEOUT): return WebDriverWait(self.driver, timeout).until( EC.presence_of_element_located((By.CSS_SELECTOR, selector)), - 'Could not find element with selector "{}"'.format(selector) + 'Could not find element with selector "{}"'.format(selector), ) - def wait_for_text_to_equal(self, selector, assertion_text, timeout=TIMEOUT): + def wait_for_text_to_equal( + self, selector, assertion_text, timeout=TIMEOUT + ): self.wait_for_element_by_css_selector(selector) WebDriverWait(self.driver, timeout).until( lambda *args: ( - (str(self.wait_for_element_by_css_selector(selector).text) - == assertion_text) or - (str(self.wait_for_element_by_css_selector( - selector).get_attribute('value')) == assertion_text) + ( + str(self.wait_for_element_by_css_selector(selector).text) + == assertion_text + ) + or ( + str( + self.wait_for_element_by_css_selector( + selector + ).get_attribute("value") + ) + == assertion_text + ) ), "Element '{}' text expects to equal '{}' but it didn't".format( - selector, - assertion_text - ) + selector, assertion_text + ), ) def request_queue_assertions( - self, check_rejected=True, expected_length=None): + self, check_rejected=True, expected_length=None + ): request_queue = self.driver.execute_script( - 'return window.store.getState().requestQueue' - ) - self.assertTrue( - all([ - (r['status'] == 200) - for r in request_queue - ]) + "return window.store.getState().requestQueue" ) + self.assertTrue(all([(r["status"] == 200) for r in request_queue])) if check_rejected: self.assertTrue( - all([ - (r['rejected'] is False) - for r in request_queue - ]) + all([(r["rejected"] is False) for r in request_queue]) ) if expected_length is not None: self.assertEqual(len(request_queue), expected_length) def click_undo(self): - undo_selector = '._dash-undo-redo span:first-child div:last-child' + undo_selector = "._dash-undo-redo span:first-child div:last-child" undo = self.wait_for_element_by_css_selector(undo_selector) - self.wait_for_text_to_equal(undo_selector, 'undo') + self.wait_for_text_to_equal(undo_selector, "undo") undo.click() def click_redo(self): - redo_selector = '._dash-undo-redo span:last-child div:last-child' - self.wait_for_text_to_equal(redo_selector, 'redo') + redo_selector = "._dash-undo-redo span:last-child div:last-child" + self.wait_for_text_to_equal(redo_selector, "redo") redo = self.wait_for_element_by_css_selector(redo_selector) redo.click() def check_undo_redo_exist(self, has_undo, has_redo): - selector = '._dash-undo-redo span div:last-child' + selector = "._dash-undo-redo span div:last-child" els = self.driver.find_elements_by_css_selector(selector) - texts = (['undo'] if has_undo else []) + (['redo'] if has_redo else []) + texts = (["undo"] if has_undo else []) + (["redo"] if has_redo else []) self.assertEqual(len(els), len(texts)) for el, text in zip(els, texts): @@ -93,326 +95,332 @@ def check_undo_redo_exist(self, has_undo, has_redo): def test_undo_redo(self): app = Dash(__name__, show_undo_redo=True) - app.layout = html.Div([dcc.Input(id='a'), html.Div(id='b')]) + app.layout = html.Div([dcc.Input(id="a"), html.Div(id="b")]) - @app.callback(Output('b', 'children'), [Input('a', 'value')]) + @app.callback(Output("b", "children"), [Input("a", "value")]) def set_b(a): return a self.startServer(app) - a = self.wait_for_element_by_css_selector('#a') - a.send_keys('xyz') + a = self.wait_for_element_by_css_selector("#a") + a.send_keys("xyz") - self.wait_for_text_to_equal('#b', 'xyz') + self.wait_for_text_to_equal("#b", "xyz") self.check_undo_redo_exist(True, False) self.click_undo() - self.wait_for_text_to_equal('#b', 'xy') + self.wait_for_text_to_equal("#b", "xy") self.check_undo_redo_exist(True, True) self.click_undo() - self.wait_for_text_to_equal('#b', 'x') + self.wait_for_text_to_equal("#b", "x") self.check_undo_redo_exist(True, True) self.click_redo() - self.wait_for_text_to_equal('#b', 'xy') + self.wait_for_text_to_equal("#b", "xy") self.check_undo_redo_exist(True, True) - self.percy_snapshot(name='undo-redo') + self.percy_snapshot(name="undo-redo") self.click_undo() self.click_undo() - self.wait_for_text_to_equal('#b', '') + self.wait_for_text_to_equal("#b", "") self.check_undo_redo_exist(False, True) def test_no_undo_redo(self): app = Dash(__name__) - app.layout = html.Div([dcc.Input(id='a'), html.Div(id='b')]) + app.layout = html.Div([dcc.Input(id="a"), html.Div(id="b")]) - @app.callback(Output('b', 'children'), [Input('a', 'value')]) + @app.callback(Output("b", "children"), [Input("a", "value")]) def set_b(a): return a self.startServer(app) - a = self.wait_for_element_by_css_selector('#a') - a.send_keys('xyz') + a = self.wait_for_element_by_css_selector("#a") + a.send_keys("xyz") - self.wait_for_text_to_equal('#b', 'xyz') - toolbar = self.driver.find_elements_by_css_selector('._dash-undo-redo') + self.wait_for_text_to_equal("#b", "xyz") + toolbar = self.driver.find_elements_by_css_selector("._dash-undo-redo") self.assertEqual(len(toolbar), 0) def test_array_of_falsy_child(self): app = Dash(__name__) - app.layout = html.Div(id='nully-wrapper', children=[0]) + app.layout = html.Div(id="nully-wrapper", children=[0]) self.startServer(app) - self.wait_for_text_to_equal('#nully-wrapper', '0') + self.wait_for_text_to_equal("#nully-wrapper", "0") self.assertTrue(self.is_console_clean()) def test_of_falsy_child(self): app = Dash(__name__) - app.layout = html.Div(id='nully-wrapper', children=0) + app.layout = html.Div(id="nully-wrapper", children=0) self.startServer(app) - self.wait_for_text_to_equal('#nully-wrapper', '0') + self.wait_for_text_to_equal("#nully-wrapper", "0") self.assertTrue(self.is_console_clean()) def test_event_properties(self): app = Dash(__name__) - app.layout = html.Div([ - html.Button('Click Me', id='button'), - html.Div(id='output') - ]) + app.layout = html.Div( + [html.Button("Click Me", id="button"), html.Div(id="output")] + ) - call_count = Value('i', 0) + call_count = Value("i", 0) - @app.callback(Output('output', 'children'), - [Input('button', 'n_clicks')]) + @app.callback( + Output("output", "children"), [Input("button", "n_clicks")] + ) def update_output(n_clicks): if not n_clicks: raise PreventUpdate call_count.value += 1 - return 'Click' + return "Click" self.startServer(app) - btn = self.driver.find_element_by_id('button') - output = lambda: self.driver.find_element_by_id('output') + btn = self.driver.find_element_by_id("button") + output = lambda: self.driver.find_element_by_id("output") self.assertEqual(call_count.value, 0) - self.assertEqual(output().text, '') + self.assertEqual(output().text, "") btn.click() - wait_for(lambda: output().text == 'Click') + wait_for(lambda: output().text == "Click") self.assertEqual(call_count.value, 1) def test_chained_dependencies_direct_lineage(self): app = Dash(__name__) - app.layout = html.Div([ - dcc.Input(id='input-1', value='input 1'), - dcc.Input(id='input-2'), - html.Div('test', id='output') - ]) - input1 = lambda: self.driver.find_element_by_id('input-1') - input2 = lambda: self.driver.find_element_by_id('input-2') - output = lambda: self.driver.find_element_by_id('output') + app.layout = html.Div( + [ + dcc.Input(id="input-1", value="input 1"), + dcc.Input(id="input-2"), + html.Div("test", id="output"), + ] + ) + input1 = lambda: self.driver.find_element_by_id("input-1") + input2 = lambda: self.driver.find_element_by_id("input-2") + output = lambda: self.driver.find_element_by_id("output") - call_counts = { - 'output': Value('i', 0), - 'input-2': Value('i', 0) - } + call_counts = {"output": Value("i", 0), "input-2": Value("i", 0)} - @app.callback(Output('input-2', 'value'), [Input('input-1', 'value')]) + @app.callback(Output("input-2", "value"), [Input("input-1", "value")]) def update_input(input1): - call_counts['input-2'].value += 1 - return '<<{}>>'.format(input1) + call_counts["input-2"].value += 1 + return "<<{}>>".format(input1) - @app.callback(Output('output', 'children'), [ - Input('input-1', 'value'), - Input('input-2', 'value') - ]) + @app.callback( + Output("output", "children"), + [Input("input-1", "value"), Input("input-2", "value")], + ) def update_output(input1, input2): - call_counts['output'].value += 1 - return '{} + {}'.format(input1, input2) + call_counts["output"].value += 1 + return "{} + {}".format(input1, input2) self.startServer(app) - wait_for(lambda: call_counts['output'].value == 1) - wait_for(lambda: call_counts['input-2'].value == 1) - self.assertEqual(input1().get_attribute('value'), 'input 1') - self.assertEqual(input2().get_attribute('value'), '<>') - self.assertEqual(output().text, 'input 1 + <>') - - input1().send_keys('x') - wait_for(lambda: call_counts['output'].value == 2) - wait_for(lambda: call_counts['input-2'].value == 2) - self.assertEqual(input1().get_attribute('value'), 'input 1x') - self.assertEqual(input2().get_attribute('value'), '<>') - self.assertEqual(output().text, 'input 1x + <>') - - input2().send_keys('y') - wait_for(lambda: call_counts['output'].value == 3) - wait_for(lambda: call_counts['input-2'].value == 2) - self.assertEqual(input1().get_attribute('value'), 'input 1x') - self.assertEqual(input2().get_attribute('value'), '<>y') - self.assertEqual(output().text, 'input 1x + <>y') + wait_for(lambda: call_counts["output"].value == 1) + wait_for(lambda: call_counts["input-2"].value == 1) + self.assertEqual(input1().get_attribute("value"), "input 1") + self.assertEqual(input2().get_attribute("value"), "<>") + self.assertEqual(output().text, "input 1 + <>") + + input1().send_keys("x") + wait_for(lambda: call_counts["output"].value == 2) + wait_for(lambda: call_counts["input-2"].value == 2) + self.assertEqual(input1().get_attribute("value"), "input 1x") + self.assertEqual(input2().get_attribute("value"), "<>") + self.assertEqual(output().text, "input 1x + <>") + + input2().send_keys("y") + wait_for(lambda: call_counts["output"].value == 3) + wait_for(lambda: call_counts["input-2"].value == 2) + self.assertEqual(input1().get_attribute("value"), "input 1x") + self.assertEqual(input2().get_attribute("value"), "<>y") + self.assertEqual(output().text, "input 1x + <>y") def test_chained_dependencies_branched_lineage(self): app = Dash(__name__) - app.layout = html.Div([ - dcc.Input(id='grandparent', value='input 1'), - dcc.Input(id='parent-a'), - dcc.Input(id='parent-b'), - html.Div(id='child-a'), - html.Div(id='child-b') - ]) - parenta = lambda: self.driver.find_element_by_id('parent-a') - parentb = lambda: self.driver.find_element_by_id('parent-b') - childa = lambda: self.driver.find_element_by_id('child-a') - childb = lambda: self.driver.find_element_by_id('child-b') + app.layout = html.Div( + [ + dcc.Input(id="grandparent", value="input 1"), + dcc.Input(id="parent-a"), + dcc.Input(id="parent-b"), + html.Div(id="child-a"), + html.Div(id="child-b"), + ] + ) + parenta = lambda: self.driver.find_element_by_id("parent-a") + parentb = lambda: self.driver.find_element_by_id("parent-b") + childa = lambda: self.driver.find_element_by_id("child-a") + childb = lambda: self.driver.find_element_by_id("child-b") call_counts = { - 'parent-a': Value('i', 0), - 'parent-b': Value('i', 0), - 'child-a': Value('i', 0), - 'child-b': Value('i', 0) + "parent-a": Value("i", 0), + "parent-b": Value("i", 0), + "child-a": Value("i", 0), + "child-b": Value("i", 0), } - @app.callback(Output('parent-a', 'value'), - [Input('grandparent', 'value')]) + @app.callback( + Output("parent-a", "value"), [Input("grandparent", "value")] + ) def update_parenta(value): - call_counts['parent-a'].value += 1 - return 'a: {}'.format(value) + call_counts["parent-a"].value += 1 + return "a: {}".format(value) - @app.callback(Output('parent-b', 'value'), - [Input('grandparent', 'value')]) + @app.callback( + Output("parent-b", "value"), [Input("grandparent", "value")] + ) def update_parentb(value): time.sleep(0.5) - call_counts['parent-b'].value += 1 - return 'b: {}'.format(value) + call_counts["parent-b"].value += 1 + return "b: {}".format(value) - @app.callback(Output('child-a', 'children'), - [Input('parent-a', 'value'), - Input('parent-b', 'value')]) + @app.callback( + Output("child-a", "children"), + [Input("parent-a", "value"), Input("parent-b", "value")], + ) def update_childa(parenta_value, parentb_value): time.sleep(1) - call_counts['child-a'].value += 1 - return '{} + {}'.format(parenta_value, parentb_value) + call_counts["child-a"].value += 1 + return "{} + {}".format(parenta_value, parentb_value) - @app.callback(Output('child-b', 'children'), - [Input('parent-a', 'value'), - Input('parent-b', 'value'), - Input('grandparent', 'value')]) + @app.callback( + Output("child-b", "children"), + [ + Input("parent-a", "value"), + Input("parent-b", "value"), + Input("grandparent", "value"), + ], + ) def update_childb(parenta_value, parentb_value, grandparent_value): - call_counts['child-b'].value += 1 - return '{} + {} + {}'.format( - parenta_value, - parentb_value, - grandparent_value + call_counts["child-b"].value += 1 + return "{} + {} + {}".format( + parenta_value, parentb_value, grandparent_value ) self.startServer(app) - wait_for(lambda: childa().text == 'a: input 1 + b: input 1') - wait_for(lambda: childb().text == 'a: input 1 + b: input 1 + input 1') + wait_for(lambda: childa().text == "a: input 1 + b: input 1") + wait_for(lambda: childb().text == "a: input 1 + b: input 1 + input 1") time.sleep(1) # wait for potential requests of app to settle down - self.assertEqual(parenta().get_attribute('value'), 'a: input 1') - self.assertEqual(parentb().get_attribute('value'), 'b: input 1') - self.assertEqual(call_counts['parent-a'].value, 1) - self.assertEqual(call_counts['parent-b'].value, 1) - self.assertEqual(call_counts['child-a'].value, 1) - self.assertEqual(call_counts['child-b'].value, 1) + self.assertEqual(parenta().get_attribute("value"), "a: input 1") + self.assertEqual(parentb().get_attribute("value"), "b: input 1") + self.assertEqual(call_counts["parent-a"].value, 1) + self.assertEqual(call_counts["parent-b"].value, 1) + self.assertEqual(call_counts["child-a"].value, 1) + self.assertEqual(call_counts["child-b"].value, 1) def test_removing_component_while_its_getting_updated(self): app = Dash(__name__) - app.layout = html.Div([ - dcc.RadioItems( - id='toc', - options=[ - {'label': i, 'value': i} for i in ['1', '2'] - ], - value='1' - ), - html.Div(id='body') - ]) + app.layout = html.Div( + [ + dcc.RadioItems( + id="toc", + options=[{"label": i, "value": i} for i in ["1", "2"]], + value="1", + ), + html.Div(id="body"), + ] + ) app.config.suppress_callback_exceptions = True - call_counts = { - 'body': Value('i', 0), - 'button-output': Value('i', 0) - } + call_counts = {"body": Value("i", 0), "button-output": Value("i", 0)} - @app.callback(Output('body', 'children'), [Input('toc', 'value')]) + @app.callback(Output("body", "children"), [Input("toc", "value")]) def update_body(chapter): - call_counts['body'].value += 1 - if chapter == '1': + call_counts["body"].value += 1 + if chapter == "1": return [ - html.Div('Chapter 1'), + html.Div("Chapter 1"), html.Button( - 'clicking this button takes forever', - id='button' + "clicking this button takes forever", id="button" ), - html.Div(id='button-output') + html.Div(id="button-output"), ] - elif chapter == '2': - return 'Chapter 2' + elif chapter == "2": + return "Chapter 2" else: - raise Exception('chapter is {}'.format(chapter)) + raise Exception("chapter is {}".format(chapter)) @app.callback( - Output('button-output', 'children'), - [Input('button', 'n_clicks')]) + Output("button-output", "children"), [Input("button", "n_clicks")] + ) def this_callback_takes_forever(n_clicks): if not n_clicks: # initial value is quick, only new value is slow # also don't let the initial value increment call_counts - return 'Initial Value' + return "Initial Value" time.sleep(5) - call_counts['button-output'].value += 1 - return 'New value!' + call_counts["button-output"].value += 1 + return "New value!" - body = lambda: self.driver.find_element_by_id('body') + body = lambda: self.driver.find_element_by_id("body") self.startServer(app) - wait_for(lambda: call_counts['body'].value == 1) + wait_for(lambda: call_counts["body"].value == 1) time.sleep(0.5) - self.driver.find_element_by_id('button').click() + self.driver.find_element_by_id("button").click() # while that callback is resolving, switch the chapter, # hiding the `button-output` tag def chapter2_assertions(): - wait_for(lambda: body().text == 'Chapter 2') + wait_for(lambda: body().text == "Chapter 2") layout = self.driver.execute_script( - 'return JSON.parse(JSON.stringify(' - 'window.store.getState().layout' - '))' + "return JSON.parse(JSON.stringify(" + "window.store.getState().layout" + "))" ) - dcc_radio = layout['props']['children'][0] - html_body = layout['props']['children'][1] + dcc_radio = layout["props"]["children"][0] + html_body = layout["props"]["children"][1] - self.assertEqual(dcc_radio['props']['id'], 'toc') - self.assertEqual(dcc_radio['props']['value'], '2') + self.assertEqual(dcc_radio["props"]["id"], "toc") + self.assertEqual(dcc_radio["props"]["value"], "2") - self.assertEqual(html_body['props']['id'], 'body') - self.assertEqual(html_body['props']['children'], 'Chapter 2') + self.assertEqual(html_body["props"]["id"], "body") + self.assertEqual(html_body["props"]["children"], "Chapter 2") - (self.driver.find_elements_by_css_selector( - 'input[type="radio"]' - )[1]).click() + ( + self.driver.find_elements_by_css_selector('input[type="radio"]')[1] + ).click() chapter2_assertions() - self.assertEqual(call_counts['button-output'].value, 0) + self.assertEqual(call_counts["button-output"].value, 0) time.sleep(5) - wait_for(lambda: call_counts['button-output'].value == 1) + wait_for(lambda: call_counts["button-output"].value == 1) time.sleep(2) # liberally wait for the front-end to process request chapter2_assertions() self.assertTrue(self.is_console_clean()) def test_rendering_layout_calls_callback_once_per_output(self): app = Dash(__name__) - call_count = Value('i', 0) - - app.config['suppress_callback_exceptions'] = True - app.layout = html.Div([ - html.Div([ - dcc.Input( - value='Input {}'.format(i), - id='input-{}'.format(i) - ) - for i in range(10) - ]), - html.Div(id='container'), - dcc.RadioItems() - ]) + call_count = Value("i", 0) + + app.config["suppress_callback_exceptions"] = True + app.layout = html.Div( + [ + html.Div( + [ + dcc.Input( + value="Input {}".format(i), id="input-{}".format(i) + ) + for i in range(10) + ] + ), + html.Div(id="container"), + dcc.RadioItems(), + ] + ) @app.callback( - Output('container', 'children'), - [Input('input-{}'.format(i), 'value') for i in range(10)]) + Output("container", "children"), + [Input("input-{}".format(i), "value") for i in range(10)], + ) def dynamic_output(*args): call_count.value += 1 return json.dumps(args, indent=2) @@ -422,76 +430,87 @@ def dynamic_output(*args): time.sleep(5) self.percy_snapshot( - name='test_rendering_layout_calls_callback_once_per_output' + name="test_rendering_layout_calls_callback_once_per_output" ) self.assertEqual(call_count.value, 1) def test_rendering_new_content_calls_callback_once_per_output(self): app = Dash(__name__) - call_count = Value('i', 0) - - app.config['suppress_callback_exceptions'] = True - app.layout = html.Div([ - html.Button( - id='display-content', - children='Display Content', - n_clicks=0 - ), - html.Div(id='container'), - dcc.RadioItems() - ]) + call_count = Value("i", 0) + + app.config["suppress_callback_exceptions"] = True + app.layout = html.Div( + [ + html.Button( + id="display-content", + children="Display Content", + n_clicks=0, + ), + html.Div(id="container"), + dcc.RadioItems(), + ] + ) @app.callback( - Output('container', 'children'), - [Input('display-content', 'n_clicks')]) + Output("container", "children"), + [Input("display-content", "n_clicks")], + ) def display_output(n_clicks): if n_clicks == 0: - return '' - return html.Div([ - html.Div([ - dcc.Input( - value='Input {}'.format(i), - id='input-{}'.format(i) - ) - for i in range(10) - ]), - html.Div(id='dynamic-output') - ]) + return "" + return html.Div( + [ + html.Div( + [ + dcc.Input( + value="Input {}".format(i), + id="input-{}".format(i), + ) + for i in range(10) + ] + ), + html.Div(id="dynamic-output"), + ] + ) @app.callback( - Output('dynamic-output', 'children'), - [Input('input-{}'.format(i), 'value') for i in range(10)]) + Output("dynamic-output", "children"), + [Input("input-{}".format(i), "value") for i in range(10)], + ) def dynamic_output(*args): call_count.value += 1 return json.dumps(args, indent=2) self.startServer(app) - self.wait_for_element_by_css_selector('#display-content').click() + self.wait_for_element_by_css_selector("#display-content").click() time.sleep(5) self.percy_snapshot( - name='test_rendering_new_content_calls_callback_once_per_output' + name="test_rendering_new_content_calls_callback_once_per_output" ) self.assertEqual(call_count.value, 1) - def test_callbacks_called_multiple_times_and_out_of_order_multi_output(self): + def test_callbacks_called_multiple_times_and_out_of_order_multi_output( + self + ): app = Dash(__name__) - app.layout = html.Div([ - html.Button(id='input', n_clicks=0), - html.Div(id='output1'), - html.Div(id='output2') - ]) + app.layout = html.Div( + [ + html.Button(id="input", n_clicks=0), + html.Div(id="output1"), + html.Div(id="output2"), + ] + ) - call_count = Value('i', 0) + call_count = Value("i", 0) @app.callback( - [Output('output1', 'children'), - Output('output2', 'children')], - [Input('input', 'n_clicks')] + [Output("output1", "children"), Output("output2", "children")], + [Input("input", "n_clicks")], ) def update_output(n_clicks): call_count.value = call_count.value + 1 @@ -500,318 +519,327 @@ def update_output(n_clicks): return n_clicks, n_clicks + 1 self.startServer(app) - button = self.wait_for_element_by_css_selector('#input') + button = self.wait_for_element_by_css_selector("#input") button.click() button.click() time.sleep(8) self.percy_snapshot( - name='test_callbacks_called_multiple_times' - '_and_out_of_order_multi_output' + name="test_callbacks_called_multiple_times" + "_and_out_of_order_multi_output" ) self.assertEqual(call_count.value, 3) - self.wait_for_text_to_equal('#output1', '2') - self.wait_for_text_to_equal('#output2', '3') + self.wait_for_text_to_equal("#output1", "2") + self.wait_for_text_to_equal("#output2", "3") request_queue = self.driver.execute_script( - 'return window.store.getState().requestQueue' + "return window.store.getState().requestQueue" ) - self.assertFalse(request_queue[0]['rejected']) + self.assertFalse(request_queue[0]["rejected"]) self.assertEqual(len(request_queue), 1) def test_callbacks_with_shared_grandparent(self): app = dash.Dash() - app.layout = html.Div([ - html.Div(id='session-id', children='id'), - dcc.Dropdown(id='dropdown-1'), - dcc.Dropdown(id='dropdown-2'), - ]) + app.layout = html.Div( + [ + html.Div(id="session-id", children="id"), + dcc.Dropdown(id="dropdown-1"), + dcc.Dropdown(id="dropdown-2"), + ] + ) - options = [{'value': 'a', 'label': 'a'}] + options = [{"value": "a", "label": "a"}] call_counts = { - 'dropdown_1': Value('i', 0), - 'dropdown_2': Value('i', 0) + "dropdown_1": Value("i", 0), + "dropdown_2": Value("i", 0), } @app.callback( - Output('dropdown-1', 'options'), - [Input('dropdown-1', 'value'), - Input('session-id', 'children')]) + Output("dropdown-1", "options"), + [Input("dropdown-1", "value"), Input("session-id", "children")], + ) def dropdown_1(value, session_id): - call_counts['dropdown_1'].value += 1 + call_counts["dropdown_1"].value += 1 return options @app.callback( - Output('dropdown-2', 'options'), - [Input('dropdown-2', 'value'), - Input('session-id', 'children')]) + Output("dropdown-2", "options"), + [Input("dropdown-2", "value"), Input("session-id", "children")], + ) def dropdown_2(value, session_id): - call_counts['dropdown_2'].value += 1 + call_counts["dropdown_2"].value += 1 return options self.startServer(app) - self.wait_for_element_by_css_selector('#session-id') + self.wait_for_element_by_css_selector("#session-id") time.sleep(2) - self.assertEqual(call_counts['dropdown_1'].value, 1) - self.assertEqual(call_counts['dropdown_2'].value, 1) + self.assertEqual(call_counts["dropdown_1"].value, 1) + self.assertEqual(call_counts["dropdown_2"].value, 1) self.assertTrue(self.is_console_clean()) def test_callbacks_triggered_on_generated_output(self): app = dash.Dash() - app.config['suppress_callback_exceptions'] = True - - call_counts = { - 'tab1': Value('i', 0), - 'tab2': Value('i', 0) - } - - app.layout = html.Div([ - dcc.Dropdown( - id='outer-controls', - options=[{'label': i, 'value': i} for i in ['a', 'b']], - value='a' - ), - dcc.RadioItems( - options=[ - {'label': 'Tab 1', 'value': 1}, - {'label': 'Tab 2', 'value': 2} - ], - value=1, - id='tabs', - ), - html.Div(id='tab-output') - ]) + app.config["suppress_callback_exceptions"] = True + + call_counts = {"tab1": Value("i", 0), "tab2": Value("i", 0)} + + app.layout = html.Div( + [ + dcc.Dropdown( + id="outer-controls", + options=[{"label": i, "value": i} for i in ["a", "b"]], + value="a", + ), + dcc.RadioItems( + options=[ + {"label": "Tab 1", "value": 1}, + {"label": "Tab 2", "value": 2}, + ], + value=1, + id="tabs", + ), + html.Div(id="tab-output"), + ] + ) - @app.callback(Output('tab-output', 'children'), - [Input('tabs', 'value')]) + @app.callback( + Output("tab-output", "children"), [Input("tabs", "value")] + ) def display_content(value): - return html.Div([ - html.Div(id='tab-{}-output'.format(value)) - ]) + return html.Div([html.Div(id="tab-{}-output".format(value))]) - @app.callback(Output('tab-1-output', 'children'), - [Input('outer-controls', 'value')]) + @app.callback( + Output("tab-1-output", "children"), + [Input("outer-controls", "value")], + ) def display_tab1_output(value): - call_counts['tab1'].value += 1 + call_counts["tab1"].value += 1 return 'Selected "{}" in tab 1'.format(value) - @app.callback(Output('tab-2-output', 'children'), - [Input('outer-controls', 'value')]) + @app.callback( + Output("tab-2-output", "children"), + [Input("outer-controls", "value")], + ) def display_tab2_output(value): - call_counts['tab2'].value += 1 + call_counts["tab2"].value += 1 return 'Selected "{}" in tab 2'.format(value) self.startServer(app) - self.wait_for_element_by_css_selector('#tab-output') + self.wait_for_element_by_css_selector("#tab-output") time.sleep(2) - self.assertEqual(call_counts['tab1'].value, 1) - self.assertEqual(call_counts['tab2'].value, 0) - self.wait_for_text_to_equal('#tab-output', 'Selected "a" in tab 1') - self.wait_for_text_to_equal('#tab-1-output', 'Selected "a" in tab 1') + self.assertEqual(call_counts["tab1"].value, 1) + self.assertEqual(call_counts["tab2"].value, 0) + self.wait_for_text_to_equal("#tab-output", 'Selected "a" in tab 1') + self.wait_for_text_to_equal("#tab-1-output", 'Selected "a" in tab 1') - (self.driver.find_elements_by_css_selector( - 'input[type="radio"]' - )[1]).click() + ( + self.driver.find_elements_by_css_selector('input[type="radio"]')[1] + ).click() time.sleep(2) - self.wait_for_text_to_equal('#tab-output', 'Selected "a" in tab 2') - self.wait_for_text_to_equal('#tab-2-output', 'Selected "a" in tab 2') - self.assertEqual(call_counts['tab1'].value, 1) - self.assertEqual(call_counts['tab2'].value, 1) + self.wait_for_text_to_equal("#tab-output", 'Selected "a" in tab 2') + self.wait_for_text_to_equal("#tab-2-output", 'Selected "a" in tab 2') + self.assertEqual(call_counts["tab1"].value, 1) + self.assertEqual(call_counts["tab2"].value, 1) self.assertTrue(self.is_console_clean()) def test_initialization_with_overlapping_outputs(self): app = dash.Dash() - app.layout = html.Div([ - - html.Div(id='input-1', children='input-1'), - html.Div(id='input-2', children='input-2'), - html.Div(id='input-3', children='input-3'), - html.Div(id='input-4', children='input-4'), - html.Div(id='input-5', children='input-5'), - - html.Div(id='output-1'), - html.Div(id='output-2'), - html.Div(id='output-3'), - html.Div(id='output-4'), - - ]) + app.layout = html.Div( + [ + html.Div(id="input-1", children="input-1"), + html.Div(id="input-2", children="input-2"), + html.Div(id="input-3", children="input-3"), + html.Div(id="input-4", children="input-4"), + html.Div(id="input-5", children="input-5"), + html.Div(id="output-1"), + html.Div(id="output-2"), + html.Div(id="output-3"), + html.Div(id="output-4"), + ] + ) call_counts = { - 'output-1': Value('i', 0), - 'output-2': Value('i', 0), - 'output-3': Value('i', 0), - 'output-4': Value('i', 0), + "output-1": Value("i", 0), + "output-2": Value("i", 0), + "output-3": Value("i", 0), + "output-4": Value("i", 0), } def generate_callback(outputid): def callback(*args): call_counts[outputid].value += 1 - return '{}, {}'.format(*args) + return "{}, {}".format(*args) + return callback for i in range(1, 5): - outputid = 'output-{}'.format(i) + outputid = "output-{}".format(i) app.callback( - Output(outputid, 'children'), + Output(outputid, "children"), [ - Input('input-{}'.format(i), 'children'), - Input('input-{}'.format(i + 1), 'children') - ] + Input("input-{}".format(i), "children"), + Input("input-{}".format(i + 1), "children"), + ], )(generate_callback(outputid)) self.startServer(app) - self.wait_for_element_by_css_selector('#output-1') + self.wait_for_element_by_css_selector("#output-1") time.sleep(5) for i in range(1, 5): - outputid = 'output-{}'.format(i) + outputid = "output-{}".format(i) self.assertEqual(call_counts[outputid].value, 1) self.wait_for_text_to_equal( - '#{}'.format(outputid), - "input-{}, input-{}".format(i, i + 1) + "#{}".format(outputid), "input-{}, input-{}".format(i, i + 1) ) def test_generate_overlapping_outputs(self): app = dash.Dash() - app.config['suppress_callback_exceptions'] = True - block = html.Div([ - - html.Div(id='input-1', children='input-1'), - html.Div(id='input-2', children='input-2'), - html.Div(id='input-3', children='input-3'), - html.Div(id='input-4', children='input-4'), - html.Div(id='input-5', children='input-5'), - - html.Div(id='output-1'), - html.Div(id='output-2'), - html.Div(id='output-3'), - html.Div(id='output-4'), - - ]) - app.layout = html.Div([ - html.Div(id='input'), - html.Div(id='container') - ]) + app.config["suppress_callback_exceptions"] = True + block = html.Div( + [ + html.Div(id="input-1", children="input-1"), + html.Div(id="input-2", children="input-2"), + html.Div(id="input-3", children="input-3"), + html.Div(id="input-4", children="input-4"), + html.Div(id="input-5", children="input-5"), + html.Div(id="output-1"), + html.Div(id="output-2"), + html.Div(id="output-3"), + html.Div(id="output-4"), + ] + ) + app.layout = html.Div([html.Div(id="input"), html.Div(id="container")]) call_counts = { - 'container': Value('i', 0), - 'output-1': Value('i', 0), - 'output-2': Value('i', 0), - 'output-3': Value('i', 0), - 'output-4': Value('i', 0), + "container": Value("i", 0), + "output-1": Value("i", 0), + "output-2": Value("i", 0), + "output-3": Value("i", 0), + "output-4": Value("i", 0), } - @app.callback(Output('container', 'children'), - [Input('input', 'children')]) + @app.callback( + Output("container", "children"), [Input("input", "children")] + ) def display_output(*args): - call_counts['container'].value += 1 + call_counts["container"].value += 1 return block def generate_callback(outputid): def callback(*args): call_counts[outputid].value += 1 - return '{}, {}'.format(*args) + return "{}, {}".format(*args) + return callback for i in range(1, 5): - outputid = 'output-{}'.format(i) + outputid = "output-{}".format(i) app.callback( - Output(outputid, 'children'), - [Input('input-{}'.format(i), 'children'), - Input('input-{}'.format(i + 1), 'children')] + Output(outputid, "children"), + [ + Input("input-{}".format(i), "children"), + Input("input-{}".format(i + 1), "children"), + ], )(generate_callback(outputid)) self.startServer(app) - wait_for(lambda: call_counts['container'].value == 1) - self.wait_for_element_by_css_selector('#output-1') + wait_for(lambda: call_counts["container"].value == 1) + self.wait_for_element_by_css_selector("#output-1") time.sleep(5) for i in range(1, 5): - outputid = 'output-{}'.format(i) + outputid = "output-{}".format(i) self.assertEqual(call_counts[outputid].value, 1) self.wait_for_text_to_equal( - '#{}'.format(outputid), - "input-{}, input-{}".format(i, i + 1) + "#{}".format(outputid), "input-{}, input-{}".format(i, i + 1) ) - self.assertEqual(call_counts['container'].value, 1) + self.assertEqual(call_counts["container"].value, 1) def test_multiple_properties_update_at_same_time_on_same_component(self): - call_count = Value('i', 0) - timestamp_1 = Value('d', -5) - timestamp_2 = Value('d', -5) + call_count = Value("i", 0) + timestamp_1 = Value("d", -5) + timestamp_2 = Value("d", -5) app = dash.Dash() - app.layout = html.Div([ - html.Div(id='container'), - html.Button('Click', id='button-1', n_clicks=0, n_clicks_timestamp=-1), - html.Button('Click', id='button-2', n_clicks=0, n_clicks_timestamp=-1) - ]) + app.layout = html.Div( + [ + html.Div(id="container"), + html.Button( + "Click", id="button-1", n_clicks=0, n_clicks_timestamp=-1 + ), + html.Button( + "Click", id="button-2", n_clicks=0, n_clicks_timestamp=-1 + ), + ] + ) @app.callback( - Output('container', 'children'), - [Input('button-1', 'n_clicks'), - Input('button-1', 'n_clicks_timestamp'), - Input('button-2', 'n_clicks'), - Input('button-2', 'n_clicks_timestamp')]) + Output("container", "children"), + [ + Input("button-1", "n_clicks"), + Input("button-1", "n_clicks_timestamp"), + Input("button-2", "n_clicks"), + Input("button-2", "n_clicks_timestamp"), + ], + ) def update_output(*args): call_count.value += 1 timestamp_1.value = args[1] timestamp_2.value = args[3] - return '{}, {}'.format(args[0], args[2]) + return "{}, {}".format(args[0], args[2]) self.startServer(app) - self.wait_for_element_by_css_selector('#container') + self.wait_for_element_by_css_selector("#container") time.sleep(2) - self.wait_for_text_to_equal('#container', '0, 0') + self.wait_for_text_to_equal("#container", "0, 0") self.assertEqual(timestamp_1.value, -1) self.assertEqual(timestamp_2.value, -1) self.assertEqual(call_count.value, 1) - self.percy_snapshot('button initialization 1') + self.percy_snapshot("button initialization 1") - self.driver.find_element_by_css_selector('#button-1').click() + self.driver.find_element_by_css_selector("#button-1").click() time.sleep(2) - self.wait_for_text_to_equal('#container', '1, 0') + self.wait_for_text_to_equal("#container", "1, 0") self.assertTrue( - timestamp_1.value > - ((time.time() - (24 * 60 * 60)) * 1000)) + timestamp_1.value > ((time.time() - (24 * 60 * 60)) * 1000) + ) self.assertEqual(timestamp_2.value, -1) self.assertEqual(call_count.value, 2) - self.percy_snapshot('button-1 click') + self.percy_snapshot("button-1 click") prev_timestamp_1 = timestamp_1.value - self.driver.find_element_by_css_selector('#button-2').click() + self.driver.find_element_by_css_selector("#button-2").click() time.sleep(2) - self.wait_for_text_to_equal('#container', '1, 1') + self.wait_for_text_to_equal("#container", "1, 1") self.assertEqual(timestamp_1.value, prev_timestamp_1) self.assertTrue( - timestamp_2.value > - ((time.time() - 24 * 60 * 60) * 1000)) + timestamp_2.value > ((time.time() - 24 * 60 * 60) * 1000) + ) self.assertEqual(call_count.value, 3) - self.percy_snapshot('button-2 click') + self.percy_snapshot("button-2 click") prev_timestamp_2 = timestamp_2.value - self.driver.find_element_by_css_selector('#button-2').click() + self.driver.find_element_by_css_selector("#button-2").click() time.sleep(2) - self.wait_for_text_to_equal('#container', '1, 2') + self.wait_for_text_to_equal("#container", "1, 2") self.assertEqual(timestamp_1.value, prev_timestamp_1) - self.assertTrue( - timestamp_2.value > - prev_timestamp_2) + self.assertTrue(timestamp_2.value > prev_timestamp_2) self.assertTrue(timestamp_2.value > timestamp_1.value) self.assertEqual(call_count.value, 4) - self.percy_snapshot('button-2 click again') + self.percy_snapshot("button-2 click again") def test_request_hooks(self): app = Dash(__name__) - app.index_string = ''' + app.index_string = """ {%metas%} @@ -856,105 +884,140 @@ def test_request_hooks(self):
With request hooks
- ''' - - app.layout = html.Div([ - dcc.Input( - id='input', - value='initial value' - ), - html.Div( - html.Div([ - html.Div(id='output-1'), - html.Div(id='output-pre'), - html.Div(id='output-pre-payload'), - html.Div(id='output-post'), - html.Div(id='output-post-payload'), - html.Div(id='output-post-response') - ]) - ) - ]) + """ + + app.layout = html.Div( + [ + dcc.Input(id="input", value="initial value"), + html.Div( + html.Div( + [ + html.Div(id="output-1"), + html.Div(id="output-pre"), + html.Div(id="output-pre-payload"), + html.Div(id="output-post"), + html.Div(id="output-post-payload"), + html.Div(id="output-post-response"), + ] + ) + ), + ] + ) - @app.callback(Output('output-1', 'children'), [Input('input', 'value')]) + @app.callback( + Output("output-1", "children"), [Input("input", "value")] + ) def update_output(value): return value self.startServer(app) - input1 = self.wait_for_element_by_css_selector('#input') - initialValue = input1.get_attribute('value') + input1 = self.wait_for_element_by_css_selector("#input") + initialValue = input1.get_attribute("value") action = ActionChains(self.driver) action.click(input1) action = action.send_keys(Keys.BACKSPACE * len(initialValue)) - action.send_keys('fire request hooks').perform() + action.send_keys("fire request hooks").perform() - self.wait_for_text_to_equal('#output-1', 'fire request hooks') - self.wait_for_text_to_equal('#output-pre', 'request_pre changed this text!') - self.wait_for_text_to_equal('#output-pre-payload', '{"output":"output-1.children","changedPropIds":["input.value"],"inputs":[{"id":"input","property":"value","value":"fire request hooks"}]}') - self.wait_for_text_to_equal('#output-post', 'request_post changed this text!') - self.wait_for_text_to_equal('#output-post-payload', '{"output":"output-1.children","changedPropIds":["input.value"],"inputs":[{"id":"input","property":"value","value":"fire request hooks"}]}') - self.wait_for_text_to_equal('#output-post-response', '{"props":{"children":"fire request hooks"}}') - self.percy_snapshot(name='request-hooks render') + self.wait_for_text_to_equal("#output-1", "fire request hooks") + self.wait_for_text_to_equal( + "#output-pre", "request_pre changed this text!" + ) + self.wait_for_text_to_equal( + "#output-pre-payload", + '{"output":"output-1.children","changedPropIds":["input.value"],"inputs":[{"id":"input","property":"value","value":"fire request hooks"}]}', + ) + self.wait_for_text_to_equal( + "#output-post", "request_post changed this text!" + ) + self.wait_for_text_to_equal( + "#output-post-payload", + '{"output":"output-1.children","changedPropIds":["input.value"],"inputs":[{"id":"input","property":"value","value":"fire request hooks"}]}', + ) + self.wait_for_text_to_equal( + "#output-post-response", + '{"props":{"children":"fire request hooks"}}', + ) + self.percy_snapshot(name="request-hooks render") def test_graphs_in_tabs_do_not_share_state(self): app = dash.Dash() app.config.suppress_callback_exceptions = True - app.layout = html.Div([ - dcc.Tabs( - id="tabs", - children=[ - dcc.Tab(label="Tab 1", value="tab1", id="tab1"), - dcc.Tab(label="Tab 2", value="tab2", id="tab2"), - ], - value="tab1", - ), - - # Tab content - html.Div(id="tab_content"), - ]) + app.layout = html.Div( + [ + dcc.Tabs( + id="tabs", + children=[ + dcc.Tab(label="Tab 1", value="tab1", id="tab1"), + dcc.Tab(label="Tab 2", value="tab2", id="tab2"), + ], + value="tab1", + ), + # Tab content + html.Div(id="tab_content"), + ] + ) tab1_layout = [ - html.Div([dcc.Graph(id='graph1', - figure={ - 'data': [{ - 'x': [1, 2, 3], - 'y': [5, 10, 6], - 'type': 'bar' - }] - })]), - - html.Pre(id='graph1_info'), + html.Div( + [ + dcc.Graph( + id="graph1", + figure={ + "data": [ + { + "x": [1, 2, 3], + "y": [5, 10, 6], + "type": "bar", + } + ] + }, + ) + ] + ), + html.Pre(id="graph1_info"), ] tab2_layout = [ - html.Div([dcc.Graph(id='graph2', - figure={ - 'data': [{ - 'x': [4, 3, 2], - 'y': [5, 10, 6], - 'type': 'bar' - }] - })]), - - html.Pre(id='graph2_info'), + html.Div( + [ + dcc.Graph( + id="graph2", + figure={ + "data": [ + { + "x": [4, 3, 2], + "y": [5, 10, 6], + "type": "bar", + } + ] + }, + ) + ] + ), + html.Pre(id="graph2_info"), ] @app.callback( - Output(component_id='graph1_info', component_property='children'), - [Input(component_id='graph1', component_property='clickData')]) + Output(component_id="graph1_info", component_property="children"), + [Input(component_id="graph1", component_property="clickData")], + ) def display_hover_data(hover_data): return json.dumps(hover_data) @app.callback( - Output(component_id='graph2_info', component_property='children'), - [Input(component_id='graph2', component_property='clickData')]) + Output(component_id="graph2_info", component_property="children"), + [Input(component_id="graph2", component_property="clickData")], + ) def display_hover_data_2(hover_data): return json.dumps(hover_data) - @app.callback(Output("tab_content", "children"), [Input("tabs", "value")]) + @app.callback( + Output("tab_content", "children"), [Input("tabs", "value")] + ) def render_content(tab): if tab == "tab1": return tab1_layout @@ -965,30 +1028,44 @@ def render_content(tab): self.startServer(app) - self.wait_for_element_by_css_selector('#graph1') + self.wait_for_element_by_css_selector("#graph1") - self.driver.find_elements_by_css_selector( - '#graph1' - )[0].click() + self.driver.find_elements_by_css_selector("#graph1")[0].click() graph_1_expected_clickdata = { - "points": [{"curveNumber": 0, "pointNumber": 1, "pointIndex": 1, "x": 2, "y": 10}] + "points": [ + { + "curveNumber": 0, + "pointNumber": 1, + "pointIndex": 1, + "x": 2, + "y": 10, + } + ] } graph_2_expected_clickdata = { - "points": [{"curveNumber": 0, "pointNumber": 1, "pointIndex": 1, "x": 3, "y": 10}] + "points": [ + { + "curveNumber": 0, + "pointNumber": 1, + "pointIndex": 1, + "x": 3, + "y": 10, + } + ] } - self.wait_for_text_to_equal('#graph1_info', json.dumps(graph_1_expected_clickdata)) + self.wait_for_text_to_equal( + "#graph1_info", json.dumps(graph_1_expected_clickdata) + ) - self.driver.find_elements_by_css_selector( - '#tab2' - )[0].click() + self.driver.find_elements_by_css_selector("#tab2")[0].click() - self.wait_for_element_by_css_selector('#graph2') + self.wait_for_element_by_css_selector("#graph2") - self.driver.find_elements_by_css_selector( - '#graph2' - )[0].click() + self.driver.find_elements_by_css_selector("#graph2")[0].click() - self.wait_for_text_to_equal('#graph2_info', json.dumps(graph_2_expected_clickdata)) + self.wait_for_text_to_equal( + "#graph2_info", json.dumps(graph_2_expected_clickdata) + ) diff --git a/tests/integration/test_scripts.py b/tests/integration/test_scripts.py index 6f86afc1d4..a14d620d96 100644 --- a/tests/integration/test_scripts.py +++ b/tests/integration/test_scripts.py @@ -30,14 +30,14 @@ def findSyncPlotlyJs(scripts): for script in scripts: - if "dash_core_components/plotly-" in script.get_attribute('src'): + if "dash_core_components/plotly-" in script.get_attribute("src"): return script def findAsyncPlotlyJs(scripts): for script in scripts: if "dash_core_components/async~plotlyjs" in script.get_attribute( - 'src' + "src" ): return script diff --git a/tests/integration/utils.py b/tests/integration/utils.py index 099801278b..43dc61974f 100644 --- a/tests/integration/utils.py +++ b/tests/integration/utils.py @@ -10,6 +10,7 @@ def wrap(): return func() except: pass + return wrap @@ -17,7 +18,7 @@ class WaitForTimeout(Exception): """This should only be raised inside the `wait_for` function.""" -def wait_for(condition_function, get_message=lambda: '', *args, **kwargs): +def wait_for(condition_function, get_message=lambda: "", *args, **kwargs): """Waits for condition_function to return True or raises WaitForTimeout. :param (function) condition_function: Should return True on success. @@ -36,6 +37,7 @@ def get_element(selector): self.fail('element never appeared...') plot = get_element(selector) # we know it exists. """ + def wrapped_condition_function(): """We wrap this to alter the call base on the closure.""" if args and kwargs: @@ -46,9 +48,9 @@ def wrapped_condition_function(): return condition_function(**kwargs) return condition_function() - if 'timeout' in kwargs: - timeout = kwargs['timeout'] - del kwargs['timeout'] + if "timeout" in kwargs: + timeout = kwargs["timeout"] + del kwargs["timeout"] else: timeout = TIMEOUT diff --git a/tests/unit/dash/test_async_resources.py b/tests/unit/dash/test_async_resources.py index f42fe136fd..eee4d605d2 100644 --- a/tests/unit/dash/test_async_resources.py +++ b/tests/unit/dash/test_async_resources.py @@ -28,7 +28,9 @@ def test_resources_eager(): filtered[1].get("dynamic") is True ) # exclude (lazy when eager -> closest to exclude) assert filtered[2].get("external_url") == "c.js" - assert filtered[2].get("dynamic") is False # include (always matches settings) + assert ( + filtered[2].get("dynamic") is False + ) # include (always matches settings) def test_resources_lazy(): @@ -51,4 +53,6 @@ def test_resources_lazy(): assert filtered[1].get("external_url") == "b.js" assert filtered[1].get("dynamic") is True # exclude (lazy when lazy) assert filtered[2].get("external_url") == "c.js" - assert filtered[2].get("dynamic") is True # exclude (always matches settings) + assert ( + filtered[2].get("dynamic") is True + ) # exclude (always matches settings) diff --git a/tests/unit/dash/test_utils.py b/tests/unit/dash/test_utils.py index 70e3c74e05..fc94d39be7 100644 --- a/tests/unit/dash/test_utils.py +++ b/tests/unit/dash/test_utils.py @@ -6,58 +6,59 @@ def test_ddut001_attribute_dict(): a = utils.AttributeDict() - assert str(a) == '{}' + assert str(a) == "{}" with pytest.raises(AttributeError): a.k with pytest.raises(KeyError): - a['k'] - assert a.first('no', 'k', 'nope') is None + a["k"] + assert a.first("no", "k", "nope") is None a.k = 1 assert a.k == 1 - assert a['k'] == 1 - assert a.first('no', 'k', 'nope') == 1 + assert a["k"] == 1 + assert a.first("no", "k", "nope") == 1 - a['k'] = 2 + a["k"] = 2 assert a.k == 2 - assert a['k'] == 2 + assert a["k"] == 2 - a.set_read_only(['k', 'q'], 'boo') + a.set_read_only(["k", "q"], "boo") with pytest.raises(AttributeError) as err: a.k = 3 - assert err.value.args == ('boo', 'k') + assert err.value.args == ("boo", "k") assert a.k == 2 with pytest.raises(AttributeError) as err: - a['k'] = 3 - assert err.value.args == ('boo', 'k') + a["k"] = 3 + assert err.value.args == ("boo", "k") assert a.k == 2 - a.set_read_only(['q']) + a.set_read_only(["q"]) a.k = 3 assert a.k == 3 with pytest.raises(AttributeError) as err: a.q = 3 - assert err.value.args == ('Attribute is read-only', 'q') - assert 'q' not in a + assert err.value.args == ("Attribute is read-only", "q") + assert "q" not in a - a.finalize('nope') + a.finalize("nope") with pytest.raises(AttributeError) as err: a.x = 4 - assert err.value.args == ('nope', 'x') - assert 'x' not in a + assert err.value.args == ("nope", "x") + assert "x" not in a a.finalize() with pytest.raises(AttributeError) as err: a.x = 4 assert err.value.args == ( - 'Object is final: No new keys may be added.', 'x' + "Object is final: No new keys may be added.", + "x", ) - assert 'x' not in a + assert "x" not in a diff --git a/tests/unit/development/conftest.py b/tests/unit/development/conftest.py index 68a523d453..412e4cbbcc 100644 --- a/tests/unit/development/conftest.py +++ b/tests/unit/development/conftest.py @@ -12,5 +12,7 @@ def load_test_metadata_json(): json_path = os.path.join(_dir, "metadata_test.json") with open(json_path) as data_file: json_string = data_file.read() - data = json.JSONDecoder(object_pairs_hook=OrderedDict).decode(json_string) + data = json.JSONDecoder(object_pairs_hook=OrderedDict).decode( + json_string + ) return data diff --git a/tests/unit/development/metadata_test.py b/tests/unit/development/metadata_test.py index 6505a41448..63b5543926 100644 --- a/tests/unit/development/metadata_test.py +++ b/tests/unit/development/metadata_test.py @@ -44,22 +44,92 @@ class Table(Component): - aria-* (string; optional) - in (string; optional) - id (string; optional)""" + @_explicitize_args - def __init__(self, children=None, optionalArray=Component.UNDEFINED, optionalBool=Component.UNDEFINED, optionalFunc=Component.UNDEFINED, optionalNumber=Component.UNDEFINED, optionalObject=Component.UNDEFINED, optionalString=Component.UNDEFINED, optionalSymbol=Component.UNDEFINED, optionalNode=Component.UNDEFINED, optionalElement=Component.UNDEFINED, optionalMessage=Component.UNDEFINED, optionalEnum=Component.UNDEFINED, optionalUnion=Component.UNDEFINED, optionalArrayOf=Component.UNDEFINED, optionalObjectOf=Component.UNDEFINED, optionalObjectWithExactAndNestedDescription=Component.UNDEFINED, optionalObjectWithShapeAndNestedDescription=Component.UNDEFINED, optionalAny=Component.UNDEFINED, customProp=Component.UNDEFINED, customArrayProp=Component.UNDEFINED, id=Component.UNDEFINED, **kwargs): - self._prop_names = ['children', 'optionalArray', 'optionalBool', 'optionalNumber', 'optionalObject', 'optionalString', 'optionalNode', 'optionalElement', 'optionalEnum', 'optionalUnion', 'optionalArrayOf', 'optionalObjectOf', 'optionalObjectWithExactAndNestedDescription', 'optionalObjectWithShapeAndNestedDescription', 'optionalAny', 'customProp', 'customArrayProp', 'data-*', 'aria-*', 'in', 'id'] - self._type = 'Table' - self._namespace = 'TableComponents' - self._valid_wildcard_attributes = ['data-', 'aria-'] - self.available_properties = ['children', 'optionalArray', 'optionalBool', 'optionalNumber', 'optionalObject', 'optionalString', 'optionalNode', 'optionalElement', 'optionalEnum', 'optionalUnion', 'optionalArrayOf', 'optionalObjectOf', 'optionalObjectWithExactAndNestedDescription', 'optionalObjectWithShapeAndNestedDescription', 'optionalAny', 'customProp', 'customArrayProp', 'data-*', 'aria-*', 'in', 'id'] - self.available_wildcard_properties = ['data-', 'aria-'] + def __init__( + self, + children=None, + optionalArray=Component.UNDEFINED, + optionalBool=Component.UNDEFINED, + optionalFunc=Component.UNDEFINED, + optionalNumber=Component.UNDEFINED, + optionalObject=Component.UNDEFINED, + optionalString=Component.UNDEFINED, + optionalSymbol=Component.UNDEFINED, + optionalNode=Component.UNDEFINED, + optionalElement=Component.UNDEFINED, + optionalMessage=Component.UNDEFINED, + optionalEnum=Component.UNDEFINED, + optionalUnion=Component.UNDEFINED, + optionalArrayOf=Component.UNDEFINED, + optionalObjectOf=Component.UNDEFINED, + optionalObjectWithExactAndNestedDescription=Component.UNDEFINED, + optionalObjectWithShapeAndNestedDescription=Component.UNDEFINED, + optionalAny=Component.UNDEFINED, + customProp=Component.UNDEFINED, + customArrayProp=Component.UNDEFINED, + id=Component.UNDEFINED, + **kwargs + ): + self._prop_names = [ + "children", + "optionalArray", + "optionalBool", + "optionalNumber", + "optionalObject", + "optionalString", + "optionalNode", + "optionalElement", + "optionalEnum", + "optionalUnion", + "optionalArrayOf", + "optionalObjectOf", + "optionalObjectWithExactAndNestedDescription", + "optionalObjectWithShapeAndNestedDescription", + "optionalAny", + "customProp", + "customArrayProp", + "data-*", + "aria-*", + "in", + "id", + ] + self._type = "Table" + self._namespace = "TableComponents" + self._valid_wildcard_attributes = ["data-", "aria-"] + self.available_properties = [ + "children", + "optionalArray", + "optionalBool", + "optionalNumber", + "optionalObject", + "optionalString", + "optionalNode", + "optionalElement", + "optionalEnum", + "optionalUnion", + "optionalArrayOf", + "optionalObjectOf", + "optionalObjectWithExactAndNestedDescription", + "optionalObjectWithShapeAndNestedDescription", + "optionalAny", + "customProp", + "customArrayProp", + "data-*", + "aria-*", + "in", + "id", + ] + self.available_wildcard_properties = ["data-", "aria-"] - _explicit_args = kwargs.pop('_explicit_args') + _explicit_args = kwargs.pop("_explicit_args") _locals = locals() _locals.update(kwargs) # For wildcard attrs - args = {k: _locals[k] for k in _explicit_args if k != 'children'} + args = {k: _locals[k] for k in _explicit_args if k != "children"} for k in []: if k not in args: raise TypeError( - 'Required argument `' + k + '` was not specified.') + "Required argument `" + k + "` was not specified." + ) super(Table, self).__init__(children=children, **args) diff --git a/tests/unit/development/test_base_component.py b/tests/unit/development/test_base_component.py index a2984b65d4..2f2b48f1a6 100644 --- a/tests/unit/development/test_base_component.py +++ b/tests/unit/development/test_base_component.py @@ -23,7 +23,8 @@ def nested_tree(): """ c1 = Component(id="0.1.x.x.0", children="string") c2 = Component( - id="0.1.x.x", children=[10, None, "wrap string", c1, "another string", 4.51] + id="0.1.x.x", + children=[10, None, "wrap string", c1, "another string", 4.51], ) c3 = Component( id="0.1.x", @@ -190,7 +191,9 @@ def test_to_plotly_json_with_nested_children_with_mixed_strings_and_without_list }, } - res = json.loads(json.dumps(c.to_plotly_json(), cls=plotly.utils.PlotlyJSONEncoder)) + res = json.loads( + json.dumps(c.to_plotly_json(), cls=plotly.utils.PlotlyJSONEncoder) + ) assert res == expected @@ -343,7 +346,12 @@ def test_to_plotly_json_with_children(): def test_to_plotly_json_with_wildcards(): c = Component( - id="a", **{"aria-expanded": "true", "data-toggle": "toggled", "data-none": None} + id="a", + **{ + "aria-expanded": "true", + "data-toggle": "toggled", + "data-none": None, + } ) c._prop_names = ("id",) c._type = "MyComponent" @@ -365,7 +373,10 @@ def test_len(): assert len(Component(children="Hello World")) == 1 assert len(Component(children=Component())) == 1 assert len(Component(children=[Component(), Component()])) == 2 - assert len(Component(children=[Component(children=Component()), Component()])) == 3 + assert ( + len(Component(children=[Component(children=Component()), Component()])) + == 3 + ) def test_iter(): @@ -379,8 +390,13 @@ def test_iter(): c = Component( id="1", children=[ - Component(id="2", children=[Component(id="3", children=Component(id="4"))]), - Component(id="5", children=[Component(id="6", children="Hello World")]), + Component( + id="2", + children=[Component(id="3", children=Component(id="4"))], + ), + Component( + id="5", children=[Component(id="6", children="Hello World")] + ), Component(), Component(children="Hello World"), Component(children=Component(id="7")), diff --git a/tests/unit/development/test_component_loader.py b/tests/unit/development/test_component_loader.py index 4c525fdf6d..11a6c5c7fb 100644 --- a/tests/unit/development/test_component_loader.py +++ b/tests/unit/development/test_component_loader.py @@ -156,7 +156,9 @@ def test_loadcomponents(write_metada_file): assert repr(a_component(**a_kwargs)) == repr(c[1](**a_kwargs)) -def test_loadcomponents_from_generated_class(write_metada_file, make_namespace): +def test_loadcomponents_from_generated_class( + write_metada_file, make_namespace +): my_component_runtime = generate_class( "MyComponent", METADATA["MyComponent.react.js"]["props"], @@ -172,7 +174,9 @@ def test_loadcomponents_from_generated_class(write_metada_file, make_namespace): ) generate_classes("default_namespace", METADATA_PATH) - from default_namespace.MyComponent import MyComponent as MyComponent_buildtime + from default_namespace.MyComponent import ( + MyComponent as MyComponent_buildtime, + ) from default_namespace.A import A as A_buildtime my_component_kwargs = { diff --git a/tests/unit/development/test_flow_metadata_conversions.py b/tests/unit/development/test_flow_metadata_conversions.py index 822df417e9..284fbcf5f6 100644 --- a/tests/unit/development/test_flow_metadata_conversions.py +++ b/tests/unit/development/test_flow_metadata_conversions.py @@ -15,12 +15,18 @@ expected_arg_strings = OrderedDict( [ - ["children", "a list of or a singular dash component, string or number"], + [ + "children", + "a list of or a singular dash component, string or number", + ], ["requiredString", "string"], ["optionalString", "string"], ["optionalBoolean", "boolean"], ["optionalFunc", ""], - ["optionalNode", "a list of or a singular dash component, string or number"], + [ + "optionalNode", + "a list of or a singular dash component, string or number", + ], ["optionalArray", "list"], ["requiredUnion", "string | number"], [ @@ -119,7 +125,9 @@ def load_test_flow_metadata_json(): path = os.path.join(_dir, "flow_metadata_test.json") with open(path) as data_file: json_string = data_file.read() - data = json.JSONDecoder(object_pairs_hook=OrderedDict).decode(json_string) + data = json.JSONDecoder(object_pairs_hook=OrderedDict).decode( + json_string + ) return data diff --git a/tests/unit/development/test_generate_class.py b/tests/unit/development/test_generate_class.py index 49ba7d5684..467f32d0e4 100644 --- a/tests/unit/development/test_generate_class.py +++ b/tests/unit/development/test_generate_class.py @@ -61,7 +61,11 @@ def test_to_plotly_json(component_class): def test_arguments_become_attributes(component_class): - kwargs = {"id": "my-id", "children": "text children", "optionalArray": [[1, 2, 3]]} + kwargs = { + "id": "my-id", + "children": "text children", + "optionalArray": [[1, 2, 3]], + } component_instance = component_class(**kwargs) for k, v in list(kwargs.items()): assert getattr(component_instance, k) == v @@ -104,7 +108,9 @@ def test_repr_with_wildcards(component_class): def test_docstring(component_class): assert not list( - unified_diff(expected_table_component_doc, component_class.__doc__.splitlines()) + unified_diff( + expected_table_component_doc, component_class.__doc__.splitlines() + ) ) diff --git a/tests/unit/development/test_generate_class_file.py b/tests/unit/development/test_generate_class_file.py index eecc2ba34e..44719e2994 100644 --- a/tests/unit/development/test_generate_class_file.py +++ b/tests/unit/development/test_generate_class_file.py @@ -64,7 +64,8 @@ def written_class_string(make_component_dir): def test_class_string(expected_class_string, component_class_string): assert not list( unified_diff( - expected_class_string.splitlines(), component_class_string.splitlines() + expected_class_string.splitlines(), + component_class_string.splitlines(), ) ) @@ -74,7 +75,8 @@ def test_class_string(expected_class_string, component_class_string): def test_class_file(expected_class_string, written_class_string): assert not list( unified_diff( - expected_class_string.splitlines(), written_class_string.splitlines() + expected_class_string.splitlines(), + written_class_string.splitlines(), ) ) assert not has_trailing_space(written_class_string) diff --git a/tests/unit/development/test_metadata_conversions.py b/tests/unit/development/test_metadata_conversions.py index 008efebe66..d9f32b4045 100644 --- a/tests/unit/development/test_metadata_conversions.py +++ b/tests/unit/development/test_metadata_conversions.py @@ -10,7 +10,10 @@ expected_arg_strings = OrderedDict( [ - ["children", "a list of or a singular dash component, string or number"], + [ + "children", + "a list of or a singular dash component, string or number", + ], ["optionalArray", "list"], ["optionalBool", "boolean"], ["optionalFunc", ""], @@ -19,12 +22,18 @@ ["optionalString", "string"], ["optionalSymbol", ""], ["optionalElement", "dash component"], - ["optionalNode", "a list of or a singular dash component, string or number"], + [ + "optionalNode", + "a list of or a singular dash component, string or number", + ], ["optionalMessage", ""], ["optionalEnum", "a value equal to: 'News', 'Photos'"], ["optionalUnion", "string | number"], ["optionalArrayOf", "list of numbers"], - ["optionalObjectOf", "dict with strings as keys and values of type number"], + [ + "optionalObjectOf", + "dict with strings as keys and values of type number", + ], [ "optionalObjectWithExactAndNestedDescription", "\n".join( @@ -75,7 +84,9 @@ def test_docstring(load_test_metadata_json): load_test_metadata_json["description"], ) prohibit_events(load_test_metadata_json["props"]), - assert not list(unified_diff(expected_table_component_doc, docstring.splitlines())) + assert not list( + unified_diff(expected_table_component_doc, docstring.splitlines()) + ) def test_docgen_to_python_args(load_test_metadata_json): diff --git a/tests/unit/development/test_r_component_gen.py b/tests/unit/development/test_r_component_gen.py index b72ecbf591..ae16fb68f1 100644 --- a/tests/unit/development/test_r_component_gen.py +++ b/tests/unit/development/test_r_component_gen.py @@ -5,9 +5,7 @@ import pytest -from dash.development._r_components_generation import ( - make_namespace_exports -) +from dash.development._r_components_generation import make_namespace_exports @pytest.fixture @@ -20,7 +18,8 @@ def make_r_dir(): def test_r_exports(make_r_dir): - extra_file = dedent(""" + extra_file = dedent( + """ # normal function syntax my_func <- function(a, b) { c <- a + b @@ -55,22 +54,25 @@ def test_r_exports(make_r_dir): # . in the middle is OK though not.secret <- function() { 42 } - """) + """ + ) components = ["Component1", "Component2"] - prefix = 'pre' + prefix = "pre" expected_exports = [prefix + c for c in components] + [ "my_func", "my_func2", "df_to_list", "util", - "not.secret" + "not.secret", ] - mock_component_file = dedent(""" + mock_component_file = dedent( + """ nope <- function() { stop("we don't look in component files") } - """) + """ + ) with open(os.path.join("R", "preComponent1.R"), "w") as f: f.write(mock_component_file) @@ -80,6 +82,6 @@ def test_r_exports(make_r_dir): exports = make_namespace_exports(components, prefix) print(exports) - matches = re.findall(r"export\(([^()]+)\)", exports.replace('\n', ' ')) + matches = re.findall(r"export\(([^()]+)\)", exports.replace("\n", " ")) assert matches == expected_exports diff --git a/tests/unit/test_app_runners.py b/tests/unit/test_app_runners.py index 784a8ff358..22bca7407a 100644 --- a/tests/unit/test_app_runners.py +++ b/tests/unit/test_app_runners.py @@ -25,7 +25,7 @@ def test_threaded_server_smoke(dash_thread_server): sys.version_info < (3,), reason="requires python3 for process testing" ) def test_process_server_smoke(dash_process_server): - dash_process_server('simple_app') + dash_process_server("simple_app") r = requests.get(dash_process_server.url) assert r.status_code == 200, "the server is reachable" assert 'id="react-entry-point"' in r.text, "the entrypoint is present" diff --git a/tests/unit/test_configs.py b/tests/unit/test_configs.py index 2481174b99..c3f3b2f5b7 100644 --- a/tests/unit/test_configs.py +++ b/tests/unit/test_configs.py @@ -41,7 +41,8 @@ def test_valid_pathname_prefix_init( empty_environ, route_prefix, req_prefix, expected_route, expected_req ): _, routes, req = pathname_configs( - routes_pathname_prefix=route_prefix, requests_pathname_prefix=req_prefix + routes_pathname_prefix=route_prefix, + requests_pathname_prefix=req_prefix, ) if expected_route is not None: @@ -57,13 +58,18 @@ def test_invalid_pathname_prefix(empty_environ): _, _, _ = pathname_configs( url_base_pathname="/invalid", routes_pathname_prefix="/invalid" ) - assert str(excinfo.value).split(".")[0].endswith("`routes_pathname_prefix`") + assert ( + str(excinfo.value).split(".")[0].endswith("`routes_pathname_prefix`") + ) with pytest.raises(_exc.InvalidConfig) as excinfo: _, _, _ = pathname_configs( - url_base_pathname="/my-path", requests_pathname_prefix="/another-path" + url_base_pathname="/my-path", + requests_pathname_prefix="/another-path", ) - assert str(excinfo.value).split(".")[0].endswith("`requests_pathname_prefix`") + assert ( + str(excinfo.value).split(".")[0].endswith("`requests_pathname_prefix`") + ) with pytest.raises(_exc.InvalidConfig, match="start with `/`"): _, _, _ = pathname_configs("my-path") From b2fc4454fc631e4c98c30fdc04ed75fa852d0c12 Mon Sep 17 00:00:00 2001 From: Stephen Tierney Date: Fri, 25 Oct 2019 21:49:21 +1100 Subject: [PATCH 4/4] Added flask back to requirements --- requires-install.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requires-install.txt b/requires-install.txt index e3d7b726d4..cba36e7c8e 100644 --- a/requires-install.txt +++ b/requires-install.txt @@ -1,4 +1,5 @@ asgiref +flask quart quart-compress plotly