From ddd79614bded727f967e84bff5f644b8866c9387 Mon Sep 17 00:00:00 2001 From: Casper Welzel Andersen Date: Tue, 13 Oct 2020 02:49:43 +0200 Subject: [PATCH 1/2] Revisit dependencies, add "server" extra MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the following (unused) dependencies: - appmode - jupyterlab - numpy Moved the following dependencies to the new `server` extra: - ase - voila Any non-native OPTIMADE adapter formats have been demoted to only be included if the package is installed. This means the download format widget can be used without having ASE installed. For a more complete experience, ASE is kept as a dependency for the `server` extra. Make the CLI work if Voilà is not installed. Update README with new dependency setup. Create `Contribute` section in README. --- README.md | 66 ++++++++++++++++++++++++++++++-------- optimade_client/cli/run.py | 14 +++++++- optimade_client/summary.py | 25 ++++++++++++--- requirements.txt | 5 --- requirements_server.txt | 2 ++ setup.py | 9 ++++-- tests/cli/test_run.py | 33 +++++++++++++++++++ 7 files changed, 128 insertions(+), 26 deletions(-) create mode 100644 requirements_server.txt create mode 100644 tests/cli/test_run.py diff --git a/README.md b/README.md index ea12e0f0..cbeaab23 100644 --- a/README.md +++ b/README.md @@ -20,19 +20,19 @@ For AiiDAlab, use the App Store in the [Home App](https://github.com/aiidalab/ai ## Usage -### Default +### AiiDAlab To use the OPTIMADE structure importer in your own AiiDAlab application write the following: ```python -from optimade_client import OptimadeQueryWidget +from aiidalab_widget_base import OptimadeQueryWidget from aiidalab_widgets_base.viewers import StructureDataViewer from ipywidgets import dlink structure_query = OptimadeQueryWidget() structure_viewer = StructureDataViewer() -# Save to `_` in order to suppress output in App Mode +# Save to `_` in order to suppress output _ = dlink((structure_query, 'structure'), (structure_viewer, 'structure')) display(structure_query) @@ -47,13 +47,40 @@ See the [OPTIMADE API specification](https://github.com/Materials-Consortia/OPTi In order to delve deeper into the details of a particular structure, you can also import and display `OptimadeResultsWidget`. See the notebook [`OPTIMADE Client.ipynb`](OPTIMADE%20Client.ipynb) for an example of how to set up a general purpose OPTIMADE importer. -### Embedded +#### Embedded The query widget may also be embedded into another app. For this a more "minimalistic" version of the widget can be used by passing `embedded=True` upon initiating the widget, i.e., `structure_query = OptimadeQueryWidget(embedded=True)`. Everything else works the same - so you would still have to link up the query widget to the rest of your app. +### General Jupyter notebook + +The package's widgets can be used in any general Jupyter notebook as well as AiiDAlab. +Example: + +```python +from optimade_client import + OptimadeQueryProviderWidget, + OptimadeQueryFilterWidget, + OptimadeSummaryWidget +from ipywidgets import dlink + +database_selector = OptimadeQueryProviderWidget() +structure_query = OptimadeQueryFilterWidget() +structure_viewer = OptimadeSummaryWidget() + +# Save to `_` in order to suppress output +_ = dlink((database_selector, 'database'), (structure_query, 'database')) +_ = dlink((structure_query, 'structure'), (structure_viewer, 'entity')) + +display(database_selector, structure_query, structure_viewer) +``` + +This will use the package's own structure viewer and summary widget. + +Note, the `OptimadeQueryWidget` mentioned above is a special wrapper widget in AiiDAlab for the `OptimadeQueryProviderWidget` and `OptimadeQueryFilterWidget` widgets. + ### Running application locally First, you will need to install the package either from [PyPI](https://pypi.org/project/optimade-client) or by retrieving the git repository hosted on [GitHub](https://github.com/CasperWA/voila-optimade-client). @@ -61,7 +88,7 @@ First, you will need to install the package either from [PyPI](https://pypi.org/ #### PyPI ```shell -$ pip install optimade-client +$ pip install optimade-client[server] ``` #### GitHub @@ -69,12 +96,12 @@ $ pip install optimade-client ```shell $ git clone https://github.com/CasperWA/voila-optimade-client.git $ cd voila-optimade-client -voila-optimade-client$ pip install . +voila-optimade-client$ pip install .[server] ``` -If you wish to contribute to the application, you can install it in "editable" mode by using the `-e` flag: `pip install -e .` +Note, it is important to install the `server` extra in order to also install the `voila` package (and the `ase` package for a wider variety of download formats). -To now run the application (notebook) [`OPTIMADE Client.ipynb`](OPTIMADE%20Client.ipynb) you can simply run the command `optimade-client` in a terminal and go to the printed URL (usually ) or pass the `--open-browser` option to let the program try to automatically open your default browser at the URL. +To now run the application (notebook) [`OPTIMADE Client.ipynb`](OPTIMADE%20Client.ipynb) you can simply run the command `optimade-client` in a terminal and go to the printed URL (usually ) or pass the `--open-browser` option to let the program try to automatically open your default browser. The application will be run in Voilà using Voilà's own `tornado`-based server. The configuration will automatically be copied to Jupyter's configuration directory before starting the server. @@ -82,17 +109,30 @@ The configuration will automatically be copied to Jupyter's configuration direct ```shell $ optimade-client ... -[Voila] Voila is running at: +[Voila] Voilà is running at: http://localhost:8866/ ... ``` For a list of all options that can be passed to `optimade-client` use the `-h/--help` option. +## Contribute + +If you wish to contribute to the application, you can install it in "editable" mode by using the `-e` flag: `pip install -e .[dev]`. +It is recommended that you use the GitHub-route mentioned above. + +You should also install `pre-commit` in the cloned git repository by running: + +```shell +voila-optimade-client$ pre-commit install +``` + +To start making contributions, fork the repository and create PRs. + ## Configuration (Voilà) -For running the application (in Voilà) on Binder, the configuration file [`jupyter_config.json`](optimade_client/cli/static/jupyter_config.json) can be used. -If you wish to start the Voilà server locally with the same configuration, either copy the [`jupyter_config.json`](optimade_client/cli/static/jupyter_config.json) file to your Jupyter config directory, renaming it to `voila.json` or pass the configurations when you start the server using the CLI. +For running the application (in Voilà) on Binder, the configuration file [`jupyter_config.json`](optimade_client/jupyter_config.json) can be used. +If you wish to start the Voilà server locally with the same configuration, either copy the [`jupyter_config.json`](optimade_client/jupyter_config.json) file to your Jupyter config directory, renaming it to `voila.json` or pass the configurations when you start the server using the CLI. > **Note**: `jupyter_config.json` is automatically copied over as `voila.json` when running the application using the `optimade-client` command. @@ -108,7 +148,7 @@ Example of passing configurations when you start the Voilà server using the CLI ```shell $ voila --enable_nbextensions=True --VoilaExecutePreprocessor.timeout=180 "OPTIMADE Client.ipynb" ... -[Voila] Voila is running at: +[Voila] Voilà is running at: http://localhost:8866/ ... ``` @@ -117,7 +157,7 @@ To see the full list of configurations you can call `voila` and pass `--help-all ## License -MIT. The terms of the license can be found in the LICENSE file. +MIT. The terms of the license can be found in the [LICENSE](LICENSE) file. ## Contact diff --git a/optimade_client/cli/run.py b/optimade_client/cli/run.py index a8d5b875..ef1753b1 100644 --- a/optimade_client/cli/run.py +++ b/optimade_client/cli/run.py @@ -3,8 +3,12 @@ from pathlib import Path from shutil import copyfile import subprocess +import sys -from voila.app import main as voila +try: + from voila.app import main as voila +except ImportError: + voila = None from optimade_client import __version__ from optimade_client.cli.options import LOGGING_LEVELS @@ -46,6 +50,14 @@ def main(): debug = args.debug open_browser = args.open_browser + # Make sure Voilà is installed + if voila is None: + sys.exit( + "Voilà is not installed.\nPlease run:\n\n pip install optimade-client[server]\n\n" + "Or the equivalent, matching the installation in your environment, to install Voilà " + "(and ASE for a larger download format selection)." + ) + # Rename jupyter_config.json to voila.json and copy it Jupyter's config dir jupyter_config_dir = subprocess.getoutput("jupyter --config-dir") copyfile( diff --git a/optimade_client/summary.py b/optimade_client/summary.py index 81597d5c..a028cb33 100644 --- a/optimade_client/summary.py +++ b/optimade_client/summary.py @@ -8,7 +8,10 @@ import nglview -from ase import Atoms as aseAtoms +try: + from ase import Atoms as aseAtoms +except ImportError: + aseAtoms = None try: from pymatgen import Molecule as pymatgenMolecule, Structure as pymatgenStructure @@ -197,6 +200,7 @@ def __init__(self, button_style: Union[ButtonStyle, str] = None, **kwargs): else: self._button_style = ButtonStyle.DEFAULT + self._initialize_options() self.dropdown = ipw.Dropdown( options=("Select a format", {}), layout={"width": "auto"} ) @@ -226,7 +230,21 @@ def _on_change_structure(self, change: dict): self._update_options() self.unfreeze() - def _update_options(self): + def _initialize_options(self) -> None: + """Initialize options according to installed packages""" + for imported_object, adapter_format in [ + (aseAtoms, "ase"), + (pymatgenStructure, "pymatgen"), + ]: + if imported_object is None: + LOGGER.debug("%s not recognized to be installed.", adapter_format) + self._formats = [ + option + for option in self._formats + if option[1].get("adapter_format", "") != adapter_format + ] + + def _update_options(self) -> None: """Update options according to chosen structure""" # Disordered structures not usable with ASE if "disorder" in self.structure.structure_features: @@ -241,10 +259,9 @@ def _update_options(self): if option[1].get("adapter_format", "") != "ase" ] ) - options.insert(0, ("Select a format", {})) else: options = sorted(self._formats) - options.insert(0, ("Select a format", {})) + options.insert(0, ("Select a format", {})) LOGGER.debug("Will set the dropdown options to: %s", options) self.dropdown.options = options diff --git a/requirements.txt b/requirements.txt index f9308852..2e88f29c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,8 @@ appdirs~=1.4.4 -appmode~=0.8.0 -ase~=3.20 cachecontrol[filecache]~=0.12.6 ipywidgets~=7.5 -jupyterlab~=2.2 nglview~=2.7 -numpy~=1.19 optimade~=0.12.1 pandas~=1.1 requests~=2.24 -voila~=0.2.3 widget_periodictable~=2.1 diff --git a/requirements_server.txt b/requirements_server.txt new file mode 100644 index 00000000..779aca02 --- /dev/null +++ b/requirements_server.txt @@ -0,0 +1,2 @@ +ase~=3.20 +voila~=0.2.3 diff --git a/setup.py b/setup.py index 683016ac..33aefb43 100644 --- a/setup.py +++ b/setup.py @@ -9,11 +9,14 @@ with open(MODULE_DIR.joinpath("requirements.txt")) as handle: REQUIREMENTS = [f"{_.strip()}" for _ in handle.readlines() if " " not in _] +with open(MODULE_DIR.joinpath("requirements_server.txt")) as handle: + SERVER = [f"{_.strip()}" for _ in handle.readlines()] + with open(MODULE_DIR.joinpath("requirements_testing.txt")) as handle: TESTING = [f"{_.strip()}" for _ in handle.readlines()] with open(MODULE_DIR.joinpath("requirements_dev.txt")) as handle: - DEV = [f"{_.strip()}" for _ in handle.readlines()] + TESTING + DEV = [f"{_.strip()}" for _ in handle.readlines()] + TESTING + SERVER setup( name="optimade-client", @@ -28,9 +31,9 @@ python_requires=">=3.6", packages=find_packages(), include_package_data=True, - package_data={"optimade_client": ["img/*.png", "cli/static/*.json"]}, + package_data={"optimade_client": ["img/*.png", "*.json"]}, install_requires=REQUIREMENTS, - extras_require={"dev": DEV, "testing": TESTING}, + extras_require={"dev": DEV, "testing": TESTING, "server": SERVER}, classifiers=[ "Development Status :: 5 - Production/Stable", "Framework :: AiiDA", diff --git a/tests/cli/test_run.py b/tests/cli/test_run.py new file mode 100644 index 00000000..0b89f642 --- /dev/null +++ b/tests/cli/test_run.py @@ -0,0 +1,33 @@ +import pytest + +try: + import voila as _ +except ImportError: + VOILA_PACKAGE_EXISTS = False +else: + VOILA_PACKAGE_EXISTS = True + + +@pytest.mark.skipif( + not VOILA_PACKAGE_EXISTS, + reason="Voilà is not installed. This test is rendered invalid.", +) +def test_default(run_cli): + """Run `optimade-client` with default settings""" + output = run_cli() + assert "[Voila] Voilà is running at:" in output, f"output:\n{output}" + + +@pytest.mark.skipif( + VOILA_PACKAGE_EXISTS, reason="Voilà is installed. This test is rendered invalid." +) +def test_voila_not_installed(run_cli): + """Ensure the CLI can handle Voilà not being installed.""" + output = run_cli(raises=True) + exit_text = ( + "Voilà is not installed.\nPlease run:\n\n pip install optimade-client[server]\n\n" + "Or the equivalent, matching the installation in your environment, to install Voilà " + "(and ASE for a larger download format selection)." + ) + assert exit_text in output + assert "[Voila]" not in output From a6fdbfb8f3aaa63c80454c7b0ff217eb4afccbea Mon Sep 17 00:00:00 2001 From: Casper Welzel Andersen Date: Tue, 13 Oct 2020 12:28:33 +0200 Subject: [PATCH 2/2] New tests for CLI Use multiprocessing to run CLI. This is to ensure we can use monkeypatch and are going through the Python API and not invoking the CLI through a separate Python process. Extend CI to take `server` extra into account. Fix and clean CLI according to tests. Remove cli/options.py. It only contained a single variable definition. This has been moved to cli/run.py. Avoid importing optimade-client package in CLI. Define package version manually in CLI. Add new manual package version number to `update-version` invoke task. Add optimade_client/cli/run.py to Publish on PyPI workflow. Create Jupyter config dir if it doesn't exist. --- .github/static/update_version.sh | 1 + .github/workflows/ci.yml | 10 ++- optimade_client/cli/options.py | 4 -- optimade_client/cli/run.py | 27 +++++---- tasks.py | 4 ++ tests/cli/conftest.py | 50 +++++++++++++++ tests/cli/test_no_voila_run.py | 25 ++++++++ tests/cli/test_run.py | 101 +++++++++++++++++++++++-------- tests/test_utils.py | 19 +++--- 9 files changed, 191 insertions(+), 50 deletions(-) delete mode 100644 optimade_client/cli/options.py create mode 100644 tests/cli/conftest.py create mode 100644 tests/cli/test_no_voila_run.py diff --git a/.github/static/update_version.sh b/.github/static/update_version.sh index 16c41ce3..dc04b198 100644 --- a/.github/static/update_version.sh +++ b/.github/static/update_version.sh @@ -18,4 +18,5 @@ echo "\n### Commit updated files ###" git add setup.py git add optimade_client/__init__.py git add optimade_client/informational.py +git add optimade_client/cli/run.py git commit -m "Release ${GITHUB_REF#refs/tags/}" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27d38db8..787e0797 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,14 +59,20 @@ jobs: with: python-version: ${{ matrix.python-version}} - - name: Install dependencies + - name: Install initial dependencies run: | python -m pip install -U pip pip install -U setuptools pip install -e .[testing] - name: PyTest - run: pytest -vvv --cov=optimade_client --cov-report=xml tests/ + run: pytest -vvv --cov=optimade_client --cov-report=xml --cov-append tests/ + + - name: Install server dependencies + run: pip install -e .[server] + + - name: PyTest (with 'server' extra) + run: pytest --cov=optimade_client --cov-report=xml --cov-append tests/cli/ - name: Upload coverage to Codecov if: matrix.python-version == 3.8 && github.repository == 'CasperWA/voila-optimade-client' diff --git a/optimade_client/cli/options.py b/optimade_client/cli/options.py deleted file mode 100644 index ea9dccfa..00000000 --- a/optimade_client/cli/options.py +++ /dev/null @@ -1,4 +0,0 @@ -import logging - - -LOGGING_LEVELS = [logging.getLevelName(level).lower() for level in range(0, 51, 10)] diff --git a/optimade_client/cli/run.py b/optimade_client/cli/run.py index ef1753b1..d6ffdf7a 100644 --- a/optimade_client/cli/run.py +++ b/optimade_client/cli/run.py @@ -1,4 +1,5 @@ import argparse +import logging import os from pathlib import Path from shutil import copyfile @@ -10,11 +11,12 @@ except ImportError: voila = None -from optimade_client import __version__ -from optimade_client.cli.options import LOGGING_LEVELS +LOGGING_LEVELS = [logging.getLevelName(level).lower() for level in range(0, 51, 10)] +VERSION = "2020.10.2" # Avoid importing optimade-client package -def main(): + +def main(args: list = None): """Run the OPTIMADE Client.""" parser = argparse.ArgumentParser( description=main.__doc__, @@ -24,7 +26,7 @@ def main(): "--version", action="version", help="Show the version and exit.", - version=f"OPTIMADE Client version {__version__}", + version=f"OPTIMADE Client version {VERSION}", ) parser.add_argument( "--log-level", @@ -36,8 +38,7 @@ def main(): parser.add_argument( "--debug", action="store_true", - help="Will set the log-level to DEBUG. Note, parameter log-level takes precedence " - "if not 'info'!", + help="Will overrule log-level option and set the log-level to 'debug'.", ) parser.add_argument( "--open-browser", @@ -45,7 +46,7 @@ def main(): help="Attempt to open a browser upon starting the Voilà tornado server.", ) - args = parser.parse_args() + args = parser.parse_args(args) log_level = args.log_level debug = args.debug open_browser = args.open_browser @@ -60,6 +61,7 @@ def main(): # Rename jupyter_config.json to voila.json and copy it Jupyter's config dir jupyter_config_dir = subprocess.getoutput("jupyter --config-dir") + Path(jupyter_config_dir).mkdir(parents=True, exist_ok=True) copyfile( Path(__file__).parent.parent.parent.joinpath("jupyter_config.json").resolve(), f"{jupyter_config_dir}/voila.json", @@ -72,13 +74,11 @@ def main(): check=False, ) - log_level = log_level.lower() - if debug and log_level == "info": - log_level = "debug" - argv = ["OPTIMADE Client.ipynb"] - if log_level == "debug": + if debug: + if log_level not in ("debug", "info"): + print("[OPTIMADE-Client] Overwriting requested log-level to: 'debug'") os.environ["OPTIMADE_CLIENT_DEBUG"] = "True" argv.append("--debug") else: @@ -87,4 +87,7 @@ def main(): if not open_browser: argv.append("--no-browser") + if "--debug" not in argv: + argv.append(f"--Voila.log_level={getattr(logging, log_level.upper())}") + voila(argv) diff --git a/tasks.py b/tasks.py index d20f0cde..c9b9c458 100644 --- a/tasks.py +++ b/tasks.py @@ -47,5 +47,9 @@ def update_version(_, version=""): TOP_DIR.joinpath("optimade_client/informational.py"), (r"Client version.*", f"Client version: {version}"), ) + update_file( + TOP_DIR.joinpath("optimade_client/cli/run.py"), + (r'VERSION = ".+"', f'VERSION = "{version}"'), + ) print(f"Bumped version to {version} !") diff --git a/tests/cli/conftest.py b/tests/cli/conftest.py new file mode 100644 index 00000000..e79772d3 --- /dev/null +++ b/tests/cli/conftest.py @@ -0,0 +1,50 @@ +from multiprocessing import Process +import os +import signal +from time import sleep +from typing import List + +import pytest + + +@pytest.fixture +def run_cli(capfd): + """Run a command in the `optimade-client` CLI (through the Python API).""" + + def _run_cli(options: List[str] = None, raises: bool = False) -> str: + """Run a command in the `optimade-client` CLI (through the Python API).""" + from optimade_client.cli import run + + if options is None: + options = [] + + try: + cli = Process(target=run.main, args=(options,)) + cli.start() + sleep(5) # Startup time + output = capfd.readouterr() + finally: + os.kill(cli.pid, signal.SIGINT) + timeout = 10 # seconds + while cli.is_alive() and timeout: + sleep(1) + timeout -= 1 + if cli.is_alive(): + cli.kill() + cli.join() + sleep(1) + + assert not cli.is_alive(), f"Could not stop CLI subprocess " + + if raises: + assert ( + cli.exitcode != 0 + ), f"\nstdout:\n{output.out}\n\nstderr:\n{output.err}" + else: + assert ( + cli.exitcode == 0 + ), f"\nstdout:\n{output.out}\n\nstderr:\n{output.err}" + + return output.out + output.err + + return _run_cli diff --git a/tests/cli/test_no_voila_run.py b/tests/cli/test_no_voila_run.py new file mode 100644 index 00000000..78809e60 --- /dev/null +++ b/tests/cli/test_no_voila_run.py @@ -0,0 +1,25 @@ +import pytest + +try: + import voila as _ # noqa: F401 +except ImportError: + VOILA_INSTALLED = False +else: + VOILA_INSTALLED = True + +pytestmark = pytest.mark.skipif( + VOILA_INSTALLED, + reason="Voilà is installed. Tests in this module are rendered invalid.", +) + + +def test_voila_not_installed(run_cli): + """Ensure the CLI can handle Voilà not being installed.""" + output = run_cli(raises=True) + exit_text = ( + "Voilà is not installed.\nPlease run:\n\n pip install optimade-client[server]\n\n" + "Or the equivalent, matching the installation in your environment, to install Voilà " + "(and ASE for a larger download format selection)." + ) + assert exit_text in output + assert "[Voila]" not in output diff --git a/tests/cli/test_run.py b/tests/cli/test_run.py index 0b89f642..06c0e1e9 100644 --- a/tests/cli/test_run.py +++ b/tests/cli/test_run.py @@ -1,33 +1,84 @@ import pytest -try: - import voila as _ -except ImportError: - VOILA_PACKAGE_EXISTS = False -else: - VOILA_PACKAGE_EXISTS = True - - -@pytest.mark.skipif( - not VOILA_PACKAGE_EXISTS, - reason="Voilà is not installed. This test is rendered invalid.", -) + +_ = pytest.importorskip("voila") + + def test_default(run_cli): """Run `optimade-client` with default settings""" output = run_cli() assert "[Voila] Voilà is running at:" in output, f"output:\n{output}" -@pytest.mark.skipif( - VOILA_PACKAGE_EXISTS, reason="Voilà is installed. This test is rendered invalid." -) -def test_voila_not_installed(run_cli): - """Ensure the CLI can handle Voilà not being installed.""" - output = run_cli(raises=True) - exit_text = ( - "Voilà is not installed.\nPlease run:\n\n pip install optimade-client[server]\n\n" - "Or the equivalent, matching the installation in your environment, to install Voilà " - "(and ASE for a larger download format selection)." - ) - assert exit_text in output - assert "[Voila]" not in output +def test_version(run_cli): + """Check `--version` flag""" + from optimade_client import __version__ + + output = run_cli(["--version"]) + assert output == f"OPTIMADE Client version {__version__}\n" + + +def test_log_level(run_cli): + """Check `--log-level` option + + Levels: + - `warning`: Above normal setting (INFO). There shouldn't be any output. + - `info`: Normal setting. There should be minimal output. + - `debug`: Below normal setting (INFO). There should be maximum output. + """ + signed_text = "Notebook already signed: OPTIMADE Client.ipynb\n" + + log_level = "warning" + + output = run_cli(["--log-level", log_level]) + assert output == signed_text + + log_level = "info" + + output = run_cli(["--log-level", log_level]) + assert output != signed_text + assert signed_text in output + assert "[Voila]" in output + assert "[Voila] template paths:" not in output + + log_level = "debug" + output = run_cli(["--log-level", log_level]) + assert output != signed_text + assert signed_text in output + assert "[Voila] template paths:" in output + + +def test_debug(run_cli): + """Check `--debug` flag + + This forcefully sets the log-level to `debug`. + """ + output = run_cli(["--debug"]) + assert "[Voila] template paths:" in output + + log_level = "info" + output = run_cli(["--log-level", log_level, "--debug"]) + assert "[OPTIMADE-Client] Overwriting requested log-level to: 'debug'" not in output + assert "[Voila] template paths:" in output + + log_level = "warning" + output = run_cli(["--log-level", log_level, "--debug"]) + assert "[OPTIMADE-Client] Overwriting requested log-level to: 'debug'" in output + assert "[Voila] template paths:" in output + + log_level = "debug" + output = run_cli(["--log-level", log_level, "--debug"]) + assert "[OPTIMADE-Client] Overwriting requested log-level to: 'debug'" not in output + assert "[Voila] template paths:" in output + + +def test_open_browser(run_cli, monkeypatch): + """Check `--open-browser` flag""" + check_text = "Testing: webbrowser.get has been overwritten" + monkeypatch.setattr("webbrowser.get", lambda *args, **kwargs: print(check_text)) + + output = run_cli(["--open-browser"]) + assert check_text in output, f"output:\n{output}" + + output = run_cli() + assert check_text not in output, f"output:\n{output}" diff --git a/tests/test_utils.py b/tests/test_utils.py index b6d09e4f..9da365d8 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,6 +1,3 @@ -from optimade_client import utils - - def test_fetch_providers_wrong_url(): """Test when fetch_providers is provided a wrong URL @@ -8,6 +5,8 @@ def test_fetch_providers_wrong_url(): """ import json + from optimade_client import utils + wrong_url = "https://this.is.a.wrong.url" providers = utils.fetch_providers(providers_urls=wrong_url) @@ -21,6 +20,8 @@ def test_fetch_providers_wrong_url(): def test_fetch_providers_content(): """Test known content in dict of database providers""" + from optimade_client.utils import fetch_providers + exmpl = { "type": "links", "id": "exmpl", @@ -33,13 +34,15 @@ def test_fetch_providers_content(): }, } - assert exmpl in utils.fetch_providers() + assert exmpl in fetch_providers() def test_exmpl_not_in_list(): """Make sure the 'exmpl' database provider is not in the final list""" from optimade.models import LinksResourceAttributes + from optimade_client.utils import get_list_of_valid_providers + exmpl = ( "Example provider", LinksResourceAttributes( @@ -82,7 +85,7 @@ def test_exmpl_not_in_list(): ), ) - list_of_database_providers = utils.get_list_of_valid_providers() + list_of_database_providers = get_list_of_valid_providers() assert exmpl not in list_of_database_providers assert mcloud in list_of_database_providers or odbx in list_of_database_providers @@ -93,6 +96,8 @@ def test_ordered_query_url(): Testing already sorted URLs, making sure they come out exactly the same as when they came in. """ + from optimade_client.utils import ordered_query_url + normal_url = ( "https://optimade.materialsproject.org/v1.0.0/structures?filter=%28+nelements%3E%3D1+AND+" "nelements%3C%3D9+AND+nsites%3E%3D1+AND+nsites%3C%3D444+%29+AND+%28+NOT+structure_features" @@ -106,8 +111,8 @@ def test_ordered_query_url(): "=json&response_format=xml" ) - ordered_url = utils.ordered_query_url(normal_url) + ordered_url = ordered_query_url(normal_url) assert ordered_url == normal_url - ordered_url = utils.ordered_query_url(multi_query_param_url) + ordered_url = ordered_query_url(multi_query_param_url) assert ordered_url == multi_query_param_url