Skip to content

Commit ad72595

Browse files
committed
Add platform-specific default paths for Poetry cache and virtualenvs
1 parent e4b1332 commit ad72595

File tree

3 files changed

+262
-19
lines changed

3 files changed

+262
-19
lines changed

src/common/utils/platformUtils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
export function isWindows(): boolean {
22
return process.platform === 'win32';
33
}
4+
5+
export function isMac(): boolean {
6+
return process.platform === 'darwin';
7+
}

src/managers/poetry/poetryUtils.ts

Lines changed: 96 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { ENVS_EXTENSION_ID } from '../../common/constants';
77
import { traceError, traceInfo } from '../../common/logging';
88
import { getWorkspacePersistentState } from '../../common/persistentState';
99
import { getUserHomeDir, untildify } from '../../common/utils/pathUtils';
10-
import { isWindows } from '../../common/utils/platformUtils';
10+
import { isMac, isWindows } from '../../common/utils/platformUtils';
1111
import {
1212
isNativeEnvInfo,
1313
NativeEnvInfo,
@@ -194,14 +194,14 @@ export async function getPoetryVirtualenvsPath(poetryExe?: string): Promise<stri
194194
if (stdout) {
195195
const venvPath = stdout.trim();
196196
// Poetry might return the path with placeholders like {cache-dir}
197-
// If it doesn't start with / or C:\ etc., assume it's using default
198-
if (!path.isAbsolute(venvPath) || venvPath.includes('{')) {
199-
const home = getUserHomeDir();
200-
if (home) {
201-
poetryVirtualenvsPath = path.join(home, '.cache', 'pypoetry', 'virtualenvs');
202-
}
203-
} else {
197+
// Resolve the placeholder if present
198+
if (venvPath.includes('{cache-dir}')) {
199+
poetryVirtualenvsPath = await resolveVirtualenvsPath(poetry, venvPath);
200+
} else if (path.isAbsolute(venvPath)) {
204201
poetryVirtualenvsPath = venvPath;
202+
} else {
203+
// Not an absolute path and no placeholder, use platform-specific default
204+
poetryVirtualenvsPath = getDefaultPoetryVirtualenvsPath();
205205
}
206206

207207
if (poetryVirtualenvsPath) {
@@ -214,10 +214,9 @@ export async function getPoetryVirtualenvsPath(poetryExe?: string): Promise<stri
214214
}
215215
}
216216

217-
// Fallback to default location
218-
const home = getUserHomeDir();
219-
if (home) {
220-
poetryVirtualenvsPath = path.join(home, '.cache', 'pypoetry', 'virtualenvs');
217+
// Fallback to platform-specific default location
218+
poetryVirtualenvsPath = getDefaultPoetryVirtualenvsPath();
219+
if (poetryVirtualenvsPath) {
221220
await state.set(POETRY_VIRTUALENVS_PATH_KEY, poetryVirtualenvsPath);
222221
return poetryVirtualenvsPath;
223222
}
@@ -231,6 +230,86 @@ import { promisify } from 'util';
231230

232231
const exec = promisify(cp.exec);
233232

233+
/**
234+
* Returns the default Poetry cache directory based on the current platform.
235+
* - Windows: %LOCALAPPDATA%\pypoetry\Cache or %APPDATA%\pypoetry\Cache
236+
* - macOS: ~/Library/Caches/pypoetry
237+
* - Linux: ~/.cache/pypoetry
238+
*/
239+
export function getDefaultPoetryCacheDir(): string | undefined {
240+
if (isWindows()) {
241+
const localAppData = process.env.LOCALAPPDATA;
242+
if (localAppData) {
243+
return path.join(localAppData, 'pypoetry', 'Cache');
244+
}
245+
const appData = process.env.APPDATA;
246+
if (appData) {
247+
return path.join(appData, 'pypoetry', 'Cache');
248+
}
249+
return undefined;
250+
}
251+
252+
const home = getUserHomeDir();
253+
if (!home) {
254+
return undefined;
255+
}
256+
257+
if (isMac()) {
258+
return path.join(home, 'Library', 'Caches', 'pypoetry');
259+
}
260+
261+
// Linux default
262+
return path.join(home, '.cache', 'pypoetry');
263+
}
264+
265+
/**
266+
* Returns the default Poetry virtualenvs path based on the current platform.
267+
* - Windows: %LOCALAPPDATA%\pypoetry\Cache\virtualenvs or %APPDATA%\pypoetry\Cache\virtualenvs
268+
* - macOS: ~/Library/Caches/pypoetry/virtualenvs
269+
* - Linux: ~/.cache/pypoetry/virtualenvs
270+
*/
271+
export function getDefaultPoetryVirtualenvsPath(): string | undefined {
272+
const cacheDir = getDefaultPoetryCacheDir();
273+
if (cacheDir) {
274+
return path.join(cacheDir, 'virtualenvs');
275+
}
276+
return undefined;
277+
}
278+
279+
/**
280+
* Resolves the {cache-dir} placeholder in a Poetry virtualenvs path.
281+
* First tries to query Poetry's cache-dir config, then falls back to platform-specific default.
282+
* @param poetry Path to the poetry executable
283+
* @param virtualenvsPath The path possibly containing {cache-dir} placeholder
284+
*/
285+
async function resolveVirtualenvsPath(poetry: string, virtualenvsPath: string): Promise<string> {
286+
if (!virtualenvsPath.includes('{cache-dir}')) {
287+
return virtualenvsPath;
288+
}
289+
290+
// Try to get the actual cache-dir from Poetry
291+
try {
292+
const { stdout } = await exec(`"${poetry}" config cache-dir`);
293+
if (stdout) {
294+
const cacheDir = stdout.trim();
295+
if (cacheDir && path.isAbsolute(cacheDir)) {
296+
return virtualenvsPath.replace('{cache-dir}', cacheDir);
297+
}
298+
}
299+
} catch (e) {
300+
traceError(`Error getting Poetry cache-dir config: ${e}`);
301+
}
302+
303+
// Fall back to platform-specific default cache dir
304+
const defaultCacheDir = getDefaultPoetryCacheDir();
305+
if (defaultCacheDir) {
306+
return virtualenvsPath.replace('{cache-dir}', defaultCacheDir);
307+
}
308+
309+
// Last resort: return the original path (will likely not be valid)
310+
return virtualenvsPath;
311+
}
312+
234313
export async function getPoetryVersion(poetry: string): Promise<string | undefined> {
235314
try {
236315
const { stdout } = await exec(`"${poetry}" --version`);
@@ -273,11 +352,11 @@ export async function nativeToPythonEnv(
273352
const normalizedVirtualenvsPath = path.normalize(virtualenvsPath);
274353
isGlobalPoetryEnv = normalizedPrefix.startsWith(normalizedVirtualenvsPath);
275354
} else {
276-
// Fall back to checking the default location if we haven't cached the path yet
277-
const homeDir = getUserHomeDir();
278-
if (homeDir) {
279-
const defaultPath = path.normalize(path.join(homeDir, '.cache', 'pypoetry', 'virtualenvs'));
280-
isGlobalPoetryEnv = normalizedPrefix.startsWith(defaultPath);
355+
// Fall back to checking the platform-specific default location if we haven't cached the path yet
356+
const defaultPath = getDefaultPoetryVirtualenvsPath();
357+
if (defaultPath) {
358+
const normalizedDefaultPath = path.normalize(defaultPath);
359+
isGlobalPoetryEnv = normalizedPrefix.startsWith(normalizedDefaultPath);
281360

282361
// Try to get the actual path asynchronously for next time
283362
getPoetryVirtualenvsPath(_poetry).catch((e) =>

src/test/managers/poetry/poetryUtils.unit.test.ts

Lines changed: 162 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
import assert from 'node:assert';
2+
import path from 'node:path';
23
import * as sinon from 'sinon';
3-
import { isPoetryVirtualenvsInProject, nativeToPythonEnv } from '../../../managers/poetry/poetryUtils';
4-
import * as utils from '../../../managers/common/utils';
54
import { EnvironmentManager, PythonEnvironment, PythonEnvironmentApi, PythonEnvironmentInfo } from '../../../api';
5+
import * as pathUtils from '../../../common/utils/pathUtils';
6+
import * as platformUtils from '../../../common/utils/platformUtils';
67
import { NativeEnvInfo } from '../../../managers/common/nativePythonFinder';
8+
import * as utils from '../../../managers/common/utils';
9+
import {
10+
getDefaultPoetryCacheDir,
11+
getDefaultPoetryVirtualenvsPath,
12+
isPoetryVirtualenvsInProject,
13+
nativeToPythonEnv,
14+
} from '../../../managers/poetry/poetryUtils';
715

816
suite('isPoetryVirtualenvsInProject', () => {
917
test('should return false when env var is not set', () => {
@@ -157,3 +165,155 @@ suite('nativeToPythonEnv - POETRY_VIRTUALENVS_IN_PROJECT integration', () => {
157165
assert.strictEqual(capturedInfo!.group, undefined, 'Non-global path should not be global');
158166
});
159167
});
168+
169+
suite('getDefaultPoetryCacheDir', () => {
170+
let isWindowsStub: sinon.SinonStub;
171+
let isMacStub: sinon.SinonStub;
172+
let getUserHomeDirStub: sinon.SinonStub;
173+
let originalLocalAppData: string | undefined;
174+
let originalAppData: string | undefined;
175+
176+
setup(() => {
177+
isWindowsStub = sinon.stub(platformUtils, 'isWindows');
178+
isMacStub = sinon.stub(platformUtils, 'isMac');
179+
getUserHomeDirStub = sinon.stub(pathUtils, 'getUserHomeDir');
180+
181+
// Save original env vars
182+
originalLocalAppData = process.env.LOCALAPPDATA;
183+
originalAppData = process.env.APPDATA;
184+
});
185+
186+
teardown(() => {
187+
sinon.restore();
188+
// Restore original env vars
189+
if (originalLocalAppData === undefined) {
190+
delete process.env.LOCALAPPDATA;
191+
} else {
192+
process.env.LOCALAPPDATA = originalLocalAppData;
193+
}
194+
if (originalAppData === undefined) {
195+
delete process.env.APPDATA;
196+
} else {
197+
process.env.APPDATA = originalAppData;
198+
}
199+
});
200+
201+
test('Windows: uses LOCALAPPDATA when available', () => {
202+
isWindowsStub.returns(true);
203+
process.env.LOCALAPPDATA = 'C:\\Users\\test\\AppData\\Local';
204+
205+
const result = getDefaultPoetryCacheDir();
206+
207+
assert.strictEqual(result, path.join('C:\\Users\\test\\AppData\\Local', 'pypoetry', 'Cache'));
208+
});
209+
210+
test('Windows: falls back to APPDATA when LOCALAPPDATA is not set', () => {
211+
isWindowsStub.returns(true);
212+
delete process.env.LOCALAPPDATA;
213+
process.env.APPDATA = 'C:\\Users\\test\\AppData\\Roaming';
214+
215+
const result = getDefaultPoetryCacheDir();
216+
217+
assert.strictEqual(result, path.join('C:\\Users\\test\\AppData\\Roaming', 'pypoetry', 'Cache'));
218+
});
219+
220+
test('Windows: returns undefined when neither LOCALAPPDATA nor APPDATA is set', () => {
221+
isWindowsStub.returns(true);
222+
delete process.env.LOCALAPPDATA;
223+
delete process.env.APPDATA;
224+
225+
const result = getDefaultPoetryCacheDir();
226+
227+
assert.strictEqual(result, undefined);
228+
});
229+
230+
test('macOS: uses ~/Library/Caches/pypoetry', () => {
231+
isWindowsStub.returns(false);
232+
isMacStub.returns(true);
233+
getUserHomeDirStub.returns('/Users/test');
234+
235+
const result = getDefaultPoetryCacheDir();
236+
237+
assert.strictEqual(result, path.join('/Users/test', 'Library', 'Caches', 'pypoetry'));
238+
});
239+
240+
test('Linux: uses ~/.cache/pypoetry', () => {
241+
isWindowsStub.returns(false);
242+
isMacStub.returns(false);
243+
getUserHomeDirStub.returns('/home/test');
244+
245+
const result = getDefaultPoetryCacheDir();
246+
247+
assert.strictEqual(result, path.join('/home/test', '.cache', 'pypoetry'));
248+
});
249+
250+
test('returns undefined when home directory is not available (non-Windows)', () => {
251+
isWindowsStub.returns(false);
252+
getUserHomeDirStub.returns(undefined);
253+
254+
const result = getDefaultPoetryCacheDir();
255+
256+
assert.strictEqual(result, undefined);
257+
});
258+
});
259+
260+
suite('getDefaultPoetryVirtualenvsPath', () => {
261+
let isWindowsStub: sinon.SinonStub;
262+
let isMacStub: sinon.SinonStub;
263+
let getUserHomeDirStub: sinon.SinonStub;
264+
let originalLocalAppData: string | undefined;
265+
266+
setup(() => {
267+
isWindowsStub = sinon.stub(platformUtils, 'isWindows');
268+
isMacStub = sinon.stub(platformUtils, 'isMac');
269+
getUserHomeDirStub = sinon.stub(pathUtils, 'getUserHomeDir');
270+
originalLocalAppData = process.env.LOCALAPPDATA;
271+
});
272+
273+
teardown(() => {
274+
sinon.restore();
275+
if (originalLocalAppData === undefined) {
276+
delete process.env.LOCALAPPDATA;
277+
} else {
278+
process.env.LOCALAPPDATA = originalLocalAppData;
279+
}
280+
});
281+
282+
test('appends virtualenvs to cache directory', () => {
283+
isWindowsStub.returns(false);
284+
isMacStub.returns(false);
285+
getUserHomeDirStub.returns('/home/test');
286+
287+
const result = getDefaultPoetryVirtualenvsPath();
288+
289+
assert.strictEqual(result, path.join('/home/test', '.cache', 'pypoetry', 'virtualenvs'));
290+
});
291+
292+
test('Windows: returns correct virtualenvs path', () => {
293+
isWindowsStub.returns(true);
294+
process.env.LOCALAPPDATA = 'C:\\Users\\test\\AppData\\Local';
295+
296+
const result = getDefaultPoetryVirtualenvsPath();
297+
298+
assert.strictEqual(result, path.join('C:\\Users\\test\\AppData\\Local', 'pypoetry', 'Cache', 'virtualenvs'));
299+
});
300+
301+
test('macOS: returns correct virtualenvs path', () => {
302+
isWindowsStub.returns(false);
303+
isMacStub.returns(true);
304+
getUserHomeDirStub.returns('/Users/test');
305+
306+
const result = getDefaultPoetryVirtualenvsPath();
307+
308+
assert.strictEqual(result, path.join('/Users/test', 'Library', 'Caches', 'pypoetry', 'virtualenvs'));
309+
});
310+
311+
test('returns undefined when cache dir is not available', () => {
312+
isWindowsStub.returns(false);
313+
getUserHomeDirStub.returns(undefined);
314+
315+
const result = getDefaultPoetryVirtualenvsPath();
316+
317+
assert.strictEqual(result, undefined);
318+
});
319+
});

0 commit comments

Comments
 (0)