diff --git a/Makefile b/Makefile index 0d849c9..864fd27 100644 --- a/Makefile +++ b/Makefile @@ -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 @@ -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 diff --git a/setup.py b/setup.py index c7be339..2a5804e 100644 --- a/setup.py +++ b/setup.py @@ -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", @@ -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", diff --git a/src/nvshim/core/__main__.py b/src/nvshim/core/__main__.py index 351467a..1d01a7c 100755 --- a/src/nvshim/core/__main__.py +++ b/src/nvshim/core/__main__.py @@ -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: @@ -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) ) @@ -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, diff --git a/src/nvshim/core/shim.py b/src/nvshim/core/shim.py index f37f936..7afb5a0 100755 --- a/src/nvshim/core/shim.py +++ b/src/nvshim/core/shim.py @@ -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__": diff --git a/src/nvshim/core/shim_nvm.py b/src/nvshim/core/shim_nvm.py new file mode 100644 index 0000000..5203c40 --- /dev/null +++ b/src/nvshim/core/shim_nvm.py @@ -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() diff --git a/src/nvshim/core/tests/__snapshots__/test_shim.ambr b/src/nvshim/core/tests/__snapshots__/test_shim.ambr deleted file mode 100644 index bb9f191..0000000 --- a/src/nvshim/core/tests/__snapshots__/test_shim.ambr +++ /dev/null @@ -1,3 +0,0 @@ -# name: test_shim_handles_process_interrupt - 'Interrupted. Ctrl+C' ---- diff --git a/src/nvshim/core/tests/test_main.py b/src/nvshim/core/tests/test_main.py index 4202167..3b876fa 100644 --- a/src/nvshim/core/tests/test_main.py +++ b/src/nvshim/core/tests/test_main.py @@ -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 " in clean_output(captured.out) diff --git a/src/nvshim/core/tests/test_shim.py b/src/nvshim/core/tests/test_shim.py index c9e8677..eb20ada 100644 --- a/src/nvshim/core/tests/test_shim.py +++ b/src/nvshim/core/tests/test_shim.py @@ -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)) diff --git a/src/nvshim/core/tests/test_shim_nvm.py b/src/nvshim/core/tests/test_shim_nvm.py new file mode 100644 index 0000000..178297d --- /dev/null +++ b/src/nvshim/core/tests/test_shim_nvm.py @@ -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" + ) diff --git a/src/nvshim/utils/message.py b/src/nvshim/utils/message.py index 80b3893..e32af51 100644 --- a/src/nvshim/utils/message.py +++ b/src/nvshim/utils/message.py @@ -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) diff --git a/src/nvshim/utils/process.py b/src/nvshim/utils/process.py index 8b625c8..c69d569 100644 --- a/src/nvshim/utils/process.py +++ b/src/nvshim/utils/process.py @@ -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): @@ -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: diff --git a/src/nvshim/utils/tests/__snapshots__/test_process.ambr b/src/nvshim/utils/tests/__snapshots__/test_process.ambr new file mode 100644 index 0000000..c72ff18 --- /dev/null +++ b/src/nvshim/utils/tests/__snapshots__/test_process.ambr @@ -0,0 +1,3 @@ +# name: test_process_run_handles_exception_interrupt + 'Interrupted. Ctrl+C' +--- diff --git a/src/nvshim/utils/tests/test_process.py b/src/nvshim/utils/tests/test_process.py index b46ada6..d80ced5 100644 --- a/src/nvshim/utils/tests/test_process.py +++ b/src/nvshim/utils/tests/test_process.py @@ -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))