From 3a674165902270e4bd66b40161c4e23a8f4e1f53 Mon Sep 17 00:00:00 2001 From: m96-chan Date: Mon, 7 Sep 2026 00:22:32 +0900 Subject: [PATCH] fix: Declare pandas in the dashboard extra `dashboard.py` guards its gradio import and then imports pandas unguarded, but the extra only listed gradio. It has worked so far because gradio depends on pandas, so the extra has been relying on another package's dependency list to be complete. If gradio ever drops that dependency the failure is a bare ModuleNotFoundError, and the advice already attached to the gradio guard -- "Install it with: pip install pyetwkit[dashboard]" -- is no help, because the user has run exactly that. The test reads the installed metadata rather than importing gradio, which is heavy and not installed in CI. It checks the extra declares what the module imports, so adding another unguarded import without declaring it will fail. Closes #93 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01HhPcm483PrDmuQEUSALBFn --- pyproject.toml | 4 ++++ tests/test_dashboard.py | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 0106a70..c732b29 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,10 @@ docs = [ ] dashboard = [ "gradio>=4.0", + # dashboard.py imports pandas directly, without the guard it puts around + # gradio. It only works today because gradio happens to depend on pandas; + # declare it rather than rely on another package's dependency list. + "pandas>=2.0", ] [project.urls] diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index 0032b35..8c5dc51 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -246,3 +246,40 @@ def test_dashboard_websocket_url(self) -> None: dashboard = Dashboard(host="localhost", port=8080) assert dashboard.ws_url == "ws://localhost:8080/ws" + + +class TestDashboardExtra: + """The `dashboard` extra must declare what dashboard.py imports. + + `_create_gradio_app` guards its gradio import and then imports pandas + unguarded. That works only because gradio happens to depend on pandas, so + the extra has been relying on another package's dependency list. If gradio + ever drops it, the failure is a bare ModuleNotFoundError with an unhelpful + "pip install pyetwkit[dashboard]" already satisfied. + """ + + def test_extra_declares_every_package_the_dashboard_imports(self) -> None: + import importlib.metadata + import re + + requires = importlib.metadata.requires("pyetwkit") or [] + # The marker is quoted differently depending on which build produced the + # metadata, so match either form rather than one of them. + marker = re.compile(r"""extra\s*==\s*['"]dashboard['"]""") + declared = { + re.split(r"[<>=!~\[ ;]", requirement)[0].strip().lower() + for requirement in requires + if marker.search(requirement) + } + + assert "gradio" in declared, sorted(declared) + assert "pandas" in declared, ( + "dashboard.py imports pandas unguarded, so the dashboard extra has " + f"to declare it. Declared: {sorted(declared)}" + ) + + def test_dashboard_module_imports_without_the_extra(self) -> None: + """Only `_create_gradio_app` needs gradio; importing must not.""" + from pyetwkit.dashboard import Dashboard + + assert Dashboard is not None