├── tests ├── README.org └── test-python-helpers.el ├── LICENSE.rst ├── README.rst └── python-pytest.el /tests/README.org: -------------------------------------------------------------------------------- 1 | The following command can be used to run all tests in the directory =tests=. The command should be run in the root directory of the project. The command explicitly loads the file =python-pytest.el= in this repository, this is done to make sure that Emacs uses the symbol definitions from that file instead of other locations that might have the same package (e.g. installed through MELPA.) 2 | 3 | #+BEGIN_SRC sh 4 | emacs \ 5 | --batch \ 6 | --eval '(load-file "./python-pytest.el")' \ 7 | --eval '(dolist (file (directory-files-recursively "tests" "\\`[^.].*\\.el\\'\''")) (load-file file))' \ 8 | --eval '(ert-run-tests-batch-and-exit)' 9 | #+END_SRC 10 | -------------------------------------------------------------------------------- /LICENSE.rst: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | ==================== 3 | 4 | Copyright (c) 2018–2020, wouter bolsterlee 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | 1. Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | 2. Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | 3. Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /tests/test-python-helpers.el: -------------------------------------------------------------------------------- 1 | (defmacro pytest-test-with-temp-text (text &rest body) 2 | (declare (indent 1) (debug t)) 3 | `(let ((inside-text (if (stringp ,text) ,text (eval ,text)))) 4 | (with-temp-buffer 5 | (setq python-indent-offset 2 6 | python-indent-guess-indent-offset nil) 7 | (python-mode) 8 | (let ((point (string-match "" inside-text))) 9 | (if point 10 | (progn 11 | (insert (replace-match "" nil nil inside-text)) 12 | (goto-char (1+ (match-beginning 0)))) 13 | (insert inside-text) 14 | (goto-char (point-min)))) 15 | (font-lock-ensure (point-min) (point-max)) 16 | ,@body))) 17 | 18 | (ert-deftest get-current-def-outside-class () 19 | (pytest-test-with-temp-text (concat 20 | "def foo():\n" 21 | " pass\n" 22 | "def bar():\n" 23 | " pass\n") 24 | (should (equal (python-pytest--node-id-def-at-point-treesit) "foo")) 25 | (forward-line 1) 26 | (should (equal (python-pytest--node-id-def-at-point-treesit) "foo")) 27 | (forward-line 1) 28 | (should (equal (python-pytest--node-id-def-at-point-treesit) "bar")) 29 | (forward-line 1) 30 | (should (equal (python-pytest--node-id-def-at-point-treesit) "bar")) 31 | ;; when the buffer is narrowed, we should get the same result. 32 | (goto-char (point-min)) 33 | (search-forward "foo") 34 | (save-restriction 35 | (narrow-to-defun) 36 | (should (equal (python-pytest--node-id-def-at-point-treesit) "foo"))) 37 | (forward-line 1) 38 | (save-restriction 39 | (narrow-to-defun) 40 | (should (equal (python-pytest--node-id-def-at-point-treesit) "foo"))) 41 | (forward-line 1) 42 | (save-restriction 43 | (narrow-to-defun) 44 | (should (equal (python-pytest--node-id-def-at-point-treesit) "bar"))) 45 | (forward-line 1) 46 | (save-restriction 47 | (narrow-to-defun) 48 | (should (equal (python-pytest--node-id-def-at-point-treesit) "bar"))))) 49 | 50 | (ert-deftest get-current-def-inside-class () 51 | (pytest-test-with-temp-text (concat 52 | "class TestGroup:\n" 53 | " def foo():\n" 54 | " pass\n" 55 | " def bar():\n" 56 | " pass\n") 57 | (should (equal (python-pytest--node-id-def-at-point-treesit) "TestGroup::foo")) 58 | (forward-line 1) 59 | (should (equal (python-pytest--node-id-def-at-point-treesit) "TestGroup::foo")) 60 | (forward-line 1) 61 | (should (equal (python-pytest--node-id-def-at-point-treesit) "TestGroup::bar")) 62 | (forward-line 1) 63 | (should (equal (python-pytest--node-id-def-at-point-treesit) "TestGroup::bar")) 64 | ;; when the buffer is narrowed, we should get the same result 65 | (goto-char (point-min)) 66 | (search-forward "foo") 67 | (save-restriction 68 | (narrow-to-defun) 69 | (should (equal (python-pytest--node-id-def-at-point-treesit) "TestGroup::foo"))) 70 | (forward-line 1) 71 | (save-restriction 72 | (narrow-to-defun) 73 | (should (equal (python-pytest--node-id-def-at-point-treesit) "TestGroup::foo"))) 74 | (forward-line 1) 75 | (save-restriction 76 | (narrow-to-defun) 77 | (should (equal (python-pytest--node-id-def-at-point-treesit) "TestGroup::bar"))) 78 | (forward-line 1) 79 | (save-restriction 80 | (narrow-to-defun) 81 | (should (equal (python-pytest--node-id-def-at-point-treesit) "TestGroup::bar"))))) 82 | 83 | (ert-deftest get-current-def-inside-multiple-classes () 84 | (pytest-test-with-temp-text (string-join 85 | '("class TestDepthOne:" 86 | " class TestDepthTwo:" 87 | " class TestDepthThree:" 88 | " def foo():" 89 | " pass" 90 | " def bar():" 91 | " pass") 92 | "\n") 93 | (should (equal (python-pytest--node-id-def-at-point-treesit) "TestDepthOne::TestDepthTwo::TestDepthThree::foo")) 94 | (forward-line 1) 95 | (should (equal (python-pytest--node-id-def-at-point-treesit) "TestDepthOne::TestDepthTwo::TestDepthThree::foo")) 96 | (forward-line 1) 97 | (should (equal (python-pytest--node-id-def-at-point-treesit) "TestDepthOne::TestDepthTwo::TestDepthThree::bar")) 98 | (forward-line 1) 99 | (should (equal (python-pytest--node-id-def-at-point-treesit) "TestDepthOne::TestDepthTwo::TestDepthThree::bar")) 100 | (forward-line 1) 101 | ;; when the buffer is narrowed, we should get the same result. 102 | (goto-char (point-min)) 103 | (save-restriction 104 | (search-forward "foo") 105 | (narrow-to-defun) 106 | (should (equal (python-pytest--node-id-def-at-point-treesit) "TestDepthOne::TestDepthTwo::TestDepthThree::foo"))) 107 | (save-restriction 108 | (forward-line 1) 109 | (narrow-to-defun) 110 | (should (equal (python-pytest--node-id-def-at-point-treesit) "TestDepthOne::TestDepthTwo::TestDepthThree::foo"))) 111 | (save-restriction 112 | (forward-line 1) 113 | (narrow-to-defun) 114 | (should (equal (python-pytest--node-id-def-at-point-treesit) "TestDepthOne::TestDepthTwo::TestDepthThree::bar"))) 115 | (save-restriction 116 | (forward-line 1) 117 | (narrow-to-defun) 118 | (should (equal (python-pytest--node-id-def-at-point-treesit) "TestDepthOne::TestDepthTwo::TestDepthThree::bar")))) 119 | (pytest-test-with-temp-text (string-join 120 | '("class TestDepthOne:" 121 | " def test_depth_one():" 122 | " pass" 123 | " class TestDepthTwo:" 124 | " def test_depth_two():" 125 | " pass" 126 | " class TestDepthThree:" 127 | " def test_depth_three():" 128 | " pass") 129 | "\n") 130 | (should (equal (python-pytest--node-id-def-at-point-treesit) "TestDepthOne::test_depth_one")) 131 | (search-forward "test_depth_two") 132 | (should (equal (python-pytest--node-id-def-at-point-treesit) "TestDepthOne::TestDepthTwo::test_depth_two")) 133 | (search-forward "test_depth_three") 134 | (should (equal (python-pytest--node-id-def-at-point-treesit) "TestDepthOne::TestDepthTwo::TestDepthThree::test_depth_three")) 135 | ;; when the buffer is narrowed, we should get the same result. 136 | (goto-char (point-min)) 137 | (save-restriction 138 | (search-forward "test_depth_one") 139 | (narrow-to-defun) 140 | (should (equal (python-pytest--node-id-def-at-point-treesit) "TestDepthOne::test_depth_one"))) 141 | (save-restriction 142 | (search-forward "test_depth_two") 143 | (narrow-to-defun) 144 | (should (equal (python-pytest--node-id-def-at-point-treesit) "TestDepthOne::TestDepthTwo::test_depth_two"))) 145 | (save-restriction 146 | (search-forward "test_depth_three") 147 | (narrow-to-defun) 148 | (should (equal (python-pytest--node-id-def-at-point-treesit) "TestDepthOne::TestDepthTwo::TestDepthThree::test_depth_three"))))) 149 | 150 | (ert-deftest get-current-def-inside-def () 151 | (pytest-test-with-temp-text (string-join 152 | '("def foo():" 153 | " def bar():" 154 | " pass") 155 | "\n") 156 | (should (equal (python-pytest--node-id-def-at-point-treesit) "foo")) 157 | (narrow-to-defun) 158 | (should (equal (python-pytest--node-id-def-at-point-treesit) "foo"))) 159 | (pytest-test-with-temp-text (string-join 160 | '("class TestDepthOne:" 161 | " class TestDepthTwo:" 162 | " class TestDepthThree:" 163 | " def foo():" 164 | " def bar():" 165 | " pass") 166 | "\n") 167 | ;; We want to get the outermost def because pytest can't 168 | ;; identify defs inside defs. In other words, pytest can 169 | ;; only identify those defs that are not contained within 170 | ;; other defs. 171 | (should (equal (python-pytest--node-id-def-at-point-treesit) "TestDepthOne::TestDepthTwo::TestDepthThree::foo")) 172 | ;; when the buffer is narrowed, we should get the same result. 173 | (narrow-to-defun) 174 | (should (equal (python-pytest--node-id-def-at-point-treesit) "TestDepthOne::TestDepthTwo::TestDepthThree::foo")))) 175 | 176 | (ert-deftest get-current-class-outside-class () 177 | (pytest-test-with-temp-text (string-join 178 | '("class Test:" 179 | " def foo():" 180 | " pass") 181 | "\n") 182 | (should (equal (python-pytest--node-id-class-at-point-treesit) "Test")) 183 | ;; when the buffer is narrowed, we should get the same result. 184 | (narrow-to-defun) 185 | (should (equal (python-pytest--node-id-class-at-point-treesit) "Test")))) 186 | 187 | (ert-deftest get-current-class-inside-class () 188 | ;; when the buffer is not narrowed 189 | (pytest-test-with-temp-text (string-join 190 | '("class TestDepthOne:" 191 | " class TestDepthTwo:" 192 | " def foo():" 193 | " pass") 194 | "\n") 195 | (should (equal 196 | (python-pytest--node-id-class-at-point-treesit) 197 | "TestDepthOne::TestDepthTwo")) 198 | ;; when the buffer is narrowed, we should get the same result. 199 | (narrow-to-defun) 200 | (should (equal 201 | (python-pytest--node-id-class-at-point-treesit) 202 | "TestDepthOne::TestDepthTwo")))) 203 | 204 | (ert-deftest get-current-class-inside-multiple-classes () 205 | (pytest-test-with-temp-text (string-join 206 | '("class TestDepthOne:" 207 | " class TestDepthTwo:" 208 | " class TestDepthThree:" 209 | " class TestDepthFour:" 210 | " class TestDepthFive:" 211 | " def foo():" 212 | " pass") 213 | "\n") 214 | (should (equal 215 | (python-pytest--node-id-class-at-point-treesit) 216 | "TestDepthOne::TestDepthTwo::TestDepthThree::TestDepthFour::TestDepthFive")) 217 | ;; when the buffer is narrowed, we should get the same result. 218 | (narrow-to-defun) 219 | (should (equal 220 | (python-pytest--node-id-class-at-point-treesit) 221 | "TestDepthOne::TestDepthTwo::TestDepthThree::TestDepthFour::TestDepthFive")))) 222 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ================ 2 | python-pytest.el 3 | ================ 4 | 5 | .. image:: https://melpa.org/packages/python-pytest-badge.svg 6 | :alt: melpa badge 7 | 8 | .. image:: https://stable.melpa.org/packages/python-pytest-badge.svg 9 | :alt: melpa stable badge 10 | 11 | 12 | overview 13 | ======== 14 | 15 | ``python-pytest.el`` is an `emacs`__ package 16 | to integrate the python `pytest`__ test runner. 17 | 18 | __ https://www.gnu.org/software/emacs/ 19 | __ https://pytest.org/ 20 | 21 | most functionality can be used via 22 | a dispatcher popup menu built using `transient`__, 23 | which gives a look and feel 24 | similar to the fantastic `magit`__ package. 25 | 26 | __ https://magit.vc/manual/transient 27 | __ https://magit.vc/ 28 | 29 | 30 | features 31 | ======== 32 | 33 | ``python-pytest.el`` offers these awesome features: 34 | 35 | * various commands with ‘do what i mean’ (dwim) behaviour, 36 | using heuristics to automatically detect test files and test 37 | functions: 38 | 39 | * run all tests 40 | 41 | * rerun previous failures 42 | 43 | * repeat the last invocation 44 | 45 | * run only tests for the current python (test) module 46 | 47 | * run only tests for the current (test) function 48 | 49 | * easy way to change common switches and options, e.g. 50 | toggling output capture, failing after the first error, 51 | and so on. 52 | 53 | * edit the automatically generated command line before executing, 54 | by invoking commands with a prefix argument (``C-u``). 55 | 56 | * basic debugger integration using the pdb tracking support 57 | from the built-in `python-mode` package, 58 | which will automatically open source files at the right location. 59 | 60 | * work simultaneously on multiple python projects. 61 | each project will use its own dedicated pytest output buffer. 62 | 63 | * various customisation options, e.g. to change whether 64 | a generated command line should be shown for editing by default. 65 | 66 | * hooks that get run before and after running pytest, 67 | which can be used to add custom behaviour. 68 | 69 | 70 | screenshot 71 | ========== 72 | 73 | :: 74 | 75 | Output 76 | -c color (--color) 77 | -q quiet (--quiet) 78 | -s no output capture (--capture=no) 79 | -v verbosity ([--verbose|--verbose --verbose]) 80 | 81 | Selection, filtering, ordering 82 | -k only names matching expression (-k=) --dm run doctests (--doctest-modules) 83 | -m only marks matching expression (-m=) --nf new first (--new-first) 84 | --sw stepwise (--stepwise) 85 | 86 | Failures, errors, debugging 87 | -l show locals (--showlocals) --ff failed first (--failed-first) 88 | -p debug on error (--pdb) --ft full tracebacks (--full-trace) 89 | -x exit after first failure (--exitfirst) --mf exit after N failures or errors (--maxfail=10) 90 | --rx run xfail tests (--runxfail) 91 | --tb traceback style (--tb=) 92 | --tr debug on each test (--trace) 93 | 94 | Run tests 95 | t all r repeat f file (dwim) m files d def/class (dwim) 96 | x last failed F file (this) M directories D def/class (this) 97 | 98 | 99 | installation 100 | ============ 101 | 102 | ``python-pytest.el`` is available from `melpa`__. 103 | 104 | __ https://melpa.org/#/python-pytest 105 | 106 | with ``use-package``: 107 | 108 | .. code-block:: elisp 109 | 110 | (use-package python-pytest) 111 | 112 | install manually:: 113 | 114 | M-x package-install RET python-pytest RET 115 | 116 | note that ``python-pytest.el`` uses `projectile`__ 117 | for some of its features, e.g. finding associated test files. 118 | this package is intended to work correctly 119 | even without any ``projectile`` configuration, 120 | since it will likely do the right thing 121 | if a project has a conventional layout. 122 | 123 | __ https://github.com/bbatsov/projectile 124 | 125 | to run function/class tests, ``tree-sitter`` is needed, including the python language grammar:: 126 | 127 | M-x treesit-install-language-grammar 128 | 129 | 130 | usage 131 | ===== 132 | 133 | basics 134 | ------ 135 | 136 | the typical usage pattern is to invoke the popup menu, 137 | named ``python-pytest-dispatch``. 138 | it is a good idea to create a dedicated keybinding for this command, 139 | but it can also be run manually: 140 | 141 | :: 142 | 143 | M-x python-pytest-dispatch 144 | 145 | this shows a dispatcher menu. 146 | change some switches and options, 147 | then run one of the actions. 148 | 149 | a dedicated pytest ``comint`` buffer will open, 150 | showing the output in real time, 151 | and allowing interaction with debuggers. 152 | 153 | using the correct environment 154 | ----------------------------- 155 | 156 | this package ultimately invokes ``pytest``. 157 | ``python-pytest.el`` does *not* guess execution environments, 158 | so emacs needs to use the right ``exec-path``, 159 | taking into account python virtual environments, and so on. 160 | 161 | to manage the execution environment, consider using `direnv`__: 162 | it can change (and revert) paths and environment variables, 163 | simply by switching to a project directory, 164 | making it perfect for automatically ‘activating’ a ``virtualenv``. 165 | use `emacs-direnv`__ and possibly `exec-path-from-shell`__ 166 | to achieve the same inside emacs. 167 | 168 | __ https://direnv.net/ 169 | __ https://github.com/wbolster/emacs-direnv 170 | __ https://github.com/purcell/exec-path-from-shell 171 | 172 | working in a monorepo 173 | --------------------- 174 | 175 | by default, ``pytest`` is run from the project root directory. if 176 | your package is not at the root of your repository, ``pytest`` might 177 | not find your modules. 178 | 179 | a workaround is to add the the package root to ``PYTHONPATH`` before 180 | running the tests. this can be found by adding a dummy file in the package 181 | root. the following hook looks for a ``.pyroot`` file in parent directories. 182 | if found, it adds the directory of the file to ``PYTHONPATH``. 183 | 184 | .. code-block:: elisp 185 | 186 | (add-hook 'python-mode-hook 187 | (lambda () 188 | (when-let ((r (locate-dominating-file default-directory ".pyroot"))) 189 | (setq python-pytest-executable 190 | (concat "PYTHONPATH=" r " " "pytest"))))) 191 | 192 | 193 | editing and repeating 194 | --------------------- 195 | 196 | to edit the command line before running it, 197 | use a prefix argument before calling the action, 198 | e.g.type ``C-u t`` instead of just ``t`` in the popup menu. 199 | 200 | when the popup menu itself is invoked with a prefix argument, 201 | this will run ``python-pytest-repeat`` to rerun pytest. 202 | this means a single key binding can be used for both 203 | an initial run (via the popup), and for repeated calls. 204 | this is great for quick ‘edit, test, edit, test` cycles. 205 | 206 | available commands 207 | ------------------ 208 | 209 | the available commands are: 210 | 211 | - ``python-pytest`` 212 | - ``python-pytest-file`` 213 | - ``python-pytest-file-dwim`` 214 | - ``python-pytest-files`` 215 | - ``python-pytest-function`` 216 | - ``python-pytest-function-dwim`` 217 | - ``python-pytest-last-failed`` 218 | - ``python-pytest-repeat`` 219 | 220 | all of these are available via the popup menu, 221 | but can also be executed directly (or bound to a key). 222 | 223 | 224 | heuristics 225 | ========== 226 | 227 | this package uses a few heuristics for its 228 | ‘do what i mean’ behaviour. 229 | 230 | test file heuristics 231 | -------------------- 232 | 233 | the ``python-pytest-file-dwim`` command tries to 234 | do the right thing both when editing the actual code 235 | and its associated test module. 236 | for instance, when editing ``foo/bar.py``, 237 | this will automatically detect ``tests/test_bar.py`` 238 | (thanks to the ``projectile`` package), 239 | and only run the tests from that test module. 240 | 241 | test function heuristics 242 | ------------------------ 243 | 244 | the ``python-pytest-function-dwim`` command 245 | tries to run only tests related to the function 246 | close to the cursor position 247 | (‘point’ in emacs terminology). 248 | 249 | when editing a test module, this runs 250 | only a single test function, 251 | namely the one currently being edited. 252 | 253 | when editing the code itself, 254 | things are more complicated. 255 | this command will make a guess 256 | to only run the right test functions. 257 | the matching behaviour can be tweaked using 258 | ``python-pytest-strict-test-name-matching`` 259 | (see configuration below). 260 | 261 | by default, the current function name will be used 262 | as a pattern to match the corresponding tests. 263 | for example, when editing ``foo()`` inside ``utils.py``, 264 | this will match ``test_foo()`` as well as ``test_foo_xyz()``, 265 | by invoking ``pytest test_utils.py -k test_foo``. 266 | if a pattern was specified in the popup (the ``-k`` option), 267 | it will try to make a combined pattern, 268 | by invoking ``pytest test_utils.py -k 'test_foo and other_filter'``. 269 | 270 | on the other hand, 271 | when ``python-pytest-strict-test-name-matching`` is non-nil, 272 | only ``test_foo()`` will match, and nothing else, 273 | by invoking ``pytest test_utils.py::test_foo``. 274 | 275 | configuration 276 | ============= 277 | 278 | settings 279 | -------- 280 | 281 | the behaviour of this package can be tweaked 282 | by customising a few `defcustom` variables. 283 | use the ``customize`` interface to explore those 284 | (each will show a description and possible values):: 285 | 286 | M-x customize-group RET python-pytest RET 287 | 288 | to set those permanently without using the customize interface, 289 | use something like this in ``init.el``: 290 | 291 | .. code-block:: elisp 292 | 293 | (use-package python-pytest 294 | :custom 295 | (python-pytest-confirm t)) 296 | 297 | the available variables are: 298 | 299 | - ``python-pytest-confirm`` 300 | 301 | whether to ask for confirmation (allowing editing) by default. 302 | this inverts the prefix argument (``C-u``) behaviour. 303 | 304 | - ``python-pytest-strict-test-name-matching`` 305 | 306 | Whether to require a strict match for the ‘test this function’ heuristic. 307 | 308 | - ``python-pytest-executable`` 309 | 310 | the name of the pytest executable (``pytest`` by default) 311 | 312 | - ``python-pytest-unsaved-buffers-behavior`` 313 | 314 | whether to ask whether unsaved buffers should be saved before 315 | running pytest. the check for unsaved buffers can be for only the 316 | current buffer, or for all project buffers, and those can be saved 317 | directly, or after confirmation. valid values: ``ask-all``, 318 | ``ask-current``, ``save-all``, ``save-current``, or ``nil``. 319 | 320 | - ``python-pytest-setup-hook``, 321 | ``python-pytest-started-hook``, and 322 | ``python-pytest-finished-hook`` 323 | 324 | hooks run before starting ``pytest``, after starting ``pytest``, 325 | and after ``pytest`` finished. 326 | 327 | - ``python-pytest-buffer-name`` and ``python-pytest-project-name-in-buffer-name`` 328 | 329 | the defaults result in ``*pytest*``. 330 | 331 | - ``python-pytest-pdb-track`` 332 | 333 | whether to enable the pdb tracking support 334 | 335 | 336 | extending the popup 337 | ------------------- 338 | 339 | when using pytest plugins that provide extra switches, 340 | it may be useful to integrate those into the popup. 341 | see the `transient`__ manual for more information. 342 | 343 | __ https://magit.vc/manual/transient 344 | 345 | as an example, this will add a ``-z`` switch that, 346 | when enabled, will invoke ``pytest --zzz``: 347 | 348 | .. code-block:: elisp 349 | 350 | (use-package python-pytest 351 | :config 352 | ;; just an extra `-y' after the `-x' suffix 353 | (transient-append-suffix 354 | 'python-pytest-dispatch 355 | "-x" 356 | '("-y" "The Y" "-y")) 357 | ;; group with `-z' after second from the last group, 358 | ;; that is before `Run tests' 359 | (transient-append-suffix 360 | 'python-pytest-dispatch 361 | '(-2) 362 | ["My Z" 363 | ("-z" "The Z" "-z")])) 364 | 365 | `transient` lets you save defaults you want for it. 366 | just select all options on ``python-pytest-dispatch`` 367 | and then 368 | 369 | - ``C-x C-s`` to save current settings as default and make 370 | them persistent, 371 | - ``C-x s`` to save current settings as default for the 372 | current emacs session. 373 | 374 | 375 | contributing 376 | ============ 377 | 378 | praise? complaints? bugs? questions? ideas? 379 | 380 | please use the github issue tracker. 381 | 382 | 383 | credits 384 | ======= 385 | 386 | this package was created by wouter bolsterlee. 387 | i am @wbolster on `github`__ and `twitter`__. 388 | 389 | __ https://github.com/wbolster 390 | __ https://twitter.com/wbolster 391 | 392 | 393 | history 394 | ======= 395 | 396 | note: 397 | `melpa`__ automatically ships the latest code from the git ``main`` branch, 398 | while `melpa stable`__ only contains tagged (released) versions. 399 | 400 | __ https://melpa.org/ 401 | __ https://stable.melpa.org/ 402 | 403 | development branch 404 | ------------------ 405 | 406 | - prefer ``tree-sitter`` for function/class at point commands 407 | (`#75 `_) 408 | 409 | - add option ``--log-cli-level`` 410 | (`#74 `_) 411 | 412 | - add ``project.el`` support 413 | (`#73 `_) 414 | 415 | - use ``completing-read-multiple`` for multiple files selection 416 | (`#72 `_) 417 | 418 | 3.4.0 (2024-03-14) 419 | ------------------ 420 | 421 | * add windows support 422 | (`#68 `_) 423 | 424 | * add ``-W`` argument to ignore warnings 425 | (`#70 `_) 426 | 427 | * add ``--collect-only`` option 428 | (`#65 `_) 429 | 430 | 3.3.0 (2022-10-18) 431 | ------------------ 432 | 433 | * add dispatch options for pytest-xdist 434 | (`#54 `_) 435 | 436 | * respect ``projectile-compilation-dir`` if it exists 437 | (`#59 `_) 438 | 439 | * Use ``read-shell-command`` instead of ``read-from-minibuffer`` 440 | (`#60 `_) 441 | 442 | * add ``0`` as a valid argument that can be passed to ``-n`` 443 | (`#61 `_) 444 | 445 | * switch to ``compilation-mode`` after pytest process finishes 446 | (`#62 `_) 447 | 448 | * fix saving of verbosity setting 449 | (`#64 `_) 450 | 451 | 3.2.0 (2021-11-11) 452 | ------------------ 453 | 454 | * do not use melpa unstable versions in package-requires 455 | (`#52 `_) 456 | 457 | 3.1.0 (2021-11-09) 458 | ------------------ 459 | 460 | * make python-pytest-files show all files if no test files are found 461 | (`#38 `_) 462 | 463 | * display buffer window before starting comint to fix size detection 464 | (`#48 `_) 465 | 466 | * correctly handle -m and -k flags 467 | (`#37 `_) 468 | 469 | * fix clearing test output buffer 470 | (`#15 `_) 471 | 472 | 3.0.0 (2020-08-10) 473 | ------------------ 474 | 475 | * redesign the menu: 476 | use better groupings, 477 | use multi-column visual layout, 478 | add some more flags, 479 | make all flags start with either ``-`` or ``--`` 480 | (mostly mimicking pytest flags) 481 | (`#28 `_) 482 | 483 | * add a ``python-pytest-directories`` command with interactive 484 | multi-directory selection 485 | (`#21 `_, 486 | `#31 `_) 487 | 488 | 2.0.0 (2020-08-04) 489 | ------------------ 490 | 491 | * switch to ``transient`` (``magit-popup`` replacement); 492 | the command for the menu is now ``python-pytest-dispatch`` 493 | (`#18 `_, 494 | `#26 `_) 495 | 496 | * add ``python-pytest-files`` command with interactive multi-file 497 | selection 498 | 499 | * improve ``python-pytest-file-dwim`` heuristic for nested functions/classes 500 | 501 | * make ``next-error`` and related-commands work 502 | 503 | * add a ``-w`` shortcut for very verbose (``--verbose --verbose``) 504 | (`#24 `_) 505 | 506 | 1.0.0 (2018-06-14) 507 | ------------------ 508 | 509 | * this package is useful for quite a few people. 510 | time to celebrate with a 1.x release! 511 | 512 | * save (or ask to save) modified buffers before running pytest 513 | (`#4 `_) 514 | 515 | * put customizable variables in the right group 516 | 517 | 0.3.1 (2018–03-07) 518 | ------------------ 519 | 520 | * fix package version number for melpa stable 521 | 522 | 0.3.0 (2018–03-07) 523 | ------------------ 524 | 525 | * repopulate the popup with the previously used values 526 | when running ``python-pytest-dispatch`` from an output buffer. 527 | (`#3 `_) 528 | 529 | 0.2.2 (2018-02-26) 530 | ------------------ 531 | 532 | * avoid ``-as->`` macro since the ``dash.el`` version 533 | currently on melpa stable does not have it. 534 | (`#2 `_) 535 | 536 | 0.2.1 (2018-02-22) 537 | ------------------ 538 | 539 | * fix autoloading for ``python-pytest-popup`` command 540 | 541 | 0.2.0 (2018-02-19) 542 | ------------------ 543 | 544 | * now available from melpa 545 | (`#1 `_) 546 | * more docs 547 | * various ‘dwim’ improvements 548 | * renamed and added a few popup flags 549 | * improved relative path handling 550 | * improved hooks 551 | * improved history 552 | * better shell escaping 553 | * remember current command in output buffer to make repeating work 554 | * misc other tweaks and fixes 555 | 556 | 0.1.0 (2018-02-03) 557 | ------------------ 558 | 559 | * initial release 560 | 561 | 562 | license 563 | ======= 564 | 565 | *(this is the osi approved 3-clause "new bsd license".)* 566 | 567 | copyright 2018 wouter bolsterlee 568 | 569 | all rights reserved. 570 | 571 | redistribution and use in source and binary forms, with or without 572 | modification, are permitted provided that the following conditions are met: 573 | 574 | * redistributions of source code must retain the above copyright notice, this 575 | list of conditions and the following disclaimer. 576 | 577 | * redistributions in binary form must reproduce the above copyright notice, this 578 | list of conditions and the following disclaimer in the documentation and/or 579 | other materials provided with the distribution. 580 | 581 | * neither the name of the author nor the names of the contributors may be used 582 | to endorse or promote products derived from this software without specific 583 | prior written permission. 584 | 585 | this software is provided by the copyright holders and contributors "as is" and 586 | any express or implied warranties, including, but not limited to, the implied 587 | warranties of merchantability and fitness for a particular purpose are 588 | disclaimed. in no event shall the copyright holder or contributors be liable 589 | for any direct, indirect, incidental, special, exemplary, or consequential 590 | damages (including, but not limited to, procurement of substitute goods or 591 | services; loss of use, data, or profits; or business interruption) however 592 | caused and on any theory of liability, whether in contract, strict liability, 593 | or tort (including negligence or otherwise) arising in any way out of the use 594 | of this software, even if advised of the possibility of such damage. 595 | -------------------------------------------------------------------------------- /python-pytest.el: -------------------------------------------------------------------------------- 1 | ;;; python-pytest.el --- helpers to run pytest -*- lexical-binding: t; -*- 2 | 3 | ;; Author: wouter bolsterlee 4 | ;; Version: 3.5.0 5 | ;; Package-Requires: ((emacs "24.4") (dash "2.18.0") (transient "0.3.7") (s "1.12.0")) 6 | ;; Keywords: pytest, test, python, languages, processes, tools 7 | ;; URL: https://github.com/wbolster/emacs-python-pytest 8 | ;; 9 | ;; This file is not part of GNU Emacs. 10 | 11 | ;;; License: 12 | 13 | ;; 3-clause "new bsd"; see readme for details. 14 | 15 | ;;; Commentary: 16 | 17 | ;; This package provides helpers to run pytest. See README for details. 18 | 19 | ;;; Code: 20 | 21 | (require 'cl-lib) 22 | (require 'comint) 23 | (require 'compile) 24 | (require 'python) 25 | 26 | (require 'dash) 27 | (require 'transient) 28 | (require 's) 29 | 30 | (require 'projectile nil t) 31 | (require 'project nil t) 32 | (require 'treesit nil t) 33 | 34 | (defgroup python-pytest nil 35 | "pytest integration" 36 | :group 'python 37 | :prefix "python-pytest-") 38 | 39 | (defcustom python-pytest-confirm nil 40 | "Whether to edit the command in the minibuffer before execution. 41 | 42 | By default, pytest will be executed without showing a minibuffer prompt. 43 | This can be changed on a case by case basis by using a prefix argument 44 | \(\\[universal-argument]\) when invoking a command. 45 | 46 | When t, this toggles the behaviour of the prefix argument." 47 | :group 'python-pytest 48 | :type 'boolean) 49 | 50 | (defcustom python-pytest-executable "pytest" 51 | "The name of the pytest executable." 52 | :group 'python-pytest 53 | :type 'string) 54 | 55 | (defcustom python-pytest-shell-executable 56 | (if (eq system-type 'windows-nt) 57 | (or (executable-find "powershell") 58 | (executable-find "cmdproxy")) 59 | "sh") 60 | "The name of the pytest shell executable." 61 | :group 'python-pytest 62 | :type 'string) 63 | 64 | (defcustom python-pytest-setup-hook nil 65 | "Hooks to run before a pytest process starts." 66 | :group 'python-pytest 67 | :type 'hook) 68 | 69 | (defcustom python-pytest-started-hook nil 70 | "Hooks to run after a pytest process starts." 71 | :group 'python-pytest 72 | :type 'hook) 73 | 74 | (defcustom python-pytest-finished-hook nil 75 | "Hooks to run after a pytest process finishes." 76 | :group 'python-pytest 77 | :type 'hook) 78 | 79 | (defcustom python-pytest-buffer-name "*pytest*" 80 | "Name of the pytest output buffer." 81 | :group 'python-pytest 82 | :type 'string) 83 | 84 | (defcustom python-pytest-project-name-in-buffer-name t 85 | "Whether to include the project name in the buffer name. 86 | 87 | This is useful when working on multiple projects simultaneously." 88 | :group 'python-pytest 89 | :type 'boolean) 90 | 91 | (defcustom python-pytest-pdb-track t 92 | "Whether to automatically track output when pdb is spawned. 93 | 94 | This results in automatically opening source files during debugging." 95 | :group 'python-pytest 96 | :type 'boolean) 97 | 98 | (defcustom python-pytest-strict-test-name-matching nil 99 | "Whether to require a strict match for the ‘test this function’ heuristic. 100 | 101 | This influences the ‘test this function’ behaviour when editing a 102 | non-test function, e.g. ‘foo()’. 103 | 104 | When nil (the default), the current function name will be used as 105 | a pattern to run the corresponding tests, which will match 106 | ‘test_foo()’ as well as ‘test_foo_xyz()’. 107 | 108 | When non-nil only ‘test_foo()’ will match, and nothing else." 109 | :group 'python-pytest 110 | :type 'boolean) 111 | 112 | (defcustom python-pytest-unsaved-buffers-behavior 'ask-all 113 | "Whether to ask whether unsaved buffers should be saved before running pytest." 114 | :group 'python-pytest 115 | :type '(choice (const :tag "Ask for all project buffers" ask-all) 116 | (const :tag "Ask for current buffer" ask-current) 117 | (const :tag "Save all project buffers" save-all) 118 | (const :tag "Save current buffer" save-current) 119 | (const :tag "Ignore" nil))) 120 | 121 | (defcustom python-pytest-preferred-project-manager 'auto 122 | "Override `projectile' or `project' auto-discovery to set preference if using both." 123 | :group 'python-pytest 124 | :type '(choice (const :tag "Projectile" projectile) 125 | (const :tag "Project" project) 126 | (const :tag "Automatically selected" auto)) 127 | :set (lambda (symbol value) 128 | (cond 129 | ((and (eq value 'projectile) 130 | (not (featurep 'projectile))) 131 | (user-error "Projectile preferred for python-pytest.el, but not available.")) 132 | ((and (eq value 'project) 133 | (not (fboundp 'project-root))) 134 | (user-error (concat "Project.el preferred for python-pytest.el, " 135 | "but need a newer version of Project (28.1+) to use."))) 136 | (t 137 | (set-default symbol value) 138 | value)))) 139 | 140 | (defcustom python-pytest-project-root-override nil 141 | "Override directory where pytest runs. 142 | It is intended to be set in monorepo which has multiple roots to run pytest, 143 | and to be set via .dir-locals.el." 144 | :group 'python-pytest 145 | :type '(choice directory 146 | (const :tag "No override" nil))) 147 | 148 | (defcustom python-pytest-project-name-override nil 149 | "Override project name. 150 | It should be set in monorepo which has multiple roots to run pytest. 151 | Each root must have setting for `python-pytest-project-root-override' 152 | in .dir-locals.el. 153 | 154 | It is also intended to be set via .dir-locals.el." 155 | :group 'python-pytest 156 | :type '(choice directory 157 | (const :tag "No override" nil))) 158 | 159 | (defcustom python-pytest-use-treesit (featurep 'treesit) 160 | "Whether to use treesit for getting the node ids of things at point. 161 | 162 | Users that are running a version of Emacs that supports treesit 163 | and have the Python language grammar for treesit should set this 164 | variable to t. Users that are running a version of Emacs that 165 | don't support treesit should set this variable to nil." 166 | :group 'python-pytest 167 | :type 'boolean) 168 | 169 | (defvar python-pytest--history nil 170 | "History for pytest invocations.") 171 | 172 | (defvar python-pytest--project-last-command (make-hash-table :test 'equal) 173 | "Last executed command lines, per project.") 174 | 175 | (defvar-local python-pytest--current-command nil 176 | "Current command; used in python-pytest-mode buffers.") 177 | 178 | ;;;###autoload (autoload 'python-pytest-dispatch "python-pytest" nil t) 179 | (transient-define-prefix python-pytest-dispatch () 180 | "Show popup for running pytest." 181 | :man-page "pytest" 182 | :incompatible '(("--exitfirst" "--maxfail=")) 183 | :value '("--color") 184 | ["Output" 185 | [("-c" "color" "--color") 186 | ("-q" "quiet" "--quiet") 187 | ("-s" "no output capture" "--capture=no") 188 | (python-pytest:-v) 189 | (python-pytest:--l)]] 190 | ["Selection, filtering, ordering" 191 | [(python-pytest:-k) 192 | (python-pytest:-m) 193 | " "] ;; visual alignment 194 | [("--dm" "run doctests" "--doctest-modules") 195 | ("--nf" "new first" "--new-first") 196 | ("--sw" "stepwise" "--stepwise") 197 | ("--co" "collect only" "--collect-only")]] 198 | ["Failures, errors, warnings, debugging" 199 | [("-l" "show locals" "--showlocals") 200 | ("-p" "debug on error" "--pdb") 201 | ("-x" "exit after first failure" "--exitfirst") 202 | (python-pytest:-W)] 203 | [("--ff" "failed first" "--failed-first") 204 | ("--ft" "full tracebacks" "--full-trace") 205 | ("--mf" "exit after N failures or errors" "--maxfail=") 206 | ("--rx" "run xfail tests" "--runxfail") 207 | (python-pytest:--tb) 208 | ("--tr" "debug on each test" "--trace")]] 209 | ["Options for pytest-xdist" 210 | [(python-pytest:-n)] 211 | [("-f" "loop on failure" "--looponfail")]] 212 | ["Run tests" 213 | [("t" "all" python-pytest)] 214 | [("r" "repeat" python-pytest-repeat) 215 | ("x" "last failed" python-pytest-last-failed)] 216 | [("f" "file (dwim)" python-pytest-file-dwim) 217 | ("F" "file (this)" python-pytest-file)] 218 | [("m" "files" python-pytest-files) 219 | ("M" "directories" python-pytest-directories)] 220 | [("d" "def at point (dwim)" python-pytest-run-def-or-class-at-point-dwim :if-not python-pytest--use-treesit-p) 221 | ("D" "def at point" python-pytest-run-def-or-class-at-point :if-not python-pytest--use-treesit-p) 222 | ("d" "def at point" python-pytest-run-def-at-point-treesit :if python-pytest--use-treesit-p) 223 | ("c" "class at point" python-pytest-run-class-at-point-treesit :if python-pytest--use-treesit-p)]]) 224 | 225 | (define-obsolete-function-alias 'python-pytest-popup 'python-pytest-dispatch "2.0.0") 226 | 227 | ;;;###autoload 228 | (defun python-pytest (&optional args) 229 | "Run pytest with ARGS. 230 | 231 | With a prefix argument, allow editing." 232 | (interactive (list (transient-args 'python-pytest-dispatch))) 233 | (python-pytest--run 234 | :args args 235 | :edit current-prefix-arg)) 236 | 237 | ;;;###autoload 238 | (defun python-pytest-file (file &optional args) 239 | "Run pytest on FILE, using ARGS. 240 | 241 | Additional ARGS are passed along to pytest. 242 | With a prefix argument, allow editing." 243 | (interactive 244 | (list 245 | (buffer-file-name) 246 | (transient-args 'python-pytest-dispatch))) 247 | (python-pytest--run 248 | :args args 249 | :file file 250 | :edit current-prefix-arg)) 251 | 252 | ;;;###autoload 253 | (defun python-pytest-file-dwim (file &optional args) 254 | "Run pytest on FILE, intelligently finding associated test modules. 255 | 256 | When run interactively, this tries to work sensibly using 257 | the current file. 258 | 259 | Additional ARGS are passed along to pytest. 260 | With a prefix argument, allow editing." 261 | (interactive 262 | (list 263 | (buffer-file-name) 264 | (transient-args 'python-pytest-dispatch))) 265 | (python-pytest-file (python-pytest--sensible-test-file file) args)) 266 | 267 | ;;;###autoload 268 | (defun python-pytest-files (files &optional args) 269 | "Run pytest on FILES, using ARGS. 270 | 271 | When run interactively, this allows for interactive file selection. 272 | 273 | Additional ARGS are passed along to pytest. 274 | With a prefix argument, allow editing." 275 | (interactive 276 | (list 277 | (python-pytest--select-test-files :type 'file) 278 | (transient-args 'python-pytest-dispatch))) 279 | (setq args (-concat args (-map 'python-pytest--shell-quote files))) 280 | (python-pytest--run 281 | :args args 282 | :edit current-prefix-arg)) 283 | 284 | ;;;###autoload 285 | (defun python-pytest-directories (directories &optional args) 286 | "Run pytest on DIRECTORIES, using ARGS. 287 | 288 | When run interactively, this allows for interactive directory selection. 289 | 290 | Additional ARGS are passed along to pytest. 291 | With a prefix argument, allow editing." 292 | (interactive 293 | (list 294 | (python-pytest--select-test-files :type 'directory) 295 | (transient-args 'python-pytest-dispatch))) 296 | (setq args (-concat args (-map 'python-pytest--shell-quote directories))) 297 | (python-pytest--run 298 | :args args 299 | :edit current-prefix-arg)) 300 | 301 | ;;;###autoload 302 | (defun python-pytest-run-def-at-point-treesit () 303 | "Run def at point." 304 | (interactive) 305 | (python-pytest--run 306 | :args (transient-args 'python-pytest-dispatch) 307 | :file (buffer-file-name) 308 | :node-id (python-pytest--node-id-def-at-point-treesit) 309 | :edit current-prefix-arg)) 310 | 311 | ;;;###autoload 312 | (defun python-pytest-run-class-at-point-treesit () 313 | "Run class at point." 314 | (interactive) 315 | (python-pytest--run 316 | :args (transient-args 'python-pytest-dispatch) 317 | :file (buffer-file-name) 318 | :node-id (python-pytest--node-id-class-at-point-treesit) 319 | :edit current-prefix-arg)) 320 | 321 | ;;;###autoload 322 | (defun python-pytest-run-def-or-class-at-point (file func args) 323 | "Run pytest on FILE with FUNC (or class). 324 | 325 | Additional ARGS are passed along to pytest. 326 | With a prefix argument, allow editing." 327 | (interactive 328 | (list 329 | (buffer-file-name) 330 | (python-pytest--node-id-def-or-class-at-point) 331 | (transient-args 'python-pytest-dispatch))) 332 | (python-pytest--run 333 | :args args 334 | :file file 335 | :node-id func 336 | :edit current-prefix-arg)) 337 | 338 | ;;;###autoload 339 | (defun python-pytest-run-def-or-class-at-point-dwim (file func args) 340 | "Run pytest on FILE using FUNC at point as the node-id. 341 | 342 | If `python-pytest--test-file-p' returns t for FILE (i.e. the file 343 | is a test file), then this function results in the same behavior 344 | as calling `python-pytest-run-def-at-point'. If 345 | `python-pytest--test-file-p' returns nil for FILE (i.e. the 346 | current file is not a test file), then this function will try to 347 | find related test files and test defs (i.e. sensible match) for 348 | the current file and the def at point. 349 | 350 | Additional ARGS are passed along to pytest. 351 | With a prefix argument, allow editing." 352 | (interactive 353 | (list 354 | (buffer-file-name) 355 | (python-pytest--node-id-def-or-class-at-point) 356 | (transient-args 'python-pytest-dispatch))) 357 | (unless (python-pytest--test-file-p file) 358 | (setq 359 | file (python-pytest--sensible-test-file file) 360 | func (python-pytest--make-test-name func)) 361 | (unless python-pytest-strict-test-name-matching 362 | (let ((k-option (-first (-partial #'s-prefix-p "-k") args))) 363 | (when k-option 364 | ;; try to use the existing ‘-k’ option in a sensible way 365 | (setq args (-remove-item k-option args) 366 | k-option (--> 367 | k-option 368 | (s-chop-prefix "-k" it) 369 | (s-trim it) 370 | (if (s-contains-p " " it) (format "(%s)" it) it)))) 371 | (setq args (-snoc 372 | args 373 | (python-pytest--shell-quote file) 374 | (if k-option 375 | (format "-k %s and %s" func k-option) 376 | (format "-k %s" func))) 377 | file nil 378 | func nil)))) 379 | (python-pytest--run 380 | :args args 381 | :file file 382 | :node-id func 383 | :edit current-prefix-arg)) 384 | 385 | ;;;###autoload 386 | (defun python-pytest-last-failed (&optional args) 387 | "Run pytest, only executing previous test failures. 388 | 389 | Additional ARGS are passed along to pytest. 390 | With a prefix argument, allow editing." 391 | (interactive (list (transient-args 'python-pytest-dispatch))) 392 | (python-pytest--run 393 | :args (-snoc args "--last-failed") 394 | :edit current-prefix-arg)) 395 | 396 | ;;;###autoload 397 | (defun python-pytest-repeat () 398 | "Run pytest with the same argument as the most recent invocation. 399 | 400 | With a prefix ARG, allow editing." 401 | (interactive) 402 | (let ((command (gethash 403 | (python-pytest--project-root) 404 | python-pytest--project-last-command))) 405 | (when python-pytest--current-command 406 | ;; existing python-pytest-mode buffer; reuse command 407 | (setq command python-pytest--current-command)) 408 | (unless command 409 | (user-error "No previous pytest run for this project")) 410 | (python-pytest--run-command 411 | :command command 412 | :edit current-prefix-arg))) 413 | 414 | 415 | ;; internal helpers 416 | 417 | (define-derived-mode python-pytest-mode 418 | comint-mode "pytest" 419 | "Major mode for pytest sessions (derived from comint-mode)." 420 | (compilation-setup)) 421 | 422 | (defvar python-pytest-finished-mode-map 423 | (let ((map (make-sparse-keymap))) 424 | (set-keymap-parent map compilation-minor-mode-map) 425 | (define-key map [remap recompile] #'python-pytest-repeat) 426 | map) 427 | "Keymap for `python-pytest-mode' major mode.") 428 | 429 | (cl-defun python-pytest--run (&key args file node-id edit) 430 | "Run pytest for the given arguments. 431 | 432 | NODE-ID should be the node id of the test to run. pytest uses 433 | double colon \"::\" for separating components in node ids. For 434 | example, the node-id for a function outside a class is the 435 | function name, the node-id for a function inside a class is 436 | TestClass::test_my_function, the node-id for a function inside a 437 | class that is inside another class is 438 | TestClassParent::TestClassChild::test_my_function." 439 | (setq args (python-pytest--transform-arguments args)) 440 | (when (and file (file-name-absolute-p file)) 441 | (setq file (python-pytest--relative-file-name file))) 442 | (let ((command) 443 | (thing (cond 444 | ((and file node-id) (format "%s::%s" file node-id)) 445 | (file file)))) 446 | (when thing 447 | (setq args (-snoc args (python-pytest--shell-quote thing)))) 448 | (setq args (cons python-pytest-executable args) 449 | command (s-join " " args)) 450 | (python-pytest--run-command 451 | :command command 452 | :edit edit))) 453 | 454 | (cl-defun python-pytest--run-command (&key command edit) 455 | "Run a pytest command line." 456 | (python-pytest--maybe-save-buffers) 457 | (let* ((default-directory (python-pytest--project-root))) 458 | (when python-pytest-confirm 459 | (setq edit (not edit))) 460 | (when edit 461 | (setq command 462 | (read-shell-command 463 | "Command: " 464 | command 'python-pytest--history))) 465 | (add-to-history 'python-pytest--history command) 466 | (setq python-pytest--history (-uniq python-pytest--history)) 467 | (puthash (python-pytest--project-root) command 468 | python-pytest--project-last-command) 469 | (python-pytest--run-as-comint :command command))) 470 | 471 | (cl-defun python-pytest--run-as-comint (&key command) 472 | "Run a pytest comint session for COMMAND." 473 | (let* ((buffer (python-pytest--get-buffer)) 474 | (process (get-buffer-process buffer))) 475 | (with-current-buffer buffer 476 | (display-buffer buffer) 477 | (when (comint-check-proc buffer) 478 | (unless (or compilation-always-kill 479 | (yes-or-no-p "Kill running pytest process?")) 480 | (user-error "Aborting; pytest still running"))) 481 | (when process 482 | (delete-process process)) 483 | (let ((inhibit-read-only t)) 484 | (erase-buffer)) 485 | (unless (eq major-mode 'python-pytest-mode) 486 | (python-pytest-mode)) 487 | (compilation-forget-errors) 488 | (insert (format "cwd: %s\ncmd: %s\n\n" default-directory command)) 489 | (setq python-pytest--current-command command) 490 | (when python-pytest-pdb-track 491 | (add-hook 492 | 'comint-output-filter-functions 493 | 'python-pdbtrack-comint-output-filter-function 494 | nil t)) 495 | (run-hooks 'python-pytest-setup-hook) 496 | (make-comint-in-buffer 497 | "pytest" buffer 498 | python-pytest-shell-executable 499 | nil "-c" command) 500 | (run-hooks 'python-pytest-started-hook) 501 | (setq process (get-buffer-process buffer)) 502 | (set-process-sentinel process #'python-pytest--process-sentinel)))) 503 | 504 | (defun python-pytest--use-treesit-p () 505 | "Return t if python-pytest-use-treesit is t. Otherwise, return nil. 506 | 507 | This function is passed to the parameter :if in 508 | `python-pytest-dispatch'. 509 | 510 | Although this function might look useless, the main reason why it 511 | was defined was that the parameter that is provided to the 512 | transient keyword :if must be a function." 513 | python-pytest-use-treesit) 514 | 515 | (defun python-pytest--shell-quote (s) 516 | "Quote S for use in a shell command. Like `shell-quote-argument', but prettier." 517 | (if (s-equals-p s (shell-quote-argument s)) 518 | s 519 | (format "'%s'" (s-replace "'" "'\"'\"'" s)))) 520 | 521 | (defun python-pytest--get-buffer () 522 | "Get a create a suitable compilation buffer." 523 | (if (eq major-mode 'python-pytest-mode) 524 | (current-buffer) ;; re-use buffer 525 | (let ((name python-pytest-buffer-name)) 526 | (when python-pytest-project-name-in-buffer-name 527 | (setq name (format "%s<%s>" name (python-pytest--project-name)))) 528 | (get-buffer-create name)))) 529 | 530 | (defun python-pytest--process-sentinel (proc _state) 531 | "Process sentinel helper to run hooks after PROC finishes." 532 | (with-current-buffer (process-buffer proc) 533 | (compilation-mode) 534 | (read-only-mode -1) ;; required for python-pytest-repeat 535 | (use-local-map python-pytest-finished-mode-map) 536 | (run-hooks 'python-pytest-finished-hook))) 537 | 538 | (defun python-pytest--transform-arguments (args) 539 | "Transform ARGS so that pytest understands them." 540 | (--> 541 | args 542 | (python-pytest--switch-to-option it "--color" "--color=yes" "--color=no"))) 543 | 544 | (defun python-pytest--switch-to-option (args name on-replacement off-replacement) 545 | "Look in ARGS for switch NAME and turn it into option with a value. 546 | 547 | When present ON-REPLACEMENT is substituted, else OFF-REPLACEMENT is appended." 548 | (if (-contains-p args name) 549 | (-replace name on-replacement args) 550 | (-snoc args off-replacement))) 551 | 552 | (defun python-pytest--quote-string-option (args option) 553 | "Quote all values in ARGS with the prefix OPTION as shell strings." 554 | (--map-when 555 | (s-prefix-p option it) 556 | (let ((s it)) 557 | (--> s 558 | (substring it (length option)) 559 | (s-trim it) 560 | (python-pytest--shell-quote it) 561 | (format "%s %s" option it))) 562 | args)) 563 | 564 | (defun python-pytest--read-quoted-argument-for-short-flag (prompt initial-input history) 565 | "Read a quoted string for use as a argument after a short-form command line flag." 566 | (let* ((input (read-from-minibuffer prompt initial-input nil nil history)) 567 | (quoted-input (python-pytest--shell-quote input)) 568 | (formatted-input (format " %s" quoted-input))) 569 | formatted-input)) 570 | 571 | (transient-define-argument python-pytest:--l () 572 | :description "set log cli level" 573 | :class 'transient-option 574 | :key "--l" 575 | :argument "--log-cli-level=" 576 | :choices '("debug" "info" "warning" "error" "critical")) 577 | 578 | (transient-define-argument python-pytest:-k () 579 | :description "only names matching expression" 580 | :class 'transient-option 581 | :argument "-k" 582 | :allow-empty nil 583 | :key "-k" 584 | :reader 'python-pytest--read-quoted-argument-for-short-flag) 585 | 586 | (transient-define-argument python-pytest:-m () 587 | :description "only marks matching expression" 588 | :class 'transient-option 589 | :argument "-m" 590 | :allow-empty nil 591 | :key "-m" 592 | :reader 'python-pytest--read-quoted-argument-for-short-flag) 593 | 594 | (transient-define-argument python-pytest:-v () 595 | :description "verbosity" 596 | :class 'transient-switches 597 | :key "-v" 598 | :argument-format "%s" 599 | :argument-regexp "^\\(--verbose\\|--verbose --verbose\\)$" 600 | :choices '("--verbose" "--verbose --verbose")) 601 | 602 | (transient-define-argument python-pytest:-W () 603 | :description "warnings" 604 | :class 'transient-option 605 | :key "-W" 606 | :argument "-W " 607 | :choices '("default" "error" "always" "module" "once" "ignore")) 608 | 609 | (transient-define-argument python-pytest:--tb () 610 | :description "traceback style" 611 | :class 'transient-option 612 | :key "--tb" 613 | :argument "--tb=" 614 | :choices '("long" "short" "line" "native" "no")) 615 | 616 | (transient-define-argument python-pytest:-n () 617 | :description "number of processes" 618 | :class 'transient-option 619 | :key "-n" 620 | :argument "--numprocesses=" 621 | :choices '("auto" "0" "1" "2" "4" "8" "16")) 622 | 623 | (defun python-pytest--using-projectile () 624 | "Returns t if projectile being used for project management." 625 | (or (eq python-pytest-preferred-project-manager 'projectile) 626 | (and (eq python-pytest-preferred-project-manager 'auto) 627 | (bound-and-true-p projectile-mode)))) 628 | 629 | ;; python helpers 630 | 631 | (defun python-pytest--point-is-inside-def-treesit () 632 | (unless (treesit-language-available-p 'python) 633 | (error "This function requires tree-sitter support for python, but it is not available.")) 634 | (save-restriction 635 | (widen) 636 | (catch 'return 637 | (let ((current-node (treesit-node-at (point) 'python))) 638 | (while (setq current-node (treesit-node-parent current-node)) 639 | (when (equal (treesit-node-type current-node) "function_definition") 640 | (throw 'return t))))))) 641 | 642 | (defun python-pytest--point-is-inside-class-treesit () 643 | (unless (treesit-language-available-p 'python) 644 | (error "This function requires tree-sitter support for python, but it is not available.")) 645 | (save-restriction 646 | (widen) 647 | (catch 'return 648 | (let ((current-node (treesit-node-at (point) 'python))) 649 | (while (setq current-node (treesit-node-parent current-node)) 650 | (when (equal (treesit-node-type current-node) "class_definition") 651 | (throw 'return t))))))) 652 | 653 | (defun python-pytest--node-id-def-at-point-treesit () 654 | "Return the node id of the def at point. 655 | 656 | + If the test function is not inside a class, its node id is the name 657 | of the function. 658 | + If the test function is defined inside a class, its node id would 659 | look like: TestGroup::test_my_function. 660 | + If the test function is defined inside a class that is defined 661 | inside another class, its node id would look like: 662 | TestGroupParent::TestGroupChild::test_my_function." 663 | (unless (python-pytest--point-is-inside-def-treesit) 664 | (error "The point is not inside a def.")) 665 | (save-restriction 666 | (widen) 667 | (let ((function 668 | ;; Move up to the outermost function 669 | (catch 'return 670 | (let ((current-node (treesit-node-at (point) 'python)) 671 | function-node) 672 | (catch 'break 673 | (while (setq current-node (treesit-node-parent current-node)) 674 | (when (equal (treesit-node-type current-node) "function_definition") 675 | (setq function-node current-node) 676 | ;; At this point, we know that we are on a 677 | ;; function. We need to move up to see if the 678 | ;; function is inside a function. If that's the 679 | ;; case, we move up. This way, we find the 680 | ;; outermost function. We need to do this because 681 | ;; pytest can't execute functions inside functions, 682 | ;; so we must get the function that is not inside 683 | ;; other function. 684 | (while (setq current-node (treesit-node-parent current-node)) 685 | (when (equal (treesit-node-type current-node) "function_definition") 686 | (setq function-node current-node))) 687 | (throw 'break nil)))) 688 | (dolist (child (treesit-node-children function-node)) 689 | (when (equal (treesit-node-type child) "identifier") 690 | (throw 'return 691 | (cons 692 | ;; Keep a reference to the node that is a 693 | ;; function_definition. We need this 694 | ;; reference because afterwards we need to 695 | ;; move up starting at the current node to 696 | ;; find the node id of the class (if there's 697 | ;; any) in which the function is defined. 698 | function-node 699 | (buffer-substring-no-properties 700 | (treesit-node-start child) 701 | (treesit-node-end child))))))))) 702 | parents) 703 | ;; Move up through the parent nodes to see if the function is 704 | ;; defined inside a class and collect the classes to finally build 705 | ;; the node id of the current function. Remember that the node id 706 | ;; of a function that is defined within nested classes must have 707 | ;; the name of the nested classes. 708 | (let ((current-node (car function))) 709 | (while (setq current-node (treesit-node-parent current-node)) 710 | (when (equal (treesit-node-type current-node) "class_definition") 711 | (dolist (child (treesit-node-children current-node)) 712 | (when (equal (treesit-node-type child) "identifier") 713 | (push (buffer-substring-no-properties 714 | (treesit-node-start child) 715 | (treesit-node-end child)) 716 | parents)))))) 717 | (string-join `(,@parents ,(cdr function)) "::")))) 718 | 719 | (defun python-pytest--node-id-class-at-point-treesit () 720 | "Return the node id of the class at point. 721 | 722 | + If the class is not inside another class, its node id is the name 723 | of the class. 724 | + If the class is defined inside another class, the node id of the 725 | class which is contained would be: TestGroupParent::TestGroupChild, 726 | while the node id of the class which contains the other class would 727 | be TestGroupParent." 728 | (unless (python-pytest--point-is-inside-class-treesit) 729 | (error "The point is not inside a class.")) 730 | (save-restriction 731 | (widen) 732 | (let ((class 733 | ;; Move up to the outermost function 734 | (catch 'return 735 | (let ((current-node (treesit-node-at (point) 'python))) 736 | (catch 'break 737 | (while (setq current-node (treesit-node-parent current-node)) 738 | (when (equal (treesit-node-type current-node) "class_definition") 739 | (throw 'break nil)))) 740 | (dolist (child (treesit-node-children current-node)) 741 | (when (equal (treesit-node-type child) "identifier") 742 | (throw 'return 743 | (cons 744 | ;; Keep a reference to the node that is a 745 | ;; function_definition 746 | current-node 747 | (buffer-substring-no-properties 748 | (treesit-node-start child) 749 | (treesit-node-end child))))))))) 750 | parents) 751 | ;; Move up through the parents to collect the list of classes in 752 | ;; which the class is contained. pytest supports running nested 753 | ;; classes, but it doesn't support runing nested functions. 754 | (let ((current-node (car class))) 755 | (while (setq current-node (treesit-node-parent current-node)) 756 | (when (equal (treesit-node-type current-node) "class_definition") 757 | (dolist (child (treesit-node-children current-node)) 758 | (when (equal (treesit-node-type child) "identifier") 759 | (push (buffer-substring-no-properties 760 | (treesit-node-start child) 761 | (treesit-node-end child)) 762 | parents)))))) 763 | (string-join `(,@parents ,(cdr class)) "::")))) 764 | (defun python-pytest--node-id-def-or-class-at-point () 765 | "Detect the current function/class (if any)." 766 | (let* ((name 767 | (or (python-info-current-defun) 768 | (save-excursion 769 | ;; As a fallback, jumping seems to make it work on empty lines. 770 | (python-nav-beginning-of-defun) 771 | (python-nav-forward-statement) 772 | (python-info-current-defun)) 773 | (user-error "No class/function found"))) 774 | (name 775 | ;; Keep at most two parts, e.g. MyClass.do_something 776 | (s-join "." (-slice (s-split-up-to "\\." name 2) 0 2))) 777 | (name 778 | ;; If the first part starts with a lowercase letter, it is likely 779 | ;; a function, not a class. Keep the first part and discard 780 | ;; nested function names or nested class names, if any. 781 | (if (s-lowercase? (substring name 0 1)) 782 | (car (s-split-up-to "\\." name 1)) 783 | name))) 784 | (s-replace "." "::" name))) 785 | 786 | (defun python-pytest--make-test-name (func) 787 | "Turn function name FUNC into a name (hopefully) matching its test name. 788 | 789 | Example: ‘MyABCThingy.__repr__’ becomes ‘test_my_abc_thingy_repr’." 790 | (--> 791 | func 792 | (s-replace "." "_" it) 793 | (s-snake-case it) 794 | (s-replace-regexp "_\+" "_" it) 795 | (s-chop-suffix "_" it) 796 | (s-chop-prefix "_" it) 797 | (format "test_%s" it))) 798 | 799 | 800 | ;; file/directory helpers 801 | 802 | (defun python-pytest--project-name () 803 | "Find the project name." 804 | (cond 805 | (python-pytest-project-name-override) 806 | ((python-pytest--using-projectile) 807 | (projectile-project-name)) 808 | ((fboundp 'project-name) 809 | (project-name (project-current))) 810 | ;; older emacs... 811 | (t (file-name-nondirectory 812 | (directory-file-name (car (project-roots (project-current)))))))) 813 | 814 | (defun python-pytest--project-root () 815 | "Find the project root directory. 816 | The return value can manually set your own `python-pytest-project-root-override' 817 | variable." 818 | (cond 819 | (python-pytest-project-root-override) 820 | ((python-pytest--using-projectile) 821 | (let ((projectile-require-project-root nil)) 822 | (projectile-compilation-dir))) 823 | ((fboundp 'project-root) 824 | (project-root (project-current))) 825 | ;; pre-emacs "28.1" 826 | (t (car (project-roots (project-current)))))) 827 | 828 | (defun python-pytest--relative-file-name (file) 829 | "Make FILE relative to the project root." 830 | ;; Note: setting default-directory gives different results 831 | ;; than providing a second argument to file-relative-name. 832 | (let ((default-directory (python-pytest--project-root))) 833 | (file-relative-name file))) 834 | 835 | (defun python-pytest--test-file-p (file) 836 | "Tell whether FILE is a test file." 837 | (if (python-pytest--using-projectile) 838 | (projectile-test-file-p file) 839 | (let ((base-name (file-name-nondirectory file))) 840 | (or (string-prefix-p "test_" base-name) 841 | (string-suffix-p "_test.py" base-name))))) 842 | 843 | (defun python-pytest--find-test-file (file) 844 | "Find a test file associated to FILE, if any." 845 | (let ((test-file)) 846 | (if (python-pytest--using-projectile) 847 | (setq test-file (projectile-find-matching-test file)) 848 | (let* ((base-name (file-name-sans-extension (file-name-nondirectory file))) 849 | (test-file-regex (concat "\\`test_" 850 | base-name "\\.py\\'\\|\\`" 851 | base-name "_test\\.py\\'"))) 852 | (setq test-file 853 | (car (cl-delete-if 854 | (lambda (full-file) 855 | (let ((file (file-name-nondirectory full-file))) 856 | (not (string-match-p 857 | test-file-regex 858 | file)))) 859 | (project-files (project-current t))))))) 860 | (unless test-file 861 | (user-error "No test file found")) 862 | test-file)) 863 | 864 | (defun python-pytest--sensible-test-file (file) 865 | "Return a sensible test file name for FILE." 866 | (if (python-pytest--test-file-p file) 867 | (python-pytest--relative-file-name file) 868 | (python-pytest--find-test-file file))) 869 | 870 | (cl-defun python-pytest--select-test-files (&key type) 871 | "Interactively choose test files." 872 | (let* ((test-files 873 | (if (python-pytest--using-projectile) 874 | (->> (projectile-project-files (python-pytest--project-root)) 875 | (-sort 'string<) 876 | (projectile-sort-by-recentf-first) 877 | ;; show test files if any found, otherwise show everything 878 | (funcall (-orfn #'projectile-test-files #'identity))) 879 | (let* ((vc-directory-exclusion-list 880 | (append vc-directory-exclusion-list '("venv" ".venv"))) 881 | (sorted-test-files 882 | (sort (cl-delete-if 883 | (lambda (file) 884 | (not (python-pytest--test-file-p file))) 885 | (project-files (project-current t))) 886 | #'string<)) 887 | (recentf-test-files '()) 888 | (test-files-prj 889 | (when (fboundp 'recentf) 890 | (dolist (file recentf-list 891 | (progn 892 | (setq sorted-test-files 893 | (append (nreverse recentf-test-files) 894 | sorted-test-files)) 895 | (cl-delete-duplicates sorted-test-files 896 | :test 'equal ))) 897 | (when (and (file-exists-p file) 898 | (python-pytest--test-file-p file)) 899 | (push (expand-file-name file) recentf-test-files)))))) 900 | test-files-prj))) 901 | (test-directories 902 | (->> test-files 903 | (-map 'file-name-directory) 904 | (-uniq) 905 | (-sort 'string<))) 906 | (candidates (if (eq type 'file) test-files test-directories)) 907 | (prompt (if (eq type 'file) "Choose test files: " "Choose test directories: "))) 908 | (unless candidates 909 | (user-error "No test files or directories found")) 910 | (completing-read-multiple prompt candidates nil t))) 911 | 912 | (defun python-pytest--maybe-save-buffers () 913 | "Maybe save modified buffers." 914 | (cond 915 | ((memq python-pytest-unsaved-buffers-behavior '(ask-current save-current)) 916 | ;; check only current buffer 917 | (when (and (buffer-modified-p) 918 | (or (eq python-pytest-unsaved-buffers-behavior 'save-current) 919 | (y-or-n-p 920 | (format "Save modified buffer (%s)? " (buffer-name))))) 921 | (save-buffer))) 922 | ((memq python-pytest-unsaved-buffers-behavior '(ask-all save-all)) 923 | ;; check all project buffers 924 | (-when-let* 925 | ((buffers 926 | (if (python-pytest--using-projectile) 927 | (projectile-buffers-with-file (projectile-project-buffers)) 928 | (-filter 'buffer-file-name (project-buffers (project-current t))))) 929 | (modified-buffers 930 | (-filter 'buffer-modified-p buffers)) 931 | (confirmed 932 | (or (eq python-pytest-unsaved-buffers-behavior 'save-all) 933 | (y-or-n-p 934 | (format "Save modified project buffers (%d)? " 935 | (length modified-buffers)))))) 936 | (--each modified-buffers 937 | (with-current-buffer it 938 | (save-buffer))))) 939 | (t nil))) 940 | 941 | 942 | ;; third party integration 943 | 944 | (with-eval-after-load 'direnv 945 | (defvar direnv-non-file-modes) 946 | (add-to-list 'direnv-non-file-modes 'python-pytest-mode)) 947 | 948 | 949 | (provide 'python-pytest) 950 | ;;; python-pytest.el ends here 951 | --------------------------------------------------------------------------------