38
38
class IsolatedEnv (metaclass = abc .ABCMeta ):
39
39
"""Abstract base of isolated build environments, as required by the build project."""
40
40
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
+
41
48
@property
42
49
@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."""
46
52
47
53
@property
48
54
@abc .abstractmethod
49
55
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."""
52
57
53
58
@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 :
55
64
"""
56
- Install packages from PEP 508 requirements in the isolated build environment.
65
+ Install packages from PEP 508 requirements in the isolated environment.
57
66
58
67
:param requirements: PEP 508 requirements
59
68
"""
60
- raise NotImplementedError
61
69
62
70
63
71
@functools .lru_cache (maxsize = None )
@@ -82,10 +90,13 @@ def _subprocess(cmd: List[str]) -> None:
82
90
83
91
84
92
class IsolatedEnvBuilder :
85
- """Builder object for isolated environments."""
93
+ """Create and dispose of isolated build environments."""
86
94
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
89
100
90
101
def __enter__ (self ) -> IsolatedEnv :
91
102
"""
@@ -95,19 +106,9 @@ def __enter__(self) -> IsolatedEnv:
95
106
"""
96
107
self ._path = tempfile .mkdtemp (prefix = 'build-env-' )
97
108
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
111
112
except Exception : # cleanup folder if creation fails
112
113
self .__exit__ (* sys .exc_info ())
113
114
raise
@@ -122,72 +123,57 @@ def __exit__(
122
123
:param exc_val: The value of exception raised (if any)
123
124
:param exc_tb: The traceback of exception raised (if any)
124
125
"""
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
126
127
shutil .rmtree (self ._path )
127
128
128
129
@staticmethod
129
130
def log (message : str ) -> None :
130
131
"""
131
- Prints message
132
+ Log a message.
132
133
133
134
The default implementation uses the logging module but this function can be
134
135
overwritten by users to have a different implementation.
135
136
136
- :param msg: Message to output
137
+ :param msg: Message to log
137
138
"""
138
139
if sys .version_info >= (3 , 8 ):
139
140
_logger .log (logging .INFO , message , stacklevel = 2 )
140
141
else :
141
142
_logger .log (logging .INFO , message )
142
143
143
144
144
- class _IsolatedEnvVenvPip (IsolatedEnv ):
145
+ class _DefaultIsolatedEnv (IsolatedEnv ):
145
146
"""
146
- Isolated build environment context manager
147
+ An isolated environment which combines virtualenv and venv with pip.
147
148
148
149
Non-standard paths injected directly to sys.path will still be passed to the environment.
149
150
"""
150
151
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 :
163
153
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
172
155
173
156
@property
174
- def executable (self ) -> str :
175
- """The python executable of the isolated build environment."""
157
+ def python_executable (self ) -> str :
176
158
return self ._python_executable
177
159
178
160
@property
179
161
def scripts_dir (self ) -> str :
180
162
return self ._scripts_dir
181
163
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 )
185
172
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.
187
176
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
- """
191
177
if not requirements :
192
178
return
193
179
@@ -199,7 +185,7 @@ def install(self, requirements: Iterable[str]) -> None:
199
185
req_file .write (os .linesep .join (requirements ))
200
186
try :
201
187
cmd = [
202
- self .executable ,
188
+ self ._python_executable ,
203
189
'-Im' ,
204
190
'pip' ,
205
191
'install' ,
@@ -255,8 +241,12 @@ def _create_isolated_env_venv(path: str) -> Tuple[str, str]:
255
241
import venv
256
242
257
243
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
259
247
248
+
249
+ def _post_create_isolated_env_venv (executable : str , purelib : str ) -> None :
260
250
# Get the version of pip in the environment
261
251
pip_distribution = next (iter (metadata .distributions (name = 'pip' , path = [purelib ]))) # type: ignore[no-untyped-call]
262
252
current_pip_version = packaging .version .Version (pip_distribution .version )
@@ -275,15 +265,14 @@ def _create_isolated_env_venv(path: str) -> Tuple[str, str]:
275
265
276
266
# Avoid the setuptools from ensurepip to break the isolation
277
267
_subprocess ([executable , '-m' , 'pip' , 'uninstall' , 'setuptools' , '-y' ])
278
- return executable , script_dir
279
268
280
269
281
270
def _find_executable_and_scripts (path : str ) -> Tuple [str , str , str ]:
282
271
"""
283
- Detect the Python executable and script folder of a virtual environment .
272
+ Detect the Python executable and sysconfig paths of a venv .
284
273
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
287
276
"""
288
277
config_vars = sysconfig .get_config_vars ().copy () # globally cached, copy before altering it
289
278
config_vars ['base' ] = path
0 commit comments