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

get_relative_path, strip_relative_path, get_asset_url #1923

Merged
merged 6 commits into from
Feb 16, 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
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ jobs:
. venv/bin/activate
set -eo pipefail
pip install -e . --progress-bar off && pip list | grep dash
npm install --production && npm run initialize
npm install npm run initialize
npm run build
npm run lint
- run:
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ This project adheres to [Semantic Versioning](https://semver.org/).

## [Unreleased]

### Added
- [#1923](https://github.com/plotly/dash/pull/1923):
- `dash.get_relative_path`
- `dash.strip_relative_path`
- `dash.get_asset_url`
This is similar to `dash.callback` where you don't need the `app` object. It makes it possible to use these
functions in the `pages` folder of a multi-page app without running into the circular `app` imports issue.

## [2.1.0] - 2022-01-22

### 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:
Expand Down
5 changes: 5 additions & 0 deletions dash/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,8 @@
from .version import __version__ # noqa: F401,E402
from ._callback_context import callback_context # noqa: F401,E402
from ._callback import callback, clientside_callback # noqa: F401,E402
from ._get_paths import ( # noqa: F401,E402
get_asset_url,
get_relative_path,
strip_relative_path,
)
155 changes: 155 additions & 0 deletions dash/_get_paths.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
from ._utils import AttributeDict
from . import exceptions

CONFIG = AttributeDict()


def get_asset_url(path):
return app_get_asset_url(CONFIG, path)


def app_get_asset_url(config, path):
if config.assets_external_path:
prefix = config.assets_external_path
else:
prefix = config.requests_pathname_prefix
return "/".join(
[
# Only take the first part of the pathname
prefix.rstrip("/"),
config.assets_url_path.lstrip("/"),
path,
]
)


def get_relative_path(path):
"""
Return a path with `requests_pathname_prefix` prefixed before it.
Use this function when specifying local URL paths that will work
in environments regardless of what `requests_pathname_prefix` is.
In some deployment environments, like Dash Enterprise,
`requests_pathname_prefix` is set to the application name,
e.g. `my-dash-app`.
When working locally, `requests_pathname_prefix` might be unset and
so a relative URL like `/page-2` can just be `/page-2`.
However, when the app is deployed to a URL like `/my-dash-app`, then
`app.get_relative_path('/page-2')` will return `/my-dash-app/page-2`.
This can be used as an alternative to `get_asset_url` as well with
`app.get_relative_path('/assets/logo.png')`

Use this function with `app.strip_relative_path` in callbacks that
deal with `dcc.Location` `pathname` routing.
That is, your usage may look like:
```
app.layout = html.Div([
dcc.Location(id='url'),
html.Div(id='content')
])
@app.callback(Output('content', 'children'), [Input('url', 'pathname')])
def display_content(path):
page_name = app.strip_relative_path(path)
if not page_name: # None or ''
return html.Div([
dcc.Link(href=app.get_relative_path('/page-1')),
dcc.Link(href=app.get_relative_path('/page-2')),
])
elif page_name == 'page-1':
return chapters.page_1
if page_name == "page-2":
return chapters.page_2
```
"""
return app_get_relative_path(CONFIG.requests_pathname_prefix, path)


def app_get_relative_path(requests_pathname, path):
if requests_pathname == "/" and path == "":
return "/"
if requests_pathname != "/" and path == "":
return requests_pathname
if not path.startswith("/"):
raise exceptions.UnsupportedRelativePath(
"""
Paths that aren't prefixed with a leading / are not supported.
You supplied: {}
""".format(
path
)
)
return "/".join([requests_pathname.rstrip("/"), path.lstrip("/")])


def strip_relative_path(path):
"""
Return a path with `requests_pathname_prefix` and leading and trailing
slashes stripped from it. Also, if None is passed in, None is returned.
Use this function with `get_relative_path` in callbacks that deal
with `dcc.Location` `pathname` routing.
That is, your usage may look like:
```
app.layout = html.Div([
dcc.Location(id='url'),
html.Div(id='content')
])
@app.callback(Output('content', 'children'), [Input('url', 'pathname')])
def display_content(path):
page_name = app.strip_relative_path(path)
if not page_name: # None or ''
return html.Div([
dcc.Link(href=app.get_relative_path('/page-1')),
dcc.Link(href=app.get_relative_path('/page-2')),
])
elif page_name == 'page-1':
return chapters.page_1
if page_name == "page-2":
return chapters.page_2
```
Note that `chapters.page_1` will be served if the user visits `/page-1`
_or_ `/page-1/` since `strip_relative_path` removes the trailing slash.

Also note that `strip_relative_path` is compatible with
`get_relative_path` in environments where `requests_pathname_prefix` set.
In some deployment environments, like Dash Enterprise,
`requests_pathname_prefix` is set to the application name, e.g. `my-dash-app`.
When working locally, `requests_pathname_prefix` might be unset and
so a relative URL like `/page-2` can just be `/page-2`.
However, when the app is deployed to a URL like `/my-dash-app`, then
`app.get_relative_path('/page-2')` will return `/my-dash-app/page-2`

The `pathname` property of `dcc.Location` will return '`/my-dash-app/page-2`'
to the callback.
In this case, `app.strip_relative_path('/my-dash-app/page-2')`
will return `'page-2'`

For nested URLs, slashes are still included:
`app.strip_relative_path('/page-1/sub-page-1/')` will return
`page-1/sub-page-1`
```
"""
return app_strip_relative_path(CONFIG.requests_pathname_prefix, path)


def app_strip_relative_path(requests_pathname, path):
if path is None:
return None
if (
requests_pathname != "/" and not path.startswith(requests_pathname.rstrip("/"))
) or (requests_pathname == "/" and not path.startswith("/")):
raise exceptions.UnsupportedRelativePath(
"""
Paths that aren't prefixed with requests_pathname_prefix are not supported.
You supplied: {} and requests_pathname_prefix was {}
""".format(
path, requests_pathname
)
)
if requests_pathname != "/" and path.startswith(requests_pathname.rstrip("/")):
path = path.replace(
# handle the case where the path might be `/my-dash-app`
# but the requests_pathname_prefix is `/my-dash-app/`
requests_pathname.rstrip("/"),
"",
1,
)
return path.strip("/")
55 changes: 0 additions & 55 deletions dash/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import io
import json
from functools import wraps
from . import exceptions

logger = logging.getLogger()

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


def get_asset_path(requests_pathname, asset_path, asset_url_path):

return "/".join(
[
# Only take the first part of the pathname
requests_pathname.rstrip("/"),
asset_url_path,
asset_path,
]
)


def get_relative_path(requests_pathname, path):
if requests_pathname == "/" and path == "":
return "/"
if requests_pathname != "/" and path == "":
return requests_pathname
if not path.startswith("/"):
raise exceptions.UnsupportedRelativePath(
"""
Paths that aren't prefixed with a leading / are not supported.
You supplied: {}
""".format(
path
)
)
return "/".join([requests_pathname.rstrip("/"), path.lstrip("/")])


def strip_relative_path(requests_pathname, path):
if path is None:
return None
if (
requests_pathname != "/" and not path.startswith(requests_pathname.rstrip("/"))
) or (requests_pathname == "/" and not path.startswith("/")):
raise exceptions.UnsupportedRelativePath(
"""
Paths that aren't prefixed with requests_pathname_prefix are not supported.
You supplied: {} and requests_pathname_prefix was {}
""".format(
path, requests_pathname
)
)
if requests_pathname != "/" and path.startswith(requests_pathname.rstrip("/")):
path = path.replace(
# handle the case where the path might be `/my-dash-app`
# but the requests_pathname_prefix is `/my-dash-app/`
requests_pathname.rstrip("/"),
"",
1,
)
return path.strip("/")


# pylint: disable=no-member
def patch_collections_abc(member):
return getattr(collections.abc, member)
Expand Down
25 changes: 10 additions & 15 deletions dash/dash.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,15 @@
AttributeDict,
format_tag,
generate_hash,
get_asset_path,
get_relative_path,
inputs_to_dict,
inputs_to_vals,
interpolate_str,
patch_collections_abc,
split_callback_id,
strip_relative_path,
to_json,
)
from . import _callback
from . import _get_paths
from . import _dash_renderer
from . import _validate
from . import _watch
Expand Down Expand Up @@ -366,6 +364,8 @@ def __init__(
"via the Dash constructor"
)

_get_paths.CONFIG = self.config

# keep title as a class property for backwards compatibility
self.title = title

Expand Down Expand Up @@ -1470,14 +1470,7 @@ def csp_hashes(self, hash_algorithm="sha256"):
]

def get_asset_url(self, path):
if self.config.assets_external_path:
prefix = self.config.assets_external_path
else:
prefix = self.config.requests_pathname_prefix

asset = get_asset_path(prefix, path, self.config.assets_url_path.lstrip("/"))

return asset
return _get_paths.app_get_asset_url(self.config, path)

def get_relative_path(self, path):
"""
Expand Down Expand Up @@ -1516,9 +1509,9 @@ def display_content(path):
return chapters.page_2
```
"""
asset = get_relative_path(self.config.requests_pathname_prefix, path)

return asset
return _get_paths.app_get_relative_path(
self.config.requests_pathname_prefix, path
)

def strip_relative_path(self, path):
"""
Expand Down Expand Up @@ -1567,7 +1560,9 @@ def display_content(path):
`page-1/sub-page-1`
```
"""
return strip_relative_path(self.config.requests_pathname_prefix, path)
return _get_paths.app_strip_relative_path(
self.config.requests_pathname_prefix, path
)

def _setup_dev_tools(self, **kwargs):
debug = kwargs.get("debug", False)
Expand Down
Loading