Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 23 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,24 +39,38 @@ jobs:
run: aiida-project init --shell bash

- name: Create
run: aiida-project create testproject

- name: Create with additional plugins
run: 'aiida-project create testproject2 --core-version=2.7 -p aiida-cp2k -p git+https://github.com/aiidateam/aiida-quantumespresso'
run: |
aiida-project create testproject
if aiida-project create testproject; then echo "ERROR: Attempting to overwrite an existing project should fail!"; fi
if aiida-project create ""; then echo "ERROR: Attempting to create a project with empty name should fail!"; fi

# NOTE: The cda bash function does not seem to work in GitHub runners so we execute it manually
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't catch this before, but have you tried source-ing the $HOME/.bashrc? The aiida-project init command will add the cda command there, but the user always has to source or open a new terminal.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I did try and couldn't make it to work.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strange... Oh well.

- name: cda
run: |
cat ~/.aiida_project.env
export $(grep -v '^#' ~/.aiida_project.env | xargs)
source "$aiida_venv_dir/testproject/bin/activate"
cd "$aiida_project_dir/testproject" && pwd
ls -lrt
uv pip list
ls -lrt && uv pip list
uv pip show aiida-core

- name: Create with additional plugins
run: |
aiida-project create testproject2 --core-version=2.7 -p aiida-cp2k -p git+https://github.com/aiidateam/aiida-quantumespresso
if aiida-project create invalid -p aiida-invalid; then echo "ERROR: Installing invalid package should fail"; fi

- name: cda again
run: |
export $(grep -v '^#' ~/.aiida_project.env | xargs)
source "$aiida_venv_dir/testproject2/bin/activate"
cd "$aiida_project_dir/testproject2" && pwd
ls -lrt && uv pip list
uv pip show aiida-core aiida-cpk2 aiida-quantumespresso

- name: Destroy
run: |
aiida-project destroy --force testproject
export $(grep -v '^#' ~/.aiida_project.env | xargs)
if [[ -d "$aiida_project_dir/testproject" ]]; then echo "Project destruction incomplete!"; exit 1; fi
if [[ -d "$aiida_venv_dir/testproject" ]]; then echo "Project venv not destroyed!"; exit 1; fi
if [[ -d "$aiida_project_dir/testproject" ]]; then echo "ERROR: Project destruction incomplete!"; exit 1; fi
if [[ -d "$aiida_venv_dir/testproject" ]]; then echo "ERROR: Project venv not destroyed!"; exit 1; fi

if aiida-project destroy -f testproject; then echo "ERROR: Destroying non-existing project did not fail!"; fi
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ build/
.tox
*.log
uv.lock
.vscode
36 changes: 26 additions & 10 deletions aiida_project/commands/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def init(shell: Optional[ShellType] = None) -> None:


