diff --git a/python/ray/_private/worker.py b/python/ray/_private/worker.py index 69a8327173c9..79b1af201f69 100644 --- a/python/ray/_private/worker.py +++ b/python/ray/_private/worker.py @@ -96,6 +96,7 @@ from ray.util.scheduling_strategies import PlacementGroupSchedulingStrategy from ray.util.tracing.tracing_helper import _import_from_string from ray.widgets import Template +from ray.widgets.util import ensure_ipywidgets_dep SCRIPT_MODE = 0 WORKER_MODE = 1 @@ -1019,6 +1020,10 @@ class BaseContext(metaclass=ABCMeta): Base class for RayContext and ClientContext """ + dashboard_url: Optional[str] + python_version: str + ray_version: str + @abstractmethod def disconnect(self): """ @@ -1036,6 +1041,73 @@ def __enter__(self): def __exit__(self): pass + def _context_table_template(self): + if self.dashboard_url: + dashboard_row = Template("context_dashrow.html.j2").render( + dashboard_url="http://" + self.dashboard_url + ) + else: + dashboard_row = None + + return Template("context_table.html.j2").render( + python_version=self.python_version, + ray_version=self.ray_version, + dashboard_row=dashboard_row, + ) + + def _repr_html_(self): + return Template("context.html.j2").render( + context_logo=Template("context_logo.html.j2").render(), + context_table=self._context_table_template(), + ) + + @ensure_ipywidgets_dep("8") + def _get_widget_bundle(self, **kwargs) -> Dict[str, Any]: + """Get the mimebundle for the widget representation of the context. + + Args: + **kwargs: Passed to the _repr_mimebundle_() function for the widget + + Returns: + Dictionary ("mimebundle") of the widget representation of the context. + """ + import ipywidgets + + disconnect_button = ipywidgets.Button( + description="Disconnect", + disabled=False, + button_style="", + tooltip="Disconnect from the Ray cluster", + layout=ipywidgets.Layout(margin="auto 0px 0px 0px"), + ) + + def disconnect_callback(button): + button.disabled = True + button.description = "Disconnecting..." + self.disconnect() + button.description = "Disconnected" + + disconnect_button.on_click(disconnect_callback) + left_content = ipywidgets.VBox( + [ + ipywidgets.HTML(Template("context_logo.html.j2").render()), + disconnect_button, + ], + layout=ipywidgets.Layout(), + ) + right_content = ipywidgets.HTML(self._context_table_template()) + widget = ipywidgets.HBox( + [left_content, right_content], layout=ipywidgets.Layout(width="100%") + ) + return widget._repr_mimebundle_(**kwargs) + + def _repr_mimebundle_(self, **kwargs): + bundle = self._get_widget_bundle(**kwargs) + + # Overwrite the widget html repr and default repr with those of the BaseContext + bundle.update({"text/html": self._repr_html_(), "text/plain": repr(self)}) + return bundle + @dataclass class RayContext(BaseContext, Mapping): @@ -1047,10 +1119,10 @@ class RayContext(BaseContext, Mapping): python_version: str ray_version: str ray_commit: str - protocol_version = Optional[str] - address_info: Dict[str, Optional[str]] + protocol_version: Optional[str] def __init__(self, address_info: Dict[str, Optional[str]]): + super().__init__() self.dashboard_url = get_dashboard_url() self.python_version = "{}.{}.{}".format(*sys.version_info[:3]) self.ray_version = ray.__version__ @@ -1092,20 +1164,6 @@ def disconnect(self): # Include disconnect() to stay consistent with ClientContext ray.shutdown() - def _repr_html_(self): - if self.dashboard_url: - dashboard_row = Template("context_dashrow.html.j2").render( - dashboard_url="http://" + self.dashboard_url - ) - else: - dashboard_row = None - - return Template("context.html.j2").render( - python_version=self.python_version, - ray_version=self.ray_version, - dashboard_row=dashboard_row, - ) - global_worker = Worker() """Worker: The global Worker object for this worker process. diff --git a/python/ray/client_builder.py b/python/ray/client_builder.py index 4e379dc5c5b6..d5cbc03142e1 100644 --- a/python/ray/client_builder.py +++ b/python/ray/client_builder.py @@ -19,7 +19,6 @@ from ray._private.worker import init as ray_driver_init from ray.job_config import JobConfig from ray.util.annotations import Deprecated, PublicAPI -from ray.widgets import Template logger = logging.getLogger(__name__) @@ -86,20 +85,6 @@ def _disconnect_with_context(self, force_disconnect: bool) -> None: # This is only a driver connected to an existing cluster. ray.shutdown() - def _repr_html_(self): - if self.dashboard_url: - dashboard_row = Template("context_dashrow.html.j2").render( - dashboard_url="http://" + self.dashboard_url - ) - else: - dashboard_row = None - - return Template("context.html.j2").render( - python_version=self.python_version, - ray_version=self.ray_version, - dashboard_row=dashboard_row, - ) - @Deprecated class ClientBuilder: diff --git a/python/ray/tests/test_widgets.py b/python/ray/tests/test_widgets.py index ad95d2b3c9e4..3697516def3a 100644 --- a/python/ray/tests/test_widgets.py +++ b/python/ray/tests/test_widgets.py @@ -1,3 +1,4 @@ +import logging from unittest import mock import pytest @@ -7,6 +8,7 @@ @mock.patch("importlib.import_module") def test_ensure_notebook_dep_missing(mock_import_module, caplog): """Test that missing notebook dependencies trigger a warning.""" + caplog.set_level(logging.INFO) class MockDep: __version__ = "8.0.0" @@ -30,6 +32,7 @@ def dummy_ipython_display(self): @mock.patch("importlib.import_module") def test_ensure_notebook_dep_outdated(mock_import_module, caplog): """Test that outdated notebook dependencies trigger a warning.""" + caplog.set_level(logging.INFO) class MockDep: __version__ = "7.0.0" @@ -49,6 +52,7 @@ def dummy_ipython_display(): @mock.patch("importlib.import_module") def test_ensure_notebook_valid(mock_import_module, caplog): """Test that valid notebook dependencies don't trigger a warning.""" + caplog.set_level(logging.INFO) class MockDep: __version__ = "8.0.0" diff --git a/python/ray/widgets/render.py b/python/ray/widgets/render.py index 1fc0c69d5f0f..f9e861d39925 100644 --- a/python/ray/widgets/render.py +++ b/python/ray/widgets/render.py @@ -19,7 +19,7 @@ def render(self, **kwargs) -> str: from the keyword arguments. Returns: - str: HTML template with the keys of the kwargs replaced with corresponding + HTML template with the keys of the kwargs replaced with corresponding values. """ rendered = self.template @@ -34,7 +34,6 @@ def list_templates() -> List[pathlib.Path]: """List the available HTML templates. Returns: - List[pathlib.Path]: A list of files with .html.j2 extensions inside - ./templates/ + A list of files with .html.j2 extensions inside ../templates/ """ return (pathlib.Path(__file__).parent / "templates").glob("*.html.j2") diff --git a/python/ray/widgets/templates/context.html.j2 b/python/ray/widgets/templates/context.html.j2 index 3e664e01eae2..26cc0ef6c878 100644 --- a/python/ray/widgets/templates/context.html.j2 +++ b/python/ray/widgets/templates/context.html.j2 @@ -1,37 +1,6 @@ -
+
-

