from whisk.project import Project
import whisk.git as git
from whisk.cd import cd
from whisk.cli.log_tree import PARENT_TREE_NODE_PREFIX, CHILD_TREE_NODE_PREFIX
import os
import subprocess
from pathlib import Path
import logging
import sys
from subprocess import PIPE
[docs]logger = logging.getLogger(__name__)
[docs]NOTEBOOK_EXAMPLE_PATH = "notebooks/getting_started.ipynb"
[docs]def log_subprocess_output(logger_with_level, log_lines):
"""
Calls the logger on each of the log_lines. The log lines are prefixed with
:attr:`whisk.cli.log_tree.CHILD_TREE_NODE_PREFIX`.
Parameters
----------
logger_with_level : Logger
A logger instance (ex: ``logger.debug``).
log_lines : str
A string of log lines separated with ``\\n``.
"""
for line in log_lines.splitlines(): # b'\n'-separated lines
logger_with_level(CHILD_TREE_NODE_PREFIX+line)
[docs]def exec(desc, cmd):
"""
Executes the `cmd`, and logs `desc` prior to execution and "DONE" after.
If the `cmd` has `stdout` or `stderr` output this is logged as well.
If the exit code is nonzero, raises a ``SystemExit`` execption.
Parameters
----------
desc : str
A description of the command operation.
cmd : str
The command to execute.
"""
logger.info(PARENT_TREE_NODE_PREFIX+desc+"...")
completed_process = None
try:
# universal_newlines is the same as `text`. text was added in 3.7:
# Changed in version 3.7: Added the text parameter, as a more
# understandable alias of universal_newlines.
# Added the capture_output parameter.
completed_process = subprocess.run(cmd, shell=True, check=True,
universal_newlines=True,
stdout=PIPE,
stderr=PIPE)
log_subprocess_output(logger.info, completed_process.stdout)
log_subprocess_output(logger.error, completed_process.stderr)
logger.info(CHILD_TREE_NODE_PREFIX+"DONE.")
except subprocess.CalledProcessError:
logger.error(CHILD_TREE_NODE_PREFIX +
"Error executing `%s`.\n Stack trace:\n\n",
cmd, exc_info=True)
raise SystemExit("""
###
SETUP FAILED. See whisk troubleshooting docs for help:
https://docs.whisk-ml.org/en/latest/troubleshooting.html
###
""")
[docs]def init_git_repo():
logger.info(PARENT_TREE_NODE_PREFIX+"Initializing the Git repo")
# Idempotent so ok to execute
os.system("git init > /dev/null 2>&1")
logger.info(CHILD_TREE_NODE_PREFIX+"DONE.")
[docs]def init_direnv():
os.system("cp .envrc.example .envrc")
# direnv command will fail if not installed ... just ignore
os.system("direnv allow . > /dev/null 2>&1")
[docs]def exec_setup(project):
"""
Sets up an environment for the given project.
Parameters
----------
project : whisk.project.Project
A whisk project.
"""
exec("Setting up venv", "python3 -m venv {}/venv".format(project.dir))
exec("Installing Python dependencies via pip (may take several minutes)",
"venv/bin/pip install -r requirements.txt > /dev/null")
logger.info(PARENT_TREE_NODE_PREFIX+"Checking if a git repo has been initialized...")
is_git_repo = project.is_git_repo()
logger.info(CHILD_TREE_NODE_PREFIX+str(is_git_repo))
if not is_git_repo:
init_git_repo()
# Would rather use --sys-prefix, but not working:
# https://github.com/jupyter/notebook/issues/4567
nbenv = project.name
exec("Setting up kernel={} for Jupyter Notebooks".format(nbenv),
"venv/bin/python -m ipykernel install --user --name={}".format(nbenv))
set_example_notebook_kernel(nbenv)
init_direnv()
if not is_git_repo and project.has_unstaged_changes():
exec("Adding files to git", "git add .")
exec("Making initial Git commit",
"git commit -m 'Initial project structure' --author=\"Whisk <whisk@whisk-ml.org>\" > /dev/null")
[docs]def notebook_exists(notebook_path):
logger.debug("Checking if notebook exists at path={}".format(notebook_path))
nb_file = Path(NOTEBOOK_EXAMPLE_PATH)
return nb_file.is_file()
[docs]def set_example_notebook_kernel(nbenv):
"""
Updates the :attr:`NOTEBOOK_EXAMPLE_PATH` notebook kernel to use
the kernel with name ``nbenv``.
"""
if not notebook_exists(NOTEBOOK_EXAMPLE_PATH):
logger.info(
f"Getting started notebook does not exist @ {NOTEBOOK_EXAMPLE_PATH}. Not applying venv."
)
return
# Read in the file
with open(NOTEBOOK_EXAMPLE_PATH, 'r') as file:
filedata = file.read()
# This could be run after the initial cookiecutter install.
filedata = filedata.replace("{{cookiecutter.project_name}}", nbenv)
# Write the file out again
with open(NOTEBOOK_EXAMPLE_PATH, 'w') as file:
file.write(filedata)
[docs]def log_success(dir):
"""Logs that the setup completed successfully and provides next steps."""
logger.info("│\n└── Setup completed.")
logger.info("""
###
YOUR PROJECT IS READY. See the docs for help: https://docs.whisk-ml.org
Get started:
""")
if os.getcwd() != dir:
# If not in the project directory,
# instruct the user to cd into the project.
logger.info("cd %s", dir)
logger.info("source venv/bin/activate")
if notebook_exists(Path(dir) / NOTEBOOK_EXAMPLE_PATH):
logger.info(f"jupyter-notebook {NOTEBOOK_EXAMPLE_PATH}")
logger.info("###\n")
[docs]def log_pip_freeze():
"""Logs the output of ``pip freeze`` at the debug level."""
reqs = subprocess.check_output([sys.executable, '-m', 'pip', 'freeze'],
universal_newlines=True)
logger.debug("pip freeze output:\n"+reqs)
[docs]def setup(dir):
"""
Sets up the project environment.
Setup performs the following actions after changing the working
directory to ``dir``:
* Creates a `Python3 venv <https://docs.python.org/3/library/venv.html />`_
named "venv"
* Installs the dependencies listed in the project's requirements.txt.
* Initializes a Git repo
* Creates an iPython kernel for use in Jupyter notebooks with
name = <project_name>.
* Creates a ``.envrc`` file based on ``.envrc.example`` for use with
`direnv <https://github.com/direnv/direnv />`_. direnv loads environment
variables listed in ``.envrc`` into the shell and is also used to
auto-activate and deactivate the venv when entering and exiting the
project directory.
* Calls ``direnv allow .`` so the ``.envrc`` file can be loaded.
* Makes an initial Git commit
Parameters
----------
dir : str
The full path to the project directory.
"""
project = Project(dir)
project.validate_in_project()
with cd(dir):
logger.debug("cwd={}".format(os.getcwd()))
exec_setup(project)
log_success(dir)
log_pip_freeze()