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 nvm shim for running nvm commands #168

Merged
merged 18 commits into from
Feb 22, 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: 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))