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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
Expand Up@@ -9,7 +9,6 @@
import io
import json
from functools import wraps
from . import exceptions

logger = logging.getLogger()

Expand DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
Expand Up@@ -9,7 +9,6 @@
import io
import json
from functools import wraps
from . import exceptions

logger = logging.getLogger()

Expand DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
Expand Up@@ -9,7 +9,6 @@
import io
import json
from functools import wraps
from . import exceptions

logger = logging.getLogger()

Expand DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
Expand Up@@ -9,7 +9,6 @@
import io
import json
from functools import wraps
from . import exceptions

logger = logging.getLogger()

Expand DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
Expand Up@@ -9,7 +9,6 @@
import io
import json
from functools import wraps
from . import exceptions

logger = logging.getLogger()

Expand DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
Expand Up@@ -9,7 +9,6 @@
import io
import json
from functools import wraps
from . import exceptions

logger = logging.getLogger()

Expand DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
Expand Up@@ -9,7 +9,6 @@
import io
import json
from functools import wraps
from . import exceptions

logger = logging.getLogger()

Expand DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
Expand Up@@ -9,7 +9,6 @@
import io
import json
from functools import wraps
from . import exceptions

logger = logging.getLogger()

Expand DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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