Skip to content
Open
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
102 changes: 102 additions & 0 deletions .github/workflows/post-test-status.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
name: Post Test Status

# This workflow runs in the base repo context (with write permissions)
# even for fork PRs, allowing us to post commit statuses and check runs.
#
# Add any status posting or check run creation for fork PRs here to avoid
# "Resource not accessible by integration" errors.

on:
workflow_run:
workflows: ["Dash Testing"]
types:
- completed

jobs:
post-skipped-statuses:
name: Post Statuses for Skipped Jobs
runs-on: ubuntu-latest
if: github.event.workflow_run.event == 'pull_request'
permissions:
statuses: write
actions: read
steps:
- name: Post statuses for skipped jobs
uses: actions/github-script@v7
with:
script: |
const { owner, repo } = context.repo;
const runId = context.payload.workflow_run.id;
const headSha = context.payload.workflow_run.head_sha;

// Define jobs that need a success status posted when skipped
const skippedStatusJobs = [
{
jobName: 'Dash Table Visual Tests',
statusContext: 'percy/dash-table-test',
description: 'Skipped — no dash-table changes'
}
// Add more jobs here as needed
];

// Get all jobs for the workflow run
const { data: { jobs } } = await github.rest.actions.listJobsForWorkflowRun({
owner,
repo,
run_id: runId,
});

// Post status for each skipped job
for (const { jobName, statusContext, description } of skippedStatusJobs) {
const job = jobs.find(j => j.name === jobName);

if (job && job.conclusion === 'skipped') {
await github.rest.repos.createCommitStatus({
owner,
repo,
sha: headSha,
state: 'success',
context: statusContext,
description: description,
});
console.log(`Posted skipped status for ${statusContext}`);
} else {
console.log(`Job "${jobName}" status: ${job?.conclusion ?? 'not found'} - no status posted`);
}
}

test-report:
name: Consolidated Test Report (Fork PR)
runs-on: ubuntu-latest
# Only run for fork PRs (non-fork PRs are handled in the main workflow)
if: |
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.head_repository.full_name != github.repository
permissions:
checks: write
actions: read
steps:
- name: Download test results artifact
uses: actions/download-artifact@v4
with:
pattern: '*-results-*'
path: test-results
merge-multiple: false
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}

- name: List downloaded results
run: find test-results -name "*.xml" -type f 2>/dev/null || echo "No XML files found"

- name: Publish Test Report
uses: dorny/test-reporter@v1
if: always()
with:
name: Test Results Summary
path: 'test-results/**/*.xml'
reporter: java-junit
fail-on-error: false
fail-on-empty: false
list-suites: 'failed'
list-tests: 'failed'
max-annotations: '50'
27 changes: 2 additions & 25 deletions .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -918,30 +918,6 @@ jobs:
if: env.PERCY_TOKEN == ''
run: echo "::notice::Skipping Percy finalize (no token available - likely a fork PR)"

report-table-percy-skipped:
name: Report Percy Table Skipped
needs: table-visual-test
runs-on: ubuntu-latest
if: |
always() &&
github.event_name == 'pull_request' &&
needs.table-visual-test.result == 'skipped'
permissions:
statuses: write
steps:
- name: Post success status for percy/dash-table-test
uses: actions/github-script@v7
with:
script: |
await github.rest.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha: context.payload.pull_request.head.sha,
state: 'success',
context: 'percy/dash-table-test',
description: 'Skipped — no dash-table changes',
});

test-report:
name: Consolidated Test Report
needs: [lint-unit, test-main, dcc-test, html-test, table-server, background-callbacks, test-typing]
Expand All @@ -963,7 +939,8 @@ jobs:

