Skip to content

Commit 6597a2a

Browse files
committed
Add logic to detect and skip already installed packages.
This mode can be enabled by a command line flag which, when set, causes the dependencies for each feature to be checked for presence within the active python environment and their installation skipped if present. In practice this means the requirements files can list dependencies exhaustively but still allow the host environment arranged by a build process to provision packages in the host environment and have these respected during installation. Further, if such a package is only one of many requirements then it alone will be skipped while all other dependencies will be correctly installed.
1 parent 0adf688 commit 6597a2a

4 files changed

Lines changed: 67 additions & 9 deletions

File tree

mig/install/features.py

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from collections import defaultdict
55
from configparser import ConfigParser
66
from enum import Enum
7+
import importlib
78
import os
89
import pip
910
import sys
@@ -80,27 +81,32 @@ def feature_is_enabled(self, feature_name):
8081

8182
return self._enabled_by_feature[feature_name]
8283

83-
def generate_pip_args(self):
84+
def generate_pip_args(self, detect_installed=False):
8485
"""
8586
Create pip arguments for each enabled feature.
8687
"""
8788

8889
per_package_args = []
8990

9091
for feature_name in self.list_enabled_features():
91-
per_package_args.append(self.generate_pip_args_for_feature(feature_name))
92+
installable_packages = self.generate_pip_args_for_feature(feature_name,
93+
detect_installed=detect_installed)
94+
if not installable_packages:
95+
continue
96+
per_package_args.append(installable_packages)
9297

9398
return per_package_args
9499

95-
def generate_pip_args_for_feature(self, feature_name):
100+
def generate_pip_args_for_feature(self, feature_name, *, detect_installed):
96101
"""
97102
Create pip arguments for a particular feature.
98103
"""
99104

100-
overrides = self._overrides_by_feature.get(feature_name, None)
105+
overrides = self._overrides_by_feature.get(feature_name, {})
101106

102-
if not overrides:
103-
# no overiddes detected therefore we can install
107+
if not (overrides or detect_installed):
108+
# no overrides detected and we do not need to check for the
109+
# dependencies being already installed therefore we can install
104110
# by simply using the requirements file as-is
105111
return ['-r', self._requirements_file_by_feature[feature_name]]
106112

@@ -111,11 +117,25 @@ def generate_pip_args_for_feature(self, feature_name):
111117
for package_name in overridden_package_names:
112118
package_args.append(f"{package_name}=={overrides[package_name]}")
113119

120+
if detect_installed:
121+
packages_to_detect = self.required_package_names(feature_name)
122+
else:
123+
packages_to_detect = set()
124+
114125
# add the remaining packages based on the requirements file
115126
for entry in self._requirements_by_feature[feature_name]:
116127
package_name = Features._strip_version_if_present(entry.requirement)
117128
if package_name in overridden_package_names:
118129
continue
130+
131+
should_detect = package_name in packages_to_detect
132+
if should_detect:
133+
skip_installation = Features._is_package_present(package_name)
134+
else:
135+
skip_installation = False
136+
137+
if skip_installation:
138+
continue
119139
package_args.append(entry.requirement)
120140

121141
return package_args
@@ -128,6 +148,10 @@ def list_enabled_features(self, return_as=list):
128148
return return_as((feature_name for feature_name in self.feature_names
129149
if self._enabled_by_feature[feature_name]))
130150

151+
def required_package_names(self, feature_name):
152+
return set((Features._strip_version_if_present(entry.requirement)
153+
for entry in self._requirements_by_feature[feature_name]))
154+
131155
@staticmethod
132156
def _interpret_feature_definition(feature_name, feature_definition, requirements_dir):
133157
"""
@@ -150,6 +174,15 @@ def _interpret_feature_definition(feature_name, feature_definition, requirements
150174
requirements_file=requirements_file,
151175
)
152176

177+
@staticmethod
178+
def _is_package_present(package_name):
179+
try:
180+
importlib.util.find_spec(package_name)
181+
182+
return True
183+
except ModuleNotFoundError as exc:
184+
return False
185+
153186
@staticmethod
154187
def _strip_version_if_present(requirement):
155188
"""
@@ -270,7 +303,7 @@ def main_install(features, args, print=print, warn=warn):
270303
warn("no feature coniguration available; showing those enabled by default only")
271304
warn()
272305

273-
all_pip_args = features.generate_pip_args()
306+
all_pip_args = features.generate_pip_args(detect_installed=args.detect_installed)
274307

275308
if args.check:
276309
for pip_args in all_pip_args:
@@ -305,6 +338,7 @@ def main(argv):
305338
install_command = subparsers.add_parser('install')
306339
install_command.add_argument('-c', default=None)
307340
install_command.add_argument('--check', action='store_true', default=False)
341+
install_command.add_argument('--detect_installed', action='store_true', default=False)
308342
install_command.add_argument('--dotenv', default=None, type=os.path.abspath)
309343
install_command.add_argument('--env', action='store_const', const=os.environ)
310344

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[EXISTS]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# deliberately choose a package that is always installed in development
2+
# such that requesting detection will find it and skip its installation
3+
autopep8

tests/test_mig_install_features.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ def test_command_install_check(self):
148148
args = SimpleNamespace(
149149
command='install',
150150
check=True,
151+
detect_installed=False,
151152
c=None,
152153
dotenv=os.path.join(example_dir, '.env--enable-foo'),
153154
env=None
@@ -161,7 +162,26 @@ def test_command_install_check(self):
161162
f"pip install -r {os.path.join(example_dir, 'requirements/foo-requirements.txt')}",
162163
])
163164

164-
def test_overridden_package_version(self):
165+
def test_command_install_conflicting_package_versions(self):
166+
fake_print = FakePrint()
167+
example_dir, features = _make_example_features_instance('detection')
168+
args = SimpleNamespace(
169+
command='install',
170+
check=True,
171+
detect_installed=True,
172+
c=None,
173+
dotenv=None,
174+
env={
175+
'ENABLE_EXISTS': 'true'
176+
}
177+
)
178+
179+
ret = args_main(args, print=fake_print, features=features)
180+
181+
self.assertEqual(ret, 0)
182+
self.assertOutputLines(fake_print, [])
183+
184+
def test_command_install_overridden_package_version(self):
165185
fake_print = FakePrint()
166186
example_dir, features = _make_example_features_instance('basic',
167187
overrides_supported={
@@ -172,6 +192,7 @@ def test_overridden_package_version(self):
172192
args = SimpleNamespace(
173193
command='install',
174194
check=True,
195+
detect_installed=False,
175196
c=None,
176197
dotenv=None,
177198
env={
@@ -189,7 +210,6 @@ def test_overridden_package_version(self):
189210
])
190211

191212

192-
193213
class MigInstallFeatures_smoke(MigTestCase):
194214
"""Unit test helper for the migrid code pointed to in class name"""
195215

0 commit comments

Comments
 (0)