Skip to content

Commit f14f8bd

Browse files
Copiloteleanorjboyd
andcommitted
Implement simplified package folder approach with direct property on environments
Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com>
1 parent f28d2fc commit f14f8bd

7 files changed

Lines changed: 108 additions & 218 deletions

File tree

src/api.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,12 @@ export interface PythonEnvironmentInfo {
213213
*/
214214
readonly sysPrefix: string;
215215

216+
/**
217+
* Path to the packages directory (e.g., site-packages) where Python packages are installed for this environment.
218+
* This is used for monitoring package installations and automatically refreshing package lists.
219+
*/
220+
readonly packageFolder?: Uri;
221+
216222
/**
217223
* Optional `group` for this environment. This is used to group environments in the Environment Manager UI.
218224
*/
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
export { SitePackagesWatcherService } from './sitePackagesWatcherService';
2-
export { resolveSitePackagesPath, isSitePackagesDirectory } from './sitePackagesUtils';
2+
export { resolvePackageFolderFromSysPrefix } from './sitePackagesUtils';
Lines changed: 31 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,48 @@
11
import * as path from 'path';
2-
import * as fs from 'fs-extra';
32
import { Uri } from 'vscode';
4-
import { PythonEnvironment } from '../../api';
5-
import { traceVerbose, traceWarn } from '../../common/logging';
3+
import { traceVerbose } from '../../common/logging';
64

75
/**
8-
* Resolves the site-packages directory path for a given Python environment.
9-
* This function handles different platforms and Python versions.
6+
* Resolves the package directory path for a given Python environment based on sysPrefix.
7+
* This is a utility function for environment managers to set the packageFolder property.
108
*
11-
* @param environment The Python environment to resolve site-packages for
12-
* @returns Promise<Uri | undefined> The Uri to the site-packages directory, or undefined if not found
9+
* @param sysPrefix The sys.prefix of the Python environment
10+
* @returns Uri | undefined The Uri to the package directory, or undefined if it cannot be determined
1311
*/
14-
export async function resolveSitePackagesPath(environment: PythonEnvironment): Promise<Uri | undefined> {
15-
const sysPrefix = environment.sysPrefix;
12+
export function resolvePackageFolderFromSysPrefix(sysPrefix: string): Uri | undefined {
1613
if (!sysPrefix) {
17-
traceWarn(`No sysPrefix available for environment: ${environment.displayName}`);
1814
return undefined;
1915
}
2016

21-
traceVerbose(`Resolving site-packages for environment: ${environment.displayName}, sysPrefix: ${sysPrefix}`);
17+
traceVerbose(`Resolving package folder for sysPrefix: ${sysPrefix}`);
2218

23-
// Common site-packages locations to check
24-
const candidates = getSitePackagesCandidates(sysPrefix);
25-
26-
// Check each candidate path
27-
for (const candidate of candidates) {
28-
try {
29-
if (await fs.pathExists(candidate)) {
30-
const uri = Uri.file(candidate);
31-
traceVerbose(`Found site-packages at: ${candidate}`);
32-
return uri;
33-
}
34-
} catch (error) {
35-
traceVerbose(`Error checking site-packages candidate ${candidate}: ${error}`);
36-
}
37-
}
19+
// For most environments, we can use a simple heuristic:
20+
// Windows: {sysPrefix}/Lib/site-packages
21+
// Unix/Linux/macOS: {sysPrefix}/lib/python*/site-packages (we'll use a common pattern)
22+
// Conda: {sysPrefix}/site-packages
3823

39-
traceWarn(`Could not find site-packages directory for environment: ${environment.displayName}`);
40-
return undefined;
41-
}
24+
let packageFolderPath: string;
4225

43-
/**
44-
* Gets candidate site-packages paths for different platforms and Python versions.
45-
*
46-
* @param sysPrefix The sys.prefix of the Python environment
47-
* @returns Array of candidate paths to check
48-
*/
49-
function getSitePackagesCandidates(sysPrefix: string): string[] {
50-
const candidates: string[] = [];
51-
52-
// Windows: typically in Lib/site-packages
5326
if (process.platform === 'win32') {
54-
candidates.push(path.join(sysPrefix, 'Lib', 'site-packages'));
55-
}
56-
57-
// Unix-like systems: typically in lib/python*/site-packages
58-
// We'll check common Python version patterns
59-
const pythonVersions = [
60-
'python3.12', 'python3.11', 'python3.10', 'python3.9', 'python3.8', 'python3.7',
61-
'python3', // fallback
62-
];
63-
64-
for (const pyVer of pythonVersions) {
65-
candidates.push(path.join(sysPrefix, 'lib', pyVer, 'site-packages'));
66-
}
67-
68-
// Additional locations for conda environments
69-
candidates.push(path.join(sysPrefix, 'site-packages')); // Some minimal environments
70-
71-
return candidates;
72-
}
73-
74-
/**
75-
* Checks if a path is likely a site-packages directory by looking for common markers.
76-
*
77-
* @param sitePkgPath Path to check
78-
* @returns Promise<boolean> True if the path appears to be a site-packages directory
79-
*/
80-
export async function isSitePackagesDirectory(sitePkgPath: string): Promise<boolean> {
81-
try {
82-
const stat = await fs.stat(sitePkgPath);
83-
if (!stat.isDirectory()) {
84-
return false;
85-
}
86-
87-
// Check for common site-packages markers
88-
const contents = await fs.readdir(sitePkgPath);
89-
90-
// Look for common packages or pip-related files
91-
const markers = [
92-
'pip', 'setuptools', 'wheel', // Common packages
93-
'__pycache__', // Python cache directory
94-
];
27+
// Windows: typically in Lib/site-packages
28+
packageFolderPath = path.join(sysPrefix, 'Lib', 'site-packages');
29+
} else {
30+
// Unix-like systems: try common locations
31+
// First try conda style
32+
const condaPath = path.join(sysPrefix, 'site-packages');
33+
// Then try standard site-packages location (use python3 as a reasonable default)
34+
const standardPath = path.join(sysPrefix, 'lib', 'python3', 'site-packages');
9535

96-
return markers.some(marker => contents.includes(marker)) || contents.length > 0;
97-
} catch {
98-
return false;
36+
// For simplicity, we'll prefer the conda style if this looks like a conda environment,
37+
// otherwise use the standard path
38+
if (sysPrefix.includes('conda') || sysPrefix.includes('miniconda') || sysPrefix.includes('anaconda')) {
39+
packageFolderPath = condaPath;
40+
} else {
41+
packageFolderPath = standardPath;
42+
}
9943
}
44+
45+
const uri = Uri.file(packageFolderPath);
46+
traceVerbose(`Resolved package folder to: ${uri.fsPath}`);
47+
return uri;
10048
}

src/features/packageWatcher/sitePackagesWatcherService.ts

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@ import { PythonEnvironment } from '../../api';
33
import { traceError, traceInfo, traceVerbose } from '../../common/logging';
44
import { createFileSystemWatcher } from '../../common/workspace.apis';
55
import { EnvironmentManagers, InternalDidChangeEnvironmentsEventArgs, InternalPackageManager } from '../../internal.api';
6-
import { resolveSitePackagesPath } from './sitePackagesUtils';
76

87
/**
9-
* Manages file system watchers for site-packages directories across all Python environments.
8+
* Manages file system watchers for package directories across all Python environments.
109
* Automatically refreshes package lists when packages are installed or uninstalled.
1110
*/
1211
export class SitePackagesWatcherService implements Disposable {
@@ -74,7 +73,7 @@ export class SitePackagesWatcherService implements Disposable {
7473
}
7574

7675
/**
77-
* Adds a file system watcher for the given environment's site-packages directory.
76+
* Adds a file system watcher for the given environment's package directory.
7877
*/
7978
private async addWatcherForEnvironment(environment: PythonEnvironment): Promise<void> {
8079
const envId = environment.envId.id;
@@ -85,14 +84,14 @@ export class SitePackagesWatcherService implements Disposable {
8584
return;
8685
}
8786

88-
try {
89-
const sitePackagesUri = await resolveSitePackagesPath(environment);
90-
if (!sitePackagesUri) {
91-
traceVerbose(`Could not resolve site-packages path for environment: ${environment.displayName}`);
92-
return;
93-
}
87+
// Check if environment has a packageFolder defined
88+
if (!environment.packageFolder) {
89+
traceVerbose(`No packageFolder defined for environment: ${environment.displayName}`);
90+
return;
91+
}
9492

95-
const pattern = `${sitePackagesUri.fsPath}/**`;
93+
try {
94+
const pattern = `${environment.packageFolder.fsPath}/**`;
9695
const watcher = createFileSystemWatcher(
9796
pattern,
9897
false, // don't ignore create events
@@ -101,12 +100,12 @@ export class SitePackagesWatcherService implements Disposable {
101100
);
102101

103102
// Set up event handlers
104-
watcher.onDidCreate(() => this.onSitePackagesChange(environment));
105-
watcher.onDidChange(() => this.onSitePackagesChange(environment));
106-
watcher.onDidDelete(() => this.onSitePackagesChange(environment));
103+
watcher.onDidCreate(() => this.onPackageDirectoryChange(environment));
104+
watcher.onDidChange(() => this.onPackageDirectoryChange(environment));
105+
watcher.onDidDelete(() => this.onPackageDirectoryChange(environment));
107106

108107
this.watchers.set(envId, watcher);
109-
traceInfo(`Created site-packages watcher for environment: ${environment.displayName} at ${sitePackagesUri.fsPath}`);
108+
traceInfo(`Created package directory watcher for environment: ${environment.displayName} at ${environment.packageFolder.fsPath}`);
110109

111110
} catch (error) {
112111
traceError(`Failed to create watcher for environment ${environment.displayName}:`, error);
@@ -123,17 +122,17 @@ export class SitePackagesWatcherService implements Disposable {
123122
if (watcher) {
124123
watcher.dispose();
125124
this.watchers.delete(envId);
126-
traceInfo(`Removed site-packages watcher for environment: ${environment.displayName}`);
125+
traceInfo(`Removed package directory watcher for environment: ${environment.displayName}`);
127126
}
128127
}
129128

130129
/**
131-
* Handles site-packages changes by triggering a package refresh.
130+
* Handles package directory changes by triggering a package refresh.
132131
* Uses debouncing to avoid excessive refresh calls when multiple files change rapidly.
133132
*/
134-
private async onSitePackagesChange(environment: PythonEnvironment): Promise<void> {
133+
private async onPackageDirectoryChange(environment: PythonEnvironment): Promise<void> {
135134
try {
136-
traceVerbose(`Site-packages changed for environment: ${environment.displayName}, triggering package refresh`);
135+
traceVerbose(`Package directory changed for environment: ${environment.displayName}, triggering package refresh`);
137136

138137
// Get the package manager for this environment
139138
const packageManager = this.getPackageManagerForEnvironment(environment);
@@ -152,7 +151,7 @@ export class SitePackagesWatcherService implements Disposable {
152151
traceVerbose(`No package manager found for environment: ${environment.displayName}`);
153152
}
154153
} catch (error) {
155-
traceError(`Error handling site-packages change for environment ${environment.displayName}:`, error);
154+
traceError(`Error handling package directory change for environment ${environment.displayName}:`, error);
156155
}
157156
}
158157

src/managers/builtin/venvUtils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
withProgress,
2020
} from '../../common/window.apis';
2121
import { getConfiguration } from '../../common/workspace.apis';
22+
import { resolvePackageFolderFromSysPrefix } from '../../features/packageWatcher';
2223
import {
2324
isNativeEnvInfo,
2425
NativeEnvInfo,
@@ -147,6 +148,7 @@ async function getPythonInfo(env: NativeEnvInfo): Promise<PythonEnvironmentInfo>
147148
environmentPath: Uri.file(env.executable),
148149
iconPath: new ThemeIcon('python'),
149150
sysPrefix: env.prefix,
151+
packageFolder: resolvePackageFolderFromSysPrefix(env.prefix),
150152
execInfo: {
151153
run: {
152154
executable: env.executable,

src/managers/conda/condaUtils.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
import { ENVS_EXTENSION_ID, EXTENSION_ROOT_DIR } from '../../common/constants';
2929
import { showErrorMessageWithLogs } from '../../common/errors/utils';
3030
import { Common, CondaStrings, PackageManagement, Pickers } from '../../common/localize';
31+
import { resolvePackageFolderFromSysPrefix } from '../../features/packageWatcher';
3132
import { traceInfo } from '../../common/logging';
3233
import { getWorkspacePersistentState } from '../../common/persistentState';
3334
import { pickProject } from '../../common/pickers/projects';
@@ -359,6 +360,7 @@ function getNamedCondaPythonInfo(
359360
tooltip: prefix,
360361
version: version,
361362
sysPrefix: prefix,
363+
packageFolder: resolvePackageFolderFromSysPrefix(prefix),
362364
execInfo: {
363365
run: { executable: path.join(executable) },
364366
activatedRun: {
@@ -453,6 +455,7 @@ function getPrefixesCondaPythonInfo(
453455
tooltip: prefix,
454456
version: version,
455457
sysPrefix: prefix,
458+
packageFolder: resolvePackageFolderFromSysPrefix(prefix),
456459
execInfo: {
457460
run: { executable: path.join(executable) },
458461
activatedRun: {
@@ -479,6 +482,7 @@ function getCondaWithoutPython(name: string, prefix: string, conda: string): Pyt
479482
tooltip: l10n.t('Conda environment without Python'),
480483
version: 'no-python',
481484
sysPrefix: prefix,
485+
packageFolder: resolvePackageFolderFromSysPrefix(prefix),
482486
iconPath: new ThemeIcon('stop'),
483487
execInfo: {
484488
run: { executable: conda },
@@ -826,6 +830,7 @@ export async function quickCreateConda(
826830
deactivation: [{ executable: 'conda', args: ['deactivate'] }],
827831
},
828832
sysPrefix: prefix,
833+
packageFolder: resolvePackageFolderFromSysPrefix(prefix),
829834
group: 'Prefix',
830835
},
831836
manager,

0 commit comments

Comments
 (0)