From 048deac75691537f0ccfae6d1ff2cc2fa3194a91 Mon Sep 17 00:00:00 2001 From: Marnik Bercx Date: Thu, 11 May 2023 22:13:07 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=91=8C=20CLI:=20Switch=20to=20using=20`ty?= =?UTF-8?q?per`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 1 - aiida_project/commands/__init__.py | 5 -- aiida_project/commands/create.py | 88 --------------------- aiida_project/commands/destroy.py | 30 -------- aiida_project/commands/main.py | 119 +++++++++++++++++++++++++++-- aiida_project/config.py | 11 ++- aiida_project/project/__init__.py | 18 ++++- pyproject.toml | 6 +- 8 files changed, 140 insertions(+), 138 deletions(-) delete mode 100644 aiida_project/commands/create.py delete mode 100644 aiida_project/commands/destroy.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f1841bc..c413a76 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -45,7 +45,6 @@ repos: - id: mypy args: [--config-file=pyproject.toml] additional_dependencies: - - click - py files: > (?x)^( diff --git a/aiida_project/commands/__init__.py b/aiida_project/commands/__init__.py index 037e848..e69de29 100644 --- a/aiida_project/commands/__init__.py +++ b/aiida_project/commands/__init__.py @@ -1,5 +0,0 @@ -from aiida_project.commands.create import create -from aiida_project.commands.destroy import destroy -from aiida_project.commands.main import main - -__all__ = ["main", "create", "destroy"] diff --git a/aiida_project/commands/create.py b/aiida_project/commands/create.py deleted file mode 100644 index d0cb572..0000000 --- a/aiida_project/commands/create.py +++ /dev/null @@ -1,88 +0,0 @@ -import subprocess -from pathlib import Path - -import click - -from ..commands.main import main -from ..project import BaseProject, EngineType - -ACTIVATE_AIIDA_SH = """ -export AIIDA_PATH={path} - -if test -x "$(command -v verdi)" -then - eval "$(_VERDI_COMPLETE=source-bash verdi)" -fi -""" - -DEACTIVATE_AIIDA_SH = "unset AIIDA_PATH" - - -def clone_pypackage(project_path, repo, branch=None): - clone_command = ["git", "clone", "--single-branch"] - if branch: - clone_command.extend(["-b", branch]) - clone_command.append(f"https://github.com/{repo}") - subprocess.call(clone_command, cwd=project_path.strpath) - - -@main.command(context_settings={"show_default": True}) -@click.argument("name") -@click.option( - "--engine", - default="virtualenv", - type=click.Choice(["virtualenv", "wrapper", "conda"]), - help="virtualenv engine", -) -@click.option( - "--core-version", - help="If specified, immediately installs the corresponding version of `aiida-core`.", -) -@click.option( - "--plugins", - multiple=True, - help="plugin specifier /:", -) -@click.option("--python", type=click.Path(file_okay=True, exists=True, dir_okay=False)) -def create(name, engine, core_version, plugins, python): - """Create a new AiiDA project named NAME.""" - from ..config import ProjectConfig, ProjectDict - - config = ProjectConfig() - - venv_path = config.aiida_venv_dir / Path(name) - project_path = config.aiida_project_dir / Path(name) - - project = EngineType[engine].value( - engine=engine, - name=name, - project_path=project_path, - venv_path=venv_path, - dir_structure=config.aiida_project_structure, - ) - assert isinstance( - project, BaseProject - ) # Necessary to have autocomplete - type hinting doesn't work Enum values? - - click.echo("✨ Creating the project environment and directory.") - project.create(python_path=python) - - click.echo("🔧 Adding the AiiDA environment variables to the activate script.") - project.append_activate_text(ACTIVATE_AIIDA_SH.format(path=project_path)) - project.append_deactivate_text(DEACTIVATE_AIIDA_SH) - - project_dict = ProjectDict() - project_dict.add_project(project) - click.echo("✅ Success! Project created.") - - # clone_pypackage(project_path, "aiidateam/aiida_core", branch=core_version) - if core_version is not None: - click.echo(f"💾 Installing AiiDA core module v{core_version}.") - project.install(f"aiida-core=={core_version}") - else: - click.echo("💾 Installing the latest release of the AiiDA core module.") - project.install("aiida-core") - - for plugin in plugins: - click.echo(f"💾 Installing {plugin}") - project.install(plugin) diff --git a/aiida_project/commands/destroy.py b/aiida_project/commands/destroy.py deleted file mode 100644 index 5152f36..0000000 --- a/aiida_project/commands/destroy.py +++ /dev/null @@ -1,30 +0,0 @@ -import click - -from .main import main - - -@main.command(context_settings={"show_default": True}) -@click.argument("name") -@click.option( - "-f", - "--force", - is_flag=True, - help="Do not ask for confirmation.", -) -def destroy(name, force): - """Fully remove both the virtual environment and project directory.""" - from ..config import ProjectDict - - config = ProjectDict() - - try: - project = config.projects[name] - except KeyError: - click.echo(f"No project with name {name} found!") - return - - if click.confirm( - f"Are you sure you want to delete the entire {name} project? This cannot be undone!" - ): - project.destroy() - config.remove_project(name) diff --git a/aiida_project/commands/main.py b/aiida_project/commands/main.py index eb09601..b6017a8 100644 --- a/aiida_project/commands/main.py +++ b/aiida_project/commands/main.py @@ -1,7 +1,116 @@ -import click +from pathlib import Path +from typing import List, Optional +import typer +from rich import print +from typing_extensions import Annotated -@click.group("aipro") -def main(): - """Manage AiiDA projects.""" - pass +from ..project import EngineType, load_project_class + +ACTIVATE_AIIDA_SH = """ +export AIIDA_PATH={path} + +if test -x "$(command -v verdi)" +then + eval "$(_VERDI_COMPLETE=source-bash verdi)" +fi +""" + +DEACTIVATE_AIIDA_SH = "unset AIIDA_PATH" + + +app = typer.Typer(pretty_exceptions_show_locals=False) + + +@app.callback() +def callback(): + """ + Tool for importing CIF files and converting them into a unique set of `StructureData`. + """ + + +@app.command() +def create( + name: str, + engine: EngineType = EngineType.virtualenv, + core_version: str = "latest", + plugins: Annotated[ + List[str], typer.Option("--plugin", "-p", help="Extra plugins to install.") + ] = [], + python: Annotated[ + Optional[Path], + typer.Option( + "--python", + exists=True, + dir_okay=False, + file_okay=True, + help="Path to the Python interpreter to use for the environmnent.", + ), + ] = None, +): + """Create a new AiiDA project named NAME.""" + from ..config import ProjectConfig, ProjectDict + + config = ProjectConfig() + + venv_path = config.aiida_venv_dir / Path(name) + project_path = config.aiida_project_dir / Path(name) + + project = load_project_class(engine.value)( + name=name, + project_path=project_path, + venv_path=venv_path, + dir_structure=config.aiida_project_structure, + ) + + typer.echo("✨ Creating the project environment and directory.") + project.create(python_path=python) + + typer.echo("🔧 Adding the AiiDA environment variables to the activate script.") + project.append_activate_text(ACTIVATE_AIIDA_SH.format(path=project_path)) + project.append_deactivate_text(DEACTIVATE_AIIDA_SH) + + project_dict = ProjectDict() + project_dict.add_project(project) + print("✅ [bold green]Success:[/bold green] Project created.") + + # clone_pypackage(project_path, "aiidateam/aiida_core", branch=core_version) + if core_version != "latest": + typer.echo(f"💾 Installing AiiDA core module v{core_version}.") + project.install(f"aiida-core=={core_version}") + else: + typer.echo("💾 Installing the latest release of the AiiDA core module.") + project.install("aiida-core") + + for plugin in plugins: + typer.echo(f"💾 Installing {plugin}") + project.install(plugin) + + +@app.command() +def destroy( + name: str, + force: Annotated[ + bool, typer.Option("--force", "-f", help="Do not ask for confirmation.") + ] = False, +): + """Fully remove both the virtual environment and project directory.""" + from ..config import ProjectDict + + config = ProjectDict() + + try: + project = config.projects[name] + except KeyError: + print(f"[bold red]Error:[/bold red] No project with name {name} found!") + return + + if not force: + typer.confirm( + f"❗️ Are you sure you want to delete the entire {name} project? This cannot be undone!", + abort=True, + ) + + project.destroy() + config.remove_project(name) + print(f"[bold green]Succes:[/bold green] Project with name {name} has been destroyed.") diff --git a/aiida_project/config.py b/aiida_project/config.py index 3e17cd8..3eb7c9f 100644 --- a/aiida_project/config.py +++ b/aiida_project/config.py @@ -5,7 +5,7 @@ import dotenv from pydantic import BaseSettings -from .project import EngineType +from .project import load_project_class from .project.base import BaseProject DEFAULT_PROJECT_STRUCTURE = { @@ -32,12 +32,17 @@ class Config: def __init__(self, **configuration): super().__init__(**configuration) - if dotenv.get_key(self.Config.env_file, "aiida_venv_dir") is None: + env_config = dotenv.dotenv_values(self.Config.env_file) + if env_config.get("aiida_venv_dir", None) is None: dotenv.set_key( self.Config.env_file, "aiida_venv_dir", environ.get("WORKON_HOME", self.aiida_venv_dir.as_posix()), ) + if env_config.get("aiida_project_dir", None) is None: + dotenv.set_key( + self.Config.env_file, "aiida_project_dir", self.aiida_project_dir.as_posix() + ) class ProjectDict: @@ -52,7 +57,7 @@ def __init__(self): def projects(self) -> Dict[str, BaseProject]: projects = {} for project_file in self._projects_path.glob("**/*.json"): - engine = EngineType[str(project_file.parent.name)].value + engine = load_project_class(str(project_file.parent.name)) project = engine.parse_file(project_file) projects[project.name] = project return projects diff --git a/aiida_project/project/__init__.py b/aiida_project/project/__init__.py index 74cc201..29fb364 100644 --- a/aiida_project/project/__init__.py +++ b/aiida_project/project/__init__.py @@ -1,12 +1,22 @@ from enum import Enum +from typing import Type from .base import BaseProject from .conda import CondaProject from .virtualenv import VirtualenvProject -__all__ = ["BaseProject"] +__all__ = ["BaseProject", "VirtualenvProject"] -class EngineType(Enum): - virtualenv = VirtualenvProject - conda = CondaProject +def load_project_class(engine_type: str) -> Type[BaseProject]: + """Load the project class corresponding the engine type.""" + engine_project_dict = { + "virtualenv": VirtualenvProject, + "conda": CondaProject, + } + return engine_project_dict[engine_type] + + +class EngineType(str, Enum): + virtualenv = "virtualenv" + conda = "conda" diff --git a/pyproject.toml b/pyproject.toml index f426eac..a270b77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,17 +25,17 @@ classifiers = [ keywords = ["aiida", "workflows"] requires-python = ">=3.8" dependencies = [ - "click~=8.1.2", "py~=1.11.0", "pydantic~=1.10.7", "python-dotenv~=1.0.0", + "typer[all]~=0.9.0" ] [project.urls] Source = "https://github.com/aiidateam/aiida-project" [project.scripts] -aiida-project = "aiida_project.commands:main" +aiida-project = "aiida_project.commands.main:app" [project.optional-dependencies] dev = [ @@ -53,5 +53,7 @@ module = [ "dotenv", "pydantic", "yaml", + "typer", + "rich" ] ignore_missing_imports = true