Skip to content

Commit 5911a46

Browse files
committed
env: refactor IsolatedEnv
The `IsolatedEnv` is made responsible for env creation to allow pip to implement custom env creation logic. `IsolatedEnvBuilder` takes an `IsolatedEnv` class as an argument so that bringing your own `IsolatedEnv` won't require re-implementing the builder.
1 parent cccaf93 commit 5911a46

File tree

1 file changed

+53
-64
lines changed

1 file changed

+53
-64
lines changed

src/build/env.py

Lines changed: 53 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -38,26 +38,34 @@
3838
class IsolatedEnv(metaclass=abc.ABCMeta):
3939
"""Abstract base of isolated build environments, as required by the build project."""
4040

41+
@abc.abstractmethod
42+
def __init__(self, path: str, logging_fn: Callable[[str], None]) -> None:
43+
"""
44+
:param path: The path where the environment will be created.
45+
:param logging_fn: Logging function.
46+
"""
47+
4148
@property
4249
@abc.abstractmethod
43-
def executable(self) -> str:
44-
"""The executable of the isolated build environment."""
45-
raise NotImplementedError
50+
def python_executable(self) -> str:
51+
"""The Python executable of the isolated environment."""
4652

4753
@property
4854
@abc.abstractmethod
4955
def scripts_dir(self) -> str:
50-
"""The scripts directory of the isolated build environment."""
51-
raise NotImplementedError
56+
"""The scripts directory of the isolated environment."""
5257

5358
@abc.abstractmethod
54-
def install(self, requirements: Iterable[str]) -> None:
59+
def create(self) -> None:
60+
"""Create the isolated environment."""
61+
62+
@abc.abstractmethod
63+
def install_packages(self, requirements: Iterable[str]) -> None:
5564
"""
56-
Install packages from PEP 508 requirements in the isolated build environment.
65+
Install packages from PEP 508 requirements in the isolated environment.
5766
5867
:param requirements: PEP 508 requirements
5968
"""
60-
raise NotImplementedError
6169

6270

6371
@functools.lru_cache(maxsize=None)
@@ -82,10 +90,13 @@ def _subprocess(cmd: List[str]) -> None:
8290

8391

8492
class IsolatedEnvBuilder:
85-
"""Builder object for isolated environments."""
93+
"""Create and dispose of isolated build environments."""
8694

87-
def __init__(self) -> None:
88-
self._path: Optional[str] = None
95+
def __init__(self, isolated_env_class: Optional[Type[IsolatedEnv]] = None) -> None:
96+
if isolated_env_class is not None:
97+
self._isolated_env_class = isolated_env_class
98+
else:
99+
self._isolated_env_class = _DefaultIsolatedEnv
89100

