Skip to content

Commit

Permalink
feat: add nvm shim for running nvm commands (#168)
Browse files Browse the repository at this point in the history
  • Loading branch information
iamogbz authored Feb 22, 2022
1 parent be6c766 commit 4015efd
Show file tree
Hide file tree
Showing 13 changed files with 120 additions and 47 deletions.
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ venv:
install: venv
$(PIP_EXEC) install --upgrade pip
$(PIP_EXEC) install -Ur requirements.txt
$(PIP_EXEC) install .
$(PYTHON_EXEC) -m python_githooks

.PHONY: clean
Expand Down Expand Up @@ -97,6 +98,7 @@ setup.sanity: setup
make sanity.check exec=$(VENV_BIN)node version="v14.5.0"
make sanity.check exec=$(VENV_BIN)npm version="6.14.5"
make sanity.check exec=$(VENV_BIN)npx version="6.14.5"
make sanity.check exec=$(VENV_BIN)nvm version="0.34.0"
rm .nvmrc

.PHONY: setup.debug
Expand Down
6 changes: 5 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ def version_scheme(version):
return datetime.now().strftime("%Y.%m.%d.%H%M%S%f")


console_scripts = [f"nvm=nvshim.core.shim_nvm:main"] + [
f"{s}=nvshim.core.shim:main" for s in shims
]

setup(
author="Emmanuel Ogbizi-Ugbe",
author_email="iamogbz+pypi@gmail.com",
Expand All @@ -86,7 +90,7 @@ def version_scheme(version):
"Topic :: Software Development",
],
description="Automagically use the correct version of node",
entry_points={"console_scripts": [f"{s}=nvshim.core.shim:main" for s in shims]},
entry_points={"console_scripts": console_scripts},
include_package_data=True,
install_requires=get_requirements("requirements/prod.txt", []),
keywords="node nvm node-shim shim shell nvm-shim",
Expand Down
33 changes: 22 additions & 11 deletions src/nvshim/core/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,24 @@ def get_files(path: str) -> [str]:
yield path


def _run_nvm_cmd(
def run_nvm_cmd(
nvm_sh_path: str, nvm_args: str, **kwargs: dict
) -> process.subprocess.CompletedProcess:
return process.run(
f". {nvm_sh_path} && nvm {nvm_args}", shell="bash", encoding="UTF-8", **kwargs
)
nvshim_file_path = f"{os.path.dirname(sys.argv[0])}/nvm_shim.sh.tmp"
try:
with open(nvshim_file_path, "w") as nvshim_file:
nvshim_file.write(f"source {nvm_sh_path}\nnvm {nvm_args}")
return process.run("bash", nvshim_file_path, **kwargs)
finally:
try:
os.remove(nvshim_file_path)
except Exception as e:
message.print_unable_to_remove_nvm_shim_temp_file(e)


@functools.lru_cache(maxsize=None)
def get_nvm_stable_version(nvm_dir) -> str:
output = _run_nvm_cmd(
output = run_nvm_cmd(
get_nvmsh_path(nvm_dir), "alias stable", stdout=subprocess.PIPE
).stdout
try:
Expand Down Expand Up @@ -218,7 +225,7 @@ def get_bin_path(
)
sys.exit(ErrorCode.VERSION_NOT_INSTALLED)

_run_nvm_cmd(nvm_sh_path, f"install {version}")
run_nvm_cmd(nvm_sh_path, f"install {version}")
node_path = get_node_version_bin_dir(
node_versions_dir, version=nvm_aliases.get(version, version)
)
Expand Down Expand Up @@ -256,16 +263,20 @@ def parse_args(args: Sequence[str]) -> (argparse.Namespace, List[str]):
return parser.parse_known_args(args)


def get_nvm_dir():
try:
return environment.get_nvm_dir()
except environment.MissingEnvironmentVariableError as error:
message.print_env_var_missing(error.env_var)
sys.exit(ErrorCode.ENV_NVM_DIR_MISSING)


def main(version_number: str = __version__):
message.print_running_version(version_number)
parsed_args, unknown_args = parse_args(sys.argv[1:])
nvmrc_path = get_nvmrc_path(os.getcwd())
version, parsed = get_nvmrc(nvmrc_path)
try:
nvm_dir = environment.get_nvm_dir()
except environment.MissingEnvironmentVariableError as error:
message.print_env_var_missing(error.env_var)
sys.exit(ErrorCode.ENV_NVM_DIR_MISSING)
nvm_dir = get_nvm_dir()

bin_path = get_bin_path(
version=version,
Expand Down
8 changes: 2 additions & 6 deletions src/nvshim/core/shim.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,8 @@


def main():
try:
sys.argv.insert(1, os.path.basename(sys.argv[0]))
core.main()
except KeyboardInterrupt as e:
print_process_interrupted(e)
sys.exit(ErrorCode.KEYBOARD_INTERRUPT)
sys.argv.insert(1, os.path.basename(sys.argv[0]))
core.main()


if __name__ == "__main__":
Expand Down
14 changes: 14 additions & 0 deletions src/nvshim/core/shim_nvm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import os
import shlex
import sys

import nvshim.core.__main__ as core


def main():
nvm_args = " ".join(shlex.quote(arg) for arg in sys.argv[1:])
core.run_nvm_cmd(core.get_nvmsh_path(core.get_nvm_dir()), nvm_args)


if __name__ == "__main__":
main()
3 changes: 0 additions & 3 deletions src/nvshim/core/tests/__snapshots__/test_shim.ambr

This file was deleted.

4 changes: 3 additions & 1 deletion src/nvshim/core/tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,9 @@ def test_runs_correct_version_of_node(
main()

mocked_process_run.assert_called_with(
(f"{node_version_dir}/bin/{test_args[1]}", *test_args[2:]), check=True
(f"{node_version_dir}/bin/{test_args[1]}", *test_args[2:]),
check=True,
encoding="UTF-8",
)
captured = capsys.readouterr()
assert "with version <v14.5.0>" in clean_output(captured.out)
Expand Down
14 changes: 0 additions & 14 deletions src/nvshim/core/tests/test_shim.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,3 @@ def test_shim_executes_with_args(mocker, test_shim_args):
mocked_core_main = mocker.patch("nvshim.core.shim.core.main", autospec=True)
main()
mocked_core_main.assert_called_once_with()


def test_shim_handles_process_interrupt(mocker, capsys, test_shim_args, snapshot):
mocked_core_main = mocker.patch(
"nvshim.core.shim.core.main",
autospec=True,
side_effect=KeyboardInterrupt("Ctrl+C"),
)
mocked_sys_exit = mocker.patch("sys.exit")
main()
mocked_core_main.assert_called_once_with()
mocked_sys_exit.assert_called_once_with(ErrorCode.KEYBOARD_INTERRUPT)
captured = capsys.readouterr()
snapshot.assert_match(clean_output(captured.out))
30 changes: 30 additions & 0 deletions src/nvshim/core/tests/test_shim_nvm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import runpy
import sys

import pytest

from nvshim.core.shim_nvm import main
from nvshim.utils.constants import ErrorCode
from nvshim.utils.process import clean_output


@pytest.fixture
def test_shim_args():
initial_args = list(sys.argv)
sys.argv = ["/full/path/to/shim/nvm", "--version", "--help"]
yield sys.argv
sys.argv = initial_args


def test_shim_nvm_executes_with_args(mocker, test_shim_args):
nvm_dir = "/home/.nvm"
mocker.patch(
"nvshim.core.shim_nvm.core.get_nvm_dir", autospec=True, return_value=nvm_dir
)
mocked_core_run_nvm_cmd = mocker.patch(
"nvshim.core.shim_nvm.core.run_nvm_cmd", autospec=True
)
main()
mocked_core_run_nvm_cmd.assert_called_once_with(
f"{nvm_dir}/nvm.sh", "--version --help"
)
9 changes: 7 additions & 2 deletions src/nvshim/utils/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,5 +88,10 @@ def print_process_interrupted(exc: KeyboardInterrupt):
_print(f"\nInterrupted. {exc}")


def print_unable_to_run_node(exc: CalledProcessError):
_print(str(exc))
def print_unable_to_run(exc: CalledProcessError):
_print(str(exc), level=MessageLevel.QUIET)


def print_unable_to_remove_nvm_shim_temp_file(exc: Exception):
_print_error("Unable to remove temporary nvm shim file")
_print(str(exc), level=MessageLevel.QUIET)
20 changes: 14 additions & 6 deletions src/nvshim/utils/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
import sys
from typing import Dict

from .constants import ErrorCode
from .environment import EnvironmentVariable, EnvDict, process_env
from .message import print_unable_to_run_node
from .message import print_process_interrupted, print_unable_to_run


def _include_venv(env: EnvDict):
Expand All @@ -18,12 +19,19 @@ def run(*args, **kwargs) -> subprocess.CompletedProcess:
env_vars = _include_venv(os.environ)
env_vars[EnvironmentVariable.AUTO_INSTALL.value] = "false"

with process_env(env_vars):
return run_with_error_handler(*args, **kwargs)


def run_with_error_handler(*args, **kwargs) -> subprocess.CompletedProcess:
try:
with process_env(env_vars):
return subprocess.run(args, **kwargs, check=True)
except subprocess.CalledProcessError as error:
print_unable_to_run_node(error)
sys.exit(error.returncode)
return subprocess.run(args, encoding="UTF-8", **kwargs, check=True)
except KeyboardInterrupt as interrupt_e:
print_process_interrupted(interrupt_e)
sys.exit(ErrorCode.KEYBOARD_INTERRUPT)
except subprocess.CalledProcessError as process_e:
print_unable_to_run(process_e)
sys.exit(process_e.returncode)


def clean_output(output: str) -> str:
Expand Down
3 changes: 3 additions & 0 deletions src/nvshim/utils/tests/__snapshots__/test_process.ambr
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# name: test_process_run_handles_exception_interrupt
'Interrupted. Ctrl+C'
---
21 changes: 18 additions & 3 deletions src/nvshim/utils/tests/test_process.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,31 @@
import subprocess
import pytest

from nvshim.utils import process
from nvshim.utils import constants, process


def test_process_run_completes_successfully():
output = process.run("echo", "success", stdout=subprocess.PIPE).stdout.strip()
assert output == b"success"
assert output == "success"


def test_process_run_raises_correct_exception():
def test_process_run_handles_exception_system_exit():
with pytest.raises(SystemExit) as exc_info:
process.run("bash", "-c", "exit 1")

assert exc_info.value.code == 1


def test_process_run_handles_exception_interrupt(mocker, capsys, snapshot):
mocked_process_run = mocker.patch(
"subprocess.run",
autospec=True,
side_effect=KeyboardInterrupt("Ctrl+C"),
)
mocked_sys_exit = mocker.patch("sys.exit")
args = ("bash", "-c", "echo 1")
process.run(*args)
mocked_process_run.assert_called_once_with(args, check=True, encoding="UTF-8")
mocked_sys_exit.assert_called_once_with(constants.ErrorCode.KEYBOARD_INTERRUPT)
captured = capsys.readouterr()
snapshot.assert_match(process.clean_output(captured.out))

0 comments on commit 4015efd

Please sign in to comment.