Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Delayed App Config #1876

Merged
merged 6 commits into from
Jan 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
22 changes: 18 additions & 4 deletions dash/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,23 +137,37 @@ 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:
raise AttributeError(final_msg, key)

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:
Expand Down
16 changes: 12 additions & 4 deletions dash/dash.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
Expand Down Expand Up @@ -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:
Expand Down
7 changes: 3 additions & 4 deletions tests/unit/dash/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")

Expand Down
16 changes: 16 additions & 0 deletions tests/unit/test_configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,3 +370,19 @@ def test_title():
assert "<title>Hello World</title>" in app.index()
app = Dash(title="Custom Title")
assert "<title>Custom Title</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")