Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add tvm project init command #14

Merged
merged 2 commits into from
May 18, 2022
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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ upgrade: ## update the requirements/*.txt files with the latest packages satisfy

quality: ## check coding style with pycodestyle and pylint
pylint tvm *.py
pycodestyle tvm *.py
pycodestyle tvm *.py --exclude='*/templates/*'
pydocstyle tvm *.py
isort --check-only --diff tvm *.py

Expand Down
2 changes: 1 addition & 1 deletion pylintrc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

[MASTER]
ignore = migrations
ignore = migrations, templates
persistent = yes

[MESSAGES CONTROL]
Expand Down
10 changes: 9 additions & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from click.exceptions import BadParameter
from click.testing import CliRunner

from tvm.cli import cli, install, uninstall, validate_version
from tvm.cli import cli, install, uninstall, validate_version, projects


def test_should_return_all_tvm_cli_commands():
Expand Down Expand Up @@ -33,3 +33,11 @@ def test_should_fail_if_version_is_not_installed():
runner = CliRunner()
result = runner.invoke(uninstall, ["v0.0.99"])
assert 'Nothing to uninstall' in result.stdout


def test_should_return_all_tvm_project_commands():
runner = CliRunner()
result = runner.invoke(projects)

assert result.exit_code == 0
assert ' init ' in result.output
107 changes: 100 additions & 7 deletions tvm/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@
import json
import os
import pathlib
import random
import re
import shutil
import stat
import string
import subprocess
import sys
import zipfile
from distutils.dir_util import copy_tree
from distutils.version import LooseVersion

import click
Expand All @@ -17,6 +20,7 @@

from tvm import __version__
from tvm.templates.tutor_switcher import TUTOR_SWITCHER_TEMPLATE
from tvm.templates.tvm_activate import TVM_ACTIVATE_SCRIPT

VERSIONS_URL = "https://api.github.com/repos/overhangio/tutor/tags"
TVM_PATH = pathlib.Path().resolve() / '.tvm'
Expand Down Expand Up @@ -48,7 +52,7 @@ def shell_complete(self, ctx, param, incomplete):
]


def validate_version(ctx, param, value): # pylint: disable=unused-argument
def version_is_valid(value):
"""Raise BadParameter if the value is not a tutor version."""
result = re.match(r'^v([0-9]+)\.([0-9]+)\.([0-9]+)$', value)
if not result:
Expand All @@ -57,22 +61,31 @@ def validate_version(ctx, param, value): # pylint: disable=unused-argument
return value


def validate_version(ctx, param, value): # pylint: disable=unused-argument
"""Raise BadParameter if the value is not a tutor version."""
return version_is_valid(value)
Alec4r marked this conversation as resolved.
Show resolved Hide resolved


def get_local_versions():
"""Return a list of strings with the local version installed. If None, returns empty array."""
if os.path.exists(f'{TVM_PATH}'):
return [x for x in os.listdir(f'{TVM_PATH}') if os.path.isdir(f'{TVM_PATH}/{x}')]
return []


def version_is_installed(value) -> bool:
"""Return false if the value is not installed."""
version_is_valid(value)
local_versions = get_local_versions()
return value in local_versions


def validate_version_installed(ctx, param, value): # pylint: disable=unused-argument
"""Raise BadParameter if the value is not a tutor version."""
validate_version(ctx, param, value)

local_versions = get_local_versions()
if value not in local_versions:
is_installed = version_is_installed(value)
if not is_installed:
raise click.BadParameter("You must install the version before using it.\n\n"
"Use `stack tvm list` for available versions.")

"Use `tvm list` for available versions.")
return value


Expand Down Expand Up @@ -356,6 +369,84 @@ def list_plugins():
click.echo('Note: the disabled notice depends on the active strain configuration.')


@click.group(
name="project",
short_help="Tutor Environment Manager",
context_settings={"help_option_names": ["--help", "-h", "help"]}
)
@click.version_option(version=__version__)
def projects() -> None:
"""Hold the main wrapper for the `tvm project` command."""


def create_project(project: str) -> None:
"""Duplicate the version directory and rename it."""
if not os.path.exists(f"{TVM_PATH}/{project}"):
tutor_version = project.split("@")[0]
tutor_version_folder = f"{TVM_PATH}/{tutor_version}"
tvm_project = f"{TVM_PATH}/{project}"
copy_tree(tutor_version_folder, tvm_project)


@click.command(name="init")
@click.argument('name', required=False)
@click.argument('version', required=False)
def init(name: str = None, version: str = None):
"""Configure a new tvm project in the current path."""
if not version:
version = get_active_version()

if not version_is_installed(version):
raise click.UsageError(f'Could not find target: {version}')

if not name:
letters = string.ascii_letters
name = ''.join(random.choice(letters) for i in range(10))

version = f"{version}@{name}"

tvm_project_folder = pathlib.Path().resolve()
tvm_environment = tvm_project_folder / '.tvm'

if not os.path.exists(tvm_environment):
pathlib.Path(f"{tvm_environment}/bin").mkdir(parents=True, exist_ok=True)

