diff --git a/neon_utils/packaging_utils.py b/neon_utils/packaging_utils.py index 884d830f..618c905a 100644 --- a/neon_utils/packaging_utils.py +++ b/neon_utils/packaging_utils.py @@ -28,6 +28,7 @@ import sys import re +import subprocess import importlib.util from typing import Tuple, Optional, List from tempfile import mkstemp @@ -267,19 +268,26 @@ def get_skill_license(): # TODO: Implement OSM version of this skill_data["requirements"]["python"].sort() return dict(dict_merge(skill_data, readme_data)) -def install_packages_from_pip(core_module: str, packages: List[str]) -> int: +def install_packages_from_pip(core_module: str, packages: List[str], + force_reinstall: bool = False) -> int: """ Install a Python package using pip :param core_module: string neon core module to install dependency for :param packages: List(string) list of packages to install + :param force_reinstall: force re-installation of packages :returns: int pip exit code """ - import pip + def _pip_install(command_args: List[str]) -> int: + try: + result = subprocess.check_call([sys.executable, '-m', 'pip'] + command_args) + return result + except subprocess.CalledProcessError as e: + LOG.error(f"Error installing {command_args}: {e}") + return e.returncode + _, tmp_constraints_file = mkstemp() _, tmp_requirements_file = mkstemp() - install_str = " ".join(packages) - with open(tmp_constraints_file, 'w', encoding="utf8") as f: constraints = '\n'.join(get_package_dependencies(core_module)) f.write(constraints) @@ -289,10 +297,14 @@ def install_packages_from_pip(core_module: str, packages: List[str]) -> int: for pkg in packages: f.write(f"{pkg}\n") - LOG.info(f"Requested installation of plugins: {install_str}") + LOG.info(f"Requested installation of plugins: {packages}") pip_args = ['install', '-r', tmp_requirements_file, '-c', tmp_constraints_file] - result = pip.main(pip_args) if hasattr(pip, 'main') else pip._internal.main(pip_args) - - if result != 0: - return result - return 0 + if stat := _pip_install(pip_args) != 0: + return stat + + if force_reinstall: + LOG.info(f"Requested forced re-installation of plugins: {packages}") + pip_args.extend(['--no-deps', '--force-reinstall']) + stat = _pip_install(pip_args) + + return stat diff --git a/tests/packaging_util_tests.py b/tests/packaging_util_tests.py index ffbdbf08..6c87bbd7 100644 --- a/tests/packaging_util_tests.py +++ b/tests/packaging_util_tests.py @@ -148,26 +148,25 @@ def test_build_skill_spec(self): # TODO: Actually validate exception cases? DM def test_install_packages_from_pip(self): - import pip + import subprocess from neon_utils.packaging_utils import install_packages_from_pip - with patch.object(pip, 'main', return_value=0) as mock_method: + with patch.object(subprocess, 'check_call', return_value=0) as mock_method: test_result = install_packages_from_pip("neon-utils", ["pip-install-test"]) args, kwargs = mock_method.call_args self.assertEqual(0, test_result) - mock_method.assert_called_once_with(['install', '-r', args[0][2], '-c', args[0][4]]) + mock_method.assert_called_once_with([sys.executable, '-m', 'pip', 'install', '-r', args[0][5], '-c', args[0][7]]) - with open(args[0][2], "r", encoding="utf8") as f: + with open(args[0][5], "r", encoding="utf8") as f: line = f.readline() self.assertEqual("pip-install-test\n", line) mock_method.reset_mock() - test_result = install_packages_from_pip("neon-utils", ["pip-install-test", "pip-install-another"]) + test_result = install_packages_from_pip("neon-utils", ["pip-install-test", "pip-install-another"], force_reinstall=True) self.assertEqual(0, test_result) - mock_method.assert_called_once() - + self.assertEqual(mock_method.call_count, 2) if __name__ == '__main__':