@app.command()
def create(
def create( # noqa: PLR0915
name: str,
engine: EngineType = EngineType.venv,
core_version: str = "latest",
Expand All @@ -107,6 +107,16 @@ def create(
if config.is_not_initialised():
sys.exit(os.EX_CONFIG)

# Guard against user putting an empty string (allowed by typer!)
if not name:
print("[bold red]Error:[/bold red] Project name cannot be an empty string.'")
sys.exit(os.EX_USAGE)

project_dict = ProjectDict()
if name in project_dict.projects:
print(f"[bold red]Error:[/bold red] Project named '{name}' already exists!")
sys.exit(os.EX_USAGE)

venv_path = config.aiida_venv_dir / Path(name)
project_path = config.aiida_project_dir / Path(name)

Expand All @@ -129,9 +139,7 @@ def create(
else:
python_path = Path(python)
if not python_path.exists():
python_which = shutil.which(python)
if python_which is None:
python_which = shutil.which(f"python{python}")
python_which = shutil.which(python) or shutil.which(f"python{python}")
if python_which is None:
print("[bold red]Error:[/bold red] Could not resolve path to Python binary.")
sys.exit(os.EX_USAGE)
Expand All @@ -142,14 +150,21 @@ def create(
"✨ Creating the project directory and environment using the Python binary:\n"
f" [purple]{python_path.resolve()}[/]"
)
project.create(python_path=python_path)

try:
project.create(python_path=python_path)
except CalledProcessError as e:
print("[bold red]Error:[/bold red] Python environment creation failed!")
typer.echo(e)
typer.echo(e.stdout.decode())
typer.echo(e.stderr.decode())
sys.exit(1)

typer.echo("🔧 Adding the AiiDA environment variables to the activate script.")
shell = load_shell(config.aiida_project_shell)
project.append_activate_text(shell.activate.format(env_file_path=project_path))
project.append_deactivate_text(shell.deactivate)

project_dict = ProjectDict()
project_dict.add_project(project)
print("✅ [bold green]Success:[/bold green] Project created.")

Expand All @@ -158,7 +173,7 @@ def create(
aiida_spec += f"=={core_version}"

packages = [aiida_spec, *plugins]
typer.echo(f"💾 Installing packages `{' '.join(packages)}`")
typer.echo(f"💾 Installing `{' '.join(packages)}`")
try:
project.install(packages)
except CalledProcessError as e:
Expand Down Expand Up @@ -188,15 +203,16 @@ def destroy(
try:
project = project_dict.projects[name]
except KeyError:
print(f"[bold red]Error:[/bold red] No project with name {name} found!")
print(f"[bold red]Error:[/bold red] No project named '{name}' found!")
sys.exit(os.EX_USAGE)

if not force:
typer.confirm(
f"❗️ Are you sure you want to delete the entire {name} project? This cannot be undone!",
f"❗️ Are you sure you want to delete the entire '{name}' project? "
f"This cannot be undone!",
abort=True,
)

project.destroy()
project_dict.remove_project(name)
print(f"[bold green]Succes:[/bold green] Project with name {name} has been destroyed.")
print(f"[bold green]Success:[/bold green] Project '{name}' has been destroyed.")
28 changes: 25 additions & 3 deletions aiida_project/project/venv.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
import shutil
import subprocess
import sys
from pathlib import Path
from typing import ClassVar

from aiida_project.config import ProjectConfig
from aiida_project.project.base import BaseProject

__all__ = ["VenvProject"]

# uv should be installed in the same place as aiida-project itself
# NOTE: We convert Path to str here for type-checking purposes. :-/
UV_EXE = (Path(sys.executable).parent / "uv").as_posix()
if not Path(UV_EXE).is_file():
if (which_uv := shutil.which("uv")) is None:
sys.exit("ERROR: Could not find uv executable. Maybe try re-installing aiida-project?")
else:
UV_EXE = which_uv


class VenvProject(BaseProject):
"""An AiiDA environment based on `venv`."""
Expand All @@ -25,12 +37,21 @@ class VenvProject(BaseProject):

def create(self, python_path: Path) -> None:
super().create(python_path)
venv_command = [f"{python_path.resolve()}", "-m", "venv", "."]
self.venv_path.mkdir(
exist_ok=True,
parents=True,
)
subprocess.run(venv_command, cwd=self.venv_path, capture_output=True)
venv_command = [
UV_EXE,
"venv",
"--no-project",
"--allow-existing",
"--seed",
"-p",
f"{python_path.resolve()}",
str(self.venv_path),
]
subprocess.run(venv_command, check=True, capture_output=True)

def destroy(self) -> None:
"""Destroy the project."""
Expand Down Expand Up @@ -60,5 +81,6 @@ def append_deactivate_text(self, text: str) -> None:
)

def install(self, packages: list[str]) -> None:
install_command = [Path(self.venv_path, "bin", "pip").as_posix(), "install", *packages]
python_path = Path(self.venv_path, "bin", "python").as_posix()
install_command = [UV_EXE, "pip", "install", "-p", python_path, *packages]
subprocess.run(install_command, capture_output=True, check=True)
4 changes: 1 addition & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ dependencies = [
"typer[all]~=0.9",
"pyyaml~=6.0",
'eval-type-backport; python_version<"3.10"',
"uv~=0.8.20",
]

[project.urls]
Expand Down Expand Up @@ -60,11 +61,8 @@ strict = true
line-length = 100

[tool.ruff.lint]
# TODO: PLW1510 should be enabled and fixed!
# See `ruff rule PLW1510`
ignore = [
'PLW2901', # `for` loop variable overwritten by assignment target
'PLW1510', # `subprocess.run` without explicit `check` argument
'PLC0415', # `import` should be at the top-level of a file
]

Expand Down