From b3bb96da2002383625b7ff595d9862a4f0085c17 Mon Sep 17 00:00:00 2001 From: John-Craig Borman Date: Sun, 2 Jan 2022 12:00:45 -0500 Subject: [PATCH 1/5] Make AttributeDict additive ._read_only attribute is now a dict of name -> msgs Overrides .update() method to use __setitem__ implementation Includes updates to unit tests --- dash/_utils.py | 22 ++++++++++++++++++---- tests/unit/dash/test_utils.py | 7 +++---- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/dash/_utils.py b/dash/_utils.py index 1c147065c4..59490053a4 100644 --- a/dash/_utils.py +++ b/dash/_utils.py @@ -137,16 +137,25 @@ def __getattr__(self, key): raise AttributeError(key) def set_read_only(self, names, msg="Attribute is read-only"): - object.__setattr__(self, "_read_only", names) - object.__setattr__(self, "_read_only_msg", msg) + """ + Designate named attributes as read-only with the corresponding msg + + Method is additive. Making additional calls to this method will update + existing messages and add to the current set of _read_only names. + """ + new_read_only = {name: msg for name in names} + if getattr(self, "_read_only", False): + self._read_only.update(new_read_only) + else: + object.__setattr__(self, "_read_only", new_read_only) def finalize(self, msg="Object is final: No new keys may be added."): """Prevent any new keys being set.""" object.__setattr__(self, "_final", msg) def __setitem__(self, key, val): - if key in self.__dict__.get("_read_only", []): - raise AttributeError(self._read_only_msg, key) + if key in self.__dict__.get("_read_only", {}): + raise AttributeError(self._read_only[key], key) final_msg = self.__dict__.get("_final") if final_msg and key not in self: @@ -154,6 +163,11 @@ def __setitem__(self, key, val): return super().__setitem__(key, val) + def update(self, other): + # Overrides dict.update() to use __setitem__ above + for k, v in other.items(): + self[k] = v + # pylint: disable=inconsistent-return-statements def first(self, *names): for name in names: diff --git a/tests/unit/dash/test_utils.py b/tests/unit/dash/test_utils.py index 3c75d685fc..f643442dd4 100644 --- a/tests/unit/dash/test_utils.py +++ b/tests/unit/dash/test_utils.py @@ -24,12 +24,13 @@ def test_ddut001_attribute_dict(): assert a.k == 2 assert a["k"] == 2 - a.set_read_only(["k", "q"], "boo") + a.set_read_only(["k"], "boo") with pytest.raises(AttributeError) as err: a.k = 3 assert err.value.args == ("boo", "k") assert a.k == 2 + assert a._read_only == {"k": "boo"} with pytest.raises(AttributeError) as err: a["k"] = 3 @@ -38,13 +39,11 @@ def test_ddut001_attribute_dict(): 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 a._read_only == {"k": "boo", "q": "Attribute is read-only"} a.finalize("nope") From bea269772c79cdb99e292b058ce25c2e420177fc Mon Sep 17 00:00:00 2001 From: John-Craig Borman Date: Sun, 2 Jan 2022 12:06:02 -0500 Subject: [PATCH 2/5] Adds kwargs to Dash.init_app() --- dash/dash.py | 25 +++++++++++++++++-------- tests/unit/test_configs.py | 16 ++++++++++++++++ 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index b567e5b9d9..3172326464 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -356,18 +356,11 @@ def __init__( "assets_folder", "assets_url_path", "eager_loading", - "url_base_pathname", - "routes_pathname_prefix", - "requests_pathname_prefix", "serve_locally", "compress", ], "Read-only: can only be set in the Dash constructor", ) - self.config.finalize( - "Invalid config key. Some settings are only available " - "via the Dash constructor" - ) # keep title as a class property for backwards compatibility self.title = title @@ -427,8 +420,23 @@ def __init__( self.logger.setLevel(logging.INFO) - def init_app(self, app=None): + def init_app(self, app=None, **kwargs): """Initialize the parts of Dash that require a flask app.""" + + self.config.update(kwargs) + self.config.set_read_only( + [ + "url_base_pathname", + "routes_pathname_prefix", + "requests_pathname_prefix", + ], + "Read-only: can only be set in the Dash constructor or during init_app()", + ) + + self.config.finalize( + "Invalid config key. Some settings are only available " + "via the Dash constructor" + ) config = self.config if app is not None: @@ -438,6 +446,7 @@ def init_app(self, app=None): config.routes_pathname_prefix.replace("/", "_"), "dash_assets" ) + print(assets_blueprint_name) self.server.register_blueprint( flask.Blueprint( assets_blueprint_name, diff --git a/tests/unit/test_configs.py b/tests/unit/test_configs.py index d403d86506..d5a416e93c 100644 --- a/tests/unit/test_configs.py +++ b/tests/unit/test_configs.py @@ -370,3 +370,19 @@ def test_title(): assert "Hello World" in app.index() app = Dash(title="Custom Title") assert "Custom Title" in app.index() + + +def test_app_delayed_config(): + app = Dash(server=False) + app.init_app(app=Flask("test"), requests_pathname_prefix="/dash/") + + assert app.config.requests_pathname_prefix == "/dash/" + + with pytest.raises(AttributeError): + app.config.name = "cannot update me" + + +def test_app_invalid_delayed_config(): + app = Dash(server=False) + with pytest.raises(AttributeError): + app.init_app(app=Flask("test"), name="too late 2 update") From 7783fe861748472005d70677a85d513fa6171e5f Mon Sep 17 00:00:00 2001 From: John-Craig Borman Date: Sun, 2 Jan 2022 12:16:25 -0500 Subject: [PATCH 3/5] Removed print statement --- dash/dash.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dash/dash.py b/dash/dash.py index 3172326464..f05955dec2 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -446,7 +446,6 @@ def init_app(self, app=None, **kwargs): config.routes_pathname_prefix.replace("/", "_"), "dash_assets" ) - print(assets_blueprint_name) self.server.register_blueprint( flask.Blueprint( assets_blueprint_name, From 11a2ab892d9f806f10226a5522ab6642e9cd8bd2 Mon Sep 17 00:00:00 2001 From: John-Craig Borman Date: Fri, 7 Jan 2022 11:01:43 -0500 Subject: [PATCH 4/5] Moves finalize back to Dash.__init__() --- dash/dash.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index f05955dec2..85b1635665 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -361,6 +361,10 @@ def __init__( ], "Read-only: can only be set in the Dash constructor", ) + self.config.finalize( + "Invalid config key. Some settings are only available " + "via the Dash constructor" + ) # keep title as a class property for backwards compatibility self.title = title @@ -433,10 +437,6 @@ def init_app(self, app=None, **kwargs): "Read-only: can only be set in the Dash constructor or during init_app()", ) - self.config.finalize( - "Invalid config key. Some settings are only available " - "via the Dash constructor" - ) config = self.config if app is not None: From 561921de5f3c5c26c3b6dfcbc88eb38827c9e6fa Mon Sep 17 00:00:00 2001 From: John-Craig Borman Date: Fri, 7 Jan 2022 11:46:11 -0500 Subject: [PATCH 5/5] Adds changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 414927d8d5..e0e31f4465 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] ### Changed +- [#1876](https://github.com/plotly/dash/pull/1876) Delays finalizing `Dash.config` attributes not used in the constructor until `init_app()`. - [#1869](https://github.com/plotly/dash/pull/1869), [#1873](https://github.com/plotly/dash/pull/1873) Upgrade Plotly.js to v2.8.3. This includes: - [Feature release 2.5.0](https://github.com/plotly/plotly.js/releases/tag/v2.5.0): - 3D traces are now compatible with `no-unsafe-eval` CSP rules.