Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

3.0 RC3 changes. #3175

Merged
merged 11 commits into from
Feb 21, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';


const ExternalComponent = ({ id, text, input_id }) => {
const ExternalComponent = ({ id, text, input_id, extra_component }) => {
const ctx = window.dash_component_api.useDashContext();
const ExternalWrapper = window.dash_component_api.ExternalWrapper;

Expand All @@ -15,6 +15,14 @@ const ExternalComponent = ({ id, text, input_id }) => {
value={text}
componentPath={[...ctx.componentPath, 'external']}
/>
{
extra_component &&
<ExternalWrapper
componentType={extra_component.type}
componentNamespace={extra_component.namespace}
componentPath={[...ctx.componentPath, 'extra']}
{...extra_component.props}
/>}
</div>
)
}
Expand All @@ -23,6 +31,11 @@ ExternalComponent.propTypes = {
id: PropTypes.string,
text: PropTypes.string,
input_id: PropTypes.string,
extra_component: PropTypes.exact({
type: PropTypes.string,
namespace: PropTypes.string,
props: PropTypes.object,
}),
};

export default ExternalComponent;
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@
All notable changes to `dash` will be documented in this file.
This project adheres to [Semantic Versioning](https://semver.org/).

## [3.0.0-rc3] - 2025-02-21

## Added

- [#3121](https://github.com/plotly/dash/pull/3121) Restyle and add version checker to dev tools.
- [#3175](https://github.com/plotly/dash/pull/3175) Add `custom_data` hook.
- [#3175](https://github.com/plotly/dash/pull/3175) Improved error for removed Dash app attribute, run_server and long_callback
- [#3175](https://github.com/plotly/dash/pull/3175) Expose `stringifyId` in `window.dash_component_api`.

## Fixed

- [#3175](https://github.com/plotly/dash/pull/3175) Fix `ExternalWrapper` rendering children and support pattern matching ids.

## [3.0.0-rc2] - 2025-02-18

## Added
Expand Down
8 changes: 8 additions & 0 deletions dash/_callback_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,14 @@ def origin(self):
"""
return _get_from_context("origin", "")

@property
@has_context
def custom_data(self):
"""
Custom data set by hooks.custom_data.
"""
return _get_from_context("custom_data", {})


callback_context = CallbackContext()

Expand Down
4 changes: 2 additions & 2 deletions dash/_dash_renderer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import os

__version__ = "2.0.1"
__version__ = "2.0.2"

_available_react_versions = {"18.3.1", "18.2.0", "16.14.0"}
_available_reactdom_versions = {"18.3.1", "18.2.0", "16.14.0"}
Expand Down Expand Up @@ -64,7 +64,7 @@ def _set_react_version(v_react, v_reactdom=None):
{
"relative_package_path": "dash-renderer/build/dash_renderer.min.js",
"dev_package_path": "dash-renderer/build/dash_renderer.dev.js",
"external_url": "https://unpkg.com/[email protected].1"
"external_url": "https://unpkg.com/[email protected].2"
"/build/dash_renderer.min.js",
"namespace": "dash",
},
Expand Down
23 changes: 23 additions & 0 deletions dash/_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def __init__(self) -> None:
"error": [],
"callback": [],
"index": [],
"custom_data": [],
}
self._js_dist = []
self._css_dist = []
Expand Down Expand Up @@ -191,6 +192,28 @@ def wrap(func):

return wrap

def custom_data(
self, namespace: str, priority: _t.Optional[int] = None, final=False
):
"""
Add data to the callback_context.custom_data property under the namespace.

The hook function takes the current context_value and before the ctx is set
and has access to the flask request context.
"""

def wrap(func: _t.Callable[[_t.Dict], _t.Any]):
self.add_hook(
"custom_data",
func,
priority=priority,
final=final,
data={"namespace": namespace},
)
return func

return wrap


hooks = _Hooks()

Expand Down
23 changes: 23 additions & 0 deletions dash/_obsolete.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# pylint: disable=too-few-public-methods
from .exceptions import ObsoleteAttributeException


class ObsoleteAttribute:
def __init__(self, message: str, exc=ObsoleteAttributeException):
self.message = message
self.exc = exc


class ObsoleteChecker:
_obsolete_attributes = {
"run_server": ObsoleteAttribute("app.run_server has been replaced by app.run"),
"long_callback": ObsoleteAttribute(
"app.long_callback has been removed, use app.callback(..., background=True) instead"
),
}

def __getattr__(self, name: str):
if name in self._obsolete_attributes:
err = self._obsolete_attributes[name]
raise err.exc(err.message)
return getattr(self, name)
4 changes: 2 additions & 2 deletions dash/dash-renderer/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions dash/dash-renderer/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dash-renderer",
"version": "2.0.1",
"version": "2.0.2",
"description": "render dash components in react",
"main": "build/dash_renderer.min.js",
"scripts": {
Expand All @@ -13,7 +13,7 @@
"build:dev": "webpack",
"build:local": "renderer build local",
"build": "renderer build && npm run prepublishOnly",
"postbuild": "es-check es2018 ../deps/*.js build/*.js",
"postbuild": "es-check es2015 ../deps/*.js build/*.js",
"test": "karma start karma.conf.js --single-run",
"format": "run-s private::format.*",
"lint": "run-s private::lint.* --continue-on-error"
Expand Down
10 changes: 9 additions & 1 deletion dash/dash-renderer/src/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {getAction} from './constants';
import cookie from 'cookie';
import {validateCallbacksToLayout} from './dependencies';
import {includeObservers, getLayoutCallbacks} from './dependencies_ts';
import {getPath} from './paths';
import {computePaths, getPath} from './paths';

export const onError = createAction(getAction('ON_ERROR'));
export const setAppLifecycle = createAction(getAction('SET_APP_LIFECYCLE'));
Expand All @@ -21,6 +21,14 @@ export const updateProps = createAction(getAction('ON_PROP_CHANGE'));
export const insertComponent = createAction(getAction('INSERT_COMPONENT'));
export const removeComponent = createAction(getAction('REMOVE_COMPONENT'));

export const addComponentToLayout = payload => (dispatch, getState) => {
const {paths} = getState();
dispatch(insertComponent(payload));
dispatch(
setPaths(computePaths(payload.component, payload.componentPath, paths))
);
};

export const dispatchError = dispatch => (message, lines) =>
dispatch(
onError({
Expand Down
4 changes: 3 additions & 1 deletion dash/dash-renderer/src/dashApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {DashContext, useDashContext} from './wrapper/DashContext';
import {getPath} from './actions/paths';
import {getStores} from './utils/stores';
import ExternalWrapper from './wrapper/ExternalWrapper';
import {stringifyId} from './actions/dependencies';

/**
* Get the dash props from a component path or id.
Expand Down Expand Up @@ -32,5 +33,6 @@ function getLayout(componentPathOrId: string[] | string): any {
ExternalWrapper,
DashContext,
useDashContext,
getLayout
getLayout,
stringifyId
};
24 changes: 18 additions & 6 deletions dash/dash-renderer/src/wrapper/ExternalWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import React, {useState, useEffect} from 'react';
import {useDispatch} from 'react-redux';
import {batch, useDispatch} from 'react-redux';

import {DashLayoutPath} from '../types/component';
import DashWrapper from './DashWrapper';
import {insertComponent, removeComponent} from '../actions';
import {
addComponentToLayout,
notifyObservers,
removeComponent,
updateProps
} from '../actions';

type Props = {
componentPath: DashLayoutPath;
Expand All @@ -21,18 +26,18 @@ function ExternalWrapper({
componentPath,
...props
}: Props) {
const dispatch = useDispatch();
const dispatch: any = useDispatch();
const [inserted, setInserted] = useState(false);

useEffect(() => {
// Give empty props for the inserted components.
// The props will come from the parent so they can be updated.
dispatch(
insertComponent({
addComponentToLayout({
component: {
type: componentType,
namespace: componentNamespace,
props: {}
props: props
},
Comment on lines +36 to 41
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this unpack in a different way than the standard dash components?

For ease of use, would it be easier to just pass a regular dash component {type, namespace, props} directly here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefixed those type and namespace with component in the ExternalWrapper props to be able to use type in the ...props since that is a common prop name.

Copy link
Contributor

@BSd3v BSd3v Feb 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But, doesnt Dash deal with this by putting those into props and passing the whole object to it? Its not normal to break apart the props in the way that the ExternalWrapper is accepting it.

eg. Just the way it is above. Its creating the props object by the leftover props by default.

componentPath
})
Expand All @@ -43,10 +48,17 @@ function ExternalWrapper({
};
}, []);

useEffect(() => {
batch(() => {
dispatch(updateProps({itempath: componentPath, props}));
dispatch(notifyObservers({id: props.id, props}));
});
}, [props]);

if (!inserted) {
return null;
}
// Render a wrapper with the actual props.
return <DashWrapper componentPath={componentPath} {...props} />;
return <DashWrapper componentPath={componentPath} />;
}
export default ExternalWrapper;
7 changes: 6 additions & 1 deletion dash/dash.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
from . import _get_app

from ._grouping import map_grouping, grouping_len, update_args_group
from ._obsolete import ObsoleteChecker

from . import _pages
from ._pages import (
Expand Down Expand Up @@ -207,7 +208,7 @@ def _do_skip(error):

# pylint: disable=too-many-instance-attributes
# pylint: disable=too-many-arguments, too-many-locals
class Dash:
class Dash(ObsoleteChecker):
"""Dash is a framework for building analytical web applications.
No JavaScript required.

Expand Down Expand Up @@ -1388,6 +1389,10 @@ def dispatch(self):
g.path = flask.request.full_path
g.remote = flask.request.remote_addr
g.origin = flask.request.origin
g.custom_data = AttributeDict({})

for hook in self._hooks.get_hooks("custom_data"):
g.custom_data[hook.data["namespace"]] = hook(g)

except KeyError as missing_callback_function:
msg = f"Callback function not found for output '{output}', perhaps you forgot to prepend the '@'?"
Expand Down
4 changes: 4 additions & 0 deletions dash/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ class ObsoleteKwargException(DashException):
pass


class ObsoleteAttributeException(DashException):
pass


class NoLayoutException(DashException):
pass

Expand Down
2 changes: 1 addition & 1 deletion dash/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "3.0.0rc2"
__version__ = "3.0.0rc3"
32 changes: 30 additions & 2 deletions tests/integration/renderer/test_external_component.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from dash import Dash, html, dcc, html, Input, Output, State
from dash import Dash, html, dcc, html, Input, Output, State, MATCH
from dash_test_components import ExternalComponent


Expand All @@ -8,7 +8,22 @@ def test_rext001_render_external_component(dash_duo):
[
dcc.Input(id="sync", value="synced"),
html.Button("sync", id="sync-btn"),
ExternalComponent("ext", input_id="external", text="external"),
ExternalComponent(
id="ext",
input_id="external",
text="external",
extra_component={
"type": "Div",
"namespace": "dash_html_components",
"props": {
"id": "extra",
"children": [
html.Div("extra children", id={"type": "extra", "index": 1})
],
},
},
),
html.Div(html.Div(id={"type": "output", "index": 1}), id="out"),
Comment on lines +11 to +26
Copy link
Contributor

@BSd3v BSd3v Feb 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldnt this be rewritten as:
extra_component = html.Div(html.Div('extra_children', id={'index':1, 'type': 'extra'}),id='extra')

Or was this to test the different ways this could be listed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it can they get serialized on the backend, there is the two ways tested here with the children as a div.

]
)

Expand All @@ -21,7 +36,20 @@ def test_rext001_render_external_component(dash_duo):
def on_sync(_, value):
return value

@app.callback(
Output({"type": "output", "index": MATCH}, "children"),
Input({"type": "extra", "index": MATCH}, "n_clicks"),
prevent_initial_call=True,
)
def click(*_):
return "clicked"

dash_duo.start_server(app)
dash_duo.wait_for_text_to_equal("#external", "external")
dash_duo.find_element("#sync-btn").click()
dash_duo.wait_for_text_to_equal("#external", "synced")

dash_duo.wait_for_text_to_equal("#extra", "extra children")

dash_duo.find_element("#extra > div").click()
dash_duo.wait_for_text_to_equal("#out", "clicked")
Loading