Skip to content

Commit 5afc4fb

Browse files
authored
Merge pull request #1923 from AnnMarieW/pages
`get_relative_path, strip_relative_path, get_asset_url`
2 parents fb34b52 + 85a1ab8 commit 5afc4fb

File tree

7 files changed

+242
-79
lines changed

7 files changed

+242
-79
lines changed

Diff for: .circleci/config.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ jobs:
111111
. venv/bin/activate
112112
set -eo pipefail
113113
pip install -e . --progress-bar off && pip list | grep dash
114-
npm install --production && npm run initialize
114+
npm install npm run initialize
115115
npm run build
116116
npm run lint
117117
- run:

Diff for: CHANGELOG.md

+10
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@ This project adheres to [Semantic Versioning](https://semver.org/).
44

55
## [Unreleased]
66

7+
### Added
8+
- [#1923](https://github.com/plotly/dash/pull/1923):
9+
- `dash.get_relative_path`
10+
- `dash.strip_relative_path`
11+
- `dash.get_asset_url`
12+
This is similar to `dash.callback` where you don't need the `app` object. It makes it possible to use these
13+
functions in the `pages` folder of a multi-page app without running into the circular `app` imports issue.
14+
15+
## [2.1.0] - 2022-01-22
16+
717
### Changed
818
- [#1876](https://github.com/plotly/dash/pull/1876) Delays finalizing `Dash.config` attributes not used in the constructor until `init_app()`.
919
- [#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:

Diff for: dash/__init__.py

+5
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,8 @@
2121
from .version import __version__ # noqa: F401,E402
2222
from ._callback_context import callback_context # noqa: F401,E402
2323
from ._callback import callback, clientside_callback # noqa: F401,E402
24+
from ._get_paths import ( # noqa: F401,E402
25+
get_asset_url,
26+
get_relative_path,
27+
strip_relative_path,
28+
)

Diff for: dash/_get_paths.py

+155
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
from ._utils import AttributeDict
2+
from . import exceptions
3+
4+
CONFIG = AttributeDict()
5+
6+
7+
def get_asset_url(path):
8+
return app_get_asset_url(CONFIG, path)
9+
10+
11+
def app_get_asset_url(config, path):
12+
if config.assets_external_path:
13+
prefix = config.assets_external_path
14+
else:
15+
prefix = config.requests_pathname_prefix
16+
return "/".join(
17+
[
18+
# Only take the first part of the pathname
19+
prefix.rstrip("/"),
20+
config.assets_url_path.lstrip("/"),
21+
path,
22+
]
23+
)
24+
25+
26+
def get_relative_path(path):
27+
"""
28+
Return a path with `requests_pathname_prefix` prefixed before it.
29+
Use this function when specifying local URL paths that will work
30+
in environments regardless of what `requests_pathname_prefix` is.
31+
In some deployment environments, like Dash Enterprise,
32+
`requests_pathname_prefix` is set to the application name,
33+
e.g. `my-dash-app`.
34+
When working locally, `requests_pathname_prefix` might be unset and
35+
so a relative URL like `/page-2` can just be `/page-2`.
36+
However, when the app is deployed to a URL like `/my-dash-app`, then
37+
`app.get_relative_path('/page-2')` will return `/my-dash-app/page-2`.
38+
This can be used as an alternative to `get_asset_url` as well with
39+
`app.get_relative_path('/assets/logo.png')`
40+
41+
Use this function with `app.strip_relative_path` in callbacks that
42+
deal with `dcc.Location` `pathname` routing.
43+
That is, your usage may look like:
44+
```
45+
app.layout = html.Div([
46+
dcc.Location(id='url'),
47+
html.Div(id='content')
48+
])
49+
@app.callback(Output('content', 'children'), [Input('url', 'pathname')])
50+
def display_content(path):
51+
page_name = app.strip_relative_path(path)
52+
if not page_name: # None or ''
53+
return html.Div([
54+
dcc.Link(href=app.get_relative_path('/page-1')),
55+
dcc.Link(href=app.get_relative_path('/page-2')),
56+
])
57+
elif page_name == 'page-1':
58+
return chapters.page_1
59+
if page_name == "page-2":
60+
return chapters.page_2
61+
```
62+
"""
63+
return app_get_relative_path(CONFIG.requests_pathname_prefix, path)
64+
65+
66+
def app_get_relative_path(requests_pathname, path):
67+
if requests_pathname == "/" and path == "":
68+
return "/"
69+
if requests_pathname != "/" and path == "":
70+
return requests_pathname
71+
if not path.startswith("/"):
72+
raise exceptions.UnsupportedRelativePath(
73+
"""
74+
Paths that aren't prefixed with a leading / are not supported.
75+
You supplied: {}
76+
""".format(
77+
path
78+
)
79+
)
80+
return "/".join([requests_pathname.rstrip("/"), path.lstrip("/")])
81+
82+
83+
def strip_relative_path(path):
84+
"""
85+
Return a path with `requests_pathname_prefix` and leading and trailing
86+
slashes stripped from it. Also, if None is passed in, None is returned.
87+
Use this function with `get_relative_path` in callbacks that deal
88+
with `dcc.Location` `pathname` routing.
89+
That is, your usage may look like:
90+
```
91+
app.layout = html.Div([
92+
dcc.Location(id='url'),
93+
html.Div(id='content')
94+
])
95+
@app.callback(Output('content', 'children'), [Input('url', 'pathname')])
96+
def display_content(path):
97+
page_name = app.strip_relative_path(path)
98+
if not page_name: # None or ''
99+
return html.Div([
100+
dcc.Link(href=app.get_relative_path('/page-1')),
101+
dcc.Link(href=app.get_relative_path('/page-2')),
102+
])
103+
elif page_name == 'page-1':
104+
return chapters.page_1
105+
if page_name == "page-2":
106+
return chapters.page_2
107+
```
108+
Note that `chapters.page_1` will be served if the user visits `/page-1`
109+
_or_ `/page-1/` since `strip_relative_path` removes the trailing slash.
110+
111+
Also note that `strip_relative_path` is compatible with
112+
`get_relative_path` in environments where `requests_pathname_prefix` set.
113+
In some deployment environments, like Dash Enterprise,
114+
`requests_pathname_prefix` is set to the application name, e.g. `my-dash-app`.
115+
When working locally, `requests_pathname_prefix` might be unset and
116+
so a relative URL like `/page-2` can just be `/page-2`.
117+
However, when the app is deployed to a URL like `/my-dash-app`, then
118+
`app.get_relative_path('/page-2')` will return `/my-dash-app/page-2`
119+
120+
The `pathname` property of `dcc.Location` will return '`/my-dash-app/page-2`'
121+
to the callback.
122+
In this case, `app.strip_relative_path('/my-dash-app/page-2')`
123+
will return `'page-2'`
124+
125+
For nested URLs, slashes are still included:
126+
`app.strip_relative_path('/page-1/sub-page-1/')` will return
127+
`page-1/sub-page-1`
128+
```
129+
"""
130+
return app_strip_relative_path(CONFIG.requests_pathname_prefix, path)
131+
132+
133+
def app_strip_relative_path(requests_pathname, path):
134+
if path is None:
135+
return None
136+
if (
137+
requests_pathname != "/" and not path.startswith(requests_pathname.rstrip("/"))
138+
) or (requests_pathname == "/" and not path.startswith("/")):
139+
raise exceptions.UnsupportedRelativePath(
140+
"""
141+
Paths that aren't prefixed with requests_pathname_prefix are not supported.
142+
You supplied: {} and requests_pathname_prefix was {}
143+
""".format(
144+
path, requests_pathname
145+
)
146+
)
147+
if requests_pathname != "/" and path.startswith(requests_pathname.rstrip("/")):
148+
path = path.replace(
149+
# handle the case where the path might be `/my-dash-app`
150+
# but the requests_pathname_prefix is `/my-dash-app/`
151+
requests_pathname.rstrip("/"),
152+
"",
153+
1,
154+
)
155+
return path.strip("/")

Diff for: dash/_utils.py

-55
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import io
1010
import json
1111
from functools import wraps
12-
from . import exceptions
1312

1413
logger = logging.getLogger()
1514

@@ -47,60 +46,6 @@ def generate_hash():
4746
return str(uuid.uuid4().hex).strip("-")
4847

4948

50-
def get_asset_path(requests_pathname, asset_path, asset_url_path):
51-
52-
return "/".join(
53-
[
54-
# Only take the first part of the pathname
55-
requests_pathname.rstrip("/"),
56-
asset_url_path,
57-
asset_path,
58-
]
59-
)
60-
61-
62-
def get_relative_path(requests_pathname, path):
63-
if requests_pathname == "/" and path == "":
64-
return "/"
65-
if requests_pathname != "/" and path == "":
66-
return requests_pathname
67-
if not path.startswith("/"):
68-
raise exceptions.UnsupportedRelativePath(
69-
"""
70-
Paths that aren't prefixed with a leading / are not supported.
71-
You supplied: {}
72-
""".format(
73-
path
74-
)
75-
)
76-
return "/".join([requests_pathname.rstrip("/"), path.lstrip("/")])
77-
78-
79-
def strip_relative_path(requests_pathname, path):
80-
if path is None:
81-
return None
82-
if (
83-
requests_pathname != "/" and not path.startswith(requests_pathname.rstrip("/"))
84-
) or (requests_pathname == "/" and not path.startswith("/")):
85-
raise exceptions.UnsupportedRelativePath(
86-
"""
87-
Paths that aren't prefixed with requests_pathname_prefix are not supported.
88-
You supplied: {} and requests_pathname_prefix was {}
89-
""".format(
90-
path, requests_pathname
91-
)
92-
)
93-
if requests_pathname != "/" and path.startswith(requests_pathname.rstrip("/")):
94-
path = path.replace(
95-
# handle the case where the path might be `/my-dash-app`
96-
# but the requests_pathname_prefix is `/my-dash-app/`
97-
requests_pathname.rstrip("/"),
98-
"",
99-
1,
100-
)
101-
return path.strip("/")
102-
103-
10449
# pylint: disable=no-member
10550
def patch_collections_abc(member):
10651
return getattr(collections.abc, member)

Diff for: dash/dash.py

+10-15
Original file line numberDiff line numberDiff line change
@@ -42,17 +42,15 @@
4242
AttributeDict,
4343
format_tag,
4444
generate_hash,
45-
get_asset_path,
46-
get_relative_path,
4745
inputs_to_dict,
4846
inputs_to_vals,
4947
interpolate_str,
5048
patch_collections_abc,
5149
split_callback_id,
52-
strip_relative_path,
5350
to_json,
5451
)
5552
from . import _callback
53+
from . import _get_paths
5654
from . import _dash_renderer
5755
from . import _validate
5856
from . import _watch
@@ -366,6 +364,8 @@ def __init__(
366364
"via the Dash constructor"
367365
)
368366

367+
_get_paths.CONFIG = self.config
368+
369369
# keep title as a class property for backwards compatibility
370370
self.title = title
371371

@@ -1470,14 +1470,7 @@ def csp_hashes(self, hash_algorithm="sha256"):
14701470
]
14711471

14721472
def get_asset_url(self, path):
1473-
if self.config.assets_external_path:
1474-
prefix = self.config.assets_external_path
1475-
else:
1476-
prefix = self.config.requests_pathname_prefix
1477-
1478-
asset = get_asset_path(prefix, path, self.config.assets_url_path.lstrip("/"))
1479-
1480-
return asset
1473+
return _get_paths.app_get_asset_url(self.config, path)
14811474

14821475
def get_relative_path(self, path):
14831476
"""
@@ -1516,9 +1509,9 @@ def display_content(path):
15161509
return chapters.page_2
15171510
```
15181511
"""
1519-
asset = get_relative_path(self.config.requests_pathname_prefix, path)
1520-
1521-
return asset
1512+
return _get_paths.app_get_relative_path(
1513+
self.config.requests_pathname_prefix, path
1514+
)
15221515

15231516
def strip_relative_path(self, path):
15241517
"""
@@ -1567,7 +1560,9 @@ def display_content(path):
15671560
`page-1/sub-page-1`
15681561
```
15691562
"""
1570-
return strip_relative_path(self.config.requests_pathname_prefix, path)
1563+
return _get_paths.app_strip_relative_path(
1564+
self.config.requests_pathname_prefix, path
1565+
)
15711566

15721567
def _setup_dev_tools(self, **kwargs):
15731568
debug = kwargs.get("debug", False)

0 commit comments

Comments
 (0)