diff --git a/dash/_configs.py b/dash/_configs.py new file mode 100644 index 0000000000..b528791bb0 --- /dev/null +++ b/dash/_configs.py @@ -0,0 +1,105 @@ +import os + +# noinspection PyCompatibility +from . import exceptions +from ._utils import AttributeDict + + +def env_configs(): + """ + Configs from the environ. + + :return: A dict with the dash environ vars + """ + return AttributeDict({x: os.getenv(x, os.getenv(x.lower())) for x 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' + )}) + + +def choose_config(config_name, init, env, default=None): + if init is not None: + return init + + env_value = env.get('DASH_{}'.format(config_name.upper())) + if env_value is None: + return default + return env_value + + +def pathname_configs(url_base_pathname=None, + routes_pathname_prefix=None, + requests_pathname_prefix=None, + environ_configs=None): + _pathname_config_error_message = ''' + {} This is ambiguous. + To fix this, set `routes_pathname_prefix` instead of `url_base_pathname`. + + Note that `requests_pathname_prefix` is the prefix for the AJAX calls that + originate from the client (the web browser) and `routes_pathname_prefix` is + the prefix for the API routes on the backend (this flask server). + `url_base_pathname` will set `requests_pathname_prefix` and + `routes_pathname_prefix` to the same value. + If you need these to be different values then you should set + `requests_pathname_prefix` and `routes_pathname_prefix`, + not `url_base_pathname`. + ''' + environ_configs = environ_configs or env_configs() + + url_base_pathname = choose_config('url_base_pathname', + url_base_pathname, + environ_configs) + + routes_pathname_prefix = choose_config('routes_pathname_prefix', + routes_pathname_prefix, + environ_configs) + + requests_pathname_prefix = choose_config('requests_pathname_prefix', + requests_pathname_prefix, + environ_configs) + + 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`.' + ) + ) + elif 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`.') + ) + elif 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 = '/' + + if not routes_pathname_prefix.startswith('/'): + raise exceptions.InvalidConfig( + '`routes_pathname_prefix` needs to start with `/`') + if not routes_pathname_prefix.endswith('/'): + raise exceptions.InvalidConfig( + '`routes_pathname_prefix` needs to end with `/`') + + app_name = environ_configs.DASH_APP_NAME + + if not requests_pathname_prefix and app_name: + 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.endswith(routes_pathname_prefix): + raise exceptions.InvalidConfig( + '`requests_pathname_prefix` needs to ends with ' + '`routes_pathname_prefix`.' + ) + + return url_base_pathname, routes_pathname_prefix, requests_pathname_prefix + diff --git a/dash/dash.py b/dash/dash.py index e89f950425..6c43a48ea2 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -24,6 +24,8 @@ from ._utils import AttributeDict as _AttributeDict from ._utils import interpolate_str as _interpolate from ._utils import format_tag as _format_tag +from . import _configs + _default_index = ''' @@ -71,14 +73,17 @@ def __init__( static_folder='static', assets_folder=None, assets_url_path='/assets', - include_assets_files=True, - url_base_pathname='/', - requests_pathname_prefix='', + assets_external_path=None, + include_assets_files=None, + url_base_pathname=None, + requests_pathname_prefix=None, + routes_pathname_prefix=None, compress=True, meta_tags=None, index_string=_default_index, external_scripts=None, external_stylesheets=None, + suppress_callback_exceptions=None, **kwargs): # pylint-disable: too-many-instance-attributes @@ -102,16 +107,30 @@ def __init__( static_folder=self._assets_folder, static_url_path=assets_url_path)) + env_configs = _configs.env_configs() + + url_base_pathname, routes_pathname_prefix, requests_pathname_prefix = \ + _configs.pathname_configs( + url_base_pathname, + routes_pathname_prefix, + requests_pathname_prefix, + environ_configs=env_configs) + self.url_base_pathname = url_base_pathname self.config = _AttributeDict({ - 'suppress_callback_exceptions': False, - 'routes_pathname_prefix': url_base_pathname, - 'requests_pathname_prefix': requests_pathname_prefix or os.getenv( - 'DASH_REQUESTS_PATHNAME_PREFIX', - '/{}'.format(os.environ['DASH_APP_NAME']) - if 'DASH_APP_NAME' in os.environ else '') + url_base_pathname, - 'include_assets_files': include_assets_files, - 'assets_external_path': '', + 'suppress_callback_exceptions': _configs.choose_config( + 'suppress_callback_exceptions', + suppress_callback_exceptions, env_configs, False + ), + 'routes_pathname_prefix': routes_pathname_prefix, + 'requests_pathname_prefix': requests_pathname_prefix, + 'include_assets_files': _configs.choose_config( + 'include_assets_files', + include_assets_files, + env_configs, + True), + 'assets_external_path': _configs.choose_config( + 'assets_external_path', assets_external_path, env_configs, ''), }) # list of dependencies diff --git a/dash/exceptions.py b/dash/exceptions.py index c459b0e5c2..9606919c38 100644 --- a/dash/exceptions.py +++ b/dash/exceptions.py @@ -52,3 +52,7 @@ class PreventUpdate(CallbackException): class InvalidCallbackReturnValue(CallbackException): pass + + +class InvalidConfig(DashException): + pass diff --git a/tests/test_configs.py b/tests/test_configs.py new file mode 100644 index 0000000000..ee29673309 --- /dev/null +++ b/tests/test_configs.py @@ -0,0 +1,93 @@ +import unittest +# noinspection PyProtectedMember +from dash import _configs +from dash import exceptions as _exc +import os + + +class MyTestCase(unittest.TestCase): + + def setUp(self): + environ = _configs.env_configs() + + for k in environ.keys(): + if k in os.environ: + os.environ.pop(k) + + def test_valid_pathname_prefix_init(self): + _, routes, req = _configs.pathname_configs() + + self.assertEqual('/', routes) + self.assertEqual('/', req) + + _, routes, req = _configs.pathname_configs( + routes_pathname_prefix='/dash/') + + self.assertEqual('/dash/', req) + + _, routes, req = _configs.pathname_configs( + requests_pathname_prefix='/my-dash-app/', + ) + + self.assertEqual(routes, '/') + self.assertEqual(req, '/my-dash-app/') + + _, routes, req = _configs.pathname_configs( + routes_pathname_prefix='/dash/', + requests_pathname_prefix='/my-dash-app/dash/' + ) + + self.assertEqual('/dash/', routes) + self.assertEqual('/my-dash-app/dash/', req) + + def test_invalid_pathname_prefix(self): + with self.assertRaises(_exc.InvalidConfig) as context: + _, _, _ = _configs.pathname_configs('/my-path', '/another-path') + + self.assertTrue('url_base_pathname' in str(context.exception)) + + with self.assertRaises(_exc.InvalidConfig) as context: + _, _, _ = _configs.pathname_configs( + url_base_pathname='/invalid', + routes_pathname_prefix='/invalid') + + self.assertTrue(str(context.exception).split('.')[0] + .endswith('`routes_pathname_prefix`')) + + with self.assertRaises(_exc.InvalidConfig) as context: + _, _, _ = _configs.pathname_configs( + url_base_pathname='/my-path', + requests_pathname_prefix='/another-path') + + self.assertTrue(str(context.exception).split('.')[0] + .endswith('`requests_pathname_prefix`')) + + with self.assertRaises(_exc.InvalidConfig) as context: + _, _, _ = _configs.pathname_configs('my-path') + + self.assertTrue('start with `/`' in str(context.exception)) + + with self.assertRaises(_exc.InvalidConfig) as context: + _, _, _ = _configs.pathname_configs('/my-path') + + self.assertTrue('end with `/`' in str(context.exception)) + + def test_pathname_prefix_from_environ_app_name(self): + os.environ['DASH_APP_NAME'] = 'my-dash-app' + _, routes, req = _configs.pathname_configs() + self.assertEqual('/my-dash-app/', req) + self.assertEqual('/', routes) + + def test_pathname_prefix_environ_routes(self): + os.environ['DASH_ROUTES_PATHNAME_PREFIX'] = '/routes/' + _, routes, req = _configs.pathname_configs() + self.assertEqual('/routes/', routes) + + def test_pathname_prefix_environ_requests(self): + os.environ['DASH_REQUESTS_PATHNAME_PREFIX'] = '/requests/' + _, routes, req = _configs.pathname_configs() + self.assertEqual('/requests/', req) + + +if __name__ == '__main__': + unittest.main()