Skip to content

Instantly share code, notes, and snippets.

@kyokley
Last active July 7, 2018 13:29
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kyokley/dceea5703506fc17f409ac257a8745d4 to your computer and use it in GitHub Desktop.
Save kyokley/dceea5703506fc17f409ac257a8745d4 to your computer and use it in GitHub Desktop.
Dynamically set Python2/3 version in Syntastic

Determining Python 2/3 Version in VIM

Introduction

I am a big fan of the syntastic package. In python, it is useful for viewing both Pyflakes and Bandit errors. However, this only works if syntastic knows what python interpreter to use. Not everyone is fortunate enough to have upgraded all of their software to python3. For reasons out of my control, I end up spending most of my time writing python2 code. This means that the times when I finally get to write python3, all of my syntax checking is broken. How cool would it be to have VIM determine what version of python to use automatically? So, begins my grand experiment...

Prerequisites

For this to work, I will be using a virtualenv for python2 and one for python3. It's not absolutely necessary to use virtualenvs but I definitely recommend it. Setting up virtualenvs is out of the scope of this gist but make sure that each virtualenv has pyflakes and bandit installed.

Detection function

Step 1: Define variables for the python bin locations

" Python configs
let g:python2_dir = $HOME . '/path/to/python2_venv/bin/'
let g:python3_dir = $HOME . '/path/to/python3_venv/bin/'

Step 2: Copy the current buffer into a temporary buffer

function! g:DetectPyVersion()
    silent %yank p
    new
    silent 0put p

I decided to name my function, crazy enough, DetectPyVersion. The first thing I need to do is make a copy of the current buffer and drop it into a temporary space.

Step 3: Determine the python version

    silent! exe '%!' . g:python2_dir . 'python -c "import ast; import sys; ast.parse(sys.stdin.read())"'
    bd!
    if v:shell_error == 0
        let b:py_version = 'py2'
        return
    endif

This is where the magic happens. I am using the python AST module to determine if the syntax in the current buffer is python2 compliant or not. The AST module can build a syntax tree for a given python grammar. If the inline python generates a traceback, then we know the code is not python2. After running an external command, VIM sets the variable v:shell_error to the return code of the command. If the command was successful, it will return 0 or some other int to signal failure. The bd! command simply removes the temporary buffer.

Step 4: Repeat Step 2 and 3 but for python3

    silent %yank p
    new
    silent 0put p

    silent! exe '%!' . g:python3_dir . 'python -c "import ast; import sys; ast.parse(sys.stdin.read())"'
    bd!
    if v:shell_error == 0
        let b:py_version = 'py3'
        return
    endif

Similar to above, repeat the steps for python3.

Step 5: Finish up detection function

Next, if neither python2 or python3 were successful just set an error string. Altogether,

function! g:DetectPyVersion()
    silent %yank p
    new
    silent 0put p

    silent! exe '%!' . g:python2_dir . 'python -c "import ast; import sys; ast.parse(sys.stdin.read())"'
    bd!
    if v:shell_error == 0
        let b:py_version = 'py2'
        return
    endif

    silent %yank p
    new
    silent 0put p

    silent! exe '%!' . g:python3_dir . 'python -c "import ast; import sys; ast.parse(sys.stdin.read())"'
    bd!
    if v:shell_error == 0
        let b:py_version = 'py3'
        return
    endif

    let b:py_version = 'Err'
endfunction

Set Syntastic Configs

Now that the function to detect the version of python has been written, next is to assign the updated python version in syntastic.

function! s:SetSyntasticPyVersion()
    if &filetype == 'python'
        if exists("b:py_version") && b:py_version == 'py3'
            let g:syntastic_python_pyflakes_exec = g:python3_dir . 'pyflakes'
            let g:syntastic_python_bandit_exec = g:python3_dir . 'bandit'
        else
            let g:syntastic_python_pyflakes_exec = g:python2_dir . 'pyflakes'
            let g:syntastic_python_bandit_exec = g:python2_dir . 'bandit'
        endif
    endif
endfunction

After checking that the current file is a python module, check that the py_version buffer variable has been defined and is set to py3. If not, use python2 versions.

Hook Autocommands

Finally, to tie everything together, autocommands are used to call the above functions at the right times. Since DetectPyVersion makes external calls to the python interpreter, it can be relatively slow. Therefore, it is best to only run it when necessary. I don't expect python syntax for a file to change very often so it should be good enough to just check it when opening the file.

    autocmd bufreadpost * call g:DetectPyVersion()

Also, notice that I have chosen to make DetectPyVersion a global function. In case I decide to switch syntaxes while working on a file, this gives the ability to re-run the detection function manually. Since the variables that control syntastic are defined globally, it is necessary to update them every time we switch buffers. I also run the update just before writing a buffer since syntastic can also be configured to run on writes.

    autocmd bufenter,bufwritepre * call s:SetSyntasticPyVersion()
@nigelmd
Copy link

nigelmd commented Jul 6, 2018

On the last line, should that be SetSyntasticPyVersion

@kyokley
Copy link
Author

kyokley commented Jul 7, 2018

Good call. Fixed it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment