Skip to content

Commit 8d59373

Browse files
authored
Merge pull request #1876 from bormanjo/delay-app-config
Delayed App Config
2 parents 78ca3ec + c50cbf4 commit 8d59373

File tree

5 files changed

+50
-12
lines changed

5 files changed

+50
-12
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
55
## [Unreleased]
66

77
### Changed
8+
- [#1876](https://github.com/plotly/dash/pull/1876) Delays finalizing `Dash.config` attributes not used in the constructor until `init_app()`.
89
- [#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:
910
- [Feature release 2.5.0](https://github.com/plotly/plotly.js/releases/tag/v2.5.0):
1011
- 3D traces are now compatible with `no-unsafe-eval` CSP rules.

dash/_utils.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,23 +137,37 @@ def __getattr__(self, key):
137137
raise AttributeError(key)
138138

139139
def set_read_only(self, names, msg="Attribute is read-only"):
140-
object.__setattr__(self, "_read_only", names)
141-
object.__setattr__(self, "_read_only_msg", msg)
140+
"""
141+
Designate named attributes as read-only with the corresponding msg
142+
143+
Method is additive. Making additional calls to this method will update
144+
existing messages and add to the current set of _read_only names.
145+
"""
146+
new_read_only = {name: msg for name in names}
147+
if getattr(self, "_read_only", False):
148+
self._read_only.update(new_read_only)
149+
else:
150+
object.__setattr__(self, "_read_only", new_read_only)
142151

143152
def finalize(self, msg="Object is final: No new keys may be added."):
144153
"""Prevent any new keys being set."""
145154
object.__setattr__(self, "_final", msg)
146155

147156
def __setitem__(self, key, val):
148-
if key in self.__dict__.get("_read_only", []):
149-
raise AttributeError(self._read_only_msg, key)
157+
if key in self.__dict__.get("_read_only", {}):
158+
raise AttributeError(self._read_only[key], key)
150159

151160
final_msg = self.__dict__.get("_final")
152161
if final_msg and key not in self:
153162
raise AttributeError(final_msg, key)
154163

155164
return super().__setitem__(key, val)
156165

166+
def update(self, other):
167+
# Overrides dict.update() to use __setitem__ above
168+
for k, v in other.items():
169+
self[k] = v
170+
157171
# pylint: disable=inconsistent-return-statements
158172
def first(self, *names):
159173
for name in names:

dash/dash.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -356,9 +356,6 @@ def __init__(
356356
"assets_folder",
357357
"assets_url_path",
358358
"eager_loading",
359-
"url_base_pathname",
360-
"routes_pathname_prefix",
361-
"requests_pathname_prefix",
362359
"serve_locally",
363360
"compress",
364361
],
@@ -427,8 +424,19 @@ def __init__(
427424

428425
self.logger.setLevel(logging.INFO)
429426

430-
def init_app(self, app=None):
427+
def init_app(self, app=None, **kwargs):
431428
"""Initialize the parts of Dash that require a flask app."""
429+
430+
self.config.update(kwargs)
431+
self.config.set_read_only(
432+
[
433+
"url_base_pathname",
434+
"routes_pathname_prefix",
435+
"requests_pathname_prefix",
436+
],
437+
"Read-only: can only be set in the Dash constructor or during init_app()",
438+
)
439+
432440
config = self.config
433441

434442
if app is not None:

tests/unit/dash/test_utils.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,13 @@ def test_ddut001_attribute_dict():
2424
assert a.k == 2
2525
assert a["k"] == 2
2626

27-
a.set_read_only(["k", "q"], "boo")
27+
a.set_read_only(["k"], "boo")
2828

2929
with pytest.raises(AttributeError) as err:
3030
a.k = 3
3131
assert err.value.args == ("boo", "k")
3232
assert a.k == 2
33+
assert a._read_only == {"k": "boo"}
3334

3435
with pytest.raises(AttributeError) as err:
3536
a["k"] = 3
@@ -38,13 +39,11 @@ def test_ddut001_attribute_dict():
3839

3940
a.set_read_only(["q"])
4041

41-
a.k = 3
42-
assert a.k == 3
43-
4442
with pytest.raises(AttributeError) as err:
4543
a.q = 3
4644
assert err.value.args == ("Attribute is read-only", "q")
4745
assert "q" not in a
46+
assert a._read_only == {"k": "boo", "q": "Attribute is read-only"}
4847

4948
a.finalize("nope")
5049

tests/unit/test_configs.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,3 +370,19 @@ def test_title():
370370
assert "<title>Hello World</title>" in app.index()
371371
app = Dash(title="Custom Title")
372372
assert "<title>Custom Title</title>" in app.index()
373+
374+
375+
def test_app_delayed_config():
376+
app = Dash(server=False)
377+
app.init_app(app=Flask("test"), requests_pathname_prefix="/dash/")
378+
379+
assert app.config.requests_pathname_prefix == "/dash/"
380+
381+
with pytest.raises(AttributeError):
382+
app.config.name = "cannot update me"
383+
384+
385+
def test_app_invalid_delayed_config():
386+
app = Dash(server=False)
387+
with pytest.raises(AttributeError):
388+
app.init_app(app=Flask("test"), name="too late 2 update")

0 commit comments

Comments
 (0)