From e2331305c0ce372098f7a94f76839a85a4ecb7f1 Mon Sep 17 00:00:00 2001 From: Carl Simon Adorf Date: Wed, 30 Jun 2021 16:22:14 +0200 Subject: [PATCH 1/9] Implement automated bug reporting feature. (#209) --- aiidalab_widgets_base/bug_report.py | 155 ++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 aiidalab_widgets_base/bug_report.py diff --git a/aiidalab_widgets_base/bug_report.py b/aiidalab_widgets_base/bug_report.py new file mode 100644 index 000000000..94a32e3a2 --- /dev/null +++ b/aiidalab_widgets_base/bug_report.py @@ -0,0 +1,155 @@ +"""Provide more user friendly error messages and automated reporting. + +Authors: + + * Carl Simon Adorf +""" +import base64 +import json +import platform +import re +import sys +import zlib +from textwrap import wrap +from urllib.parse import ( + urlencode, + urlsplit, + urlunsplit, +) + +import ipywidgets as ipw + +from aiidalab.utils import find_installed_packages + + +def get_environment_fingerprint(encoding="utf-8"): + packages = find_installed_packages() + data = { + "version": 1, + "platform": { + "architecture": platform.architecture(), + "python_version": platform.python_version(), + "version": platform.version(), + }, + "packages": {package.name: package.version for package in packages}, + } + json_data = json.dumps(data, separators=(",", ":")) + return base64.urlsafe_b64encode(zlib.compress(json_data.encode(encoding), level=9)) + + +def parse_environment_fingerprint(data, encoding="utf-8"): + packages = json.loads( + zlib.decompress(base64.urlsafe_b64decode(data)).decode(encoding) + ) + return packages + + +ERROR_MESSAGE = """
+

Oh no... the application crashed due to an unexpected error.

+

Please click here to submit an automatically created bug report.

+
+ View the full traceback +
{traceback}
+
+
""" + + +BUG_REPORT_TITLE = """Bug report: Application crashed with {exception_type}""" + +BUG_REPORT_BODY = """## Automated report + +_This issue was created with the app's automated bug reporting feature. +Attached to this issue is the full traceback as well as an environment +fingerprint that contains information about the operating system as well as all +installed libraries._ + +## Additional comments (optional): + +_Example: I submitted a band structure calculation for Silica._ + +## Attachments + +
+Traceback + +```python-traceback +{traceback} +``` +
+ +
+Environment fingerprint +
{environment_fingerprint}
+
+ +**By submitting this issue I confirm that I am aware that this information can +potentially be used to determine what kind of calculation was performed at the +time of error.** +""" + + +def _strip_ansi_codes(msg): + """Remove any ANSI codes (e.g. color codes).""" + return re.sub(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])", "", msg) + + +_ORIGINAL_EXCEPTION_HANDLER = None + + +def install_create_github_issue_exception_handler(output, url, labels=None): + """Install a GitHub bug report exception handler. + + After installing this handler, kernel exception will show a generic error + message to the user, with the option to file an automatic bug report at the + given URL. + """ + global _ORIGINAL_EXCEPTION_HANDLER + + if labels is None: + labels = [] + + ipython = get_ipython() # noqa + _ORIGINAL_EXCEPTION_HANDLER = _ORIGINAL_EXCEPTION_HANDLER or ipython._showtraceback + + def create_github_issue_exception_handler(exception_type, exception, traceback): + try: + output.clear_output() + + truncated_traceback = _strip_ansi_codes("\n".join(traceback[-25:])) + environment_fingerprint = "\n".join( + wrap(get_environment_fingerprint().decode("utf-8"), 100) + ) + + bug_report_query = { + "title": BUG_REPORT_TITLE.format( + exception_type=str(exception_type.__name__) + ), + "body": BUG_REPORT_BODY.format( + traceback=truncated_traceback, + environment_fingerprint=environment_fingerprint, + ), + "labels": ",".join(labels), + } + issue_url = urlunsplit( + urlsplit(url)._replace(query=urlencode(bug_report_query)) + ) + + with output: + msg = ipw.HTML( + ERROR_MESSAGE.format( + issue_url=issue_url, + traceback=truncated_traceback, + len_url=len(issue_url), + ) + ) + display(msg) # noqa + except Exception as error: + print(f"Error while generating bug report: {error}", file=sys.stderr) + _ORIGINAL_EXCEPTION_HANDLER(exception_type, exception, traceback) + + def restore_original_exception_handler(): + ipython._showtraceback = _ORIGINAL_EXCEPTION_HANDLER + + ipython._showtraceback = create_github_issue_exception_handler + + return restore_original_exception_handler From b8d49e39d48c34aaf1524e8cd2215f430df6357b Mon Sep 17 00:00:00 2001 From: Casper Welzel Andersen <43357585+CasperWA@users.noreply.github.com> Date: Tue, 13 Jul 2021 13:40:35 +0200 Subject: [PATCH 2/9] Curate providers for OptimadeQueryWidget (#211) Extend the initialization of `OptimadeQueryProviderWidget` and `OptimadeQueryFilterWidget` with relevant parameters, popping them from the `kwargs` with default fallbacks being either `None` or "private" variables defined for the `OptimadeQueryWidget` class. These variables mirror what is used for the OPTIMADE Client on Materials Cloud Tools, curating the providers and Materials Cloud databases to show. Furthermore, it groups the Materials Cloud databases into "General" and "Projects" according to what has been previously decided with Nicola. --- aiidalab_widgets_base/databases.py | 56 +++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/aiidalab_widgets_base/databases.py b/aiidalab_widgets_base/databases.py index 5a23ed249..6bf757bd5 100644 --- a/aiidalab_widgets_base/databases.py +++ b/aiidalab_widgets_base/databases.py @@ -148,25 +148,65 @@ class OptimadeQueryWidget(ipw.VBox): structure = Instance(Atoms, allow_none=True) + _disable_providers = [ + "cod", + "tcod", + "nmd", + "omdb", + "oqmd", + "aflow", + "matcloud", + "httk", + "mpds", + "necro", + ] + _skip_databases = {"Materials Cloud": ["optimade-sample", "li-ion-conductors"]} + _database_grouping = { + "Materials Cloud": { + "General": ["curated-cofs"], + "Projects": [ + "2dstructures", + "2dtopo", + "pyrene-mofs", + "scdm", + "sssp", + "stoceriaitf", + "tc-applicability", + "threedd", + ], + }, + } + def __init__( self, embedded: bool = True, title: str = None, **kwargs, ) -> None: - providers = OptimadeQueryProviderWidget(embedded=embedded) - filters = OptimadeQueryFilterWidget(embedded=embedded) + providers = OptimadeQueryProviderWidget( + embedded=embedded, + width_ratio=kwargs.pop("width_ratio", None), + width_space=kwargs.pop("width_space", None), + database_limit=kwargs.pop("database_limit", None), + disable_providers=kwargs.pop("disable_providers", self._disable_providers), + skip_databases=kwargs.pop("skip_databases", self._skip_databases), + provider_database_groupings=kwargs.pop( + "provider_database_groupings", self._database_grouping + ), + ) + filters = OptimadeQueryFilterWidget( + embedded=embedded, + button_style=kwargs.pop("button_style", None), + result_limit=kwargs.pop("results_limit", None), + subparts_order=kwargs.pop("subparts_order", None), + ) ipw.dlink((providers, "database"), (filters, "database")) filters.observe(self._update_structure, names="structure") - self.title = title if title is not None else "OPTIMADE" - layout = ( - kwargs.pop("layout") - if "layout" in kwargs - else {"width": "auto", "height": "auto"} - ) + self.title = title or "OPTIMADE" + layout = kwargs.pop("layout", {"width": "auto", "height": "auto"}) super().__init__( children=(providers, filters), From d4b7500e962635f465b6673454a01e16a019bc2c Mon Sep 17 00:00:00 2001 From: Carl Simon Adorf Date: Mon, 19 Jul 2021 11:00:48 +0200 Subject: [PATCH 3/9] Add isort to pre-commit checks. (#214) Keeps Python imports order consistent. --- .pre-commit-config.yaml | 6 ++++ aiidalab_widgets_base/__init__.py | 22 +++++--------- aiidalab_widgets_base/bug_report.py | 7 +---- aiidalab_widgets_base/codes.py | 6 ++-- aiidalab_widgets_base/computers.py | 16 +++++----- aiidalab_widgets_base/data/__init__.py | 4 +-- aiidalab_widgets_base/databases.py | 8 ++--- aiidalab_widgets_base/export.py | 4 ++- aiidalab_widgets_base/misc.py | 1 + aiidalab_widgets_base/nodes.py | 19 ++++++------ aiidalab_widgets_base/process.py | 37 +++++++++++------------ aiidalab_widgets_base/structures.py | 23 +++++++------- aiidalab_widgets_base/viewers.py | 42 ++++++++++++-------------- aiidalab_widgets_base/wizard.py | 5 ++- 14 files changed, 95 insertions(+), 105 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c8f525816..7057ca6f2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,3 +18,9 @@ repos: hooks: - id: flake8 args: [--count, --show-source, --statistics] + + - repo: https://github.com/pycqa/isort + rev: 5.6.4 + hooks: + - id: isort + args: ["--profile", "black", "--filter-files"] diff --git a/aiidalab_widgets_base/__init__.py b/aiidalab_widgets_base/__init__.py index 341dea8e6..34ee599b7 100644 --- a/aiidalab_widgets_base/__init__.py +++ b/aiidalab_widgets_base/__init__.py @@ -3,13 +3,11 @@ load_profile() -from .codes import CodeDropdown, AiiDACodeSetup -from .computers import SshComputerSetup -from .computers import AiidaComputerSetup -from .computers import ComputerDropdown +from .codes import AiiDACodeSetup, CodeDropdown +from .computers import AiidaComputerSetup, ComputerDropdown, SshComputerSetup from .databases import ( - CodQueryWidget, CodeDatabaseWidget, + CodQueryWidget, ComputerDatabaseWidget, OptimadeQueryWidget, ) @@ -28,20 +26,16 @@ RunningCalcJobOutputWidget, SubmitButtonWidget, ) -from .structures import StructureManagerWidget from .structures import ( + BasicStructureEditor, + SmilesWidget, StructureBrowserWidget, StructureExamplesWidget, + StructureManagerWidget, StructureUploadWidget, - SmilesWidget, ) -from .structures import BasicStructureEditor -from .viewers import viewer -from .viewers import register_viewer_widget - -from .wizard import WizardAppWidget -from .wizard import WizardAppWidgetStep - +from .viewers import register_viewer_widget, viewer +from .wizard import WizardAppWidget, WizardAppWidgetStep __all__ = [ "AiiDACodeSetup", diff --git a/aiidalab_widgets_base/bug_report.py b/aiidalab_widgets_base/bug_report.py index 94a32e3a2..d22c38cfd 100644 --- a/aiidalab_widgets_base/bug_report.py +++ b/aiidalab_widgets_base/bug_report.py @@ -11,14 +11,9 @@ import sys import zlib from textwrap import wrap -from urllib.parse import ( - urlencode, - urlsplit, - urlunsplit, -) +from urllib.parse import urlencode, urlsplit, urlunsplit import ipywidgets as ipw - from aiidalab.utils import find_installed_packages diff --git a/aiidalab_widgets_base/codes.py b/aiidalab_widgets_base/codes.py index 66b0d7596..68802c89f 100644 --- a/aiidalab_widgets_base/codes.py +++ b/aiidalab_widgets_base/codes.py @@ -3,11 +3,11 @@ from subprocess import check_output import ipywidgets as ipw +from aiida.orm import Code, Computer, QueryBuilder, User +from aiida.plugins.entry_point import get_entry_point_names from IPython.display import clear_output from traitlets import Bool, Dict, Instance, Unicode, Union, dlink, link, validate -from aiida.orm import Code, Computer, QueryBuilder, User -from aiida.plugins.entry_point import get_entry_point_names from aiidalab_widgets_base.computers import ComputerDropdown @@ -269,7 +269,7 @@ def _setup_code(self, _=None): def exists(self): """Returns True if the code exists, returns False otherwise.""" - from aiida.common import NotExistent, MultipleObjectsError + from aiida.common import MultipleObjectsError, NotExistent try: Code.get_from_string(f"{self.label}@{self.computer.name}") diff --git a/aiidalab_widgets_base/computers.py b/aiidalab_widgets_base/computers.py index 5e5e4522e..7add6db6f 100644 --- a/aiidalab_widgets_base/computers.py +++ b/aiidalab_widgets_base/computers.py @@ -1,13 +1,16 @@ """All functionality needed to setup a computer.""" import os -from os import path from copy import copy -from subprocess import check_output, call +from os import path +from subprocess import call, check_output +import ipywidgets as ipw import pexpect import shortuuid -import ipywidgets as ipw +from aiida.common import NotExistent +from aiida.orm import Computer, QueryBuilder, User +from aiida.transports.plugins.ssh import parse_sshconfig from IPython.display import clear_output from traitlets import ( Bool, @@ -22,11 +25,6 @@ validate, ) -from aiida.common import NotExistent -from aiida.orm import Computer, QueryBuilder, User - -from aiida.transports.plugins.ssh import parse_sshconfig - STYLE = {"description_width": "200px"} @@ -581,8 +579,8 @@ class AiidaComputerSetup(ipw.VBox): safe_interval = Union([Unicode(), Float()]) def __init__(self, **kwargs): - from aiida.transports import Transport from aiida.schedulers import Scheduler + from aiida.transports import Transport # List of widgets to be displayed. inp_computer_name = ipw.Text( diff --git a/aiidalab_widgets_base/data/__init__.py b/aiidalab_widgets_base/data/__init__.py index dacea1376..0ad3a6184 100644 --- a/aiidalab_widgets_base/data/__init__.py +++ b/aiidalab_widgets_base/data/__init__.py @@ -1,7 +1,7 @@ """Useful functions that provide access to some data. """ -from ase import Atom, Atoms -import numpy as np import ipywidgets as ipw +import numpy as np +from ase import Atom, Atoms # The first atom is anchoring, so the new bond will be connecting it # The direction of the new bond is (-1, -1, -1). diff --git a/aiidalab_widgets_base/databases.py b/aiidalab_widgets_base/databases.py index 6bf757bd5..253f89b95 100644 --- a/aiidalab_widgets_base/databases.py +++ b/aiidalab_widgets_base/databases.py @@ -1,13 +1,11 @@ """Widgets that allow to query online databases.""" -import requests import ipywidgets as ipw -from traitlets import Bool, Float, Instance, Int, Unicode, default, observe +import requests +from aiida.tools.dbimporters.plugins.cod import CodDbImporter from ase import Atoms - from optimade_client.query_filter import OptimadeQueryFilterWidget from optimade_client.query_provider import OptimadeQueryProviderWidget - -from aiida.tools.dbimporters.plugins.cod import CodDbImporter +from traitlets import Bool, Float, Instance, Int, Unicode, default, observe class CodQueryWidget(ipw.VBox): diff --git a/aiidalab_widgets_base/export.py b/aiidalab_widgets_base/export.py index 46a383671..16e1b87b5 100644 --- a/aiidalab_widgets_base/export.py +++ b/aiidalab_widgets_base/export.py @@ -1,7 +1,8 @@ """Widgets to manage AiiDA export.""" import os -from ipywidgets import Button + from IPython.display import display +from ipywidgets import Button class ExportButtonWidget(Button): @@ -22,6 +23,7 @@ def export_aiida_subgraph(self, change=None): # pylint: disable=unused-argument import base64 import subprocess from tempfile import mkdtemp + from IPython.display import Javascript fname = os.path.join(mkdtemp(), "export.aiida") diff --git a/aiidalab_widgets_base/misc.py b/aiidalab_widgets_base/misc.py index d8d00d76d..818e7ee27 100644 --- a/aiidalab_widgets_base/misc.py +++ b/aiidalab_widgets_base/misc.py @@ -1,6 +1,7 @@ """Some useful classes used acrross the repository.""" import io import tokenize + import ipywidgets as ipw from traitlets import Unicode diff --git a/aiidalab_widgets_base/nodes.py b/aiidalab_widgets_base/nodes.py index 41ffb136c..e3c128f81 100644 --- a/aiidalab_widgets_base/nodes.py +++ b/aiidalab_widgets_base/nodes.py @@ -1,16 +1,17 @@ """Widgets to work with AiiDA nodes.""" -import traitlets import ipywidgets as ipw -from IPython.display import clear_output -from IPython.display import display +import traitlets from aiida.cmdline.utils.ascii_vis import calc_info from aiida.engine import ProcessState -from aiida.orm import CalcFunctionNode -from aiida.orm import CalcJobNode -from aiida.orm import Node -from aiida.orm import ProcessNode -from aiida.orm import WorkChainNode -from aiida.orm import load_node +from aiida.orm import ( + CalcFunctionNode, + CalcJobNode, + Node, + ProcessNode, + WorkChainNode, + load_node, +) +from IPython.display import clear_output, display from ipytree import Node as TreeNode from ipytree import Tree diff --git a/aiidalab_widgets_base/process.py b/aiidalab_widgets_base/process.py index d3537997e..80525ce34 100644 --- a/aiidalab_widgets_base/process.py +++ b/aiidalab_widgets_base/process.py @@ -3,25 +3,29 @@ # Built-in imports import os import warnings -from inspect import isclass -from inspect import signature -from threading import Event -from threading import Lock -from threading import Thread +from inspect import isclass, signature +from threading import Event, Lock, Thread from time import sleep # External imports import ipywidgets as ipw import pandas as pd import traitlets -from IPython.display import HTML, Javascript, clear_output, display -from traitlets import Instance, Int, List, Unicode, Union, default, observe, validate - # AiiDA imports. from aiida.cmdline.utils.ascii_vis import format_call_graph +from aiida.cmdline.utils.common import ( + get_calcjob_report, + get_process_function_report, + get_workchain_report, +) from aiida.cmdline.utils.query.calculation import CalculationQueryBuilder -from aiida.engine import submit, Process, ProcessBuilder +from aiida.common.exceptions import ( + MultipleObjectsError, + NotExistent, + NotExistentAttributeError, +) +from aiida.engine import Process, ProcessBuilder, submit from aiida.orm import ( CalcFunctionNode, CalcJobNode, @@ -31,20 +35,13 @@ WorkFunctionNode, load_node, ) -from aiida.cmdline.utils.common import ( - get_calcjob_report, - get_workchain_report, - get_process_function_report, -) -from aiida.common.exceptions import ( - MultipleObjectsError, - NotExistent, - NotExistentAttributeError, -) +from IPython.display import HTML, Javascript, clear_output, display +from traitlets import Instance, Int, List, Unicode, Union, default, observe, validate + +from .nodes import NodesTreeWidget # Local imports. from .viewers import viewer -from .nodes import NodesTreeWidget def get_running_calcs(process): diff --git a/aiidalab_widgets_base/structures.py b/aiidalab_widgets_base/structures.py index 410ed0df9..7ef292c5d 100644 --- a/aiidalab_widgets_base/structures.py +++ b/aiidalab_widgets_base/structures.py @@ -1,18 +1,14 @@ """Module to provide functionality to import structures.""" # pylint: disable=no-self-use -import io import datetime +import io from collections import OrderedDict -import numpy as np -import ipywidgets as ipw -from traitlets import Instance, Int, List, Unicode, Union, dlink, link, default, observe -from sklearn.decomposition import PCA # ASE imports import ase -from ase import Atom, Atoms -from ase.data import chemical_symbols, covalent_radii +import ipywidgets as ipw +import numpy as np # AiiDA imports from aiida.engine import calcfunction @@ -20,16 +16,21 @@ CalcFunctionNode, CalcJobNode, Data, - QueryBuilder, Node, + QueryBuilder, WorkChainNode, ) from aiida.plugins import DataFactory +from ase import Atom, Atoms +from ase.data import chemical_symbols, covalent_radii +from sklearn.decomposition import PCA +from traitlets import Instance, Int, List, Unicode, Union, default, dlink, link, observe + +from .data import LigandSelectorWidget # Local imports from .utils import get_ase_from_file from .viewers import StructureDataViewer -from .data import LigandSelectorWidget CifData = DataFactory("cif") # pylint: disable=invalid-name StructureData = DataFactory("structure") # pylint: disable=invalid-name @@ -615,8 +616,8 @@ def __init__(self, title=""): self.title = title try: - from openbabel import pybel # noqa: F401 from openbabel import openbabel # noqa: F401 + from openbabel import pybel # noqa: F401 except ImportError: super().__init__( [ @@ -662,8 +663,8 @@ def make_ase(self, species, positions): def _pybel_opt(self, smile, steps): """Optimize a molecule using force field and pybel (needed for complex SMILES).""" - from openbabel import pybel as pb from openbabel import openbabel as ob + from openbabel import pybel as pb obconversion = ob.OBConversion() obconversion.SetInFormat("smi") diff --git a/aiidalab_widgets_base/viewers.py b/aiidalab_widgets_base/viewers.py index 0dc5e4c57..f900e91e5 100644 --- a/aiidalab_widgets_base/viewers.py +++ b/aiidalab_widgets_base/viewers.py @@ -3,27 +3,16 @@ import base64 import warnings -import numpy as np -from numpy.linalg import norm +from copy import deepcopy + import ipywidgets as ipw -from IPython.display import display import nglview -from ase import Atoms -from ase import neighborlist -from vapory import ( - Camera, - LightSource, - Scene, - Sphere, - Finish, - Texture, - Pigment, - Cylinder, - Background, -) +import numpy as np +from aiida.orm import Node +from ase import Atoms, neighborlist +from IPython.display import display from matplotlib.colors import to_rgb -from copy import deepcopy - +from numpy.linalg import norm from traitlets import ( Instance, Int, @@ -35,12 +24,21 @@ observe, validate, ) -from aiida.orm import Node +from vapory import ( + Background, + Camera, + Cylinder, + Finish, + LightSource, + Pigment, + Scene, + Sphere, + Texture, +) -from .utils import string_range_to_list, list_to_string_range from .dicts import Colors, Radius from .misc import CopyToClipboardButton, ReversePolishNotation - +from .utils import list_to_string_range, string_range_to_list AIIDA_VIEWER_MAPPING = dict() @@ -943,7 +941,7 @@ class BandsDataViewer(ipw.VBox): :type bands: BandsData""" def __init__(self, bands, **kwargs): - from bokeh.io import show, output_notebook + from bokeh.io import output_notebook, show from bokeh.models import Span from bokeh.plotting import figure diff --git a/aiidalab_widgets_base/wizard.py b/aiidalab_widgets_base/wizard.py index 4f931f352..c13d49561 100644 --- a/aiidalab_widgets_base/wizard.py +++ b/aiidalab_widgets_base/wizard.py @@ -5,12 +5,11 @@ * Carl Simon Adorf """ from enum import Enum -from time import sleep -from time import time from threading import Thread +from time import sleep, time -import traitlets import ipywidgets as ipw +import traitlets class WizardAppWidgetStep(traitlets.HasTraits): From aa683c6b47d0f7aef5a8da289a20a3bdacad2d8a Mon Sep 17 00:00:00 2001 From: Carl Simon Adorf Date: Mon, 19 Jul 2021 11:06:15 +0200 Subject: [PATCH 4/9] Disable test-app-action for latest. (#215) Currently not functional. --- .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 d0dfa0458..57557fba2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: strategy: matrix: - tag: [ stable, latest ] + tag: [ stable ] browser: [ chrome, firefox ] fail-fast: false From eefd9d62b1d9cb3054aabd47d88b944a927768af Mon Sep 17 00:00:00 2001 From: Carl Simon Adorf Date: Mon, 19 Jul 2021 11:38:06 +0200 Subject: [PATCH 5/9] CI: Add GitHub actions workflow to release package on (Test) PyPI. (#216) --- .github/workflows/publish.yml | 86 +++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 000000000..c3b609505 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,86 @@ +name: Publish on Test PyPI and PyPI + +on: + push: + branches: + # Commits pushed to release/ branches are published on Test PyPI if they + # have a new version number. + - "release/**" + tags: + # Tags that start with the "v" prefix are published on PyPI. + - "v*" + +jobs: + + build-and-publish-test: + + name: Build and publish on TestPyPI + if: startsWith(github.ref, 'refs/heads/release/') + + runs-on: ubuntu-latest + environment: + name: Test PyPI + url: https://test.pypi.org/project/aiidalab-widgets-base/ + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + + - name: Install pypa/build + run: python -m pip install build + + - name: Build a binary wheel and a source tarball + run: >- + python -m + build + --sdist + --wheel + --outdir dist/ + . + + - name: Publish distribution on Test PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} + repository_url: https://test.pypi.org/legacy/ + + build-and-publish: + + name: Build and publish on PyPI + if: startsWith(github.ref, 'refs/tags') + + runs-on: ubuntu-latest + environment: + name: PyPI + url: https://pypi.org/project/aiidalab-widgets-base/ + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + + - name: Install pypa/build + run: python -m pip install build + + - name: Build a binary wheel and a source tarball + run: >- + python -m + build + --sdist + --wheel + --outdir dist/ + . + + - name: Publish distribution on PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} From bd4a18c1cd3c49612515fc42f9014bac5d27296c Mon Sep 17 00:00:00 2001 From: Aliaksandr Yakutovich Date: Mon, 19 Jul 2021 11:43:44 +0200 Subject: [PATCH 6/9] StructureManagerWidget: connect structure_node trait to the viewer. (#217) Previously, it was structure trait that was connected to a viewer. However, structure trait is not an AiiDA node, therefore it does not contain the provenance information. --- aiidalab_widgets_base/structures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiidalab_widgets_base/structures.py b/aiidalab_widgets_base/structures.py index 7ef292c5d..ab7059092 100644 --- a/aiidalab_widgets_base/structures.py +++ b/aiidalab_widgets_base/structures.py @@ -92,7 +92,7 @@ def __init__( self.viewer = viewer else: self.viewer = StructureDataViewer(downloadable=False) - dlink((self, "structure"), (self.viewer, "structure")) + dlink((self, "structure_node"), (self.viewer, "structure")) # Store button. self.btn_store = ipw.Button(description="Store in AiiDA", disabled=True) From e3887aa826ac9ec017be29d89400883f4d0ed3d0 Mon Sep 17 00:00:00 2001 From: Aliaksandr Yakutovich Date: Mon, 19 Jul 2021 12:02:48 +0200 Subject: [PATCH 7/9] Add AiidaNodeViewWidget class. (#213) * Add AiidaNodeViewWidget class: Unlike viewer, AiidaNodeViewWidget always has a `node` attribute and can always be displayed (even if no object has been provided). * Expose the AiidaNodeViewWidget widget in the package root namespace. Co-authored-by: Carl Simon Adorf --- aiidalab_widgets_base/__init__.py | 3 ++- aiidalab_widgets_base/viewers.py | 26 +++++++++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/aiidalab_widgets_base/__init__.py b/aiidalab_widgets_base/__init__.py index 34ee599b7..7caec85eb 100644 --- a/aiidalab_widgets_base/__init__.py +++ b/aiidalab_widgets_base/__init__.py @@ -34,12 +34,13 @@ StructureManagerWidget, StructureUploadWidget, ) -from .viewers import register_viewer_widget, viewer +from .viewers import AiidaNodeViewWidget, register_viewer_widget, viewer from .wizard import WizardAppWidget, WizardAppWidgetStep __all__ = [ "AiiDACodeSetup", "AiidaComputerSetup", + "AiidaNodeViewWidget", "BasicStructureEditor", "CodQueryWidget", "CodeDatabaseWidget", diff --git a/aiidalab_widgets_base/viewers.py b/aiidalab_widgets_base/viewers.py index f900e91e5..5f911d662 100644 --- a/aiidalab_widgets_base/viewers.py +++ b/aiidalab_widgets_base/viewers.py @@ -8,9 +8,10 @@ import ipywidgets as ipw import nglview import numpy as np +import traitlets from aiida.orm import Node from ase import Atoms, neighborlist -from IPython.display import display +from IPython.display import clear_output, display from matplotlib.colors import to_rgb from numpy.linalg import norm from traitlets import ( @@ -79,6 +80,29 @@ def viewer(obj, downloadable=True, **kwargs): raise exc +class AiidaNodeViewWidget(ipw.VBox): + node = traitlets.Instance(Node, allow_none=True) + + def __init__(self, **kwargs): + self._output = ipw.Output() + super().__init__( + children=[ + self._output, + ], + **kwargs, + ) + + @traitlets.observe("node") + def _observe_node(self, change): + from aiidalab_widgets_base import viewer + + if change["new"] != change["old"]: + with self._output: + clear_output() + if change["new"]: + display(viewer(change["new"])) + + @register_viewer_widget("data.dict.Dict.") class DictViewer(ipw.VBox): From f2fc174b998fb16c3f8df803887d60ae2db8b928 Mon Sep 17 00:00:00 2001 From: Carl Simon Adorf Date: Tue, 20 Jul 2021 10:49:33 +0200 Subject: [PATCH 8/9] Feature/improve bug report (#219) * Improve visual representation and add restart button. * Preserve colours in traceback in bug report mode (requires ansi2html dependency). * Truncate the traceback to be included with the bug report more robustly. This method ensures that the traceback included with the issue report does not have more than 3000 characters, which appears to be roughly the limit that can be submitted via the query parameter. --- aiidalab_widgets_base/bug_report.py | 61 ++++++++++++++++++++++------- setup.cfg | 1 + 2 files changed, 47 insertions(+), 15 deletions(-) diff --git a/aiidalab_widgets_base/bug_report.py b/aiidalab_widgets_base/bug_report.py index d22c38cfd..ef0cd13b5 100644 --- a/aiidalab_widgets_base/bug_report.py +++ b/aiidalab_widgets_base/bug_report.py @@ -15,6 +15,7 @@ import ipywidgets as ipw from aiidalab.utils import find_installed_packages +from ansi2html import Ansi2HTMLConverter def get_environment_fingerprint(encoding="utf-8"): @@ -40,13 +41,26 @@ def parse_environment_fingerprint(data, encoding="utf-8"): ERROR_MESSAGE = """
-

Oh no... the application crashed due to an unexpected error.

-

Please click here to submit an automatically created bug report.

-
- View the full traceback -
{traceback}
-
-
""" +

+ Oh no... the application crashed due to an unexpected error. +

+ + Create bug report + + +
+
+ + View the full traceback + +
{traceback}
+
+
""" BUG_REPORT_TITLE = """Bug report: Application crashed with {exception_type}""" @@ -88,6 +102,22 @@ def _strip_ansi_codes(msg): return re.sub(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])", "", msg) +def _convert_ansi_codes_to_html(msg): + """Convert any ANSI codes (e.g. color codes) into HTML.""" + converter = Ansi2HTMLConverter() + return converter.produce_headers().strip() + converter.convert(msg, full=False) + + +def _format_truncated_traceback(traceback, max_num_chars=3000): + """Truncate the traceback to the given character length.""" + n = 0 + for i, line in enumerate(reversed(traceback)): + n += len(_strip_ansi_codes(line)) + 2 # add 2 for newline control characters + if n > max_num_chars: + break + return _strip_ansi_codes("\n".join(traceback[-i:])) + + _ORIGINAL_EXCEPTION_HANDLER = None @@ -110,18 +140,19 @@ def create_github_issue_exception_handler(exception_type, exception, traceback): try: output.clear_output() - truncated_traceback = _strip_ansi_codes("\n".join(traceback[-25:])) - environment_fingerprint = "\n".join( - wrap(get_environment_fingerprint().decode("utf-8"), 100) - ) - bug_report_query = { "title": BUG_REPORT_TITLE.format( exception_type=str(exception_type.__name__) ), "body": BUG_REPORT_BODY.format( - traceback=truncated_traceback, - environment_fingerprint=environment_fingerprint, + # Truncate the traceback to a maximum of 3000 characters + # and strip all ansi control characters: + traceback=_format_truncated_traceback(traceback, 3000), + # Determine and format the environment fingerprint to be + # included with the bug report: + environment_fingerprint="\n".join( + wrap(get_environment_fingerprint().decode("utf-8"), 100) + ), ), "labels": ",".join(labels), } @@ -133,7 +164,7 @@ def create_github_issue_exception_handler(exception_type, exception, traceback): msg = ipw.HTML( ERROR_MESSAGE.format( issue_url=issue_url, - traceback=truncated_traceback, + traceback=_convert_ansi_codes_to_html("\n".join(traceback)), len_url=len(issue_url), ) ) diff --git a/setup.cfg b/setup.cfg index f407bd232..fbfb0c0ce 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,6 +21,7 @@ classifiers = packages = find: install_requires = aiida-core~=1.0 + ansi2html~=1.6 ase<3.20 bokeh~=2.0 ipytree~=0.2 From 6f57b18e81936a04350b1de4519d659c80f6885a Mon Sep 17 00:00:00 2001 From: Aliaksandr Yakutovich Date: Thu, 22 Jul 2021 14:53:36 +0000 Subject: [PATCH 9/9] Prepare release 1.0.0b19 --- 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 7caec85eb..b4c13beea 100644 --- a/aiidalab_widgets_base/__init__.py +++ b/aiidalab_widgets_base/__init__.py @@ -74,4 +74,4 @@ "viewer", ] -__version__ = "1.0.0b18" +__version__ = "1.0.0b19" diff --git a/setup.cfg b/setup.cfg index fbfb0c0ce..18a8e06d5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,7 +8,7 @@ url = https://github.com/aiidalab/aiidalab-widgets-base author = The AiiDAlab team author_email = aiidalab@materialscloud.org license = MIT -license_file = LICENSE +license_file = LICENSE.txt classifiers = Development Status :: 4 - Beta Framework :: AiiDA