From 73df986936f6a25a41c66cf0dfaefdbe32d9359c Mon Sep 17 00:00:00 2001 From: Edan Bainglass <45081142+edan-bainglass@users.noreply.github.com> Date: Fri, 30 Aug 2024 17:05:13 +0200 Subject: [PATCH] Implement static CSS loader utility (#624) `ipywidgets` provides its own mechanism for styling widgets. However, it is often insufficient and clunky, only exposing a limited set of style properties. This PR implements a CSS stylesheet loader utility and a dedicated static folder for CSS stylesheets (loaded on import by the utility) to extend widget styling to the full breadth of standard features allowed by CSS. The utility can be imported by apps wishing to leverage this approach to widget styling. --- aiidalab_widgets_base/__init__.py | 40 +++++++---- aiidalab_widgets_base/static/__init__.py | 0 aiidalab_widgets_base/static/styles/README.md | 3 + .../static/styles/__init__.py | 0 .../static/styles/global.css | 0 aiidalab_widgets_base/utils/loaders.py | 33 +++++++++ docs/source/contribute/index.rst | 17 +++++ setup.cfg | 4 ++ tests/test_loaders.py | 10 +++ tests_notebooks/static/styles/test.css | 3 + tests_notebooks/test_notebook.ipynb | 67 +++++++++++++++++++ tests_notebooks/test_notebooks.py | 9 +++ 12 files changed, 172 insertions(+), 14 deletions(-) create mode 100644 aiidalab_widgets_base/static/__init__.py create mode 100644 aiidalab_widgets_base/static/styles/README.md create mode 100644 aiidalab_widgets_base/static/styles/__init__.py create mode 100644 aiidalab_widgets_base/static/styles/global.css create mode 100644 aiidalab_widgets_base/utils/loaders.py create mode 100644 tests/test_loaders.py create mode 100644 tests_notebooks/static/styles/test.css create mode 100644 tests_notebooks/test_notebook.ipynb diff --git a/aiidalab_widgets_base/__init__.py b/aiidalab_widgets_base/__init__.py index 63b2bf7c2..af7470cf8 100644 --- a/aiidalab_widgets_base/__init__.py +++ b/aiidalab_widgets_base/__init__.py @@ -1,7 +1,5 @@ """Reusable widgets for AiiDAlab applications.""" -from aiida.manage import get_profile - _WARNING_TEMPLATE = """
Warning:
@@ -13,6 +11,20 @@ """ +def load_default_profile(): + """Loads the default profile if none loaded and warn of deprecation.""" + from aiida import load_profile + + load_profile() + + profile = get_profile() + assert profile is not None, "Failed to load the default profile" + + # raise a deprecation warning + warning = HTML(_WARNING_TEMPLATE.format(profile=profile.name, version="v3.0.0")) + display(warning) + + # We only detect profile and throw a warning if it is on the notebook # It is not necessary to do this in the unit tests def is_running_in_jupyter(): @@ -27,22 +39,22 @@ def is_running_in_jupyter(): return False -# load the default profile if no profile is loaded, and raise a deprecation warning -# this is a temporary solution to avoid breaking existing notebooks -# this will be removed in the next major release -if is_running_in_jupyter() and get_profile() is None: - # if no profile is loaded, load the default profile and raise a deprecation warning - from aiida import load_profile +if is_running_in_jupyter(): + from pathlib import Path + + from aiida.manage import get_profile from IPython.display import HTML, display - load_profile() + # load the default profile if no profile is loaded, and raise a deprecation warning + # this is a temporary solution to avoid breaking existing notebooks + # this will be removed in the next major release + if get_profile() is None: + load_default_profile() - profile = get_profile() - assert profile is not None, "Failed to load the default profile" + from .utils.loaders import load_css + + load_css(css_path=Path(__file__).parent / "static/styles") - # raise a deprecation warning - warning = HTML(_WARNING_TEMPLATE.format(profile=profile.name, version="v3.0.0")) - display(warning) from .computational_resources import ( ComputationalResourcesWidget, diff --git a/aiidalab_widgets_base/static/__init__.py b/aiidalab_widgets_base/static/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/aiidalab_widgets_base/static/styles/README.md b/aiidalab_widgets_base/static/styles/README.md new file mode 100644 index 000000000..9ec5be400 --- /dev/null +++ b/aiidalab_widgets_base/static/styles/README.md @@ -0,0 +1,3 @@ +# Stylesheets for AiiDAlab Widgets Base + +This folder contains `.css` stylesheets, which are loaded on any import from the AiiDAlab widgets base package. diff --git a/aiidalab_widgets_base/static/styles/__init__.py b/aiidalab_widgets_base/static/styles/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/aiidalab_widgets_base/static/styles/global.css b/aiidalab_widgets_base/static/styles/global.css new file mode 100644 index 000000000..e69de29bb diff --git a/aiidalab_widgets_base/utils/loaders.py b/aiidalab_widgets_base/utils/loaders.py new file mode 100644 index 000000000..0fdade316 --- /dev/null +++ b/aiidalab_widgets_base/utils/loaders.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +from pathlib import Path + +from IPython.display import Javascript, display + + +def load_css(css_path: Path | str) -> None: + """Load and inject CSS stylesheets into the DOM. + + Parameters + ---------- + `css_path` : `Path` | `str` + The path to the CSS stylesheet. If the path is a directory, + all CSS files in the directory will be loaded. + """ + path = Path(css_path) + + if not path.exists(): + raise FileNotFoundError(f"CSS file or directory not found: {path}") + + filenames = [*path.glob("*.css")] if path.is_dir() else [path] + + for fn in filenames: + stylesheet = fn.read_text() + display( + Javascript(f""" + var style = document.createElement('style'); + style.type = 'text/css'; + style.innerHTML = `{stylesheet}`; + document.head.appendChild(style); + """) + ) diff --git a/docs/source/contribute/index.rst b/docs/source/contribute/index.rst index 47af92fc9..7a2a9675b 100644 --- a/docs/source/contribute/index.rst +++ b/docs/source/contribute/index.rst @@ -8,3 +8,20 @@ Contributions to the AiiDAlab widgets are highly welcome and can take different * `Report bugs