Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support multiple virtual environments using workspace folders #1059

Open
notEvil opened this issue Mar 9, 2024 · 0 comments
Open

Support multiple virtual environments using workspace folders #1059

notEvil opened this issue Mar 9, 2024 · 0 comments
Assignees

Comments

@notEvil
Copy link

notEvil commented Mar 9, 2024

Hi,

I recently found out that if a Pipenv environment is defined in some sub directory, Pyright would recognize it and create a workspace folder for it. For instance, with two Pipenv environments in ~/T/d1/ and ~/T/d2/, cwd at ~/T and after opening a file in each directory, the output of :CocCommand workspace.showOutput shows

Workspace: /home/user/T/d1
Using python from /usr/bin/python

[Info  - ...] Pyright language server 1.1.352 starting
[Info  - ...] Server root directory: file:///home/user/git/coc-pyright/node_modules/pyright/dist
[Info  - ...] Starting service instance "d1"
[Info  - ...] Setting pythonPath for service "d1": "/usr/bin/python"
[Info  - ...] Assuming Python version 3.11
[Info  - ...] Found 1 source file
[Info  - ...] Starting service instance "d2"
[Info  - ...] Setting pythonPath for service "d2": "/usr/bin/python"
[Info  - ...] Assuming Python version 3.11
[Info  - ...] Found 1 source file

This got me digging and found

config['pythonPath'] = PythonSettings.getInstance().pythonPath;

const workspaceFolder = workspace.workspaceFolders.length > 0 ? workspace.workspaceFolders[0] : undefined;

and

return child_process.spawnSync('pipenv', ['--py'], { encoding: 'utf8' }).stdout.trim();

Their combined effect is

  • Pipenv is not resolved at this.workspaceRoot like for instance Poetry (missing cwd: ...)
  • All workspace folders detected by Pyright are ignored, except the first which is used regardless of context

The following patch changes the behavior such that Pyright uses the virtual environment corresponding to the workspace folder the current file is in.

