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
107 changes: 107 additions & 0 deletions .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ jobs:
# This output will be 'true' if files in the 'table_related_paths' list changed, 'false' otherwise.
table_paths_changed: ${{ steps.filter.outputs.table_related_paths }}
background_cb_changed: ${{ steps.filter.outputs.background_paths }}
backend_cb_changed: ${{ steps.filter.outputs.backend_paths }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
Expand All @@ -37,6 +38,9 @@ jobs:
- 'tests/background_callback/**'
- 'tests/async_tests/**'
- 'requirements/**'
backend_paths:
- 'dash/backend/**'
- 'tests/backend/**'

build:
name: Build Dash Package
Expand Down Expand Up @@ -271,6 +275,109 @@ jobs:
cd bgtests
pytest --headless --nopercyfinalize tests/async_tests -v -s

backend-tests:
name: Run Backend Callback Tests (Python ${{ matrix.python-version }})
needs: [build, changes_filter]
if: |
(github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/dev')) ||
needs.changes_filter.outputs.backend_cb_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.12"]

services:
redis:
image: redis:6
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5

env:
REDIS_URL: redis://localhost:6379
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Install Node.js dependencies
run: npm ci

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'

- name: Download built Dash packages
uses: actions/download-artifact@v4
with:
name: dash-packages
path: packages/

- name: Install Dash packages
run: |
python -m pip install --upgrade pip wheel
python -m pip install "setuptools<78.0.0"
python -m pip install "selenium==4.32.0"
find packages -name dash-*.whl -print -exec sh -c 'pip install "{}[async,ci,testing,dev,celery,diskcache,fastapi,quart]"' \;

- name: Install Google Chrome
run: |
sudo apt-get update
sudo apt-get install -y google-chrome-stable

- name: Install ChromeDriver
run: |
echo "Determining Chrome version..."
CHROME_BROWSER_VERSION=$(google-chrome --version)
echo "Installed Chrome Browser version: $CHROME_BROWSER_VERSION"
CHROME_MAJOR_VERSION=$(echo "$CHROME_BROWSER_VERSION" | cut -f 3 -d ' ' | cut -f 1 -d '.')
echo "Detected Chrome Major version: $CHROME_MAJOR_VERSION"
if [ "$CHROME_MAJOR_VERSION" -ge 115 ]; then
echo "Fetching ChromeDriver version for Chrome $CHROME_MAJOR_VERSION using CfT endpoint..."
CHROMEDRIVER_VERSION_STRING=$(curl -sS "https://googlechromelabs.github.io/chrome-for-testing/LATEST_RELEASE_${CHROME_MAJOR_VERSION}")
if [ -z "$CHROMEDRIVER_VERSION_STRING" ]; then
echo "Could not automatically find ChromeDriver version for Chrome $CHROME_MAJOR_VERSION via LATEST_RELEASE. Please check CfT endpoints."
exit 1
fi
CHROMEDRIVER_URL="https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/${CHROMEDRIVER_VERSION_STRING}/linux64/chromedriver-linux64.zip"
else
echo "Fetching ChromeDriver version for Chrome $CHROME_MAJOR_VERSION using older method..."
CHROMEDRIVER_VERSION_STRING=$(curl -sS "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_${CHROME_MAJOR_VERSION}")
CHROMEDRIVER_URL="https://chromedriver.storage.googleapis.com/${CHROMEDRIVER_VERSION_STRING}/chromedriver_linux64.zip"
fi
echo "Using ChromeDriver version string: $CHROMEDRIVER_VERSION_STRING"
echo "Downloading ChromeDriver from: $CHROMEDRIVER_URL"
wget -q -O chromedriver.zip "$CHROMEDRIVER_URL"
unzip -o chromedriver.zip -d /tmp/
sudo mv /tmp/chromedriver-linux64/chromedriver /usr/local/bin/chromedriver || sudo mv /tmp/chromedriver /usr/local/bin/chromedriver
sudo chmod +x /usr/local/bin/chromedriver
echo "/usr/local/bin" >> $GITHUB_PATH
shell: bash

- name: Build/Setup test components
run: npm run setup-tests.py

- name: Run Backend Callback Tests
run: |
mkdir bgtests
cp -r tests bgtests/tests
cd bgtests
touch __init__.py
pytest --headless --nopercyfinalize tests/backend_tests -v -s

table-unit:
name: Table Unit/Lint Tests (Python ${{ matrix.python-version }})
needs: [build, changes_filter]
Expand Down
41 changes: 21 additions & 20 deletions dash/_callback.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
from typing import Callable, Optional, Any, List, Tuple, Union
from functools import wraps
import collections
import hashlib
from functools import wraps

from typing import Callable, Optional, Any, List, Tuple, Union


import asyncio
from dash.backend import get_request_adapter

from .dependencies import (
handle_callback_args,
Expand Down Expand Up @@ -39,10 +35,11 @@
clean_property_name,
)

from . import _validate
from .background_callback.managers import BaseBackgroundCallbackManager
from ._callback_context import context_value
from ._no_update import NoUpdate
from . import _validate
from . import backends


async def _async_invoke_callback(
Expand Down Expand Up @@ -176,7 +173,6 @@ def callback(
Note that the endpoint will not appear in the list of registered
callbacks in the Dash devtools.
"""

background_spec = None

config_prevent_initial_callbacks = _kwargs.pop(
Expand Down Expand Up @@ -376,7 +372,8 @@ def _get_callback_manager(
" and store results on redis.\n"
)

old_job = get_request_adapter().get_args().getlist("oldJob")
adapter = backends.request_adapter()
old_job = adapter.args.getlist("oldJob") if hasattr(adapter.args, "getlist") else []

if old_job:
for job in old_job:
Expand All @@ -390,21 +387,20 @@ def _setup_background_callback(
):
"""Set up the background callback and manage jobs."""
callback_manager = _get_callback_manager(kwargs, background)
if not callback_manager:
return to_json({"error": "No background callback manager configured"})

progress_outputs = background.get("progress")

cache_ignore_triggered = background.get("cache_ignore_triggered", True)

cache_key = callback_manager.build_cache_key(
func,
# Inputs provided as dict is kwargs.
func_args if func_args else func_kwargs,
background.get("cache_args_to_ignore", []),
None if cache_ignore_triggered else callback_ctx.get("triggered_inputs", []),
)

job_fn = callback_manager.func_registry.get(background_key)

ctx_value = AttributeDict(**context_value.get())
ctx_value.ignore_register_page = True
ctx_value.pop("background_callback_manager")
Expand Down Expand Up @@ -436,7 +432,8 @@ def _setup_background_callback(

def _progress_background_callback(response, callback_manager, background):
progress_outputs = background.get("progress")
cache_key = get_request_adapter().get_args().get("cacheKey")
adapter = backends.request_adapter()
cache_key = adapter.args.get("cacheKey")

if progress_outputs:
# Get the progress before the result as it would be erased after the results.
Expand All @@ -453,8 +450,9 @@ def _update_background_callback(
"""Set up the background callback and manage jobs."""
callback_manager = _get_callback_manager(kwargs, background)

cache_key = get_request_adapter().get_args().get("cacheKey")
job_id = get_request_adapter().get_args().get("job")
adapter = backends.request_adapter()
cache_key = adapter.args.get("cacheKey") if adapter else None
job_id = adapter.args.get("job") if adapter else None

_progress_background_callback(response, callback_manager, background)

Expand All @@ -474,8 +472,9 @@ def _handle_rest_background_callback(
multi,
has_update=False,
):
cache_key = get_request_adapter().get_args().get("cacheKey")
job_id = get_request_adapter().get_args().get("job")
adapter = backends.request_adapter()
cache_key = adapter.args.get("cacheKey") if adapter else None
job_id = adapter.args.get("job") if adapter else None
# Must get job_running after get_result since get_results terminates it.
job_running = callback_manager.job_running(job_id)
if not job_running and output_value is callback_manager.UNDEFINED:
Expand Down Expand Up @@ -688,10 +687,11 @@ def add_context(*args, **kwargs):
)

response: dict = {"multi": True}
jsonResponse = None
jsonResponse: Optional[str] = None
try:
if background is not None:
if not get_request_adapter().get_args().get("cacheKey"):
adapter = backends.request_adapter()
if not (adapter and adapter.args.get("cacheKey")):
return _setup_background_callback(
kwargs,
background,
Expand Down Expand Up @@ -762,7 +762,8 @@ async def async_add_context(*args, **kwargs):

try:
if background is not None:
if not get_request_adapter().get_args().get("cacheKey"):
adapter = backends.request_adapter()
if not (adapter and adapter.args.get("cacheKey")):
return _setup_background_callback(
kwargs,
background,
Expand Down
32 changes: 17 additions & 15 deletions dash/_pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,18 +318,22 @@ def register_page(
)
page.update(
supplied_title=title,
title=title
if title is not None
else CONFIG.title
if CONFIG.title != "Dash"
else page["name"],
title=(
title
if title is not None
else CONFIG.title
if CONFIG.title != "Dash"
else page["name"]
),
)
page.update(
description=description
if description
else CONFIG.description
if CONFIG.description
else "",
description=(
description
if description
else CONFIG.description
if CONFIG.description
else ""
),
order=order,
supplied_order=order,
supplied_layout=layout,
Expand Down Expand Up @@ -390,15 +394,13 @@ def _path_to_page(path_id):


def _page_meta_tags(app, request):
request_path = request.get_path()
request_path = request.path
start_page, path_variables = _path_to_page(request_path.strip("/"))

image = start_page.get("image", "")
if image:
image = app.get_asset_url(image)
assets_image_url = (
"".join([request.get_root(), image.lstrip("/")]) if image else None
)
assets_image_url = "".join([request.root, image.lstrip("/")]) if image else None
supplied_image_url = start_page.get("image_url")
image_url = supplied_image_url if supplied_image_url else assets_image_url

Expand All @@ -413,7 +415,7 @@ def _page_meta_tags(app, request):
return [
{"name": "description", "content": description},
{"property": "twitter:card", "content": "summary_large_image"},
{"property": "twitter:url", "content": request.get_url()},
{"property": "twitter:url", "content": request.url},
{"property": "twitter:title", "content": title},
{"property": "twitter:description", "content": description},
{"property": "twitter:image", "content": image_url or ""},
Expand Down
39 changes: 39 additions & 0 deletions dash/_validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from ._grouping import grouping_len, map_grouping
from ._no_update import NoUpdate
from .development.base_component import Component
from . import backends
from . import exceptions
from ._utils import (
patch_collections_abc,
Expand Down Expand Up @@ -585,3 +586,41 @@ def _valid(out):
return

_valid(output)


def check_async(use_async):
if use_async is None:
try:
import asgiref # pylint: disable=unused-import, import-outside-toplevel # noqa

use_async = True
except ImportError:
pass
elif use_async:
try:
import asgiref # pylint: disable=unused-import, import-outside-toplevel # noqa
except ImportError as exc:
raise Exception(
"You are trying to use dash[async] without having installed the requirements please install via: `pip install dash[async]`"
) from exc


def check_backend(backend, inferred_backend):
if backend is not None:
if isinstance(backend, type):
# get_backend returns the backend class for a string
# So we compare the class names
expected_backend_cls, _ = backends.get_backend(inferred_backend)
if (
backend.__module__ != expected_backend_cls.__module__
or backend.__name__ != expected_backend_cls.__name__
):
raise ValueError(
f"Conflict between provided backend '{backend.__name__}' and server type '{inferred_backend}'."
)
elif not isinstance(backend, str):
raise ValueError("Invalid backend argument")
elif backend.lower() != inferred_backend:
raise ValueError(
f"Conflict between provided backend '{backend}' and server type '{inferred_backend}'."
)
15 changes: 0 additions & 15 deletions dash/backend/__init__.py

This file was deleted.

Loading
Loading