Ray

- - - - - - - - - - - - - - - {{ dashboard_row }} -
Python version:{{ python_version }}
Ray version: {{ ray_version }}
+ {{ context_logo }} + {{ context_table }}
diff --git a/python/ray/widgets/templates/context_logo.html.j2 b/python/ray/widgets/templates/context_logo.html.j2 new file mode 100644 index 000000000000..9233fe3a7722 --- /dev/null +++ b/python/ray/widgets/templates/context_logo.html.j2 @@ -0,0 +1,13 @@ +
+ + + + + + + + + + + +
diff --git a/python/ray/widgets/templates/context_table.html.j2 b/python/ray/widgets/templates/context_table.html.j2 new file mode 100644 index 000000000000..d06822d0c1f5 --- /dev/null +++ b/python/ray/widgets/templates/context_table.html.j2 @@ -0,0 +1,11 @@ + + + + + + + + + + {{ dashboard_row }} +
Python version:{{ python_version }}
Ray version:{{ ray_version }}
diff --git a/python/ray/widgets/util.py b/python/ray/widgets/util.py index 6991384779f2..3fbe3b4aa9ce 100644 --- a/python/ray/widgets/util.py +++ b/python/ray/widgets/util.py @@ -73,9 +73,9 @@ def ensure_notebook_deps( ) -> Callable[[F], F]: """Generate a decorator which checks for soft dependencies. - This decorator is meant to wrap repr methods. If the dependency is not found, - or a version is specified here and the version of the package is older than the - specified version, the original repr is used. + This decorator is meant to wrap _repr_mimebundle_ methods. If the dependency is not + found, or a version is specified here and the version of the package is older than + the specified version, the original repr is used. If the dependency is missing or the version is old, a log message is displayed. Args: @@ -155,11 +155,11 @@ def _has_missing( message = f"Run `pip install {' '.join(missing)}` for rich notebook output." if sys.version_info < (3, 8): - logger.warning(f"Missing packages: {missing}. {message}") + logger.info(f"Missing packages: {missing}. {message}") else: # stacklevel=3: First level is this function, then ensure_notebook_deps, # then the actual function affected. - logger.warning(f"Missing packages: {missing}. {message}", stacklevel=3) + logger.info(f"Missing packages: {missing}. {message}", stacklevel=3) return missing @@ -190,13 +190,11 @@ def _has_outdated( message = f"Run `pip install -U {install_str}` for rich notebook output." if sys.version_info < (3, 8): - logger.warning(f"Outdated packages:\n{outdated_str}\n{message}") + logger.info(f"Outdated packages:\n{outdated_str}\n{message}") else: # stacklevel=3: First level is this function, then ensure_notebook_deps, # then the actual function affected. - logger.warning( - f"Outdated packages:\n{outdated_str}\n{message}", stacklevel=3 - ) + logger.info(f"Outdated packages:\n{outdated_str}\n{message}", stacklevel=3) return outdated