diff --git a/CHANGELOG.md b/CHANGELOG.md index 57207d67bb..3f9e38078e 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. 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/dash/dash.py b/dash/dash.py index b567e5b9d9..85b1635665 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -356,9 +356,6 @@ def __init__( "assets_folder", "assets_url_path", "eager_loading", - "url_base_pathname", - "routes_pathname_prefix", - "requests_pathname_prefix", "serve_locally", "compress", ], @@ -427,8 +424,19 @@ 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()", + ) + config = self.config if app is not None: 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") 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 "