From 05f43eb60f686855584a732b14aec26952a81d17 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Wed, 3 Sep 2025 09:49:33 -0700 Subject: [PATCH 1/2] Update pip package installation method --- neon_utils/packaging_utils.py | 28 +++++++++++++++++++++------- tests/packaging_util_tests.py | 13 ++++++------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/neon_utils/packaging_utils.py b/neon_utils/packaging_utils.py index 884d830f..764212a5 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,14 +268,23 @@ 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 {install_str}: {e}") + return e.returncode + _, tmp_constraints_file = mkstemp() _, tmp_requirements_file = mkstemp() @@ -291,8 +301,12 @@ def install_packages_from_pip(core_module: str, packages: List[str]) -> int: LOG.info(f"Requested installation of plugins: {install_str}") 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: {install_str}") + 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__': From f003df087358d5ff011124b6b1d316bfd047b034 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Wed, 3 Sep 2025 11:09:00 -0700 Subject: [PATCH 2/2] Remove variable used only for logging --- neon_utils/packaging_utils.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/neon_utils/packaging_utils.py b/neon_utils/packaging_utils.py index 764212a5..618c905a 100644 --- a/neon_utils/packaging_utils.py +++ b/neon_utils/packaging_utils.py @@ -282,14 +282,12 @@ def _pip_install(command_args: List[str]) -> int: result = subprocess.check_call([sys.executable, '-m', 'pip'] + command_args) return result except subprocess.CalledProcessError as e: - LOG.error(f"Error installing {install_str}: {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) @@ -299,13 +297,13 @@ def _pip_install(command_args: 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] if stat := _pip_install(pip_args) != 0: return stat if force_reinstall: - LOG.info(f"Requested forced re-installation of plugins: {install_str}") + LOG.info(f"Requested forced re-installation of plugins: {packages}") pip_args.extend(['--no-deps', '--force-reinstall']) stat = _pip_install(pip_args)