Pyright LSP: Virtual environment packages not resolved due to incorrect workspace/configuration handling #3846

Open
opened 2026-02-16 17:41:40 -05:00 by yindo · 2 comments
Owner

Originally created by @minzique on GitHub (Dec 24, 2025).

Originally assigned to: @thdxr on GitHub.

Bug Description

Pyright LSP fails to resolve packages from virtual environments in monorepo setups, even when a .venv symlink exists at the project root. The LSP reports Import "X" could not be resolved for all third-party packages.

Environment

  • OpenCode version: Latest (Dec 25, 2025)
  • OS: macOS (Darwin)
  • Python: 3.12
  • Pyright version: 1.1.407

Reproduction Steps

  1. Have a monorepo with a Python project in a subdirectory (e.g., apps/api/)
  2. Virtual environment is at apps/api/.venv
  3. Create a symlink at project root: ln -s apps/api/.venv .venv
  4. Have a pyrightconfig.json at project root
  5. Run OpenCode and open a Python file
  6. LSP diagnostics show import errors for installed packages

Expected Behavior

Pyright LSP should resolve packages from the virtual environment, similar to how the CLI works:

# CLI pyright works correctly:
$ .venv/bin/pyright apps/api/app/main.py
# Only shows 3 real errors, no import errors

# But LSP in OpenCode shows:
# error[Pyright] (reportMissingImports): Import "fastapi" could not be resolved
# error[Pyright] (reportMissingImports): Import "uvicorn" could not be resolved

Root Cause Analysis

After analyzing the source code, I identified the issue in packages/opencode/src/lsp/client.ts:

Current Implementation (lines 67-70):

connection.onRequest("workspace/configuration", async () => {
  // Return server initialization options
  return [input.server.initialization ?? {}]
})

The Problem

  1. Ignores request parameters: The handler ignores the ConfigurationParams which contains items specifying which configuration sections are being requested.

  2. Pyright requests multiple sections: Pyright makes separate configuration requests for:

    • python section (for pythonPath, venvPath)
    • python.analysis section (for stubPath, typeshedPaths, etc.)
  3. Same response for all requests: OpenCode returns the same flat object regardless of which section is requested, which breaks pyright's expectation of getting section-specific configuration.

