python-lsp / pylsp-mypy

Mypy plugin for the Python LSP Server.
MIT License
129 stars 37 forks source link

cached lookup of mypy executable/venv based on document.path and workspace #57

Closed petergaultney closed 1 month ago

petergaultney commented 1 year ago

The general idea here is that Python programmers can probably think of a hundred different ways to dynamically configure which install of mypy gets used and how it gets run. I'm doing this by having pylsp-mypy look for a script called get-venv.py (which probably needs a different, more general/descriptive name, and/or could potentially have a configurable name/path via standard python-language-server config), and if it exists, it calls it with a JSON payload of the document path and workspace path, and processing the output as a simple JSON payload to modify how mypy is called.

An example get-venv.py script is shown below (a simplified version of my own). In my case, I always have mypy installed within the venvs themselves, so the simplest option for me is to use my venv manager to run mypy.

Other people might have different ways of setting up their config dynamically - in most cases I think they would be able to just add more items to the returned list, though in some cases they might end up wishing to modify the full invocation, which currently is not possible because I haven't provided it to the get-venv.py script. That would be easy enough to change.

#!/usr/bin/env python
import argparse
import json
import os
import typing as ty

POETRY = ("poetry", "run", "mypy")
PIPENV = ("pipenv", "run", "mypy")

def get_correct_invocation(cmd: ty.List[str], path: str, workspace: str = ""):
    assert isinstance(cmd, list), f"Command must be a list of strings but is {cmd}"
    if cmd == ["mypy"]:
        if path.startswith(os.path.expanduser("~/work/COPIES/ds-monorepo/")):
            return dict(cmd=POETRY, path=path, workspace=workspace)
        if path.startswith(os.path.expanduser("~/work/")):
            return dict(cmd=PIPENV, path=path, workspace=workspace)
    return dict(cmd=cmd, path=path, workspace=workspace)

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("json")
    args = json.loads(parser.parse_args().json)

    print(json.dumps(get_correct_invocation(args["cmd"], args["path"], args["workspace"])))

if __name__ == "__main__":
    main()
Richardk2n commented 1 year ago

Is there any reason, why this is a separate file executed using subprocess.run and not just a normal part of the code?

petergaultney commented 1 year ago

yes, the idea is that it allows users to write any kind of logic they want for determining the python environment based on the path of the file being checked.

For poetry and pipenv, something like my code could easily be standard, but this approach lets users essentially write arbitrary config for their own setups.