9
9
import subprocess
10
10
import sys
11
11
12
+ from functools import cached_property
12
13
from pathlib import Path
13
14
from subprocess import CalledProcessError
14
15
from typing import TYPE_CHECKING
45
46
from poetry .utils .env .base_env import Env
46
47
47
48
49
+ class EnvsFile (TOMLFile ):
50
+ """
51
+ This file contains one section per project with the project's base env name
52
+ as section name. Each section contains the minor and patch version of the
53
+ python executable used to create the currently active virtualenv.
54
+
55
+ Example:
56
+
57
+ [poetry-QRErDmmj]
58
+ minor = "3.9"
59
+ patch = "3.9.13"
60
+
61
+ [poetry-core-m5r7DkRA]
62
+ minor = "3.11"
63
+ patch = "3.11.6"
64
+ """
65
+
66
+ def remove_section (self , name : str , minor : str | None = None ) -> str | None :
67
+ """
68
+ Remove a section from the envs file.
69
+
70
+ If "minor" is given, the section is only removed if its minor value
71
+ matches "minor".
72
+
73
+ Returns the "minor" value of the removed section.
74
+ """
75
+ envs = self .read ()
76
+ current_env = envs .get (name )
77
+ if current_env is not None and (not minor or current_env ["minor" ] == minor ):
78
+ del envs [name ]
79
+ self .write (envs )
80
+ minor = current_env ["minor" ]
81
+ assert isinstance (minor , str )
82
+ return minor
83
+
84
+ return None
85
+
86
+
48
87
class EnvManager :
49
88
"""
50
89
Environments manager
@@ -121,11 +160,19 @@ def in_project_venv(self) -> Path:
121
160
venv : Path = self ._poetry .file .path .parent / ".venv"
122
161
return venv
123
162
163
+ @cached_property
164
+ def envs_file (self ) -> EnvsFile :
165
+ return EnvsFile (self ._poetry .config .virtualenvs_path / self .ENVS_FILE )
166
+
167
+ @cached_property
168
+ def base_env_name (self ) -> str :
169
+ return self .generate_env_name (
170
+ self ._poetry .package .name ,
171
+ str (self ._poetry .file .path .parent ),
172
+ )
173
+
124
174
def activate (self , python : str ) -> Env :
125
175
venv_path = self ._poetry .config .virtualenvs_path
126
- cwd = self ._poetry .file .path .parent
127
-
128
- envs_file = TOMLFile (venv_path / self .ENVS_FILE )
129
176
130
177
try :
131
178
python_version = Version .parse (python )
@@ -170,10 +217,9 @@ def activate(self, python: str) -> Env:
170
217
return self .get (reload = True )
171
218
172
219
envs = tomlkit .document ()
173
- base_env_name = self .generate_env_name (self ._poetry .package .name , str (cwd ))
174
- if envs_file .exists ():
175
- envs = envs_file .read ()
176
- current_env = envs .get (base_env_name )
220
+ if self .envs_file .exists ():
221
+ envs = self .envs_file .read ()
222
+ current_env = envs .get (self .base_env_name )
177
223
if current_env is not None :
178
224
current_minor = current_env ["minor" ]
179
225
current_patch = current_env ["patch" ]
@@ -182,7 +228,7 @@ def activate(self, python: str) -> Env:
182
228
# We need to recreate
183
229
create = True
184
230
185
- name = f"{ base_env_name } -py{ minor } "
231
+ name = f"{ self . base_env_name } -py{ minor } "
186
232
venv = venv_path / name
187
233
188
234
# Create if needed
@@ -202,29 +248,21 @@ def activate(self, python: str) -> Env:
202
248
self .create_venv (executable = python_path , force = create )
203
249
204
250
# Activate
205
- envs [base_env_name ] = {"minor" : minor , "patch" : patch }
206
- envs_file .write (envs )
251
+ envs [self . base_env_name ] = {"minor" : minor , "patch" : patch }
252
+ self . envs_file .write (envs )
207
253
208
254
return self .get (reload = True )
209
255
210
256
def deactivate (self ) -> None :
211
257
venv_path = self ._poetry .config .virtualenvs_path
212
- name = self .generate_env_name (
213
- self ._poetry .package .name , str (self ._poetry .file .path .parent )
214
- )
215
258
216
- envs_file = TOMLFile (venv_path / self .ENVS_FILE )
217
- if envs_file .exists ():
218
- envs = envs_file .read ()
219
- env = envs .get (name )
220
- if env is not None :
221
- venv = venv_path / f"{ name } -py{ env ['minor' ]} "
222
- self ._io .write_error_line (
223
- f"Deactivating virtualenv: <comment>{ venv } </comment>"
224
- )
225
- del envs [name ]
226
-
227
- envs_file .write (envs )
259
+ if self .envs_file .exists () and (
260
+ minor := self .envs_file .remove_section (self .base_env_name )
261
+ ):
262
+ venv = venv_path / f"{ self .base_env_name } -py{ minor } "
263
+ self ._io .write_error_line (
264
+ f"Deactivating virtualenv: <comment>{ venv } </comment>"
265
+ )
228
266
229
267
def get (self , reload : bool = False ) -> Env :
230
268
if self ._env is not None and not reload :
@@ -237,15 +275,10 @@ def get(self, reload: bool = False) -> Env:
237
275
precision = 2 , prefer_active_python = prefer_active_python , io = self ._io
238
276
).to_string ()
239
277
240
- venv_path = self ._poetry .config .virtualenvs_path
241
-
242
- cwd = self ._poetry .file .path .parent
243
- envs_file = TOMLFile (venv_path / self .ENVS_FILE )
244
278
env = None
245
- base_env_name = self .generate_env_name (self ._poetry .package .name , str (cwd ))
246
- if envs_file .exists ():
247
- envs = envs_file .read ()
248
- env = envs .get (base_env_name )
279
+ if self .envs_file .exists ():
280
+ envs = self .envs_file .read ()
281
+ env = envs .get (self .base_env_name )
249
282
if env :
250
283
python_minor = env ["minor" ]
251
284
@@ -272,7 +305,7 @@ def get(self, reload: bool = False) -> Env:
272
305
273
306
venv_path = self ._poetry .config .virtualenvs_path
274
307
275
- name = f"{ base_env_name } -py{ python_minor .strip ()} "
308
+ name = f"{ self . base_env_name } -py{ python_minor .strip ()} "
276
309
277
310
venv = venv_path / name
278
311
@@ -313,12 +346,6 @@ def check_env_is_for_current_project(env: str, base_env_name: str) -> bool:
313
346
return env .startswith (base_env_name )
314
347
315
348
def remove (self , python : str ) -> Env :
316
- venv_path = self ._poetry .config .virtualenvs_path
317
-
318
- cwd = self ._poetry .file .path .parent
319
- envs_file = TOMLFile (venv_path / self .ENVS_FILE )
320
- base_env_name = self .generate_env_name (self ._poetry .package .name , str (cwd ))
321
-
322
349
python_path = Path (python )
323
350
if python_path .is_file ():
324
351
# Validate env name if provided env is a full path to python
@@ -327,34 +354,21 @@ def remove(self, python: str) -> Env:
327
354
[python , "-c" , GET_ENV_PATH_ONELINER ], text = True
328
355
).strip ("\n " )
329
356
env_name = Path (env_dir ).name
330
- if not self .check_env_is_for_current_project (env_name , base_env_name ):
357
+ if not self .check_env_is_for_current_project (
358
+ env_name , self .base_env_name
359
+ ):
331
360
raise IncorrectEnvError (env_name )
332
361
except CalledProcessError as e :
333
362
raise EnvCommandError (e )
334
363
335
- if self .check_env_is_for_current_project (python , base_env_name ):
364
+ if self .check_env_is_for_current_project (python , self . base_env_name ):
336
365
venvs = self .list ()
337
366
for venv in venvs :
338
367
if venv .path .name == python :
339
368
# Exact virtualenv name
340
- if not envs_file .exists ():
341
- self .remove_venv (venv .path )
342
-
343
- return venv
344
-
345
- venv_minor = "." .join (str (v ) for v in venv .version_info [:2 ])
346
- base_env_name = self .generate_env_name (cwd .name , str (cwd ))
347
- envs = envs_file .read ()
348
-
349
- current_env = envs .get (base_env_name )
350
- if not current_env :
351
- self .remove_venv (venv .path )
352
-
353
- return venv
354
-
355
- if current_env ["minor" ] == venv_minor :
356
- del envs [base_env_name ]
357
- envs_file .write (envs )
369
+ if self .envs_file .exists ():
370
+ venv_minor = "." .join (str (v ) for v in venv .version_info [:2 ])
371
+ self .envs_file .remove_section (self .base_env_name , venv_minor )
358
372
359
373
self .remove_venv (venv .path )
360
374
@@ -389,21 +403,14 @@ def remove(self, python: str) -> Env:
389
403
python_version = Version .parse (python_version_string .strip ())
390
404
minor = f"{ python_version .major } .{ python_version .minor } "
391
405
392
- name = f"{ base_env_name } -py{ minor } "
406
+ name = f"{ self . base_env_name } -py{ minor } "
393
407
venv_path = venv_path / name
394
408
395
409
if not venv_path .exists ():
396
410
raise ValueError (f'<warning>Environment "{ name } " does not exist.</warning>' )
397
411
398
- if envs_file .exists ():
399
- envs = envs_file .read ()
400
- current_env = envs .get (base_env_name )
401
- if current_env is not None :
402
- current_minor = current_env ["minor" ]
403
-
404
- if current_minor == minor :
405
- del envs [base_env_name ]
406
- envs_file .write (envs )
412
+ if self .envs_file .exists ():
413
+ self .envs_file .remove_section (self .base_env_name , minor )
407
414
408
415
self .remove_venv (venv_path )
409
416
0 commit comments