# Create config json
tvm_project_config_file = f"{tvm_environment}/config.json"
data = {
"version": f"{version}",
"tutor_root": f"{tvm_project_folder}",
"tutor_plugins_root": f"{tvm_project_folder}/plugins"
}
with open(tvm_project_config_file, 'w', encoding='utf-8') as info_file:
json.dump(data, info_file, indent=4)

# Create activate script
context = {
"version": f"{version}",
"tutor_root": f"{tvm_project_folder}",
"tutor_plugins_root": f"{tvm_project_folder}/plugins"
}
activate_script = f"{tvm_environment}/bin/activate"
with open(activate_script, 'w', encoding='utf-8') as activate_file:
activate_file.write(TVM_ACTIVATE_SCRIPT.render(**context))

# Create tutor switcher
context = {
'version': data.get('version', None),
'tvm': f"{TVM_PATH}",
}
tutor_file = f"{tvm_environment}/bin/tutor"
with open(tutor_file, 'w', encoding='utf-8') as switcher_file:
switcher_file.write(TUTOR_SWITCHER_TEMPLATE.render(**context))
# set execute permissions
os.chmod(tutor_file, stat.S_IRWXU | stat.S_IRWXG | stat.S_IROTH | stat.S_IXOTH)

create_project(project=version)
else:
raise click.UsageError('There is already a project initiated.') from IndexError


if __name__ == "__main__":
main()

Expand All @@ -365,5 +456,7 @@ def list_plugins():
cli.add_command(use)
cli.add_command(install_global)
cli.add_command(pip)
cli.add_command(projects)
projects.add_command(init)
cli.add_command(plugins)
plugins.add_command(list_plugins)
78 changes: 78 additions & 0 deletions tvm/templates/tvm_activate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""Tutor switcher jinja template."""
from jinja2 import Template

TEMPLATE = '''
# This file must be used with "source bin/activate" *from bash*
# you cannot run it directly
if [ "${BASH_SOURCE-}" = "$0" ]; then
echo "You must source this script: \$ source $0" >&2
exit 33
fi
tvmoff () {
# reset old environment variables
# ! [ -z ${VAR+_} ] returns true if VAR is declared at all
if ! [ -z "${_TVM_OLD_VIRTUAL_PATH:+_}" ] ; then
PATH="$_TVM_OLD_VIRTUAL_PATH"
export PATH
unset _TVM_OLD_VIRTUAL_PATH
fi
if ! [ -z "${_TVM_OLD_TUTOR_ROOT:+_}" ] ; then
TUTOR_ROOT="$_TVM_OLD_TUTOR_ROOT"
export TUTOR_ROOT
unset _TVM_OLD_TUTOR_ROOT
else
unset TUTOR_ROOT
fi
if ! [ -z "${_TVM_OLD_TUTOR_PLUGINS_ROOT:+_}" ] ; then
TUTOR_PLUGINS_ROOT="$_TVM_OLD_TUTOR_PLUGINS_ROOT"
export TUTOR_PLUGINS_ROOT
unset _TVM_OLD_TUTOR_PLUGINS_ROOT
else
unset TUTOR_PLUGINS_ROOT
fi
# The hash command must be called to get it to forget past
# commands. Without forgetting past commands the $PATH changes
# we made may not be respected
hash -r 2>/dev/null
if ! [ -z "${_TVM_OLD_VIRTUAL_PS1+_}" ] ; then
PS1="$_TVM_OLD_VIRTUAL_PS1"
export PS1
unset _TVM_OLD_VIRTUAL_PS1
fi
unset VIRTUAL_ENV
if [ ! "${1-}" = "nondestructive" ] ; then
# Self destruct!
unset -f tvmoff
fi
}
# unset irrelevant variables
tvmoff nondestructive
TVM_PROJECT_ENV="$PWD/.tvm"
export TVM_PROJECT_ENV
_TVM_OLD_VIRTUAL_PATH="$PATH"
PATH="$TVM_PROJECT_ENV/bin:$PATH"
export PATH
if ! [ -z "${TUTOR_ROOT+_}" ] ; then
_TVM_OLD_TUTOR_ROOT="$TUTOR_ROOT"
fi
TUTOR_ROOT="{{ tutor_root }}"
export TUTOR_ROOT
if ! [ -z "${TUTOR_PLUGINS_ROOT+_}" ] ; then
_TVM_OLD_TUTOR_PLUGINS_ROOT="$TUTOR_PLUGINS_ROOT"
fi
TUTOR_PLUGINS_ROOT="{{ tutor_plugins_root }}"
export TUTOR_PLUGINS_ROOT
_TVM_OLD_VIRTUAL_PS1="${PS1-}"
if [ "x{{version}}" != x ] ; then
PS1="[{{version}}] ${PS1-}"
else
PS1="(`basename \"$VIRTUAL_ENV\"`) ${PS1-}"
fi
export PS1
# The hash command must be called to get it to forget past
# commands. Without forgetting past commands the $PATH changes
# we made may not be respected
hash -r 2>/dev/null
'''

TVM_ACTIVATE_SCRIPT = Template(TEMPLATE)