4

I have two simple Python tests in my_tests.py file:

from unittest import TestCase

class MyTestCase(TestCase):
    def test_true(self):
        self.assertTrue(True)

    def test_false(self):
        self.assertFalse(True)

which can be executed using pytest my_tests.py.

I want to write VIM shortcut, which would execute one test on which the cursor is on. For example, the shortcut could call: :!pytest my_tests.py::MyTestCase::test_false<CR>.

My question is how to get current filename, Python class name and Python method name on which the cursor is, which are needed for the shortcut.

4
  • It's pretty easy to get file path with expand(%:p) and method name with <C-R><C-W> if cursor on it. But the problem with class name. If your indentation is always same, you probably can use [[ and [m motions to navigate to class definition and method definition. So you can write vim function that builds command and call it on mapping. But it's not very reliable. Better to use some python parser like YouCompleteMe plugin does, but I don't know much about this, sorry.
    – Dmitry
    Jul 13, 2020 at 12:06
  • 1
    You could use some kind of search() calls to get the nearest def/class lines, and then you’d need to parse out the names... I can try to mock something up this evening
    – D. Ben Knoble
    Jul 13, 2020 at 13:39
  • @D.BenKnoble I think your approach would work just fine, however I am not sure how to implement it.
    – niekas
    Jul 13, 2020 at 13:45
  • Hey OP, just wanted to mention a quick edit; I accidentally used return instead of finish in my function!
    – D. Ben Knoble
    Jul 14, 2020 at 20:47

2 Answers 2

1

Here's a pure vimscript function that builds up the command by searching for functions (def) and classes (class). You may need to adjust the filename modifiers or the command; parameters or global vars could be used for that if one wanted to build a plugin.

" ~/.vim/autoload/python.vim

function python#runTest() abort
  const l:file = expand('%:p')
  if empty(l:file)
    return
  else
    const l:func = search('^\s*def', 'bcnW')->getline()
          \ ->substitute('^\s*def\s*\(\k*\).*', '\1', '')
    const l:class = search('^\s*class', 'bcnW')->getline()
          \ ->substitute('^\s*class\s*\(\k*\).*', '\1', '')
    const l:cmd = printf('!pytest %s%s%s',
          \ l:file,
          \ (empty(l:class) ? '' : '::'.l:class),
          \ (empty(l:func) ? '' : '::'.l:func))
    execute l:cmd
  endif
endfunction

Then you could put a mapping in ~/.vim/after/ftplugin/python.vim like

nnoremap <buffer> <localleader>t :call python#runTest()<CR>
1

I have managed to automate this feature using vimscript-python. Add this function to your ~/.vimrc:

function! RunCurrentPythonTest()
python3 << EOF

import re
import vim  # https://vimhelp.org/if_pyth.txt.html

cursor = vim.current.window.cursor
test_filename = vim.eval("expand('%p')")

test_name = None
class_name = None
for line_no in range(cursor[0]-1, -1, -1):
    line = vim.current.buffer[line_no]
    if not test_name and line.lstrip().startswith('def test'):
        test_name = re.findall('def (\w+)\(', line)[0]
    if not class_name and line.startswith('class'):
        class_name = re.findall('class (\w+)\(', line)[0]
        break

cmd = f'!pytest {test_filename}'
if class_name:
    cmd += f'::{class_name}'
if test_name:
    cmd += f'::{test_name}'
vim.command(cmd)

EOF
endfunction

And map this function to ;t:

map     ;t          :call RunCurrentPythonTest()<CR>

Hover on a Python test, press ;t and great success - only that one test is executed:

================================================================== FAILURES ===================================================================
____________________________________________________________ MyTestCase.test_false ____________________________________________________________

self = <my_tests.MyTestCase testMethod=test_false>

    def test_false(self):
>       self.assertFalse(True)
E       AssertionError: True is not false

bin/my_tests.py:9: AssertionError
============================================================== 1 failed in 0.04s ==============================================================

This implementation has additional feature: If you hover on a TestCase definition line - whole test case will be executed. And if you navigate to the begging of the file - all tests of the file will be executed.

2
  • There appears to be a slight logical bug in the end of thr code—what if class is None but test is not? Will pytest accept that syntax?
    – D. Ben Knoble
    Jul 14, 2020 at 12:50
  • 1
    @D.BenKnoble Yes, pytest accepts this notation, e.g. my_tests.py::test_function, but tests have to be written as plain functions. I have just tested it and it works fine.
    – niekas
    Jul 14, 2020 at 12:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.