diff --git a/src/configSettings.ts b/src/configSettings.ts
index 8be347d..6a5853c 100644
--- a/src/configSettings.ts
+++ b/src/configSettings.ts
@@ -18,17 +18,25 @@ export class PythonSettings implements IPythonSettings {
   private _pythonPath = '';
   private _stdLibs: string[] = [];
 
-  constructor() {
+  constructor(workspaceFolder = undefined) {
+    this.workspaceFolder = workspaceFolder;
     this.workspaceRoot = workspace.root ? workspace.root : __dirname;
     this.initialize();
   }
 
-  public static getInstance(): PythonSettings {
-    const workspaceFolder = workspace.workspaceFolders.length > 0 ? workspace.workspaceFolders[0] : undefined;
+  public static getInstance(uri: string = undefined): PythonSettings {
+    let workspaceFolder = undefined;
+    if (uri) {
+      for (const _workspaceFolder of workspace.workspaceFolders) {
+        if (_workspaceFolder.uri.startsWith('file://') && uri.startsWith(_workspaceFolder.uri + '/') && (!workspaceFolder || workspaceFolder.uri.length < _workspaceFolder.uri.length)) {
+          workspaceFolder = _workspaceFolder;
+        }
+      }
+    }
     const workspaceFolderKey = workspaceFolder ? workspaceFolder.name : 'unknown';
 
     if (!PythonSettings.pythonSettings.has(workspaceFolderKey)) {
-      const settings = new PythonSettings();
+      const settings = new PythonSettings(workspaceFolder);
       PythonSettings.pythonSettings.set(workspaceFolderKey, settings);
     }
     return PythonSettings.pythonSettings.get(workspaceFolderKey)!;
@@ -50,6 +58,8 @@ export class PythonSettings implements IPythonSettings {
       return fs.existsSync(fullPath) ? fullPath : undefined;
     }
 
+    const rootPath = this.workspaceFolder ? this.workspaceFolder.uri.substring(7) : this.workspaceRoot;
+
     try {
       // virtualenv
       if (process.env.VIRTUAL_ENV && fs.existsSync(path.join(process.env.VIRTUAL_ENV, 'pyvenv.cfg'))) {
@@ -62,7 +72,7 @@ export class PythonSettings implements IPythonSettings {
       }
 
       // `pyenv local` creates `.python-version`, but not `PYENV_VERSION`
-      let p = path.join(this.workspaceRoot, '.python-version');
+      let p = path.join(rootPath, '.python-version');
       if (fs.existsSync(p)) {
         if (!process.env.PYENV_VERSION) {
           // pyenv local can special multiple Python, use first one only
@@ -72,15 +82,15 @@ export class PythonSettings implements IPythonSettings {
       }
 
       // pipenv
-      p = path.join(this.workspaceRoot, 'Pipfile');
+      p = path.join(rootPath, 'Pipfile');
       if (fs.existsSync(p)) {
-        return child_process.spawnSync('pipenv', ['--py'], { encoding: 'utf8' }).stdout.trim();
+        return child_process.spawnSync('pipenv', ['--py'], { encoding: 'utf8', cwd: rootPath }).stdout.trim();
       }
 
       // poetry
-      p = path.join(this.workspaceRoot, 'poetry.lock');
+      p = path.join(rootPath, 'poetry.lock');
       if (fs.existsSync(p)) {
-        const list = child_process.spawnSync('poetry', ['env', 'list', '--full-path', '--no-ansi'], { encoding: 'utf8', cwd: this.workspaceRoot }).stdout.trim();
+        const list = child_process.spawnSync('poetry', ['env', 'list', '--full-path', '--no-ansi'], { encoding: 'utf8', cwd: rootPath }).stdout.trim();
         let info = '';
         for (const item of list.split('\n')) {
           if (item.includes('(Activated)')) {
@@ -95,15 +105,15 @@ export class PythonSettings implements IPythonSettings {
       }
 
       // pdm
-      p = path.join(this.workspaceRoot, '.pdm-python');
+      p = path.join(rootPath, '.pdm-python');
       if (fs.existsSync(p)) {
-        return child_process.spawnSync('pdm', ['info', '--python'], { encoding: 'utf8' }).stdout.trim();
+        return child_process.spawnSync('pdm', ['info', '--python'], { encoding: 'utf8', cwd: rootPath }).stdout.trim();
       }
 
       // virtualenv in the workspace root
-      const files = fs.readdirSync(this.workspaceRoot);
+      const files = fs.readdirSync(rootPath);
       for (const file of files) {
-        const x = path.join(this.workspaceRoot, file);
+        const x = path.join(rootPath, file);
         if (fs.existsSync(path.join(x, 'pyvenv.cfg'))) {
           return pythonBinFromPath(x);
         }
diff --git a/src/features/refactor.ts b/src/features/refactor.ts
index c983881..af04d0d 100644
--- a/src/features/refactor.ts
+++ b/src/features/refactor.ts
@@ -218,7 +218,7 @@ export async function extractVariable(root: string, document: TextDocument, rang
   const workspaceFolder = workspace.getWorkspaceFolder(doc.uri);
   const workspaceRoot = workspaceFolder ? Uri.parse(workspaceFolder.uri).fsPath : workspace.cwd;
 
-  const pythonSettings = PythonSettings.getInstance();
+  const pythonSettings = PythonSettings.getInstance(doc.uri);
   return validateDocumentForRefactor(doc).then(() => {
     const newName = `newvariable${new Date().getMilliseconds().toString()}`;
     const proxy = new RefactorProxy(root, pythonSettings, workspaceRoot);
@@ -243,7 +243,7 @@ export async function extractMethod(root: string, document: TextDocument, range:
   const workspaceFolder = workspace.getWorkspaceFolder(doc.uri);
   const workspaceRoot = workspaceFolder ? Uri.parse(workspaceFolder.uri).fsPath : workspace.cwd;
 
-  const pythonSettings = PythonSettings.getInstance();
+  const pythonSettings = PythonSettings.getInstance(doc.uri);
   return validateDocumentForRefactor(doc).then(() => {
     const newName = `newmethod${new Date().getMilliseconds().toString()}`;
     const proxy = new RefactorProxy(root, pythonSettings, workspaceRoot);
diff --git a/src/middleware.ts b/src/middleware.ts
index f876938..523bdc1 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -40,7 +40,7 @@ export function configuration(params: ConfigurationParams, token: CancellationTo
   if (pythonItem) {
     const custom = () => {
       const config = toJSONObject(workspace.getConfiguration(pythonItem.section, pythonItem.scopeUri));
-      config['pythonPath'] = PythonSettings.getInstance().pythonPath;
+      config['pythonPath'] = PythonSettings.getInstance(pythonItem.scopeUri + '/').pythonPath;
 
       // expand relative path
       const analysis = config['analysis'];

:CocCommand workspace.showOutput:

Workspace: /home/user/T/d1
Using python from /home/user/.local/share/virtualenvs/d1-YdIZU2Ui/bin/python

[Info  - ...] Pyright language server 1.1.352 starting
[Info  - ...] Server root directory: file:///home/user/git/coc-pyright/node_modules/pyright/dist
[Info  - ...] Starting service instance "d1"
[Info  - ...] Setting pythonPath for service "d1": "/home/user/.local/share/virtualenvs/d1-YdIZU2Ui/bin/python"
[Info  - ...] Assuming Python version 3.11
[Info  - ...] Found 1 source file
[Info  - ...] Starting service instance "d2"
[Info  - ...] Setting pythonPath for service "d2": "/home/user/.local/share/virtualenvs/d2-_IcopAFM/bin/python"
[Info  - ...] Assuming Python version 3.11
[Info  - ...] Found 1 source file

Motivating use case: start Neovim in a directory containing multiple projects. With this patch, Pyright behaves correctly in each.

What's the output of :CocCommand pyright.version

[coc.nvim] coc-pyright 1.1.351 with Pyright 1.1.352

What's the output of :CocCommand workspace.showOutput Pyright

see above

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants