Skip to content

Commit 3da5543

Browse files
committed
check for local pip requirements in conda environments
1 parent 4f3f192 commit 3da5543

File tree

3 files changed

+102
-56
lines changed

3 files changed

+102
-56
lines changed

repo2docker/buildpacks/conda/__init__.py

Lines changed: 74 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from ruamel.yaml import YAML
77

88
from ..base import BaseImage
9+
from ...utils import is_local_pip_requirement
910

1011
# pattern for parsing conda dependency line
1112
PYTHON_REGEX = re.compile(r"python\s*=+\s*([\d\.]*)")
@@ -127,6 +128,50 @@ def get_build_script_files(self):
127128
files.update(super().get_build_script_files())
128129
return files
129130

131+
_environment_yaml = None
132+
133+
@property
134+
def environment_yaml(self):
135+
if self._environment_yaml is not None:
136+
return self._environment_yaml
137+
138+
environment_yml = self.binder_path("environment.yml")
139+
if not os.path.exists(environment_yml):
140+
self._environment_yaml = {}
141+
return self._environment_yaml
142+
143+
with open(environment_yml) as f:
144+
env = YAML().load(f)
145+
# check if the env file is empty, if so instantiate an empty dictionary.
146+
if env is None:
147+
env = {}
148+
# check if the env file provided a dick-like thing not a list or other data structure.
149+
if not isinstance(env, Mapping):
150+
raise TypeError(
151+
"environment.yml should contain a dictionary. Got %r" % type(env)
152+
)
153+
self._environment_yaml = env
154+
155+
return self._environment_yaml
156+
157+
@property
158+
def _should_preassemble_env(self):
159+
"""Check for local pip requirements in environment.yaml
160+
161+
If there are any local references, e.g. `-e .`,
162+
stage the whole repo prior to installation.
163+
"""
164+
dependencies = self.environment_yaml.get("dependencies", [])
165+
pip_requirements = None
166+
for dep in dependencies:
167+
if isinstance(dep, dict) and dep.get("pip"):
168+
pip_requirements = dep["pip"]
169+
if isinstance(pip_requirements, list):
170+
for line in pip_requirements:
171+
if is_local_pip_requirement(line):
172+
return False
173+
return True
174+
130175
@property
131176
def python_version(self):
132177
"""Detect the Python version for a given `environment.yml`
@@ -135,31 +180,17 @@ def python_version(self):
135180
or a Falsy empty string '' if not found.
136181
137182
"""
138-
environment_yml = self.binder_path("environment.yml")
139-
if not os.path.exists(environment_yml):
140-
return ""
141-
142183
if not hasattr(self, "_python_version"):
143184
py_version = None
144-
with open(environment_yml) as f:
145-
env = YAML().load(f)
146-
# check if the env file is empty, if so instantiate an empty dictionary.
147-
if env is None:
148-
env = {}
149-
# check if the env file provided a dick-like thing not a list or other data structure.
150-
if not isinstance(env, Mapping):
151-
raise TypeError(
152-
"environment.yml should contain a dictionary. Got %r"
153-
% type(env)
154-
)
155-
for dep in env.get("dependencies", []):
156-
if not isinstance(dep, str):
157-
continue
158-
match = PYTHON_REGEX.match(dep)
159-
if not match:
160-
continue
161-
py_version = match.group(1)
162-
break
185+
env = self.environment_yaml
186+
for dep in env.get("dependencies", []):
187+
if not isinstance(dep, str):
188+
continue
189+
match = PYTHON_REGEX.match(dep)
190+
if not match:
191+
continue
192+
py_version = match.group(1)
193+
break
163194

164195
# extract major.minor
165196
if py_version:
@@ -185,19 +216,20 @@ def get_preassemble_script_files(self):
185216
repo contents change
186217
"""
187218
assemble_files = super().get_preassemble_script_files()
188-
environment_yml = self.binder_path("environment.yml")
189-
if os.path.exists(environment_yml):
190-
assemble_files[environment_yml] = environment_yml
219+
if self._should_preassemble_env:
220+
environment_yml = self.binder_path("environment.yml")
221+
if os.path.exists(environment_yml):
222+
assemble_files[environment_yml] = environment_yml
191223
return assemble_files
192224

193-
def get_preassemble_scripts(self):
225+
def get_env_scripts(self):
194226
"""Return series of build-steps specific to this source repository.
195227
"""
196-
assembly_scripts = []
228+
scripts = []
197229
environment_yml = self.binder_path("environment.yml")
198230
env_prefix = "${KERNEL_PYTHON_PREFIX}" if self.py2 else "${NB_PYTHON_PREFIX}"
199231
if os.path.exists(environment_yml):
200-
assembly_scripts.append(
232+
scripts.append(
201233
(
202234
"${NB_USER}",
203235
r"""
@@ -209,7 +241,19 @@ def get_preassemble_scripts(self):
209241
),
210242
)
211243
)
212-
return super().get_preassemble_scripts() + assembly_scripts
244+
return scripts
245+
246+
def get_preassemble_scripts(self):
247+
scripts = super().get_preassemble_scripts()
248+
if self._should_preassemble_env:
249+
scripts.extend(self.get_env_scripts())
250+
return scripts
251+
252+
def get_assemble_scripts(self):
253+
scripts = super().get_assemble_scripts()
254+
if not self._should_preassemble_env:
255+
scripts.extend(self.get_env_scripts())
256+
return scripts
213257

214258
def detect(self):
215259
"""Check if current repo should be built with the Conda BuildPack.

repo2docker/buildpacks/python/__init__.py

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import os
33

44
from ..conda import CondaBuildPack
5+
from ...utils import is_local_pip_requirement
56

67

78
class PythonBuildPack(CondaBuildPack):
@@ -34,31 +35,6 @@ def python_version(self):
3435
self._python_version = py_version
3536
return self._python_version
3637

37-
def _is_local_requirement(self, line):
38-
"""Return whether a line in a requirements.txt file references a local file"""
39-
# trim comments and skip empty lines
40-
line = line.split("#", 1)[0].strip()
41-
if not line:
42-
return False
43-
if line.startswith(("-r", "-c")):
44-
# local -r or -c references break isolation
45-
return True
46-
# strip off `-e, etc.`
47-
if line.startswith("-"):
48-
line = line.split(None, 1)[1]
49-
if "file://" in line:
50-
# file references break isolation
51-
return True
52-
if "://" in line:
53-
# handle git://../local/file
54-
path = line.split("://", 1)[1]
55-
else:
56-
path = line
57-
if path.startswith("."):
58-
# references a local file
59-
return True
60-
return False
61-
6238
def _get_pip_scripts(self):
6339
"""Get pip install scripts
6440
@@ -112,7 +88,7 @@ def _should_preassemble_pip(self):
11288
continue
11389
with open(requirements_txt) as f:
11490
for line in f:
115-
if self._is_local_requirement(line):
91+
if is_local_pip_requirement(line):
11692
return False
11793

11894
# didn't find any local references,

repo2docker/utils.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,3 +431,29 @@ def normalize_doi(val):
431431
(e.g. https://doi.org/10.1234/jshd123)"""
432432
m = doi_regexp.match(val)
433433
return m.group(2)
434+
435+
436+
def is_local_pip_requirement(line):
437+
"""Return whether a pip requirement (e.g. in requirements.txt file) references a local file"""
438+
# trim comments and skip empty lines
439+
line = line.split("#", 1)[0].strip()
440+
if not line:
441+
return False
442+
if line.startswith(("-r", "-c")):
443+
# local -r or -c references break isolation
444+
return True
445+
# strip off `-e, etc.`
446+
if line.startswith("-"):
447+
line = line.split(None, 1)[1]
448+
if "file://" in line:
449+
# file references break isolation
450+
return True
451+
if "://" in line:
452+
# handle git://../local/file
453+
path = line.split("://", 1)[1]
454+
else:
455+
path = line
456+
if path.startswith("."):
457+
# references a local file
458+
return True
459+
return False

0 commit comments

Comments
 (0)