Skip to content

Add plotly run command #3329

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

Open
wants to merge 6 commits into
base: dev
Choose a base branch
from
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
44 changes: 44 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 }}
cli_changed: ${{ steps.filter.outputs.cli_paths }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
Expand All @@ -35,6 +36,11 @@ jobs:
- 'dash/_callback.py'
- 'dash/_callback_context.py'
- 'requirements/**'
cli_paths:
- 'dash/_cli.py'
- 'dash/__main__.py'
- 'dash/dash.py'
- 'tests/tooling/test_cli.py'

build:
name: Build Dash Package
Expand Down Expand Up @@ -135,6 +141,44 @@ jobs:
run: |
cd tests
pytest compliance/test_typing.py

test-run-cli:
name: Test plotly run CLI
runs-on: ubuntu-latest
needs: [build, changes_filter]
if: |
(github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/dev')) ||
needs.changes_filter.outputs.cli_changed == 'true'
timeout-minutes: 30
strategy:
fail-fast: false

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
cache-dependency-path: requirements/*.txt

- 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<80.0.0"
find packages -name dash-*.whl -print -exec sh -c 'pip install "{}[ci,testing]"' \;

- name: Run CLI tests
run: |
pytest tests/tooling/test_cli.py

background-callbacks:
name: Run Background Callback Tests (Python ${{ matrix.python-version }})
Expand Down
3 changes: 3 additions & 0 deletions dash/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from ._cli import cli

cli()
172 changes: 172 additions & 0 deletions dash/_cli.py
Copy link

Choose a reason for hiding this comment

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

Do we want to use that in the CI for some test to avoid regression?

Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import argparse
import importlib
import sys
from typing import Any, Dict

from dash import Dash


def load_app(app_path: str) -> Dash:
"""
Load a Dash app instance from a string like "module:variable".

:param app_path: The import path to the Dash app instance.
:return: The loaded Dash app instance.
"""
app_split = app_path.split(":")
Copy link
Contributor

Choose a reason for hiding this comment

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

check that len(app_split) == 2 here?

Copy link
Contributor

Choose a reason for hiding this comment

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

OK, I see that further down you're allowing users to provide just the module name, then using the first Dash you find in that module.

module_str = app_split[0]

if not module_str:
raise ValueError(f"Invalid app path: '{app_path}'. ")

try:
module = importlib.import_module(module_str)
except ImportError as e:
raise ImportError(f"Could not import module '{module_str}'.") from e

app_instance = None
if len(app_split) == 2:
app_str = app_split[1]
try:
app_instance = getattr(module, app_str)
except AttributeError as e:
raise AttributeError(
f"Could not find variable '{app_str}' in module '{module_str}'."
) from e
else:
for module_var in vars(module).values():
if isinstance(module_var, Dash):
app_instance = module_var
break

if not isinstance(app_instance, Dash):
Copy link
Contributor

Choose a reason for hiding this comment

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

if I run with just the module name, but the module doesn't define a Dash, I think app_instance will be uninitialized at the point of the isinstance() check.

raise TypeError(f"'{app_path}' did not resolve to a Dash app instance.")

return app_instance


def create_parser() -> argparse.ArgumentParser:
"""Create the argument parser for the Plotly CLI."""
parser = argparse.ArgumentParser(
description="A command line interface for Plotly Dash."
)
subparsers = parser.add_subparsers(dest="command", required=True)

# --- `run` command ---
run_parser = subparsers.add_parser(
"run",
help="Run a Dash app.",
description="Run a local development server for a Dash app.",
)

run_parser.add_argument(
"app",
help='The Dash app to run, in the format "module:variable" '
'or just "module" to find the app instance automatically. (eg: plotly run app)',
)

# Server options
run_parser.add_argument(
"--host",
type=str,
help='Host IP used to serve the application (Default: "127.0.0.1").',
)
run_parser.add_argument(
"--port",
"-p",
type=int,
help='Port used to serve the application (Default: "8050").',
)
run_parser.add_argument(
"--proxy",
type=str,
help='Proxy configuration string, e.g., "http://0.0.0.0:8050::https://my.domain.com".',
)

run_parser.add_argument(
"--debug",
"-d",
action="store_true",
help="Enable/disable Flask debug mode and dev tools.",
)

# Dev Tools options
dev_tools_group = run_parser.add_argument_group("dev tools options")
dev_tools_group.add_argument(
"--dev-tools-ui",
action="store_true",
help="Enable/disable the dev tools UI.",
)
dev_tools_group.add_argument(
"--dev-tools-props-check",
action="store_true",
help="Enable/disable component prop validation.",
)
dev_tools_group.add_argument(
"--dev-tools-serve-dev-bundles",
action="store_true",
help="Enable/disable serving of dev bundles.",
)
dev_tools_group.add_argument(
"--dev-tools-hot-reload",
action="store_true",
help="Enable/disable hot reloading.",
)
dev_tools_group.add_argument(
"--dev-tools-hot-reload-interval",
type=float,
help="Interval in seconds for hot reload polling (Default: 3).",
)
dev_tools_group.add_argument(
"--dev-tools-hot-reload-watch-interval",
type=float,
help="Interval in seconds for server-side file watch polling (Default: 0.5).",
)
dev_tools_group.add_argument(
"--dev-tools-hot-reload-max-retry",
type=int,
help="Max number of failed hot reload requests before failing (Default: 8).",
)
dev_tools_group.add_argument(
"--dev-tools-silence-routes-logging",
action="store_true",
help="Enable/disable silencing of Werkzeug's route logging.",
)
dev_tools_group.add_argument(
"--dev-tools-disable-version-check",
action="store_true",
help="Enable/disable the Dash version upgrade check.",
)
dev_tools_group.add_argument(
"--dev-tools-prune-errors",
action="store_true",
help="Enable/disable pruning of tracebacks to user code only.",
)

return parser


def cli():
"""The main entry point for the Plotly CLI."""
sys.path.insert(0, ".")
parser = create_parser()
args = parser.parse_args()

try:
if args.command == "run":
app = load_app(args.app)

# Collect arguments to pass to the app.run() method.
# Only include arguments that were actually provided on the CLI
# or have a default value in the parser.
run_options: Dict[str, Any] = {
key: value
for key, value in vars(args).items()
if value is not None and key not in ["command", "app"]
}

app.run(**run_options)

except (ValueError, ImportError, AttributeError, TypeError) as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
37 changes: 20 additions & 17 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from setuptools import setup, find_packages

main_ns = {}
exec(open("dash/version.py", encoding="utf-8").read(), main_ns) # pylint: disable=exec-used, consider-using-with
# pylint: disable=exec-used, consider-using-with
exec(open("dash/version.py", encoding="utf-8").read(), main_ns)


def read_req_file(req_type):
Expand All @@ -21,10 +22,10 @@ def read_req_file(req_type):
include_package_data=True,
license="MIT",
description=(
"A Python framework for building reactive web-apps. "
"Developed by Plotly."
"A Python framework for building reactive web-apps. " "Developed by Plotly."
),
long_description=io.open("README.md", encoding="utf-8").read(), # pylint: disable=consider-using-with
# pylint: disable=consider-using-with
long_description=io.open("README.md", encoding="utf-8").read(),
long_description_content_type="text/markdown",
install_requires=read_req_file("install"),
python_requires=">=3.8",
Expand All @@ -34,14 +35,14 @@ def read_req_file(req_type):
"testing": read_req_file("testing"),
"celery": read_req_file("celery"),
"diskcache": read_req_file("diskcache"),
"compress": read_req_file("compress")
"compress": read_req_file("compress"),
},
entry_points={
"console_scripts": [
"dash-generate-components = "
"dash.development.component_generator:cli",
"dash-generate-components = dash.development.component_generator:cli",
"renderer = dash.development.build_process:renderer",
"dash-update-components = dash.development.update_components:cli"
"dash-update-components = dash.development.update_components:cli",
"plotly = dash._cli:cli",
],
"pytest11": ["dash = dash.testing.plugin"],
},
Expand Down Expand Up @@ -78,16 +79,18 @@ def read_req_file(req_type):
],
data_files=[
# like `jupyter nbextension install --sys-prefix`
("share/jupyter/nbextensions/dash", [
"dash/nbextension/main.js",
]),
(
"share/jupyter/nbextensions/dash",
[
"dash/nbextension/main.js",
],
),
# like `jupyter nbextension enable --sys-prefix`
("etc/jupyter/nbconfig/notebook.d", [
"dash/nbextension/dash.json"
]),
("etc/jupyter/nbconfig/notebook.d", ["dash/nbextension/dash.json"]),
# Place jupyterlab extension in extension directory
("share/jupyter/lab/extensions", [
"dash/labextension/dist/dash-jupyterlab.tgz"
]),
(
"share/jupyter/lab/extensions",
["dash/labextension/dist/dash-jupyterlab.tgz"],
),
],
)
Loading