How Pyright Uses Configuration (from pyright's server.ts):

// Pyright requests the 'python' section
const pythonSection = await this.getConfiguration(workspace.rootUri, 'python');
if (pythonSection) {
    const pythonPath = pythonSection.pythonPath;
    // ...
    const venvPath = pythonSection.venvPath;
    // ...
}

// Pyright requests the 'python.analysis' section separately
const pythonAnalysisSection = await this.getConfiguration(workspace.rootUri, 'python.analysis');
// ...

OpenCode's Pyright Initialization (server.ts lines 523-537):

const initialization: Record<string, string> = {}

const potentialVenvPaths = [process.env["VIRTUAL_ENV"], path.join(root, ".venv"), path.join(root, "venv")]
for (const venvPath of potentialVenvPaths) {
  const potentialPythonPath = path.join(venvPath, "bin", "python")
  if (await Bun.file(potentialPythonPath).exists()) {
    initialization["pythonPath"] = potentialPythonPath  // Sets flat key
    break
  }
}

This sets pythonPath correctly, but pyright also needs venvPath to be set for proper package resolution without running the interpreter.

Suggested Fix

Option 1: Properly handle configuration sections

connection.onRequest("workspace/configuration", async (params: ConfigurationParams) => {
  const results: any[] = [];
  
  for (const item of params.items) {
    if (item.section === 'python') {
      results.push({
        pythonPath: input.server.initialization?.pythonPath,
        venvPath: input.server.initialization?.venvPath,
      });
    } else if (item.section === 'python.analysis') {
      results.push(input.server.initialization?.pythonAnalysis ?? {});
    } else {
      results.push(input.server.initialization ?? {});
    }
  }
  
  return results;
})

Option 2: Add venvPath to initialization

In server.ts Pyright spawn function:

const potentialVenvPaths = [process.env["VIRTUAL_ENV"], path.join(root, ".venv"), path.join(root, "venv")]
for (const venvPath of potentialVenvPaths) {
  const potentialPythonPath = path.join(venvPath, "bin", "python")
  if (await Bun.file(potentialPythonPath).exists()) {
    initialization["pythonPath"] = potentialPythonPath
    initialization["venvPath"] = path.dirname(venvPath)  // Add parent directory
    initialization["venv"] = path.basename(venvPath)      // Add venv folder name
    break
  }
}

Additional Context

  • The CLI pyright command works correctly because it reads pyrightconfig.json
  • The pyright-langserver should also read pyrightconfig.json, but the LSP settings seem to override or interfere with it
  • Setting pythonPath alone requires pyright to execute the Python interpreter to discover sys.path, which may fail in certain environments
  • Setting venvPath + venv directly tells pyright where to find site-packages without needing to run Python

Workaround

Currently, no reliable workaround exists within OpenCode. Users must rely on pyrightconfig.json being read correctly, which doesn't appear to work with the LSP integration.

Originally created by @minzique on GitHub (Dec 24, 2025). Originally assigned to: @thdxr on GitHub. ## Bug Description Pyright LSP fails to resolve packages from virtual environments in monorepo setups, even when a `.venv` symlink exists at the project root. The LSP reports `Import "X" could not be resolved` for all third-party packages. ## Environment - OpenCode version: Latest (Dec 25, 2025) - OS: macOS (Darwin) - Python: 3.12 - Pyright version: 1.1.407 ## Reproduction Steps 1. Have a monorepo with a Python project in a subdirectory (e.g., `apps/api/`) 2. Virtual environment is at `apps/api/.venv` 3. Create a symlink at project root: `ln -s apps/api/.venv .venv` 4. Have a `pyrightconfig.json` at project root 5. Run OpenCode and open a Python file 6. LSP diagnostics show import errors for installed packages ## Expected Behavior Pyright LSP should resolve packages from the virtual environment, similar to how the CLI works: ```bash # CLI pyright works correctly: $ .venv/bin/pyright apps/api/app/main.py # Only shows 3 real errors, no import errors # But LSP in OpenCode shows: # error[Pyright] (reportMissingImports): Import "fastapi" could not be resolved # error[Pyright] (reportMissingImports): Import "uvicorn" could not be resolved ``` ## Root Cause Analysis After analyzing the source code, I identified the issue in `packages/opencode/src/lsp/client.ts`: ### Current Implementation (lines 67-70): ```typescript connection.onRequest("workspace/configuration", async () => { // Return server initialization options return [input.server.initialization ?? {}] }) ``` ### The Problem 1. **Ignores request parameters**: The handler ignores the `ConfigurationParams` which contains `items` specifying which configuration sections are being requested. 2. **Pyright requests multiple sections**: Pyright makes separate configuration requests for: - `python` section (for `pythonPath`, `venvPath`) - `python.analysis` section (for `stubPath`, `typeshedPaths`, etc.) 3. **Same response for all requests**: OpenCode returns the same flat object regardless of which section is requested, which breaks pyright's expectation of getting section-specific configuration. ### How Pyright Uses Configuration (from pyright's server.ts): ```typescript // Pyright requests the 'python' section const pythonSection = await this.getConfiguration(workspace.rootUri, 'python'); if (pythonSection) { const pythonPath = pythonSection.pythonPath; // ... const venvPath = pythonSection.venvPath; // ... } // Pyright requests the 'python.analysis' section separately const pythonAnalysisSection = await this.getConfiguration(workspace.rootUri, 'python.analysis'); // ... ``` ### OpenCode's Pyright Initialization (server.ts lines 523-537): ```typescript const initialization: Record<string, string> = {} const potentialVenvPaths = [process.env["VIRTUAL_ENV"], path.join(root, ".venv"), path.join(root, "venv")] for (const venvPath of potentialVenvPaths) { const potentialPythonPath = path.join(venvPath, "bin", "python") if (await Bun.file(potentialPythonPath).exists()) { initialization["pythonPath"] = potentialPythonPath // Sets flat key break } } ``` This sets `pythonPath` correctly, but pyright also needs `venvPath` to be set for proper package resolution without running the interpreter. ## Suggested Fix ### Option 1: Properly handle configuration sections ```typescript connection.onRequest("workspace/configuration", async (params: ConfigurationParams) => { const results: any[] = []; for (const item of params.items) { if (item.section === 'python') { results.push({ pythonPath: input.server.initialization?.pythonPath, venvPath: input.server.initialization?.venvPath, }); } else if (item.section === 'python.analysis') { results.push(input.server.initialization?.pythonAnalysis ?? {}); } else { results.push(input.server.initialization ?? {}); } } return results; }) ``` ### Option 2: Add venvPath to initialization In `server.ts` Pyright spawn function: ```typescript const potentialVenvPaths = [process.env["VIRTUAL_ENV"], path.join(root, ".venv"), path.join(root, "venv")] for (const venvPath of potentialVenvPaths) { const potentialPythonPath = path.join(venvPath, "bin", "python") if (await Bun.file(potentialPythonPath).exists()) { initialization["pythonPath"] = potentialPythonPath initialization["venvPath"] = path.dirname(venvPath) // Add parent directory initialization["venv"] = path.basename(venvPath) // Add venv folder name break } } ``` ## Additional Context - The CLI `pyright` command works correctly because it reads `pyrightconfig.json` - The pyright-langserver should also read `pyrightconfig.json`, but the LSP settings seem to override or interfere with it - Setting `pythonPath` alone requires pyright to execute the Python interpreter to discover `sys.path`, which may fail in certain environments - Setting `venvPath` + `venv` directly tells pyright where to find site-packages without needing to run Python ## Workaround Currently, no reliable workaround exists within OpenCode. Users must rely on `pyrightconfig.json` being read correctly, which doesn't appear to work with the LSP integration.
Author
Owner

@minzique commented on GitHub (Dec 25, 2025):

Workaround Found

TL;DR: Disable pyright, use ty instead. It properly reads pyrightconfig.json.

Steps

  1. Disable pyright (rename both binaries):

    mv ~/.bun/bin/pyright ~/.bun/bin/pyright.bak
    mv ~/.bun/bin/pyright-langserver ~/.bun/bin/pyright-langserver.bak
    
  2. Install ty (Astral's new type checker):

    uv tool install ty
    # or: pip install ty
    
  3. Configure pyrightconfig.json at project root:

    {
      "venvPath": ".",
      "venv": ".venv"
    }
    
  4. Create symlink (if venv is in subdirectory):

    ln -s apps/api/.venv .venv
    
  5. Restart OpenCode

Why This Works

  • OpenCode's pyright LSP integration has a bug in workspace/configuration handling (ignores section parameters)
  • ty reads pyrightconfig.json directly without relying on LSP configuration messages
  • With pyright disabled, OpenCode falls back to ty for .py files

Result

Before (pyright): Import "fastapi" could not be resolved
After (ty): Real type errors only, all imports resolved correctly

@minzique commented on GitHub (Dec 25, 2025): ## Workaround Found **TL;DR**: Disable pyright, use `ty` instead. It properly reads `pyrightconfig.json`. ### Steps 1. **Disable pyright** (rename both binaries): ```bash mv ~/.bun/bin/pyright ~/.bun/bin/pyright.bak mv ~/.bun/bin/pyright-langserver ~/.bun/bin/pyright-langserver.bak ``` 2. **Install ty** (Astral's new type checker): ```bash uv tool install ty # or: pip install ty ``` 3. **Configure `pyrightconfig.json`** at project root: ```json { "venvPath": ".", "venv": ".venv" } ``` 4. **Create symlink** (if venv is in subdirectory): ```bash ln -s apps/api/.venv .venv ``` 5. **Restart OpenCode** ### Why This Works - OpenCode's pyright LSP integration has a bug in `workspace/configuration` handling (ignores section parameters) - `ty` reads `pyrightconfig.json` directly without relying on LSP configuration messages - With pyright disabled, OpenCode falls back to ty for `.py` files ### Result Before (pyright): `Import "fastapi" could not be resolved` After (ty): Real type errors only, all imports resolved correctly
Author
Owner

@minzique commented on GitHub (Dec 25, 2025):

Update: Cleaner approach - uninstall pyright instead of renaming:

# For bun (check with: which pyright)
bun remove -g pyright

# For npm
npm uninstall -g pyright

# For yarn
yarn global remove pyright

Then install ty and restart OpenCode.

@minzique commented on GitHub (Dec 25, 2025): **Update**: Cleaner approach - uninstall pyright instead of renaming: ```bash # For bun (check with: which pyright) bun remove -g pyright # For npm npm uninstall -g pyright # For yarn yarn global remove pyright ``` Then install ty and restart OpenCode.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: anomalyco/opencode#3846