90101
def __enter__(self) -> IsolatedEnv:
91102
"""
@@ -95,19 +106,9 @@ def __enter__(self) -> IsolatedEnv:
95106
"""
96107
self._path = tempfile.mkdtemp(prefix='build-env-')
97108
try:
98-
# use virtualenv when available (as it's faster than venv)
99-
if _should_use_virtualenv():
100-
self.log('Creating virtualenv isolated environment...')
101-
executable, scripts_dir = _create_isolated_env_virtualenv(self._path)
102-
else:
103-
self.log('Creating venv isolated environment...')
104-
executable, scripts_dir = _create_isolated_env_venv(self._path)
105-
return _IsolatedEnvVenvPip(
106-
path=self._path,
107-
python_executable=executable,
108-
scripts_dir=scripts_dir,
109-
log=self.log,
110-
)
109+
isolated_env = self._isolated_env_class(self._path, self.log)
110+
isolated_env.create()
111+
return isolated_env
111112
except Exception: # cleanup folder if creation fails
112113
self.__exit__(*sys.exc_info())
113114
raise
@@ -122,72 +123,57 @@ def __exit__(
122123
:param exc_val: The value of exception raised (if any)
123124
:param exc_tb: The traceback of exception raised (if any)
124125
"""
125-
if self._path is not None and os.path.exists(self._path): # in case the user already deleted skip remove
126+
if os.path.exists(self._path): # in case the user already deleted skip remove
126127
shutil.rmtree(self._path)
127128

128129
@staticmethod
129130
def log(message: str) -> None:
130131
"""
131-
Prints message
132+
Log a message.
132133
133134
The default implementation uses the logging module but this function can be
134135
overwritten by users to have a different implementation.
135136
136-
:param msg: Message to output
137+
:param msg: Message to log
137138
"""
138139
if sys.version_info >= (3, 8):
139140
_logger.log(logging.INFO, message, stacklevel=2)
140141
else:
141142
_logger.log(logging.INFO, message)
142143

143144

144-
class _IsolatedEnvVenvPip(IsolatedEnv):
145+
class _DefaultIsolatedEnv(IsolatedEnv):
145146
"""
146-
Isolated build environment context manager
147+
An isolated environment which combines virtualenv and venv with pip.
147148
148149
Non-standard paths injected directly to sys.path will still be passed to the environment.
149150
"""
150151

151-
def __init__(
152-
self,
153-
path: str,
154-
python_executable: str,
155-
scripts_dir: str,
156-
log: Callable[[str], None],
157-
) -> None:
158-
"""
159-
:param path: The path where the environment exists
160-
:param python_executable: The python executable within the environment
161-
:param log: Log function
162-
"""
152+
def __init__(self, path: str, logging_fn: Callable[[str], None]) -> None:
163153
self._path = path
164-
self._python_executable = python_executable
165-
self._scripts_dir = scripts_dir
166-
self._log = log
167-
168-
@property
169-
def path(self) -> str:
170-
"""The location of the isolated build environment."""
171-
return self._path
154+
self._log = logging_fn
172155

173156
@property
174-
def executable(self) -> str:
175-
"""The python executable of the isolated build environment."""
157+
def python_executable(self) -> str:
176158
return self._python_executable
177159

178160
@property
179161
def scripts_dir(self) -> str:
180162
return self._scripts_dir
181163

182-
def install(self, requirements: Iterable[str]) -> None:
183-
"""
184-
Install packages from PEP 508 requirements in the isolated build environment.
164+
def create(self) -> None:
165+
# use virtualenv when available (as it's faster than venv)
166+
if _should_use_virtualenv():
167+
self._log('Creating virtualenv isolated environment...')
168+
self._python_executable, self._scripts_dir = _create_isolated_env_virtualenv(self._path)
169+
else:
170+
self._log('Creating venv isolated environment...')
171+
self._python_executable, self._scripts_dir = _create_isolated_env_venv(self._path)
185172

186-
:param requirements: PEP 508 requirement specification to install
173+
def install_packages(self, requirements: Iterable[str]) -> None:
174+
# Passing non-PEP 508 strings will result in undefined behavior, you *should not* rely on it. It is
175+
# merely an implementation detail, it may change any time without warning.
187176

188-
:note: Passing non-PEP 508 strings will result in undefined behavior, you *should not* rely on it. It is
189-
merely an implementation detail, it may change any time without warning.
190-
"""
191177
if not requirements:
192178
return
193179

@@ -199,7 +185,7 @@ def install(self, requirements: Iterable[str]) -> None:
199185
req_file.write(os.linesep.join(requirements))
200186
try:
201187
cmd = [
202-
self.executable,
188+
self._python_executable,
203189
'-Im',
204190
'pip',
205191
'install',
@@ -255,8 +241,12 @@ def _create_isolated_env_venv(path: str) -> Tuple[str, str]:
255241
import venv
256242

257243
venv.EnvBuilder(with_pip=True, symlinks=_fs_supports_symlink()).create(path)
258-
executable, script_dir, purelib = _find_executable_and_scripts(path)
244+
executable, scripts_dir, purelib = _find_executable_and_scripts(path)
245+
_post_create_isolated_env_venv(executable, purelib)
246+
return executable, scripts_dir
259247

248+
249+
def _post_create_isolated_env_venv(executable: str, purelib: str) -> None:
260250
# Get the version of pip in the environment
261251
pip_distribution = next(iter(metadata.distributions(name='pip', path=[purelib]))) # type: ignore[no-untyped-call]
262252
current_pip_version = packaging.version.Version(pip_distribution.version)
@@ -275,15 +265,14 @@ def _create_isolated_env_venv(path: str) -> Tuple[str, str]:
275265

276266
# Avoid the setuptools from ensurepip to break the isolation
277267
_subprocess([executable, '-m', 'pip', 'uninstall', 'setuptools', '-y'])
278-
return executable, script_dir
279268

280269

281270
def _find_executable_and_scripts(path: str) -> Tuple[str, str, str]:
282271
"""
283-
Detect the Python executable and script folder of a virtual environment.
272+
Detect the Python executable and sysconfig paths of a venv.
284273
285-
:param path: The location of the virtual environment
286-
:return: The Python executable, script folder, and purelib folder
274+
:param path: venv path
275+
:return: The Python executable, scripts directory, and purelib directory
287276
"""
288277
config_vars = sysconfig.get_config_vars().copy() # globally cached, copy before altering it
289278
config_vars['base'] = path

0 commit comments

Comments
 (0)