From d8a95dd171287c983742be4999c9eec5d32c24b5 Mon Sep 17 00:00:00 2001 From: Daniel Hollas Date: Mon, 27 May 2024 21:42:40 +0100 Subject: [PATCH 01/24] Test with aiida-core=2.5 (#599) * Ignore sqlalchemy warnings * Bump test dependencies * Remove fixture_localhost * add FORCE_COLOR to CI * pk -> id --- .github/workflows/ci.yml | 7 +++++-- aiidalab_widgets_base/elns.py | 5 ++--- aiidalab_widgets_base/export.py | 6 +++--- aiidalab_widgets_base/process.py | 6 +++--- pyproject.toml | 7 ++++--- setup.cfg | 18 +++++++----------- tests/conftest.py | 12 ++---------- tests/test_computational_resources.py | 2 +- tests/test_export.py | 2 +- 9 files changed, 28 insertions(+), 37 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 95302b352..e739255de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,8 @@ on: - main pull_request: +env: + FORCE_COLOR: 1 # https://docs.github.com/en/actions/using-jobs/using-concurrency concurrency: @@ -23,7 +25,8 @@ jobs: strategy: matrix: browser: [Chrome, Firefox] - aiida-core-version: [2.1.2, 2.4.3] # test on the latest and the oldest supported version + # test on the latest and the oldest supported version + aiida-core-version: [2.1.2, 2.5.1] fail-fast: false runs-on: ubuntu-latest @@ -84,7 +87,7 @@ jobs: matrix: python-version: ['3.9', '3.10'] # Test on the latest and oldest supported version - aiida-core-version: [2.2.2, 2.4.3] + aiida-core-version: [2.2.2, 2.5.1] fail-fast: false runs-on: ubuntu-latest diff --git a/aiidalab_widgets_base/elns.py b/aiidalab_widgets_base/elns.py index 08c34f7df..752e4dadc 100644 --- a/aiidalab_widgets_base/elns.py +++ b/aiidalab_widgets_base/elns.py @@ -124,9 +124,8 @@ def _observe_node(self, _=None): if self.node is None or self.eln is None: return - if "eln" in self.node.extras: - info = self.node.extras["eln"] - else: + info = self.node.base.extras.get("eln", {}) + if not info: try: q = orm.QueryBuilder().append( orm.Node, diff --git a/aiidalab_widgets_base/export.py b/aiidalab_widgets_base/export.py index 0e12a2fdc..753a15a09 100644 --- a/aiidalab_widgets_base/export.py +++ b/aiidalab_widgets_base/export.py @@ -11,7 +11,7 @@ class ExportButtonWidget(ipw.Button): def __init__(self, process, **kwargs): self.process = process if "description" not in kwargs: - kwargs["description"] = f"Export workflow ({self.process.id})" + kwargs["description"] = f"Export workflow ({self.process.pk})" if "layout" not in kwargs: kwargs["layout"] = {} kwargs["layout"]["width"] = "initial" @@ -28,7 +28,7 @@ def export_aiida_subgraph(self, change=None): # pylint: disable=unused-argument fname = os.path.join(tempfile.mkdtemp(), "export.aiida") subprocess.call( - ["verdi", "archive", "create", fname, "-N", str(self.process.id)] + ["verdi", "archive", "create", fname, "-N", str(self.process.pk)] ) with open(fname, "rb") as fobj: b64 = base64.b64encode(fobj.read()) @@ -41,6 +41,6 @@ def export_aiida_subgraph(self, change=None): # pylint: disable=unused-argument document.body.appendChild(link); link.click(); document.body.removeChild(link); - """.format(payload=payload, filename=f"export_{self.process.id}.aiida") + """.format(payload=payload, filename=f"export_{self.process.pk}.aiida") ) display(javas) diff --git a/aiidalab_widgets_base/process.py b/aiidalab_widgets_base/process.py index 69ded2ddb..e5a770285 100644 --- a/aiidalab_widgets_base/process.py +++ b/aiidalab_widgets_base/process.py @@ -249,7 +249,7 @@ def show_selected_output(self, change=None): clear_output() if change["new"]: selected_output = self.process.outputs[change["new"]] - self.info.value = f"PK: {selected_output.id}" + self.info.value = f"PK: {selected_output.pk}" display(viewer(selected_output)) @@ -537,7 +537,7 @@ def __init__(self, title="Running Job Output", **kwargs): self.title = title self.selection = ipw.Dropdown( description="Select calculation:", - options=tuple((p.id, p) for p in get_running_calcs(self.process)), + options=tuple((p.pk, p) for p in get_running_calcs(self.process)), style={"description_width": "initial"}, ) self.output = CalcJobOutputWidget() @@ -551,7 +551,7 @@ def update(self): with self.hold_trait_notifications(): old_label = self.selection.label self.selection.options = tuple( - (str(p.id), p) for p in get_running_calcs(self.process) + (str(p.pk), p) for p in get_running_calcs(self.process) ) # If the selection remains the same. if old_label in self.selection.options: diff --git a/pyproject.toml b/pyproject.toml index 3126edfcc..589f978ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,8 +8,12 @@ build-backend = "setuptools.build_meta" [tool.pytest.ini_options] filterwarnings = [ 'error', + # This is needed since SQLAlchemy 2.0, see + # https://github.com/aiidalab/aiidalab-widgets-base/issues/605 + 'ignore:Object of type .* not in session, .* operation along .* will not proceed:sqlalchemy.exc.SAWarning', 'ignore::DeprecationWarning:bokeh.core.property.primitive', 'ignore:Creating AiiDA configuration:UserWarning:aiida', + 'ignore:The `Code` class:aiida.common.warnings.AiidaDeprecationWarning:', 'ignore:crystal system:UserWarning:ase.io.cif', 'ignore::DeprecationWarning:ase.atoms', # TODO: This comes from a transitive dependency of ipyoptimade @@ -20,10 +24,7 @@ filterwarnings = [ # For some reason we get this error, see # https://github.com/aiidalab/aiidalab-widgets-base/issues/551 'ignore:Exception ignored in:pytest.PytestUnraisableExceptionWarning:_pytest', - 'ignore::DeprecationWarning:pytest_html', 'ignore::DeprecationWarning:jupyter_client', - 'ignore::DeprecationWarning:selenium', - 'ignore::DeprecationWarning:pytest_selenium', ] [tool.ruff] diff --git a/setup.cfg b/setup.cfg index 57bb33c2d..0660941a7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -43,19 +43,15 @@ zip_safe = False [options.extras_require] dev = - bumpver~=2023.1129 + bumpver>=2023.1129 pgtest~=1.3 - pre-commit~=3.5 - # NOTE: pytest-selenium currently incompatible with pytest>=7.2 - # Maybe could be made to work by installing 'py' dependency, see: - # https://docs.pytest.org/en/7.4.x/changelog.html#pytest-7-2-0-2022-10-23 - pytest~=7.1.0 - pytest-cov~=4.0 - pytest-docker~=2.0 - pytest-selenium~=4.0 + pre-commit>=3.5 + pytest~=8.2.0 + pytest-cov~=5.0 + pytest-docker~=3.0 + pytest-selenium~=4.1 pytest-timeout~=2.2 - selenium~=4.7.0 - webdriver-manager~=3.8 + selenium==4.20.0 optimade = ipyoptimade~=0.1 smiles = diff --git a/tests/conftest.py b/tests/conftest.py index c8d3dc6b4..4a81ecdde 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,15 +14,7 @@ @pytest.fixture -def fixture_localhost(aiida_localhost): - """Return a localhost `Computer`.""" - localhost = aiida_localhost - localhost.set_default_mpiprocs_per_machine(1) - return localhost - - -@pytest.fixture -def generate_calc_job_node(fixture_localhost): +def generate_calc_job_node(aiida_localhost): """Fixture to generate a mock `CalcJobNode` for testing parsers.""" def flatten_inputs(inputs, prefix=""): @@ -60,7 +52,7 @@ def _generate_calc_job_node( from plumpy import ProcessState if computer is None: - computer = fixture_localhost + computer = aiida_localhost filepath_folder = None diff --git a/tests/test_computational_resources.py b/tests/test_computational_resources.py index 4b2fb8b45..db0caa3b7 100644 --- a/tests/test_computational_resources.py +++ b/tests/test_computational_resources.py @@ -287,7 +287,7 @@ def test_aiida_code_setup(aiida_localhost): assert code.label == "bash" assert code.description == "Bash interpreter" assert str(code.filepath_executable) == "/bin/bash" - assert code.get_input_plugin_name() == "core.arithmetic.add" + assert code.default_calc_job_plugin == "core.arithmetic.add" # Reset the widget and check that a few attributes are reset. widget.code_setup = {} diff --git a/tests/test_export.py b/tests/test_export.py index 1f0bd8378..b193b3b29 100644 --- a/tests/test_export.py +++ b/tests/test_export.py @@ -9,7 +9,7 @@ def test_export_button_widget(multiply_add_completed_workchain, monkeypatch, tmp process = multiply_add_completed_workchain button = export.ExportButtonWidget(process) - assert button.description == f"Export workflow ({process.id})" + assert button.description == f"Export workflow ({process.pk})" # Test the export button. monkeypatch the `mkdtemp` function to return a # temporary directory in the `tmp_path` fixture to store the export file. From df106c5ae09829bfd9040a6d407453ade3797e42 Mon Sep 17 00:00:00 2001 From: Daniel Hollas Date: Fri, 31 May 2024 14:12:04 +0100 Subject: [PATCH 02/24] Make ELN widgets optional (#609) Move `aiidalab-eln` dependency into a new `eln` extras. --- .github/workflows/ci.yml | 2 +- aiidalab_widgets_base/elns.py | 17 ++++++++++++++++- setup.cfg | 3 ++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e739255de..5069c941f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -120,7 +120,7 @@ jobs: # Ideally, these would be fixed, but vapory is largely unmaintained, # so here we simply keep the pip behaviour with the --compile flag. # See https://github.com/astral-sh/uv/issues/1928#issuecomment-1968857514 - run: uv pip install --compile --system .[dev,smiles,optimade] aiida-core==${{ matrix.aiida-core-version }} + run: uv pip install --compile --system .[dev,smiles,optimade,eln] aiida-core==${{ matrix.aiida-core-version }} - name: Run pytest run: pytest -v tests --cov diff --git a/aiidalab_widgets_base/elns.py b/aiidalab_widgets_base/elns.py index 752e4dadc..fe9555594 100644 --- a/aiidalab_widgets_base/elns.py +++ b/aiidalab_widgets_base/elns.py @@ -5,7 +5,6 @@ import requests_cache import traitlets as tl from aiida import orm -from aiidalab_eln import get_eln_connector from IPython.display import clear_output, display ELN_CONFIG = Path.home() / ".aiidalab" / "aiidalab-eln-config.json" @@ -15,6 +14,13 @@ def connect_to_eln(eln_instance=None, **kwargs): + try: + from aiidalab_eln import get_eln_connector + except ImportError: + return ( + None, + "AiiDAlab-ELN connector not installed. Install with `pip install aiidalab-eln`", + ) # assuming that the connection can only be established to the ELNs # with the stored configuration. try: @@ -276,6 +282,15 @@ def check_connection(self, _=None): def display_eln_config(self, value=None): """Display ELN configuration specific to the selected type of ELN.""" + try: + from aiidalab_eln import get_eln_connector + except ImportError: + with self._output: + clear_output() + msg = "AiiDAlab-ELN connector not installed. Install with `pip install aiidalab-eln`" + display(ipw.HTML(f"❌ {msg}")) + return + try: eln_class = get_eln_connector(self.eln_types.value) except NotImplementedError as err: diff --git a/setup.cfg b/setup.cfg index 0660941a7..190a5c76b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,7 +22,6 @@ install_requires = PyCifRW~=4.4 aiida-core>=2.1,<3 aiidalab>=21.11.2 - aiidalab-eln>=0.1.2,~=0.1 ansi2html~=1.6 ase~=3.18 bokeh~=2.0 @@ -54,6 +53,8 @@ dev = selenium==4.20.0 optimade = ipyoptimade~=0.1 +eln = + aiidalab-eln>=0.1.2,~=0.1 smiles = rdkit>=2021.09.2 scikit-learn~=1.0.0 From cef51dd53f487c1945ac6666fb601fc93b1f4ea6 Mon Sep 17 00:00:00 2001 From: Daniel Hollas Date: Sun, 23 Jun 2024 18:39:42 +0200 Subject: [PATCH 03/24] CI fix: Test on push to master branch, not main! --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5069c941f..dc3feec11 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ name: CI on: push: branches: - - main + - master pull_request: env: From 7984c8ef3726f41894e0e8911c45c6694e909329 Mon Sep 17 00:00:00 2001 From: Daniel Hollas Date: Sun, 23 Jun 2024 18:50:04 +0200 Subject: [PATCH 04/24] Constrain ASE version <3.23 Recently released version ase 3.23 broke one of our tests that hardcodes a binary output from ase.Atoms.write. I opened #612 with more details. --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 190a5c76b..59695725a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,7 +23,7 @@ install_requires = aiida-core>=2.1,<3 aiidalab>=21.11.2 ansi2html~=1.6 - ase~=3.18 + ase~=3.18,<3.23 bokeh~=2.0 humanfriendly~=10.0 ipytree~=0.2 From e06ace12b8f6de6bd80faa1a4a347dd2d0d1b13e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Jul 2024 18:50:58 +0100 Subject: [PATCH 05/24] [pre-commit.ci] pre-commit autoupdate (#615) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.5.0 → v4.6.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.5.0...v4.6.0) - [github.com/astral-sh/ruff-pre-commit: v0.3.4 → v0.5.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.4...v0.5.0) - [github.com/sirosen/check-jsonschema: 0.28.1 → 0.28.6](https://github.com/sirosen/check-jsonschema/compare/0.28.1...0.28.6) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2fe7a6bae..cff2a0778 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ ci: repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: check-json - id: check-yaml @@ -13,7 +13,7 @@ repos: exclude: miscellaneous/structures - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.4 + rev: v0.5.0 hooks: - id: ruff-format exclude: ^docs/.* @@ -27,7 +27,7 @@ repos: args: [--preserve-quotes] - repo: https://github.com/sirosen/check-jsonschema - rev: 0.28.1 + rev: 0.28.6 hooks: - id: check-github-workflows From d8b7e7199cb9bf1e30239a329f0b77722bf5e0b8 Mon Sep 17 00:00:00 2001 From: Daniel Hollas Date: Mon, 1 Jul 2024 22:04:43 +0100 Subject: [PATCH 06/24] CI: Don't track coverage of tests/ (#614) --- .github/workflows/ci.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc3feec11..1e15289f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,8 @@ on: pull_request: env: - FORCE_COLOR: 1 + FORCE_COLOR: "1" + UV_VERSION: "0.2.17" # https://docs.github.com/en/actions/using-jobs/using-concurrency concurrency: @@ -43,7 +44,7 @@ jobs: python-version: '3.10' - name: Install uv - run: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/uv/releases/download/0.1.44/uv-installer.sh | sh + run: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-installer.sh | sh - name: Install package test dependencies # Notebook tests happen in the container, here we only need to install @@ -111,7 +112,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install uv - run: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/uv/releases/download/0.1.27/uv-installer.sh | sh + run: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-installer.sh | sh - name: Install package # NOTE: uv (unlike pip) does not compile python to bytecode after install. @@ -120,10 +121,10 @@ jobs: # Ideally, these would be fixed, but vapory is largely unmaintained, # so here we simply keep the pip behaviour with the --compile flag. # See https://github.com/astral-sh/uv/issues/1928#issuecomment-1968857514 - run: uv pip install --compile --system .[dev,smiles,optimade,eln] aiida-core==${{ matrix.aiida-core-version }} + run: uv pip install --compile --system -e .[dev,smiles,optimade,eln] aiida-core==${{ matrix.aiida-core-version }} - name: Run pytest - run: pytest -v tests --cov + run: pytest -v tests --cov=aiidalab_widgets_base - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v4 From 6075e7c7b3946164bd8717d16321f701509b501b Mon Sep 17 00:00:00 2001 From: Miki Bonacci <46074008+mikibonacci@users.noreply.github.com> Date: Fri, 5 Jul 2024 11:49:11 +0200 Subject: [PATCH 07/24] Adding support for `orm.Containerized` codes (#617) This is obtained using the `AiidaCodeSetup().code_setup` attribute. I am not sure is the better way, as we should also provide the possibility to change the image_name and the engine_command via the GUI. However, I think that for now this is acceptable, as the containerized codes are mainly set up by using the information on the aiida-resource-registry repo. Actually, it is a really new thing and for now only the phonopy@merlin-cpu is defined. --- .../computational_resources.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/aiidalab_widgets_base/computational_resources.py b/aiidalab_widgets_base/computational_resources.py index 937607f43..7adde22dd 100644 --- a/aiidalab_widgets_base/computational_resources.py +++ b/aiidalab_widgets_base/computational_resources.py @@ -1196,8 +1196,18 @@ def on_setup_code(self, _=None): "append_text", ] + containerized_code_additional_items = [ + "image_name", + "engine_command", + ] + kwargs = {key: getattr(self, key).value for key in items_to_configure} + # Check for additional keys needed for orm.ContainerizedCode + for container_key in containerized_code_additional_items: + if container_key in self.code_setup.keys(): + kwargs[container_key] = self.code_setup[container_key] + # set computer from its widget value the UUID of the computer. computer = orm.load_computer(self.computer.value) @@ -1205,7 +1215,7 @@ def on_setup_code(self, _=None): qb = orm.QueryBuilder() qb.append(orm.Computer, filters={"uuid": computer.uuid}, tag="computer") qb.append( - orm.InstalledCode, + orm.AbstractCode, with_computer="computer", filters={"label": kwargs["label"]}, ) @@ -1223,7 +1233,10 @@ def on_setup_code(self, _=None): return False try: - code = orm.InstalledCode(computer=computer, **kwargs) + if "image_name" in kwargs.keys(): + code = orm.ContainerizedCode(computer=computer, **kwargs) + else: + code = orm.InstalledCode(computer=computer, **kwargs) except (common.exceptions.InputValidationError, KeyError) as exception: self.message = wrap_message( f"Invalid input for code creation: {exception}", From 6dfe6facd623166cab5adb8d596bbbde3672f4e8 Mon Sep 17 00:00:00 2001 From: Daniel Hollas Date: Mon, 8 Jul 2024 03:53:37 +0100 Subject: [PATCH 08/24] Support and test with py3.11 (#619) - update scikit-learn dependency to support python 3.11 --- .github/workflows/ci.yml | 2 +- pyproject.toml | 11 ++++++++++- setup.cfg | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1e15289f0..72931c2c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,7 +86,7 @@ jobs: strategy: matrix: - python-version: ['3.9', '3.10'] + python-version: ['3.9', '3.11'] # Test on the latest and oldest supported version aiida-core-version: [2.2.2, 2.5.1] fail-fast: false diff --git a/pyproject.toml b/pyproject.toml index 589f978ee..a32a34198 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,10 +8,16 @@ build-backend = "setuptools.build_meta" [tool.pytest.ini_options] filterwarnings = [ 'error', + # The following deprecation warnings come from Python 3.12 stdlib modules + "ignore:datetime.datetime.:DeprecationWarning:", + # This one is coming from plumpy + "ignore:There is no current event loop:DeprecationWarning:", + # This deprecation warning coming from sqlite3 module might go away if we update bokeh + "ignore:The default datetime adapter is deprecated as of Python 3.12; see the sqlite3 documentation for suggested replacement recipes:DeprecationWarning:", # This is needed since SQLAlchemy 2.0, see # https://github.com/aiidalab/aiidalab-widgets-base/issues/605 'ignore:Object of type .* not in session, .* operation along .* will not proceed:sqlalchemy.exc.SAWarning', - 'ignore::DeprecationWarning:bokeh.core.property.primitive', + 'ignore::DeprecationWarning:bokeh', 'ignore:Creating AiiDA configuration:UserWarning:aiida', 'ignore:The `Code` class:aiida.common.warnings.AiidaDeprecationWarning:', 'ignore:crystal system:UserWarning:ase.io.cif', @@ -25,6 +31,9 @@ filterwarnings = [ # https://github.com/aiidalab/aiidalab-widgets-base/issues/551 'ignore:Exception ignored in:pytest.PytestUnraisableExceptionWarning:_pytest', 'ignore::DeprecationWarning:jupyter_client', + # This warning is coming from circus (aiida-core dependency): + # https://github.com/circus-tent/circus/issues/1215 + "ignore:'pipes' is deprecated and slated for removal in Python 3.13:DeprecationWarning:", ] [tool.ruff] diff --git a/setup.cfg b/setup.cfg index 59695725a..092ad70df 100644 --- a/setup.cfg +++ b/setup.cfg @@ -57,7 +57,7 @@ eln = aiidalab-eln>=0.1.2,~=0.1 smiles = rdkit>=2021.09.2 - scikit-learn~=1.0.0 + scikit-learn~=1.1 docs = sphinx~=7.3 sphinx-design~=0.5 From acf6cb6515f5a8f5d091e171b1ec2c85b1415a78 Mon Sep 17 00:00:00 2001 From: Daniel Hollas Date: Mon, 8 Jul 2024 03:59:32 +0100 Subject: [PATCH 09/24] Make auto-generated release notes nicer (#618) Adds a configuration for auto-generated release notes to make them a bit nicer: - Exclude PRs from dependabot and pre-commit-ci bot - Autogenerate sections based on PR labels. --- .github/release.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/release.yml diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 000000000..6a959ba96 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,25 @@ +--- +# Configuration for automatically generated release notes: +# https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes#configuring-automatically-generated-release-notes +# We exclude PRs from bots and categorize PRs based on labels +changelog: + exclude: + authors: + - dependabot + - pre-commit-ci + categories: + - title: Breaking Changes 🛠 + labels: + - breaking + - title: New Features 🎉 + labels: + - enhancement + - title: Bug fixes 🐛 + labels: + - bug + - title: Documentation 📝 + labels: + - docs + - title: Other Changes + labels: + - "*" From 4ceb377d909a7c6b0e20899486c2368b12f91125 Mon Sep 17 00:00:00 2001 From: Daniel Hollas Date: Mon, 8 Jul 2024 04:01:45 +0100 Subject: [PATCH 10/24] deps: Vendor more_itertools.consecutive_groups (#613) more_itertools package contains useful functions built on top of stdlib itertools, but we only use a single function from the package so let's vendor it and remove the dependency. This means app developers can happily use this package without worrying about version clashes with AWB. Part of #392 --- aiidalab_widgets_base/utils/__init__.py | 39 ++++++++++++++++++------- setup.cfg | 1 - 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/aiidalab_widgets_base/utils/__init__.py b/aiidalab_widgets_base/utils/__init__.py index f6dd03da6..8a6f53304 100644 --- a/aiidalab_widgets_base/utils/__init__.py +++ b/aiidalab_widgets_base/utils/__init__.py @@ -1,16 +1,17 @@ """Some utility functions used acrross the repository.""" +import itertools +import operator import threading from enum import Enum from typing import Any +import ase +import ase.io import ipywidgets as ipw -import more_itertools as mit import numpy as np -import traitlets +import traitlets as tl from aiida.plugins import DataFactory -from ase import Atoms -from ase.io import read CifData = DataFactory("core.cif") # pylint: disable=invalid-name StructureData = DataFactory("core.structure") # pylint: disable=invalid-name @@ -43,9 +44,9 @@ def get_ase_from_file(fname, file_format=None): # pylint: disable=redefined-bui # store_tags parameter is useful for CIF files # https://wiki.fysik.dtu.dk/ase/ase/io/formatoptions.html#cif if file_format == "cif": - traj = read(fname, format=file_format, index=":", store_tags=True) + traj = ase.io.read(fname, format=file_format, index=":", store_tags=True) else: - traj = read(fname, format=file_format, index=":") + traj = ase.io.read(fname, format=file_format, index=":") if not traj: raise ValueError(f"Could not read any information from the file {fname}") return traj @@ -53,7 +54,7 @@ def get_ase_from_file(fname, file_format=None): # pylint: disable=redefined-bui def find_ranges(iterable): """Yield range of consecutive numbers.""" - for grp in mit.consecutive_groups(iterable): + for grp in _consecutive_groups(iterable): group = list(grp) if len(group) == 1: yield group[0] @@ -61,6 +62,22 @@ def find_ranges(iterable): yield group[0], group[-1] +def _consecutive_groups(iterable, ordering=lambda x: x): + """Yield groups of consecutive items using :func:`itertools.groupby`. + The *ordering* function determines whether two items are adjacent by + returning their position. + + This is a vendored version of more_itertools.consecutive_groups + https://more-itertools.readthedocs.io/en/v10.3.0/_modules/more_itertools/more.html#consecutive_groups + Distributed under MIT license: https://more-itertools.readthedocs.io/en/v10.3.0/license.html + Thank you Bo Bayles for the original implementation. <3 + """ + for _, g in itertools.groupby( + enumerate(iterable), key=lambda x: x[0] - ordering(x[1]) + ): + yield map(operator.itemgetter(1), g) + + def list_to_string_range(lst, shift=1): """Converts a list like [0, 2, 3, 4] into a string like '1 3..5'. @@ -124,7 +141,7 @@ def inverse_matrix(self): return np.linalg.inv(self.matrix) -class _StatusWidgetMixin(traitlets.HasTraits): +class _StatusWidgetMixin(tl.HasTraits): """Show temporary messages for example for status updates. This is a mixin class that is meant to be part of an inheritance tree of an actual widget with a 'value' traitlet that is used @@ -132,7 +149,7 @@ class _StatusWidgetMixin(traitlets.HasTraits): for examples. """ - message = traitlets.Unicode(default_value="", allow_none=True) + message = tl.Unicode(default_value="", allow_none=True) new_line = "\n" def __init__(self, clear_after=3, *args, **kwargs): @@ -169,7 +186,7 @@ class StatusHTML(_StatusWidgetMixin, ipw.HTML): # This method should be part of _StatusWidgetMixin, but that does not work # for an unknown reason. - @traitlets.observe("message") + @tl.observe("message") def _observe_message(self, change): self.show_temporary_message(change["new"]) @@ -201,7 +218,7 @@ def wrap_message(message, level=MessageLevel.INFO): """ -def ase2spglib(ase_structure: Atoms) -> tuple[Any, Any, Any]: +def ase2spglib(ase_structure: ase.Atoms) -> tuple[Any, Any, Any]: """ Convert ase Atoms instance to spglib cell in the format defined at https://spglib.github.io/spglib/python-spglib.html#crystal-structure-cell diff --git a/setup.cfg b/setup.cfg index 092ad70df..5d3db08a6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,7 +30,6 @@ install_requires = traitlets~=5.9.0 ipywidgets~=7.7 widgetsnbextension<3.6.3 - more-itertools~=8.0 pymysql~=0.9 nglview~=3.0 spglib>=1.14,<3 From 96d3292d0f863db4f42af7e99ffce062fc8e6ba4 Mon Sep 17 00:00:00 2001 From: Daniel Hollas Date: Mon, 8 Jul 2024 04:07:38 +0100 Subject: [PATCH 11/24] Bump version v2.2.2 -> v2.3.0a0 --- aiidalab_widgets_base/__init__.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aiidalab_widgets_base/__init__.py b/aiidalab_widgets_base/__init__.py index 0522cff7c..5d9066ec6 100644 --- a/aiidalab_widgets_base/__init__.py +++ b/aiidalab_widgets_base/__init__.py @@ -117,4 +117,4 @@ def is_running_in_jupyter(): "viewer", ] -__version__ = "2.2.2" +__version__ = "2.3.0a0" diff --git a/setup.cfg b/setup.cfg index 5d3db08a6..33097da82 100644 --- a/setup.cfg +++ b/setup.cfg @@ -64,7 +64,7 @@ docs = myst-nb~=1.1 [bumpver] -current_version = "v2.2.2" +current_version = "v2.3.0a0" version_pattern = "vMAJOR.MINOR.PATCH[PYTAGNUM]" commit_message = "Bump version {old_version} -> {new_version}" commit = True From c97a25ffc13c3400e411e73ebc0600e70a815f2e Mon Sep 17 00:00:00 2001 From: Daniel Hollas Date: Mon, 8 Jul 2024 20:06:20 +0100 Subject: [PATCH 12/24] Various dependency fixes (#621) --- aiidalab_widgets_base/elns.py | 11 ++++++++--- aiidalab_widgets_base/nodes.py | 3 ++- setup.cfg | 3 ++- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/aiidalab_widgets_base/elns.py b/aiidalab_widgets_base/elns.py index fe9555594..d496b5630 100644 --- a/aiidalab_widgets_base/elns.py +++ b/aiidalab_widgets_base/elns.py @@ -2,7 +2,6 @@ from pathlib import Path import ipywidgets as ipw -import requests_cache import traitlets as tl from aiida import orm from IPython.display import clear_output, display @@ -68,6 +67,8 @@ class ElnImportWidget(ipw.VBox): node = tl.Instance(orm.Node, allow_none=True) def __init__(self, path_to_root="../", **kwargs): + import requests_cache + # Used to output additional settings. self._output = ipw.Output() @@ -83,8 +84,9 @@ def __init__(self, path_to_root="../", **kwargs): return tl.dlink((eln, "node"), (self, "node")) + # Since the requests cache is enabled globally in aiidalab package, we disable it here to get correct results. + # This can be removed once https://github.com/aiidalab/aiidalab/issues/196 is fixed. with requests_cache.disabled(): - # Since the cache is enabled in AiiDAlab, we disable it here to get correct results. eln.import_data() @@ -151,10 +153,13 @@ def _observe_node(self, _=None): self.eln.set_sample_config(**info) def send_to_eln(self, _=None): + import requests_cache + if self.eln and self.eln.is_connected: self.message.value = f"\u29d7 Sending data to {self.eln.eln_instance}..." + # Since the requests cache is enabled globally in aiidalab package, we disable it here to get correct results. + # This can be removed once https://github.com/aiidalab/aiidalab/issues/196 is fixed. with requests_cache.disabled(): - # Since the cache is enabled in AiiDAlab, we disable it here to get correct results. self.eln.export_data() self.message.value = ( f"\u2705 The data were successfully sent to {self.eln.eln_instance}." diff --git a/aiidalab_widgets_base/nodes.py b/aiidalab_widgets_base/nodes.py index 9f4bf766a..f08b9af84 100644 --- a/aiidalab_widgets_base/nodes.py +++ b/aiidalab_widgets_base/nodes.py @@ -7,7 +7,6 @@ import traitlets as tl from aiida import common, engine, orm from aiida.cmdline.utils.ascii_vis import calc_info -from aiidalab.app import _AiidaLabApp from IPython.display import clear_output, display CALCULATION_TYPES = [ @@ -319,6 +318,8 @@ def find_node(self, pk, namespaces=None): class _AppIcon: def __init__(self, app, path_to_root, node): + from aiidalab.app import _AiidaLabApp + name = app["name"] app_object = _AiidaLabApp.from_id(name) self.logo = app_object.metadata["logo"] diff --git a/setup.cfg b/setup.cfg index 33097da82..77b052cdf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,7 +27,7 @@ install_requires = bokeh~=2.0 humanfriendly~=10.0 ipytree~=0.2 - traitlets~=5.9.0 + traitlets~=5.9 ipywidgets~=7.7 widgetsnbextension<3.6.3 pymysql~=0.9 @@ -54,6 +54,7 @@ optimade = ipyoptimade~=0.1 eln = aiidalab-eln>=0.1.2,~=0.1 + requests-cache~=1.0 smiles = rdkit>=2021.09.2 scikit-learn~=1.1 From 6ce5b6c997701d9ee67e30b404eef544af9bce81 Mon Sep 17 00:00:00 2001 From: Daniel Hollas Date: Mon, 8 Jul 2024 20:07:41 +0100 Subject: [PATCH 13/24] Bump version v2.3.0a0 -> v2.3.0a1 --- aiidalab_widgets_base/__init__.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aiidalab_widgets_base/__init__.py b/aiidalab_widgets_base/__init__.py index 5d9066ec6..63b2bf7c2 100644 --- a/aiidalab_widgets_base/__init__.py +++ b/aiidalab_widgets_base/__init__.py @@ -117,4 +117,4 @@ def is_running_in_jupyter(): "viewer", ] -__version__ = "2.3.0a0" +__version__ = "2.3.0a1" diff --git a/setup.cfg b/setup.cfg index 77b052cdf..1c13fdc92 100644 --- a/setup.cfg +++ b/setup.cfg @@ -65,7 +65,7 @@ docs = myst-nb~=1.1 [bumpver] -current_version = "v2.3.0a0" +current_version = "v2.3.0a1" version_pattern = "vMAJOR.MINOR.PATCH[PYTAGNUM]" commit_message = "Bump version {old_version} -> {new_version}" commit = True From de5f0cc23e1e058b46922a2203dd75b292b0f13e Mon Sep 17 00:00:00 2001 From: Daniel Hollas Date: Wed, 7 Aug 2024 12:49:48 +0100 Subject: [PATCH 14/24] Don't turn warnings into errors in pytest (#628) Turning warnings into errors is nice in principle, but it turn out to be too much of a churn. * Ignore Cryptography Deprecation warning * Ignore spglib deprecationwarning * pytest: Don't error out on warnings --- pyproject.toml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a32a34198..be4277d3c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,6 @@ build-backend = "setuptools.build_meta" [tool.pytest.ini_options] filterwarnings = [ - 'error', # The following deprecation warnings come from Python 3.12 stdlib modules "ignore:datetime.datetime.:DeprecationWarning:", # This one is coming from plumpy @@ -16,12 +15,15 @@ filterwarnings = [ "ignore:The default datetime adapter is deprecated as of Python 3.12; see the sqlite3 documentation for suggested replacement recipes:DeprecationWarning:", # This is needed since SQLAlchemy 2.0, see # https://github.com/aiidalab/aiidalab-widgets-base/issues/605 - 'ignore:Object of type .* not in session, .* operation along .* will not proceed:sqlalchemy.exc.SAWarning', + 'ignore:Object of type.*not in session,.*operation along.*will not proceed:sqlalchemy.exc.SAWarning', 'ignore::DeprecationWarning:bokeh', 'ignore:Creating AiiDA configuration:UserWarning:aiida', 'ignore:The `Code` class:aiida.common.warnings.AiidaDeprecationWarning:', 'ignore:crystal system:UserWarning:ase.io.cif', 'ignore::DeprecationWarning:ase.atoms', + # This popped up in spglib 2.5. Since we still try to support spglib v1, + # it's not clear if we can get rid of it. + "ignore:dict interface.*is deprecated.Use attribute interface:DeprecationWarning:spglib", # TODO: This comes from a transitive dependency of ipyoptimade # Remove this when this issue is addressed: # https://github.com/CasperWA/ipywidgets-extended/issues/85 @@ -34,6 +36,7 @@ filterwarnings = [ # This warning is coming from circus (aiida-core dependency): # https://github.com/circus-tent/circus/issues/1215 "ignore:'pipes' is deprecated and slated for removal in Python 3.13:DeprecationWarning:", + "ignore::cryptography.utils.CryptographyDeprecationWarning:", ] [tool.ruff] From 771dae1631073935bc4a4a8a8940cba7ed3abbd2 Mon Sep 17 00:00:00 2001 From: Daniel Hollas Date: Thu, 22 Aug 2024 18:02:33 +0100 Subject: [PATCH 15/24] Downgrade minimum traitlets version to 5.4 (#629) As we learned in aiidalab/aiidalab-docker-stack#494, it is really not good if traitlets dependency is installed outside of the conda enviroment (i.e. to ~/.local) as it may break the jupyter notebook. Here we downgrade the minimum traitlets version back to 5.4. (NOTE: That the original motivation to upgrading to 5.9 was performance, I believe it is better if we upgrade directly in the image). --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 1c13fdc92..b5fa9edb1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,7 +27,7 @@ install_requires = bokeh~=2.0 humanfriendly~=10.0 ipytree~=0.2 - traitlets~=5.9 + traitlets~=5.4 ipywidgets~=7.7 widgetsnbextension<3.6.3 pymysql~=0.9 From 0c9f6fba5b237d3bfeafec05da9d5eb138d08933 Mon Sep 17 00:00:00 2001 From: Daniel Hollas Date: Fri, 23 Aug 2024 14:45:27 +0100 Subject: [PATCH 16/24] Fix notebook tests (#630) * Update test deps --- setup.cfg | 10 +++++----- tests_notebooks/conftest.py | 8 ++++++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/setup.cfg b/setup.cfg index b5fa9edb1..53706087f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -44,12 +44,12 @@ dev = bumpver>=2023.1129 pgtest~=1.3 pre-commit>=3.5 - pytest~=8.2.0 + pytest~=8.3.0 pytest-cov~=5.0 - pytest-docker~=3.0 - pytest-selenium~=4.1 - pytest-timeout~=2.2 - selenium==4.20.0 + pytest-docker~=3.1.0 + pytest-selenium~=4.1.0 + pytest-timeout~=2.3.0 + selenium~=4.23.0 optimade = ipyoptimade~=0.1 eln = diff --git a/tests_notebooks/conftest.py b/tests_notebooks/conftest.py index 4a43da646..41dfe9d08 100644 --- a/tests_notebooks/conftest.py +++ b/tests_notebooks/conftest.py @@ -99,16 +99,20 @@ def _selenium_driver(nb_path): url, f"apps/apps/aiidalab-widgets-base/{nb_path}?token={token}" ) selenium.get(f"{url_with_token}") - # By default, let's allow selenium functions to retry for 10s + # By default, let's allow selenium functions to retry for 60s # till a given element is loaded, see: # https://selenium-python.readthedocs.io/waits.html#implicit-waits - selenium.implicitly_wait(30) + selenium.implicitly_wait(60) window_width = 800 window_height = 600 selenium.set_window_size(window_width, window_height) selenium.find_element(By.ID, "ipython-main-app") selenium.find_element(By.ID, "notebook-container") + selenium.find_element(By.ID, "appmode-busy") + # We wait until the appmode spinner disappears. However, + # this does not seem to be fully robust, as the spinner might flash + # while the page is still loading. So we add explicit sleep here as well. WebDriverWait(selenium, 240).until( ec.invisibility_of_element((By.ID, "appmode-busy")) ) 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 17/24] 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 `_. * `Feature requests `_. * Help us improve the documentation of widgets. + +************** +Widget styling +************** + +Though ``ipywidgets`` does provide some basic styling options via the ``layout`` and ``style`` attributes, it is often not enough to create a visually appealing widget. +As such, we recommend the use of `CSS `_ stylesheets to style your widgets. +These may be packaged under ``aiidalab_widgets_base/static/styles``, which are automatically loaded on import via the ``load_css`` utility. + +A ``global.css`` stylesheet is made available for global html-tag styling and ``ipywidgets`` or ``Jupyter`` style overrides. +For more specific widgets and components, please add a dedicated stylesheet. +Note that all stylesheets in the ``styles`` directory will be loaded on import. + +We recommend using classes to avoid style leaking outside of the target widget. +We also advise causion when using the `!important `_ flag on CSS properties, as it may interfere with other stylesheets. + +If you are unsure about the styling of your widget, feel free to ask for help on the `AiiDAlab Discourse channel `_. diff --git a/setup.cfg b/setup.cfg index 53706087f..b5b886124 100644 --- a/setup.cfg +++ b/setup.cfg @@ -64,6 +64,10 @@ docs = pydata-sphinx-theme~=0.15 myst-nb~=1.1 + +[options.package_data] +aiidalab_widgets_base.static.styles = *.css + [bumpver] current_version = "v2.3.0a1" version_pattern = "vMAJOR.MINOR.PATCH[PYTAGNUM]" diff --git a/tests/test_loaders.py b/tests/test_loaders.py new file mode 100644 index 000000000..23cee1b18 --- /dev/null +++ b/tests/test_loaders.py @@ -0,0 +1,10 @@ +from pathlib import Path + +from aiidalab_widgets_base.utils.loaders import load_css + + +def test_load_css(): + """Test `load_css` utility.""" + css_dir = Path("aiidalab_widgets_base/static/styles") + load_css(css_path=css_dir) + load_css(css_path=css_dir / "global.css") diff --git a/tests_notebooks/static/styles/test.css b/tests_notebooks/static/styles/test.css new file mode 100644 index 000000000..9e805f497 --- /dev/null +++ b/tests_notebooks/static/styles/test.css @@ -0,0 +1,3 @@ +.red-text { + color: rgb(255, 0, 0); +} diff --git a/tests_notebooks/test_notebook.ipynb b/tests_notebooks/test_notebook.ipynb new file mode 100644 index 000000000..edffd72d5 --- /dev/null +++ b/tests_notebooks/test_notebook.ipynb @@ -0,0 +1,67 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import ipywidgets as ipw" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from aiida import load_profile\n", + "\n", + "load_profile();" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from aiidalab_widgets_base.utils.loaders import load_css\n", + "\n", + "load_css(css_path=\"static/styles/test.css\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "label = ipw.Label(\"Testing\")\n", + "label.add_class(\"red-text\")\n", + "display(label)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.13" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tests_notebooks/test_notebooks.py b/tests_notebooks/test_notebooks.py index a1d6c3977..9fdf08f23 100644 --- a/tests_notebooks/test_notebooks.py +++ b/tests_notebooks/test_notebooks.py @@ -5,6 +5,15 @@ from selenium.webdriver.common.keys import Keys +def test_loaded_css(selenium_driver): + driver = selenium_driver("tests_notebooks/test_notebook.ipynb") + element = driver.find_element(By.CLASS_NAME, "red-text") + assert element.value_of_css_property("color") in ( + "rgba(255, 0, 0, 1)", # Chrome + "rgb(255, 0, 0)", # Firefox + ) + + def test_notebook_service_available(notebook_service): url, token = notebook_service response = requests.get(f"{url}/?token={token}") From d926767f68d6a1a14129b5eeabe63fa3d70c897a Mon Sep 17 00:00:00 2001 From: Daniel Hollas Date: Fri, 30 Aug 2024 16:17:55 +0100 Subject: [PATCH 18/24] Bump version v2.3.0a1 -> v2.3.0a2 --- aiidalab_widgets_base/__init__.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aiidalab_widgets_base/__init__.py b/aiidalab_widgets_base/__init__.py index af7470cf8..36459f67b 100644 --- a/aiidalab_widgets_base/__init__.py +++ b/aiidalab_widgets_base/__init__.py @@ -129,4 +129,4 @@ def is_running_in_jupyter(): "viewer", ] -__version__ = "2.3.0a1" +__version__ = "2.3.0a2" diff --git a/setup.cfg b/setup.cfg index b5b886124..80b2ec9db 100644 --- a/setup.cfg +++ b/setup.cfg @@ -69,7 +69,7 @@ docs = aiidalab_widgets_base.static.styles = *.css [bumpver] -current_version = "v2.3.0a1" +current_version = "v2.3.0a2" version_pattern = "vMAJOR.MINOR.PATCH[PYTAGNUM]" commit_message = "Bump version {old_version} -> {new_version}" commit = True From 1d119fb47c7fd85cf0a64c6112bf93d4c7546132 Mon Sep 17 00:00:00 2001 From: Daniel Hollas Date: Wed, 18 Sep 2024 14:53:37 +0100 Subject: [PATCH 19/24] Test with aiida 2.6.2 (#616) --- .github/workflows/ci.yml | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72931c2c8..6d2eeb79a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ on: env: FORCE_COLOR: "1" - UV_VERSION: "0.2.17" + UV_VERSION: "0.4.6" # https://docs.github.com/en/actions/using-jobs/using-concurrency concurrency: @@ -27,7 +27,7 @@ jobs: matrix: browser: [Chrome, Firefox] # test on the latest and the oldest supported version - aiida-core-version: [2.1.2, 2.5.1] + aiida-core-version: [2.1.2, 2.6.2] fail-fast: false runs-on: ubuntu-latest @@ -43,8 +43,10 @@ jobs: with: python-version: '3.10' - - name: Install uv - run: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-installer.sh | sh + - name: Setup uv + uses: astral-sh/setup-uv@v1 + with: + version: ${{ env.UV_VERSION }} - name: Install package test dependencies # Notebook tests happen in the container, here we only need to install @@ -88,7 +90,7 @@ jobs: matrix: python-version: ['3.9', '3.11'] # Test on the latest and oldest supported version - aiida-core-version: [2.2.2, 2.5.1] + aiida-core-version: [2.2.2, 2.6.2] fail-fast: false runs-on: ubuntu-latest @@ -111,8 +113,10 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Install uv - run: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-installer.sh | sh + - name: Setup uv + uses: astral-sh/setup-uv@v1 + with: + version: ${{ env.UV_VERSION }} - name: Install package # NOTE: uv (unlike pip) does not compile python to bytecode after install. From c697d27088b2bb1ce129ef422b46318adf986f27 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 22:42:13 +0800 Subject: [PATCH 20/24] Bump astral-sh/setup-uv from 1 to 3 in the gha-dependencies group (#634) Updates `astral-sh/setup-uv` from 1 to 3 - [Release notes](https://github.com/astral-sh/setup-uv/releases) - [Commits](https://github.com/astral-sh/setup-uv/compare/v1...v3) --- updated-dependencies: - dependency-name: astral-sh/setup-uv dependency-type: direct:production update-type: version-update:semver-major dependency-group: gha-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6d2eeb79a..0d27e3bbc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,7 +44,7 @@ jobs: python-version: '3.10' - name: Setup uv - uses: astral-sh/setup-uv@v1 + uses: astral-sh/setup-uv@v3 with: version: ${{ env.UV_VERSION }} @@ -114,7 +114,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Setup uv - uses: astral-sh/setup-uv@v1 + uses: astral-sh/setup-uv@v3 with: version: ${{ env.UV_VERSION }} From 8be9a59ad2156eb053d0880ad7155829fd69bc09 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 9 Oct 2024 22:54:29 +0800 Subject: [PATCH 21/24] [pre-commit.ci] pre-commit autoupdate (#635) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/pre-commit/pre-commit-hooks: v4.6.0 → v5.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.6.0...v5.0.0) - [github.com/astral-sh/ruff-pre-commit: v0.5.0 → v0.6.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.0...v0.6.9) - [github.com/sirosen/check-jsonschema: 0.28.6 → 0.29.3](https://github.com/sirosen/check-jsonschema/compare/0.28.6...0.29.3) * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix lints in notebooks --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Daniel Hollas --- .pre-commit-config.yaml | 6 +- notebooks/eln_import.ipynb | 19 ++-- notebooks/process.ipynb | 33 +++++-- notebooks/process_list.ipynb | 82 ++++++++-------- notebooks/structures.ipynb | 7 +- notebooks/viewers.ipynb | 182 ++++++++++++++++++++--------------- notebooks/wizard_apps.ipynb | 54 ++++++----- 7 files changed, 222 insertions(+), 161 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cff2a0778..e5d30f0ad 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ ci: repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: check-json - id: check-yaml @@ -13,7 +13,7 @@ repos: exclude: miscellaneous/structures - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.0 + rev: v0.6.9 hooks: - id: ruff-format exclude: ^docs/.* @@ -27,7 +27,7 @@ repos: args: [--preserve-quotes] - repo: https://github.com/sirosen/check-jsonschema - rev: 0.28.6 + rev: 0.29.3 hooks: - id: check-github-workflows diff --git a/notebooks/eln_import.ipynb b/notebooks/eln_import.ipynb index 51e566821..e3863c868 100644 --- a/notebooks/eln_import.ipynb +++ b/notebooks/eln_import.ipynb @@ -42,10 +42,15 @@ "metadata": {}, "outputs": [], "source": [ - "from aiidalab_widgets_base import AiidaNodeViewWidget, OpenAiidaNodeInAppWidget, ElnImportWidget\n", "import urllib.parse as urlparse\n", - "from aiidalab_widgets_base import viewer\n", - "from traitlets import dlink" + "\n", + "from traitlets import dlink\n", + "\n", + "from aiidalab_widgets_base import (\n", + " AiidaNodeViewWidget,\n", + " ElnImportWidget,\n", + " OpenAiidaNodeInAppWidget,\n", + ")" ] }, { @@ -55,9 +60,9 @@ "metadata": {}, "outputs": [], "source": [ - "url = urlparse.urlsplit(jupyter_notebook_url)\n", + "url = urlparse.urlsplit(jupyter_notebook_url) # noqa: F821\n", "parsed_url = urlparse.parse_qs(url.query)\n", - "params = {key:value[0] for key, value in parsed_url.items()}\n", + "params = {key: value[0] for key, value in parsed_url.items()}\n", "eln_widget = ElnImportWidget(**params)" ] }, @@ -71,8 +76,8 @@ "object_displayed = AiidaNodeViewWidget()\n", "open_in_app = OpenAiidaNodeInAppWidget()\n", "\n", - "_ = dlink((eln_widget, 'node'), (object_displayed, 'node'))\n", - "_ = dlink((eln_widget, 'node'), (open_in_app, 'node'))" + "_ = dlink((eln_widget, \"node\"), (object_displayed, \"node\"))\n", + "_ = dlink((eln_widget, \"node\"), (open_in_app, \"node\"))" ] }, { diff --git a/notebooks/process.ipynb b/notebooks/process.ipynb index 8584e6754..1dc8c6a53 100644 --- a/notebooks/process.ipynb +++ b/notebooks/process.ipynb @@ -37,13 +37,19 @@ "metadata": {}, "outputs": [], "source": [ - "import ipywidgets as ipw\n", - "from IPython.display import clear_output\n", - "from aiida.cmdline.utils.ascii_vis import format_call_graph\n", "import urllib.parse as urlparse\n", + "\n", "from aiida.orm import load_node\n", - "from aiidalab_widgets_base import ProcessFollowerWidget, ProgressBarWidget, ProcessReportWidget\n", - "from aiidalab_widgets_base import ProcessInputsWidget, ProcessOutputsWidget, ProcessCallStackWidget, RunningCalcJobOutputWidget" + "\n", + "from aiidalab_widgets_base import (\n", + " ProcessCallStackWidget,\n", + " ProcessFollowerWidget,\n", + " ProcessInputsWidget,\n", + " ProcessOutputsWidget,\n", + " ProcessReportWidget,\n", + " ProgressBarWidget,\n", + " RunningCalcJobOutputWidget,\n", + ")" ] }, { @@ -52,10 +58,10 @@ "metadata": {}, "outputs": [], "source": [ - "url = urlparse.urlsplit(jupyter_notebook_url)\n", + "url = urlparse.urlsplit(jupyter_notebook_url) # noqa: F821\n", "url_dict = urlparse.parse_qs(url.query)\n", - "if 'id' in url_dict:\n", - " pk = int(url_dict['id'][0])\n", + "if \"id\" in url_dict:\n", + " pk = int(url_dict[\"id\"][0])\n", " process = load_node(pk)\n", "else:\n", " process = None" @@ -101,8 +107,15 @@ "source": [ "follower = ProcessFollowerWidget(\n", " process,\n", - " followers=[ProgressBarWidget(), ProcessReportWidget(), ProcessCallStackWidget(), RunningCalcJobOutputWidget()], path_to_root=\"../../\", \n", - " update_interval=2)\n", + " followers=[\n", + " ProgressBarWidget(),\n", + " ProcessReportWidget(),\n", + " ProcessCallStackWidget(),\n", + " RunningCalcJobOutputWidget(),\n", + " ],\n", + " path_to_root=\"../../\",\n", + " update_interval=2,\n", + ")\n", "display(follower)\n", "follower.follow(detach=True)" ] diff --git a/notebooks/process_list.ipynb b/notebooks/process_list.ipynb index 6d2d8fc05..5765ad6ae 100644 --- a/notebooks/process_list.ipynb +++ b/notebooks/process_list.ipynb @@ -38,9 +38,10 @@ "outputs": [], "source": [ "import ipywidgets as ipw\n", - "from aiidalab_widgets_base import ProcessListWidget\n", + "from plumpy import ProcessState\n", "from traitlets import dlink\n", - "from plumpy import ProcessState" + "\n", + "from aiidalab_widgets_base import ProcessListWidget" ] }, { @@ -51,63 +52,68 @@ "source": [ "process_list = ProcessListWidget(path_to_root=\"../../\")\n", "\n", - "past_days_widget = ipw.IntText(value=7, description='Past days:')\n", - "dlink((past_days_widget, 'value'), (process_list, 'past_days'))\n", + "past_days_widget = ipw.IntText(value=7, description=\"Past days:\")\n", + "dlink((past_days_widget, \"value\"), (process_list, \"past_days\"))\n", "\n", "\n", "all_days_checkbox = ipw.Checkbox(description=\"All days\", value=False)\n", - "dlink((all_days_checkbox, 'value'), (past_days_widget, 'disabled'))\n", - "dlink((all_days_checkbox, 'value'), (process_list, 'past_days'), transform=lambda v: -1 if v else past_days_widget.value)\n", + "dlink((all_days_checkbox, \"value\"), (past_days_widget, \"disabled\"))\n", + "dlink(\n", + " (all_days_checkbox, \"value\"),\n", + " (process_list, \"past_days\"),\n", + " transform=lambda v: -1 if v else past_days_widget.value,\n", + ")\n", "\n", "incoming_node_widget = ipw.Text(\n", - " description='Incoming node:',\n", - " style={'description_width': 'initial'}\n", + " description=\"Incoming node:\", style={\"description_width\": \"initial\"}\n", ")\n", - "dlink((incoming_node_widget, 'value'), (process_list, 'incoming_node'))\n", + "dlink((incoming_node_widget, \"value\"), (process_list, \"incoming_node\"))\n", "\n", "\n", "outgoing_node_widget = ipw.Text(\n", - " description='Outgoing node:',\n", - " style={'description_width': 'initial'}\n", + " description=\"Outgoing node:\", style={\"description_width\": \"initial\"}\n", ")\n", - "dlink((outgoing_node_widget, 'value'), (process_list, 'outgoing_node'))\n", + "dlink((outgoing_node_widget, \"value\"), (process_list, \"outgoing_node\"))\n", "\n", "\n", "available_states = [state.value for state in ProcessState]\n", - "process_state_widget = ipw.SelectMultiple(options=available_states,\n", - " value=available_states,\n", - " description='Process State:',\n", - " style={'description_width': 'initial'},\n", - " disabled=False)\n", - "dlink((process_state_widget, 'value'), (process_list, 'process_states'))\n", + "process_state_widget = ipw.SelectMultiple(\n", + " options=available_states,\n", + " value=available_states,\n", + " description=\"Process State:\",\n", + " style={\"description_width\": \"initial\"},\n", + " disabled=False,\n", + ")\n", + "dlink((process_state_widget, \"value\"), (process_list, \"process_states\"))\n", "\n", "process_label_widget = ipw.Text(\n", - " description='Process label:',\n", - " style={'description_width': 'initial'}\n", + " description=\"Process label:\", style={\"description_width\": \"initial\"}\n", ")\n", - "dlink((process_label_widget, 'value'), (process_list, 'process_label'))\n", + "dlink((process_label_widget, \"value\"), (process_list, \"process_label\"))\n", "\n", "description_contains_widget = ipw.Text(\n", - " description='Description contains:',\n", - " style={'description_width': 'initial'}\n", + " description=\"Description contains:\", style={\"description_width\": \"initial\"}\n", ")\n", - "dlink((description_contains_widget, 'value'), (process_list, 'description_contains'))\n", + "dlink((description_contains_widget, \"value\"), (process_list, \"description_contains\"))\n", "\n", "display(\n", - " ipw.HBox([\n", - " ipw.VBox([past_days_widget, process_state_widget]),\n", - " ipw.VBox(\n", - " [\n", - " all_days_checkbox,\n", - " incoming_node_widget,\n", - " outgoing_node_widget,\n", - " process_label_widget,\n", - " description_contains_widget\n", - " ],\n", - " layout={'margin': '0px 0px 0px 40px'}\n", - " )\n", - " ]),\n", - " process_list)" + " ipw.HBox(\n", + " [\n", + " ipw.VBox([past_days_widget, process_state_widget]),\n", + " ipw.VBox(\n", + " [\n", + " all_days_checkbox,\n", + " incoming_node_widget,\n", + " outgoing_node_widget,\n", + " process_label_widget,\n", + " description_contains_widget,\n", + " ],\n", + " layout={\"margin\": \"0px 0px 0px 40px\"},\n", + " ),\n", + " ]\n", + " ),\n", + " process_list,\n", + ")" ] }, { diff --git a/notebooks/structures.ipynb b/notebooks/structures.ipynb index f5974aee6..7720474ee 100644 --- a/notebooks/structures.ipynb +++ b/notebooks/structures.ipynb @@ -57,10 +57,11 @@ " title=\"From Examples\",\n", " examples=[\n", " (\"Silicon oxide\", \"../miscellaneous/structures/SiO2.xyz\"),\n", - " (\"Silicon\", \"../miscellaneous/structures/Si.xyz\")\n", - " ]),\n", + " (\"Silicon\", \"../miscellaneous/structures/Si.xyz\"),\n", + " ],\n", + " ),\n", " ],\n", - " editors = [\n", + " editors=[\n", " awb.BasicStructureEditor(title=\"Basic Editor\"),\n", " awb.BasicCellEditor(title=\"Basic Cell Editor\"),\n", " ],\n", diff --git a/notebooks/viewers.ipynb b/notebooks/viewers.ipynb index 2d93bdb0b..2b1dc9826 100644 --- a/notebooks/viewers.ipynb +++ b/notebooks/viewers.ipynb @@ -7,7 +7,8 @@ "outputs": [], "source": [ "# Load the default AiiDA profile.\n", - "from aiida import orm, load_profile\n", + "from aiida import load_profile, orm\n", + "\n", "load_profile();" ] }, @@ -39,12 +40,14 @@ "source": [ "from aiidalab_widgets_base import viewer\n", "\n", - "p = orm.Dict(dict={\n", - " 'parameter 1' : 'some string',\n", - " 'parameter 2' : 2,\n", - " 'parameter 3' : 3.0,\n", - " 'parameter 4' : [1, 2, 3],\n", - "})\n", + "p = orm.Dict(\n", + " dict={\n", + " \"parameter 1\": \"some string\",\n", + " \"parameter 2\": 2,\n", + " \"parameter 3\": 3.0,\n", + " \"parameter 4\": [1, 2, 3],\n", + " }\n", + ")\n", "vwr = viewer(p.store(), downloadable=True)\n", "display(vwr)" ] @@ -67,14 +70,17 @@ "outputs": [], "source": [ "from aiida import orm\n", + "\n", "from aiidalab_widgets_base import AiidaNodeViewWidget\n", "\n", - "p = orm.Dict(dict={\n", - " 'parameter 1' : 'some string',\n", - " 'parameter 2' : 2,\n", - " 'parameter 3' : 3.0,\n", - " 'parameter 4' : [1, 2, 3],\n", - "})\n", + "p = orm.Dict(\n", + " dict={\n", + " \"parameter 1\": \"some string\",\n", + " \"parameter 2\": 2,\n", + " \"parameter 3\": 3.0,\n", + " \"parameter 4\": [1, 2, 3],\n", + " }\n", + ")\n", "wdgt = AiidaNodeViewWidget(node=p.store())\n", "display(wdgt)" ] @@ -96,10 +102,10 @@ "from aiida import plugins\n", "from ase.build import molecule\n", "\n", - "m = molecule('H2O')\n", + "m = molecule(\"H2O\")\n", "m.center(vacuum=2.0)\n", "\n", - "StructureData = plugins.DataFactory('core.structure')\n", + "StructureData = plugins.DataFactory(\"core.structure\")\n", "s = StructureData(ase=m)\n", "\n", "wdgt.node = s.store()" @@ -158,16 +164,19 @@ "metadata": {}, "outputs": [], "source": [ - "from aiidalab_widgets_base.viewers import DictViewer\n", "from aiida import plugins\n", "\n", - "Dict = plugins.DataFactory('core.dict')\n", - "p = Dict(dict={\n", - " 'Parameter' :'super long string '*4,\n", - " 'parameter 2' :'value 2',\n", - " 'parameter 3' : 1,\n", - " 'parameter 4' : 2,\n", - "})\n", + "from aiidalab_widgets_base.viewers import DictViewer\n", + "\n", + "Dict = plugins.DataFactory(\"core.dict\")\n", + "p = Dict(\n", + " dict={\n", + " \"Parameter\": \"super long string \" * 4,\n", + " \"parameter 2\": \"value 2\",\n", + " \"parameter 3\": 1,\n", + " \"parameter 4\": 2,\n", + " }\n", + ")\n", "vwr = DictViewer(p.store(), downloadable=True)\n", "display(vwr)" ] @@ -188,27 +197,27 @@ "metadata": {}, "outputs": [], "source": [ - "from ase.build import molecule, bulk\n", - "from aiidalab_widgets_base.viewers import StructureDataViewer\n", + "from ase.build import bulk, molecule\n", "\n", + "from aiidalab_widgets_base.viewers import StructureDataViewer\n", "\n", "# create bulk Pt\n", - "pt = bulk('Pt', cubic = True)\n", + "pt = bulk(\"Pt\", cubic=True)\n", "\n", "# Cif data.\n", - "CifData = plugins.DataFactory('core.cif')\n", + "CifData = plugins.DataFactory(\"core.cif\")\n", "s = CifData(ase=pt)\n", "vwr_cif = StructureDataViewer(s.store())\n", "display(vwr_cif)\n", "\n", "# Structure data.\n", - "m = molecule('H2O')\n", + "m = molecule(\"H2O\")\n", "m.center(vacuum=2.0)\n", "\n", - "StructureData = plugins.DataFactory('core.structure')\n", + "StructureData = plugins.DataFactory(\"core.structure\")\n", "s = StructureData(ase=m)\n", "vwr_structure = StructureDataViewer(s.store())\n", - "display(vwr_structure)\n" + "display(vwr_structure)" ] }, { @@ -229,42 +238,59 @@ "source": [ "import numpy as np\n", "from aiida import plugins\n", + "\n", "from aiidalab_widgets_base.viewers import BandsDataViewer\n", "\n", - "BandsData = plugins.DataFactory('core.array.bands')\n", + "BandsData = plugins.DataFactory(\"core.array.bands\")\n", "bs = BandsData()\n", - "kpoints = np.array([[0. , 0. , 0. ], # array shape is 12 * 3\n", - " [0.1 , 0. , 0.1 ],\n", - " [0.2 , 0. , 0.2 ],\n", - " [0.3 , 0. , 0.3 ],\n", - " [0.4 , 0. , 0.4 ],\n", - " [0.5 , 0. , 0.5 ],\n", - " [0.5 , 0. , 0.5 ],\n", - " [0.525 , 0.05 , 0.525 ],\n", - " [0.55 , 0.1 , 0.55 ],\n", - " [0.575 , 0.15 , 0.575 ],\n", - " [0.6 , 0.2 , 0.6 ],\n", - " [0.625 , 0.25 , 0.625 ]])\n", - "\n", - "bands = np.array([\n", - " [-5.64024889, 6.66929678, 6.66929678, 6.66929678, 8.91047649], # array shape is 12 * 5, where 12 is the size of the kpoints mesh\n", - " [-5.46976726, 5.76113772, 5.97844699, 5.97844699, 8.48186734], # and 5 is the number of states\n", - " [-4.93870761, 4.06179965, 4.97235487, 4.97235488, 7.68276008],\n", - " [-4.05318686, 2.21579935, 4.18048674, 4.18048675, 7.04145185],\n", - " [-2.83974972, 0.37738276, 3.69024464, 3.69024465, 6.75053465],\n", - " [-1.34041116, -1.34041115, 3.52500177, 3.52500178, 6.92381041],\n", - " [-1.34041116, -1.34041115, 3.52500177, 3.52500178, 6.92381041],\n", - " [-1.34599146, -1.31663872, 3.34867603, 3.54390139, 6.93928289],\n", - " [-1.36769345, -1.24523403, 2.94149041, 3.6004033 , 6.98809593],\n", - " [-1.42050683, -1.12604118, 2.48497007, 3.69389815, 7.07537154],\n", - " [-1.52788845, -0.95900776, 2.09104321, 3.82330632, 7.20537566],\n", - " [-1.71354964, -0.74425095, 1.82242466, 3.98697455, 7.37979746]])\n", + "kpoints = np.array(\n", + " [\n", + " [0.0, 0.0, 0.0], # array shape is 12 * 3\n", + " [0.1, 0.0, 0.1],\n", + " [0.2, 0.0, 0.2],\n", + " [0.3, 0.0, 0.3],\n", + " [0.4, 0.0, 0.4],\n", + " [0.5, 0.0, 0.5],\n", + " [0.5, 0.0, 0.5],\n", + " [0.525, 0.05, 0.525],\n", + " [0.55, 0.1, 0.55],\n", + " [0.575, 0.15, 0.575],\n", + " [0.6, 0.2, 0.6],\n", + " [0.625, 0.25, 0.625],\n", + " ]\n", + ")\n", + "\n", + "bands = np.array(\n", + " [\n", + " [\n", + " -5.64024889,\n", + " 6.66929678,\n", + " 6.66929678,\n", + " 6.66929678,\n", + " 8.91047649,\n", + " ], # array shape is 12 * 5, where 12 is the size of the kpoints mesh\n", + " [\n", + " -5.46976726,\n", + " 5.76113772,\n", + " 5.97844699,\n", + " 5.97844699,\n", + " 8.48186734,\n", + " ], # and 5 is the number of states\n", + " [-4.93870761, 4.06179965, 4.97235487, 4.97235488, 7.68276008],\n", + " [-4.05318686, 2.21579935, 4.18048674, 4.18048675, 7.04145185],\n", + " [-2.83974972, 0.37738276, 3.69024464, 3.69024465, 6.75053465],\n", + " [-1.34041116, -1.34041115, 3.52500177, 3.52500178, 6.92381041],\n", + " [-1.34041116, -1.34041115, 3.52500177, 3.52500178, 6.92381041],\n", + " [-1.34599146, -1.31663872, 3.34867603, 3.54390139, 6.93928289],\n", + " [-1.36769345, -1.24523403, 2.94149041, 3.6004033, 6.98809593],\n", + " [-1.42050683, -1.12604118, 2.48497007, 3.69389815, 7.07537154],\n", + " [-1.52788845, -0.95900776, 2.09104321, 3.82330632, 7.20537566],\n", + " [-1.71354964, -0.74425095, 1.82242466, 3.98697455, 7.37979746],\n", + " ]\n", + ")\n", "bs.set_kpoints(kpoints)\n", "bs.set_bands(bands)\n", - "labels = [(0, u'GAMMA'),\n", - " (5, u'X'),\n", - " (6, u'Z'),\n", - " (11, u'U')]\n", + "labels = [(0, \"GAMMA\"), (5, \"X\"), (6, \"Z\"), (11, \"U\")]\n", "bs.labels = labels\n", "\n", "\n", @@ -289,17 +315,17 @@ "outputs": [], "source": [ "import io\n", + "\n", "from aiida import plugins\n", - "from aiidalab_widgets_base.viewers import FolderDataViewer\n", "\n", - "FolderData = plugins.DataFactory('core.folder')\n", + "FolderData = plugins.DataFactory(\"core.folder\")\n", "fd = FolderData()\n", - "with io.StringIO('content of test1 file') as fobj:\n", - " fd.put_object_from_filelike(fobj, path='test1.txt')\n", - "with io.StringIO('content of test2 file') as fobj:\n", - " fd.put_object_from_filelike(fobj, path='test2.txt')\n", - "with io.StringIO(u'content of test_long file'*1000) as fobj:\n", - " fd.put_object_from_filelike(fobj, path='test_long.txt')\n", + "with io.StringIO(\"content of test1 file\") as fobj:\n", + " fd.put_object_from_filelike(fobj, path=\"test1.txt\")\n", + "with io.StringIO(\"content of test2 file\") as fobj:\n", + " fd.put_object_from_filelike(fobj, path=\"test2.txt\")\n", + "with io.StringIO(\"content of test_long file\" * 1000) as fobj:\n", + " fd.put_object_from_filelike(fobj, path=\"test_long.txt\")\n", "vwr = viewer(fd.store(), downloadable=True)\n", "display(vwr)" ] @@ -320,10 +346,14 @@ "metadata": {}, "outputs": [], "source": [ - "from aiida.workflows.arithmetic.add_multiply import add, add_multiply\n", "from aiida import engine, orm\n", + "from aiida.workflows.arithmetic.add_multiply import add, add_multiply\n", + "\n", "from aiidalab_widgets_base.viewers import ProcessNodeViewerWidget\n", - "result, workfunction = engine.run_get_node(add_multiply, orm.Int(3), orm.Int(4), orm.Int(5))\n", + "\n", + "result, workfunction = engine.run_get_node(\n", + " add_multiply, orm.Int(3), orm.Int(4), orm.Int(5)\n", + ")\n", "vwr_workfunction = ProcessNodeViewerWidget(workfunction)\n", "display(vwr_workfunction)\n", "\n", @@ -354,15 +384,17 @@ "metadata": {}, "outputs": [], "source": [ - "from aiidalab_widgets_base import register_viewer_widget\n", "import ipywidgets as ipw\n", - "from aiida.orm import Int\n", "\n", - "@register_viewer_widget('data.core.int.Int.')\n", + "from aiidalab_widgets_base import register_viewer_widget\n", + "\n", + "\n", + "@register_viewer_widget(\"data.core.int.Int.\")\n", "class IntViewerWidget(ipw.HTML):\n", " def __init__(self, node, **kwargs):\n", " super().__init__(**kwargs)\n", - " self.value = f'Int object: {node.value}'\n", + " self.value = f\"Int object: {node.value}\"\n", + "\n", "\n", "vwr = viewer(orm.Int(3).store())\n", "display(vwr)" diff --git a/notebooks/wizard_apps.ipynb b/notebooks/wizard_apps.ipynb index 1dcfc4606..c8a5fba9a 100644 --- a/notebooks/wizard_apps.ipynb +++ b/notebooks/wizard_apps.ipynb @@ -31,21 +31,25 @@ "source": [ "import enum\n", "import json\n", - "import time\n", "import threading\n", + "import time\n", "\n", - "import traitlets\n", "import ipywidgets as ipw\n", + "import traitlets\n", "\n", - "from aiidalab_widgets_base import WizardAppWidget\n", - "from aiidalab_widgets_base import WizardAppWidgetStep\n", - "\n", + "from aiidalab_widgets_base import WizardAppWidget, WizardAppWidgetStep\n", "\n", "OrderStatus = enum.Enum(\n", " \"OrderStatus\",\n", - " {\"init\": 0, \"in_preparation\": 40, \"in_transit\": 65, \"delivered\": 100, \"unavailable\": -1}\n", + " {\n", + " \"init\": 0,\n", + " \"in_preparation\": 40,\n", + " \"in_transit\": 65,\n", + " \"delivered\": 100,\n", + " \"unavailable\": -1,\n", + " },\n", ")\n", - " \n", + "\n", "\n", "class OrderTracker(traitlets.HasTraits):\n", " \"\"\"Helper class to keep track of our pizza order.\"\"\"\n", @@ -70,24 +74,25 @@ " self.status = OrderStatus.delivered\n", "\n", " threading.Thread(target=simulate_order).start()\n", - " \n", - " \n", + "\n", " @traitlets.default(\"status\")\n", " def _default_status(self):\n", " \"\"\"Initialize the initial (default) order status.\"\"\"\n", " return OrderStatus.init\n", - " \n", - " \n", + "\n", + "\n", "class OrderProgressWidget(ipw.HBox):\n", " \"\"\"Widget to nicely represent the order status.\"\"\"\n", - " \n", + "\n", " status = traitlets.Instance(OrderStatus)\n", "\n", " def __init__(self, **kwargs):\n", - " self._progress_bar = ipw.FloatProgress(style={\"description_width\": \"initial\"}, description=\"Delivery progress:\")\n", + " self._progress_bar = ipw.FloatProgress(\n", + " style={\"description_width\": \"initial\"}, description=\"Delivery progress:\"\n", + " )\n", " self._status_text = ipw.HTML()\n", " super().__init__([self._progress_bar, self._status_text], **kwargs)\n", - " \n", + "\n", " @traitlets.observe(\"status\")\n", " def _observe_status(self, change):\n", " with self.hold_trait_notifications():\n", @@ -99,7 +104,7 @@ " OrderStatus.delivered: \"Delivered! :)\",\n", " OrderStatus.unavailable: \"Your order is not available\",\n", " }.get(change[\"new\"], change[\"new\"].name)\n", - " \n", + "\n", " self._progress_bar.value = change[\"new\"].value\n", " self._progress_bar.bar_style = {\n", " OrderStatus.delivered: \"success\",\n", @@ -112,7 +117,6 @@ "\n", "\n", "class ConfigureOrderStep(ipw.HBox, WizardAppWidgetStep):\n", - "\n", " disabled = traitlets.Bool()\n", " configuration = traitlets.Dict(allow_none=True)\n", "\n", @@ -146,14 +150,17 @@ "\n", " def _confirm_configuration(self, button):\n", " \"Confirm the pizza configuration and expose as trait.\"\n", - " self.configuration = dict(style=self.style.value, toppings=self.toppings.value)\n", - " \n", + " self.configuration = {\n", + " \"style\": self.style.value,\n", + " \"toppings\": self.toppings.value,\n", + " }\n", + "\n", " def reset(self):\n", " with self.hold_trait_notifications():\n", " self.style.value = None\n", " self.toppings.value = []\n", " self.configuration = {}\n", - " \n", + "\n", " @traitlets.default(\"state\")\n", " def _default_state(self):\n", " return self.State.READY\n", @@ -183,7 +190,7 @@ " with self.hold_trait_notifications():\n", " self.disabled = change[\"new\"] == self.State.SUCCESS\n", " self.confirm_button.disabled = change[\"new\"] is not self.State.CONFIGURED\n", - " \n", + "\n", " @traitlets.observe(\"disabled\")\n", " def _observe_disabled(self, change):\n", " with self.hold_trait_notifications():\n", @@ -192,7 +199,6 @@ "\n", "\n", "class ReviewAndSubmitOrderStep(ipw.VBox, WizardAppWidgetStep):\n", - "\n", " # We use traitlets to connect the different steps.\n", " # Note that we can use dlinked transformations, they do not need to be of the same type.\n", " configuration = traitlets.Dict()\n", @@ -212,7 +218,7 @@ " self.observe(self._update_state, [\"configuration\", \"order\"])\n", "\n", " super().__init__([self.configuration_label, self.order_button], **kwargs)\n", - " \n", + "\n", " def reset(self):\n", " self.order = None\n", "\n", @@ -246,7 +252,6 @@ "\n", "\n", "class TrackOrderStep(ipw.VBox, WizardAppWidgetStep):\n", - "\n", " # We receive the order from the previous step and then display information about\n", " # its state in this widget.\n", " order = traitlets.Instance(OrderTracker, allow_none=True)\n", @@ -276,7 +281,6 @@ " else:\n", " self.state = self.State.INIT\n", "\n", - "\n", " def can_reset(self):\n", " \"Do not allow reset during active order processing.\"\n", " return self.state is not self.State.ACTIVE\n", @@ -284,7 +288,7 @@ " def _update_state(self, change=None):\n", " \"Update the step's state based on the order status configuration traits.\"\n", " new_status = change[\"new\"]\n", - " \n", + "\n", " if new_status in (OrderStatus.in_preparation, OrderStatus.in_transit):\n", " self.state = self.State.ACTIVE\n", " elif new_status is OrderStatus.delivered:\n", From 152c2bb9a6279632a686f6c60e2e99345208c42f Mon Sep 17 00:00:00 2001 From: Xing Wang Date: Mon, 21 Oct 2024 18:15:04 +0200 Subject: [PATCH 22/24] Allow user to hide the header of the Wizard App (#638) This PR adds `show_header` property to allow the user to hide the header --- aiidalab_widgets_base/wizard.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/aiidalab_widgets_base/wizard.py b/aiidalab_widgets_base/wizard.py index 074433769..0c633683d 100644 --- a/aiidalab_widgets_base/wizard.py +++ b/aiidalab_widgets_base/wizard.py @@ -88,7 +88,7 @@ class WizardAppWidget(ipw.VBox): selected_index = tl.Int(allow_none=True) - def __init__(self, steps, **kwargs): + def __init__(self, steps, show_header=True, **kwargs): # The number of steps must be greater than one # for this app's logic to make sense. if len(steps) < 2: @@ -142,11 +142,20 @@ def __init__(self, steps, **kwargs): ) self.next_button.on_click(self._on_click_next_button) - header = ipw.HBox( + self.header = ipw.HBox( children=[self.back_button, self.reset_button, self.next_button] ) + self.show_header = show_header - super().__init__(children=[header, self.accordion], **kwargs) + super().__init__(children=[self.header, self.accordion], **kwargs) + + @property + def show_header(self): + return self.header.layout.display != "none" + + @show_header.setter + def show_header(self, value): + self.header.layout.display = "flex" if value else "none" def _update_titles(self): for i, (title, widget) in enumerate(zip(self.titles, self.accordion.children)): From 1f400bc68a4071489c79db9849e73bdbd1de5a54 Mon Sep 17 00:00:00 2001 From: Xing Wang Date: Mon, 21 Oct 2024 19:05:06 +0200 Subject: [PATCH 23/24] Avoid black sphere in structure viewer (#637) In the case of a structure with only one atom, the structure viewer shows a black sphere. This could be because of the overlap between the atoms and the viewer's control. This PR implements a quick solution by shifting the center of the control in the z direction by 1. --- aiidalab_widgets_base/viewers.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/aiidalab_widgets_base/viewers.py b/aiidalab_widgets_base/viewers.py index df715081c..b70d91d87 100644 --- a/aiidalab_widgets_base/viewers.py +++ b/aiidalab_widgets_base/viewers.py @@ -1247,6 +1247,14 @@ def _observe_displayed_structure(self, change): self._viewer.add_unitcell() self._viewer._add_shape(set(bonds), name="bonds") self._viewer.center() + # In case of a structure with only one atom, the `center()` method will show a black sphere. + if len(self.displayed_structure) == 1: + # get center of mass of the displayed structure + com = self.displayed_structure.get_center_of_mass() + # The default camera should be in the z direction, so we + # shift the center of the control in z direction by 1 + com[2] -= 1 + self._viewer.control.center(com) self.displayed_selection = [] From 26773684a5b671bfcb9cee4ade248e7978bc26ff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 14:04:50 +0100 Subject: [PATCH 24/24] Bump the gha-dependencies group with 2 updates (#645) Bumps the gha-dependencies group with 2 updates: [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) and [codecov/codecov-action](https://github.com/codecov/codecov-action). Updates `astral-sh/setup-uv` from 3 to 4 - [Release notes](https://github.com/astral-sh/setup-uv/releases) - [Commits](https://github.com/astral-sh/setup-uv/compare/v3...v4) Updates `codecov/codecov-action` from 4 to 5 - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v4...v5) --- updated-dependencies: - dependency-name: astral-sh/setup-uv dependency-type: direct:production update-type: version-update:semver-major dependency-group: gha-dependencies - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major dependency-group: gha-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0d27e3bbc..fb12ac92f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,7 +44,7 @@ jobs: python-version: '3.10' - name: Setup uv - uses: astral-sh/setup-uv@v3 + uses: astral-sh/setup-uv@v4 with: version: ${{ env.UV_VERSION }} @@ -114,7 +114,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Setup uv - uses: astral-sh/setup-uv@v3 + uses: astral-sh/setup-uv@v4 with: version: ${{ env.UV_VERSION }} @@ -131,7 +131,7 @@ jobs: run: pytest -v tests --cov=aiidalab_widgets_base - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: flags: python-${{ matrix.python-version }} token: ${{ secrets.CODECOV_TOKEN }}