- name: Publish Test Report
uses: dorny/test-reporter@v1
if: always()
# Skip for fork PRs - handled by post-test-status.yml workflow_run
if: always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
with:
name: Test Results Summary
path: 'test-results/**/*.xml'
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
## Fixed
- [#3690](https://github.com/plotly/dash/pull/3690) Fixes Input when min or max is set to None
- [#3723](https://github.com/plotly/dash/pull/3723) Fix misaligned `dcc.Slider` marks when some labels are empty strings
- [#3738](https://github.com/plotly/dash/pull/3738) Add missing `stacklevel=2` to `warnings.warn()` calls so warnings report the caller's location instead of internal Dash source lines
- [#3740](https://github.com/plotly/dash/pull/3740) Fix cannot tab into dropdowns in Safari
- [#2462](https://github.com/plotly/dash/issues/2462) Allow `MATCH` in `Input`/`State` when the callback's `Output` has no wildcards (fixed-id Output, no Output, or `ALL`-only wildcard Output). `ALLSMALLER` still requires a corresponding `MATCH` in an Output.

Expand Down
4 changes: 3 additions & 1 deletion dash/_callback_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from . import exceptions
from ._utils import AttributeDict, stringify_id


context_value: contextvars.ContextVar[
typing.Dict[str, typing.Any]
] = contextvars.ContextVar("callback_context")
Expand Down Expand Up @@ -177,6 +176,7 @@ def outputs_list(self):
warnings.warn(
"outputs_list is deprecated, use outputs_grouping instead",
DeprecationWarning,
stacklevel=2,
)

return getattr(_get_context_value(), "outputs_list", [])
Expand All @@ -188,6 +188,7 @@ def inputs_list(self):
warnings.warn(
"inputs_list is deprecated, use args_grouping instead",
DeprecationWarning,
stacklevel=2,
)

return getattr(_get_context_value(), "inputs_list", [])
Expand All @@ -199,6 +200,7 @@ def states_list(self):
warnings.warn(
"states_list is deprecated, use args_grouping instead",
DeprecationWarning,
stacklevel=2,
)
return getattr(_get_context_value(), "states_list", [])

Expand Down
43 changes: 27 additions & 16 deletions dash/dash.py
Original file line number Diff line number Diff line change
Expand Up @@ -663,7 +663,8 @@ def __init__( # pylint: disable=too-many-statements
if self.__class__.__name__ == "JupyterDash":
warnings.warn(
"JupyterDash is deprecated, use Dash instead.\n"
"See https://dash.plotly.com/dash-in-jupyter for more details."
"See https://dash.plotly.com/dash-in-jupyter for more details.",
stacklevel=2,
)
self.setup_startup_routes()

Expand Down Expand Up @@ -1139,9 +1140,11 @@ def _generate_css_dist_html(self):

return "\n".join(
[
format_tag("link", link, opened=True)
if isinstance(link, dict)
else f'<link rel="stylesheet" href="{link}">'
(
format_tag("link", link, opened=True)
if isinstance(link, dict)
else f'<link rel="stylesheet" href="{link}">'
)
for link in (external_links + links)
]
)
Expand Down Expand Up @@ -1195,9 +1198,11 @@ def _generate_scripts_html(self) -> str:

return "\n".join(
[
format_tag("script", src)
if isinstance(src, dict)
else f'<script src="{src}"></script>'
(
format_tag("script", src)
if isinstance(src, dict)
else f'<script src="{src}"></script>'
)
for src in srcs
]
+ [f"<script>{src}</script>" for src in self._inline_scripts]
Expand Down Expand Up @@ -1674,9 +1679,11 @@ def _setup_server(self):
# For each callback function, if the hidden parameter uses the default value None,
# replace it with the actual value of the self.config.hide_all_callbacks.
self._callback_list = [
{**_callback, "hidden": self.config.get("hide_all_callbacks", False)}
if _callback.get("hidden") is None
else _callback
(
{**_callback, "hidden": self.config.get("hide_all_callbacks", False)}
if _callback.get("hidden") is None
else _callback
)
for _callback in self._callback_list
]

Expand Down Expand Up @@ -2636,9 +2643,11 @@ async def update(pathname_, search_, **states):
if not self.config.suppress_callback_exceptions:
self.validation_layout = html.Div(
[
asyncio.run(execute_async_function(page["layout"]))
if callable(page["layout"])
else page["layout"]
(
asyncio.run(execute_async_function(page["layout"]))
if callable(page["layout"])
else page["layout"]
)
for page in _pages.PAGE_REGISTRY.values()
]
+ [
Expand Down Expand Up @@ -2708,9 +2717,11 @@ def update(pathname_, search_, **states):
]
self.validation_layout = html.Div(
[
page["layout"]()
if callable(page["layout"])
else page["layout"]
(
page["layout"]()
if callable(page["layout"])
else page["layout"]
)
for page in _pages.PAGE_REGISTRY.values()
]
+ layout
Expand Down
11 changes: 7 additions & 4 deletions dash/development/_jl_components_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,9 +355,11 @@ def nothing_or_string(v):
external_url=nothing_or_string(resource.get("external_url", "")),
dynamic=str(resource.get("dynamic", "nothing")).lower(),
type=metatype,
async_string=":{}".format(str(resource.get("async")).lower())
if "async" in resource.keys()
else "nothing",
async_string=(
":{}".format(str(resource.get("async")).lower())
if "async" in resource.keys()
else "nothing"
),
)
for resource in resources
]
Expand Down Expand Up @@ -468,7 +470,8 @@ def generate_class_string(name, props, description, project_shortname, prefix):
(
'WARNING: prop "{}" in component "{}" is a Julia keyword'
" - REMOVED FROM THE JULIA COMPONENT"
).format(item, name)
).format(item, name),
stacklevel=2,
)

default_paramtext += ", ".join(":{}".format(p) for p in prop_keys)
Expand Down
4 changes: 2 additions & 2 deletions dash/development/_r_components_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
from ._all_keywords import r_keywords
from ._py_components_generation import reorder_props


# Declaring longer string templates as globals to improve
# readability, make method logic clearer to anyone inspecting
# code below
Expand Down Expand Up @@ -216,7 +215,8 @@ def generate_class_string(name, props, project_shortname, prefix):
(
'WARNING: prop "{}" in component "{}" is an R keyword'
" - REMOVED FROM THE R COMPONENT"
).format(item, name)
).format(item, name),
stacklevel=2,
)

default_argtext += ", ".join("{}=NULL".format(p) for p in prop_keys)
Expand Down
4 changes: 3 additions & 1 deletion dash/development/base_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,9 @@ def _validate_deprecation(self):
_ns = getattr(self, "_namespace", "")
deprecation_message = _deprecated_components.get(_ns, {}).get(_type)
if deprecation_message:
warnings.warn(DeprecationWarning(textwrap.dedent(deprecation_message)))
warnings.warn(
DeprecationWarning(textwrap.dedent(deprecation_message)), stacklevel=2
)


ComponentSingleType = typing.Union[str, int, float, Component, None]
Expand Down
4 changes: 2 additions & 2 deletions dash/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from .development.base_component import ComponentRegistry
from . import exceptions


# ResourceType has `async` key, use the init form to be able to provide it.
ResourceType = _tx.TypedDict(
"ResourceType",
Expand Down Expand Up @@ -111,7 +110,8 @@ def _filter_resources(
"or `app.css.append_css`, use `external_scripts` "
"or `external_stylesheets` instead.\n"
"See https://dash.plotly.com/external-resources"
)
),
stacklevel=2,
)
continue
else:
Expand Down
6 changes: 4 additions & 2 deletions dash/testing/browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
from dash.testing.errors import DashAppLoadingError, BrowserError, TestingTimeoutError
from dash.testing.consts import SELENIUM_GRID_DEFAULT


logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -629,7 +628,10 @@ def get_logs(self):
for entry in self.driver.get_log("browser")
if entry["timestamp"] > self._last_ts
]
warnings.warn("get_logs always return None with webdrivers other than Chrome")
warnings.warn(
"get_logs always return None with webdrivers other than Chrome",
stacklevel=2,
)
return None

def reset_log_timestamp(self):
Expand Down
Loading