├── LICENSE.rst ├── README.rst ├── python-pytest.el └── tests ├── README.org └── test-python-helpers.el /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 | -------------------------------------------------------------------------------- /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-setup-hook nil 56 | "Hooks to run before a pytest process starts." 57 | :group 'python-pytest 58 | :type 'hook) 59 | 60 | (defcustom python-pytest-started-hook nil 61 | "Hooks to run after a pytest process starts." 62 | :group 'python-pytest 63 | :type 'hook) 64 | 65 | (defcustom python-pytest-finished-hook nil 66 | "Hooks to run after a pytest process finishes." 67 | :group 'python-pytest 68 | :type 'hook) 69 | 70 | (defcustom python-pytest-buffer-name "*pytest*" 71 | "Name of the pytest output buffer." 72 | :group 'python-pytest 73 | :type 'string) 74 | 75 | (defcustom python-pytest-project-name-in-buffer-name t 76 | "Whether to include the project name in the buffer name. 77 | 78 | This is useful when working on multiple projects simultaneously." 79 | :group 'python-pytest 80 | :type 'boolean) 81 | 82 | (defcustom python-pytest-pdb-track t 83 | "Whether to automatically track output when pdb is spawned. 84 | 85 | This results in automatically opening source files during debugging." 86 | :group 'python-pytest 87 | :type 'boolean) 88 | 89 | (defcustom python-pytest-strict-test-name-matching nil 90 | "Whether to require a strict match for the ‘test this function’ heuristic. 91 | 92 | This influences the ‘test this function’ behaviour when editing a 93 | non-test function, e.g. ‘foo()’. 94 | 95 | When nil (the default), the current function name will be used as 96 | a pattern to run the corresponding tests, which will match 97 | ‘test_foo()’ as well as ‘test_foo_xyz()’. 98 | 99 | When non-nil only ‘test_foo()’ will match, and nothing else." 100 | :group 'python-pytest 101 | :type 'boolean) 102 | 103 | (defcustom python-pytest-unsaved-buffers-behavior 'ask-all 104 | "Whether to ask whether unsaved buffers should be saved before running pytest." 105 | :group 'python-pytest 106 | :type '(choice (const :tag "Ask for all project buffers" ask-all) 107 | (const :tag "Ask for current buffer" ask-current) 108 | (const :tag "Save all project buffers" save-all) 109 | (const :tag "Save current buffer" save-current) 110 | (const :tag "Ignore" nil))) 111 | 112 | (defcustom python-pytest-preferred-project-manager 'auto 113 | "Override `projectile' or `project' auto-discovery to set preference if using both." 114 | :group 'python-pytest 115 | :type '(choice (const :tag "Projectile" projectile) 116 | (const :tag "Project" project) 117 | (const :tag "Automatically selected" auto)) 118 | :set (lambda (symbol value) 119 | (cond 120 | ((and (eq value 'projectile) 121 | (not (featurep 'projectile))) 122 | (user-error "Projectile preferred for python-pytest.el, but not available.")) 123 | ((and (eq value 'project) 124 | (not (fboundp 'project-root))) 125 | (user-error (concat "Project.el preferred for python-pytest.el, " 126 | "but need a newer version of Project (28.1+) to use."))) 127 | (t 128 | (set-default symbol value) 129 | value)))) 130 | 131 | (defcustom python-pytest-use-treesit (featurep 'treesit) 132 | "Whether to use treesit for getting the node ids of things at point. 133 | 134 | Users that are running a version of Emacs that supports treesit 135 | and have the Python language grammar for treesit should set this 136 | variable to t. Users that are running a version of Emacs that 137 | don't support treesit should set this variable to nil." 138 | :group 'python-pytest 139 | :type 'boolean) 140 | 141 | (defvar python-pytest--history nil 142 | "History for pytest invocations.") 143 | 144 | (defvar python-pytest--project-last-command (make-hash-table :test 'equal) 145 | "Last executed command lines, per project.") 146 | 147 | (defvar-local python-pytest--current-command nil 148 | "Current command; used in python-pytest-mode buffers.") 149 | 150 | ;;;###autoload (autoload 'python-pytest-dispatch "python-pytest" nil t) 151 | (transient-define-prefix python-pytest-dispatch () 152 | "Show popup for running pytest." 153 | :man-page "pytest" 154 | :incompatible '(("--exitfirst" "--maxfail=")) 155 | :value '("--color") 156 | ["Output" 157 | [("-c" "color" "--color") 158 | ("-q" "quiet" "--quiet") 159 | ("-s" "no output capture" "--capture=no") 160 | (python-pytest:-v) 161 | (python-pytest:--l)]] 162 | ["Selection, filtering, ordering" 163 | [(python-pytest:-k) 164 | (python-pytest:-m) 165 | " "] ;; visual alignment 166 | [("--dm" "run doctests" "--doctest-modules") 167 | ("--nf" "new first" "--new-first") 168 | ("--sw" "stepwise" "--stepwise") 169 | ("--co" "collect only" "--collect-only")]] 170 | ["Failures, errors, warnings, debugging" 171 | [("-l" "show locals" "--showlocals") 172 | ("-p" "debug on error" "--pdb") 173 | ("-x" "exit after first failure" "--exitfirst") 174 | (python-pytest:-W)] 175 | [("--ff" "failed first" "--failed-first") 176 | ("--ft" "full tracebacks" "--full-trace") 177 | ("--mf" "exit after N failures or errors" "--maxfail=") 178 | ("--rx" "run xfail tests" "--runxfail") 179 | (python-pytest:--tb) 180 | ("--tr" "debug on each test" "--trace")]] 181 | ["Options for pytest-xdist" 182 | [(python-pytest:-n)] 183 | [("-f" "loop on failure" "--looponfail")]] 184 | ["Run tests" 185 | [("t" "all" python-pytest)] 186 | [("r" "repeat" python-pytest-repeat) 187 | ("x" "last failed" python-pytest-last-failed)] 188 | [("f" "file (dwim)" python-pytest-file-dwim) 189 | ("F" "file (this)" python-pytest-file)] 190 | [("m" "files" python-pytest-files) 191 | ("M" "directories" python-pytest-directories)] 192 | [("d" "def at point (dwim)" python-pytest-run-def-or-class-at-point-dwim :if-not python-pytest--use-treesit-p) 193 | ("D" "def at point" python-pytest-run-def-or-class-at-point :if-not python-pytest--use-treesit-p) 194 | ("d" "def at point" python-pytest-run-def-at-point-treesit :if python-pytest--use-treesit-p) 195 | ("c" "class at point" python-pytest-run-class-at-point-treesit :if python-pytest--use-treesit-p)]]) 196 | 197 | (define-obsolete-function-alias 'python-pytest-popup 'python-pytest-dispatch "2.0.0") 198 | 199 | ;;;###autoload 200 | (defun python-pytest (&optional args) 201 | "Run pytest with ARGS. 202 | 203 | With a prefix argument, allow editing." 204 | (interactive (list (transient-args 'python-pytest-dispatch))) 205 | (python-pytest--run 206 | :args args 207 | :edit current-prefix-arg)) 208 | 209 | ;;;###autoload 210 | (defun python-pytest-file (file &optional args) 211 | "Run pytest on FILE, using ARGS. 212 | 213 | Additional ARGS are passed along to pytest. 214 | With a prefix argument, allow editing." 215 | (interactive 216 | (list 217 | (buffer-file-name) 218 | (transient-args 'python-pytest-dispatch))) 219 | (python-pytest--run 220 | :args args 221 | :file file 222 | :edit current-prefix-arg)) 223 | 224 | ;;;###autoload 225 | (defun python-pytest-file-dwim (file &optional args) 226 | "Run pytest on FILE, intelligently finding associated test modules. 227 | 228 | When run interactively, this tries to work sensibly using 229 | the current file. 230 | 231 | Additional ARGS are passed along to pytest. 232 | With a prefix argument, allow editing." 233 | (interactive 234 | (list 235 | (buffer-file-name) 236 | (transient-args 'python-pytest-dispatch))) 237 | (python-pytest-file (python-pytest--sensible-test-file file) args)) 238 | 239 | ;;;###autoload 240 | (defun python-pytest-files (files &optional args) 241 | "Run pytest on FILES, using ARGS. 242 | 243 | When run interactively, this allows for interactive file selection. 244 | 245 | Additional ARGS are passed along to pytest. 246 | With a prefix argument, allow editing." 247 | (interactive 248 | (list 249 | (python-pytest--select-test-files :type 'file) 250 | (transient-args 'python-pytest-dispatch))) 251 | (setq args (-concat args (-map 'python-pytest--shell-quote files))) 252 | (python-pytest--run 253 | :args args 254 | :edit current-prefix-arg)) 255 | 256 | ;;;###autoload 257 | (defun python-pytest-directories (directories &optional args) 258 | "Run pytest on DIRECTORIES, using ARGS. 259 | 260 | When run interactively, this allows for interactive directory selection. 261 | 262 | Additional ARGS are passed along to pytest. 263 | With a prefix argument, allow editing." 264 | (interactive 265 | (list 266 | (python-pytest--select-test-files :type 'directory) 267 | (transient-args 'python-pytest-dispatch))) 268 | (setq args (-concat args (-map 'python-pytest--shell-quote directories))) 269 | (python-pytest--run 270 | :args args 271 | :edit current-prefix-arg)) 272 | 273 | ;;;###autoload 274 | (defun python-pytest-run-def-at-point-treesit () 275 | "Run def at point." 276 | (interactive) 277 | (python-pytest--run 278 | :args (transient-args 'python-pytest-dispatch) 279 | :file (buffer-file-name) 280 | :node-id (python-pytest--node-id-def-at-point-treesit) 281 | :edit current-prefix-arg)) 282 | 283 | ;;;###autoload 284 | (defun python-pytest-run-class-at-point-treesit () 285 | "Run class at point." 286 | (interactive) 287 | (python-pytest--run 288 | :args (transient-args 'python-pytest-dispatch) 289 | :file (buffer-file-name) 290 | :node-id (python-pytest--node-id-class-at-point-treesit) 291 | :edit current-prefix-arg)) 292 | 293 | ;;;###autoload 294 | (defun python-pytest-run-def-or-class-at-point (file func args) 295 | "Run pytest on FILE with FUNC (or class). 296 | 297 | Additional ARGS are passed along to pytest. 298 | With a prefix argument, allow editing." 299 | (interactive 300 | (list 301 | (buffer-file-name) 302 | (python-pytest--node-id-def-or-class-at-point) 303 | (transient-args 'python-pytest-dispatch))) 304 | (python-pytest--run 305 | :args args 306 | :file file 307 | :node-id func 308 | :edit current-prefix-arg)) 309 | 310 | ;;;###autoload 311 | (defun python-pytest-run-def-or-class-at-point-dwim (file func args) 312 | "Run pytest on FILE using FUNC at point as the node-id. 313 | 314 | If `python-pytest--test-file-p' returns t for FILE (i.e. the file 315 | is a test file), then this function results in the same behavior 316 | as calling `python-pytest-run-def-at-point'. If 317 | `python-pytest--test-file-p' returns nil for FILE (i.e. the 318 | current file is not a test file), then this function will try to 319 | find related test files and test defs (i.e. sensible match) for 320 | the current file and the def at point. 321 | 322 | Additional ARGS are passed along to pytest. 323 | With a prefix argument, allow editing." 324 | (interactive 325 | (list 326 | (buffer-file-name) 327 | (python-pytest--node-id-def-or-class-at-point) 328 | (transient-args 'python-pytest-dispatch))) 329 | (unless (python-pytest--test-file-p file) 330 | (setq 331 | file (python-pytest--sensible-test-file file) 332 | func (python-pytest--make-test-name func)) 333 | (unless python-pytest-strict-test-name-matching 334 | (let ((k-option (-first (-partial #'s-prefix-p "-k") args))) 335 | (when k-option 336 | ;; try to use the existing ‘-k’ option in a sensible way 337 | (setq args (-remove-item k-option args) 338 | k-option (--> 339 | k-option 340 | (s-chop-prefix "-k" it) 341 | (s-trim it) 342 | (if (s-contains-p " " it) (format "(%s)" it) it)))) 343 | (setq args (-snoc 344 | args 345 | (python-pytest--shell-quote file) 346 | (if k-option 347 | (format "-k %s and %s" func k-option) 348 | (format "-k %s" func))) 349 | file nil 350 | func nil)))) 351 | (python-pytest--run 352 | :args args 353 | :file file 354 | :node-id func 355 | :edit current-prefix-arg)) 356 | 357 | ;;;###autoload 358 | (defun python-pytest-last-failed (&optional args) 359 | "Run pytest, only executing previous test failures. 360 | 361 | Additional ARGS are passed along to pytest. 362 | With a prefix argument, allow editing." 363 | (interactive (list (transient-args 'python-pytest-dispatch))) 364 | (python-pytest--run 365 | :args (-snoc args "--last-failed") 366 | :edit current-prefix-arg)) 367 | 368 | ;;;###autoload 369 | (defun python-pytest-repeat () 370 | "Run pytest with the same argument as the most recent invocation. 371 | 372 | With a prefix ARG, allow editing." 373 | (interactive) 374 | (let ((command (gethash 375 | (python-pytest--project-root) 376 | python-pytest--project-last-command))) 377 | (when python-pytest--current-command 378 | ;; existing python-pytest-mode buffer; reuse command 379 | (setq command python-pytest--current-command)) 380 | (unless command 381 | (user-error "No previous pytest run for this project")) 382 | (python-pytest--run-command 383 | :command command 384 | :edit current-prefix-arg))) 385 | 386 | 387 | ;; internal helpers 388 | 389 | (define-derived-mode python-pytest-mode 390 | comint-mode "pytest" 391 | "Major mode for pytest sessions (derived from comint-mode)." 392 | (compilation-setup)) 393 | 394 | (defvar python-pytest-finished-mode-map 395 | (let ((map (make-sparse-keymap))) 396 | (set-keymap-parent map compilation-minor-mode-map) 397 | (define-key map [remap recompile] #'python-pytest-repeat) 398 | map) 399 | "Keymap for `python-pytest-mode' major mode.") 400 | 401 | (cl-defun python-pytest--run (&key args file node-id edit) 402 | "Run pytest for the given arguments. 403 | 404 | NODE-ID should be the node id of the test to run. pytest uses 405 | double colon \"::\" for separating components in node ids. For 406 | example, the node-id for a function outside a class is the 407 | function name, the node-id for a function inside a class is 408 | TestClass::test_my_function, the node-id for a function inside a 409 | class that is inside another class is 410 | TestClassParent::TestClassChild::test_my_function." 411 | (setq args (python-pytest--transform-arguments args)) 412 | (when (and file (file-name-absolute-p file)) 413 | (setq file (python-pytest--relative-file-name file))) 414 | (let ((command) 415 | (thing (cond 416 | ((and file node-id) (format "%s::%s" file node-id)) 417 | (file file)))) 418 | (when thing 419 | (setq args (-snoc args (python-pytest--shell-quote thing)))) 420 | (setq args (cons python-pytest-executable args) 421 | command (s-join " " args)) 422 | (python-pytest--run-command 423 | :command command 424 | :edit edit))) 425 | 426 | (cl-defun python-pytest--run-command (&key command edit) 427 | "Run a pytest command line." 428 | (python-pytest--maybe-save-buffers) 429 | (let* ((default-directory (python-pytest--project-root))) 430 | (when python-pytest-confirm 431 | (setq edit (not edit))) 432 | (when edit 433 | (setq command 434 | (read-shell-command 435 | "Command: " 436 | command 'python-pytest--history))) 437 | (add-to-history 'python-pytest--history command) 438 | (setq python-pytest--history (-uniq python-pytest--history)) 439 | (puthash (python-pytest--project-root) command 440 | python-pytest--project-last-command) 441 | (python-pytest--run-as-comint :command command))) 442 | 443 | (cl-defun python-pytest--run-as-comint (&key command) 444 | "Run a pytest comint session for COMMAND." 445 | (let* ((buffer (python-pytest--get-buffer)) 446 | (process (get-buffer-process buffer))) 447 | (with-current-buffer buffer 448 | (display-buffer buffer) 449 | (when (comint-check-proc buffer) 450 | (unless (or compilation-always-kill 451 | (yes-or-no-p "Kill running pytest process?")) 452 | (user-error "Aborting; pytest still running"))) 453 | (when process 454 | (delete-process process)) 455 | (let ((inhibit-read-only t)) 456 | (erase-buffer)) 457 | (unless (eq major-mode 'python-pytest-mode) 458 | (python-pytest-mode)) 459 | (compilation-forget-errors) 460 | (insert (format "cwd: %s\ncmd: %s\n\n" default-directory command)) 461 | (setq python-pytest--current-command command) 462 | (when python-pytest-pdb-track 463 | (add-hook 464 | 'comint-output-filter-functions 465 | 'python-pdbtrack-comint-output-filter-function 466 | nil t)) 467 | (run-hooks 'python-pytest-setup-hook) 468 | (make-comint-in-buffer 469 | "pytest" buffer 470 | (if (eq system-type 'windows-nt) "cmdproxy" "sh") 471 | nil "-c" command) 472 | (run-hooks 'python-pytest-started-hook) 473 | (setq process (get-buffer-process buffer)) 474 | (set-process-sentinel process #'python-pytest--process-sentinel)))) 475 | 476 | (defun python-pytest--use-treesit-p () 477 | "Return t if python-pytest-use-treesit is t. Otherwise, return nil. 478 | 479 | This function is passed to the parameter :if in 480 | `python-pytest-dispatch'. 481 | 482 | Although this function might look useless, the main reason why it 483 | was defined was that the parameter that is provided to the 484 | transient keyword :if must be a function." 485 | python-pytest-use-treesit) 486 | 487 | (defun python-pytest--shell-quote (s) 488 | "Quote S for use in a shell command. Like `shell-quote-argument', but prettier." 489 | (if (s-equals-p s (shell-quote-argument s)) 490 | s 491 | (format "'%s'" (s-replace "'" "'\"'\"'" s)))) 492 | 493 | (defun python-pytest--get-buffer () 494 | "Get a create a suitable compilation buffer." 495 | (if (eq major-mode 'python-pytest-mode) 496 | (current-buffer) ;; re-use buffer 497 | (let ((name python-pytest-buffer-name)) 498 | (when python-pytest-project-name-in-buffer-name 499 | (setq name (format "%s<%s>" name (python-pytest--project-name)))) 500 | (get-buffer-create name)))) 501 | 502 | (defun python-pytest--process-sentinel (proc _state) 503 | "Process sentinel helper to run hooks after PROC finishes." 504 | (with-current-buffer (process-buffer proc) 505 | (compilation-mode) 506 | (read-only-mode -1) ;; required for python-pytest-repeat 507 | (use-local-map python-pytest-finished-mode-map) 508 | (run-hooks 'python-pytest-finished-hook))) 509 | 510 | (defun python-pytest--transform-arguments (args) 511 | "Transform ARGS so that pytest understands them." 512 | (--> 513 | args 514 | (python-pytest--switch-to-option it "--color" "--color=yes" "--color=no"))) 515 | 516 | (defun python-pytest--switch-to-option (args name on-replacement off-replacement) 517 | "Look in ARGS for switch NAME and turn it into option with a value. 518 | 519 | When present ON-REPLACEMENT is substituted, else OFF-REPLACEMENT is appended." 520 | (if (-contains-p args name) 521 | (-replace name on-replacement args) 522 | (-snoc args off-replacement))) 523 | 524 | (defun python-pytest--quote-string-option (args option) 525 | "Quote all values in ARGS with the prefix OPTION as shell strings." 526 | (--map-when 527 | (s-prefix-p option it) 528 | (let ((s it)) 529 | (--> s 530 | (substring it (length option)) 531 | (s-trim it) 532 | (python-pytest--shell-quote it) 533 | (format "%s %s" option it))) 534 | args)) 535 | 536 | (defun python-pytest--read-quoted-argument-for-short-flag (prompt initial-input history) 537 | "Read a quoted string for use as a argument after a short-form command line flag." 538 | (let* ((input (read-from-minibuffer prompt initial-input nil nil history)) 539 | (quoted-input (python-pytest--shell-quote input)) 540 | (formatted-input (format " %s" quoted-input))) 541 | formatted-input)) 542 | 543 | (transient-define-argument python-pytest:--l () 544 | :description "set log cli level" 545 | :class 'transient-option 546 | :key "--l" 547 | :argument "--log-cli-level=" 548 | :choices '("debug" "info" "warning" "error" "critical")) 549 | 550 | (transient-define-argument python-pytest:-k () 551 | :description "only names matching expression" 552 | :class 'transient-option 553 | :argument "-k" 554 | :allow-empty nil 555 | :key "-k" 556 | :reader 'python-pytest--read-quoted-argument-for-short-flag) 557 | 558 | (transient-define-argument python-pytest:-m () 559 | :description "only marks matching expression" 560 | :class 'transient-option 561 | :argument "-m" 562 | :allow-empty nil 563 | :key "-m" 564 | :reader 'python-pytest--read-quoted-argument-for-short-flag) 565 | 566 | (transient-define-argument python-pytest:-v () 567 | :description "verbosity" 568 | :class 'transient-switches 569 | :key "-v" 570 | :argument-format "%s" 571 | :argument-regexp "^\\(--verbose\\|--verbose --verbose\\)$" 572 | :choices '("--verbose" "--verbose --verbose")) 573 | 574 | (transient-define-argument python-pytest:-W () 575 | :description "warnings" 576 | :class 'transient-option 577 | :key "-W" 578 | :argument "-W " 579 | :choices '("default" "error" "always" "module" "once" "ignore")) 580 | 581 | (transient-define-argument python-pytest:--tb () 582 | :description "traceback style" 583 | :class 'transient-option 584 | :key "--tb" 585 | :argument "--tb=" 586 | :choices '("long" "short" "line" "native" "no")) 587 | 588 | (transient-define-argument python-pytest:-n () 589 | :description "number of processes" 590 | :class 'transient-option 591 | :key "-n" 592 | :argument "--numprocesses=" 593 | :choices '("auto" "0" "1" "2" "4" "8" "16")) 594 | 595 | (defun python-pytest--using-projectile () 596 | "Returns t if projectile being used for project management." 597 | (or (eq python-pytest-preferred-project-manager 'projectile) 598 | (and (eq python-pytest-preferred-project-manager 'auto) 599 | (bound-and-true-p projectile-mode)))) 600 | 601 | ;; python helpers 602 | 603 | (defun python-pytest--point-is-inside-def-treesit () 604 | (unless (treesit-language-available-p 'python) 605 | (error "This function requires tree-sitter support for python, but it is not available.")) 606 | (save-restriction 607 | (widen) 608 | (catch 'return 609 | (let ((current-node (treesit-node-at (point) 'python))) 610 | (while (setq current-node (treesit-node-parent current-node)) 611 | (when (equal (treesit-node-type current-node) "function_definition") 612 | (throw 'return t))))))) 613 | 614 | (defun python-pytest--point-is-inside-class-treesit () 615 | (unless (treesit-language-available-p 'python) 616 | (error "This function requires tree-sitter support for python, but it is not available.")) 617 | (save-restriction 618 | (widen) 619 | (catch 'return 620 | (let ((current-node (treesit-node-at (point) 'python))) 621 | (while (setq current-node (treesit-node-parent current-node)) 622 | (when (equal (treesit-node-type current-node) "class_definition") 623 | (throw 'return t))))))) 624 | 625 | (defun python-pytest--node-id-def-at-point-treesit () 626 | "Return the node id of the def at point. 627 | 628 | + If the test function is not inside a class, its node id is the name 629 | of the function. 630 | + If the test function is defined inside a class, its node id would 631 | look like: TestGroup::test_my_function. 632 | + If the test function is defined inside a class that is defined 633 | inside another class, its node id would look like: 634 | TestGroupParent::TestGroupChild::test_my_function." 635 | (unless (python-pytest--point-is-inside-def-treesit) 636 | (error "The point is not inside a def.")) 637 | (save-restriction 638 | (widen) 639 | (let ((function 640 | ;; Move up to the outermost function 641 | (catch 'return 642 | (let ((current-node (treesit-node-at (point) 'python)) 643 | function-node) 644 | (catch 'break 645 | (while (setq current-node (treesit-node-parent current-node)) 646 | (when (equal (treesit-node-type current-node) "function_definition") 647 | (setq function-node current-node) 648 | ;; At this point, we know that we are on a 649 | ;; function. We need to move up to see if the 650 | ;; function is inside a function. If that's the 651 | ;; case, we move up. This way, we find the 652 | ;; outermost function. We need to do this because 653 | ;; pytest can't execute functions inside functions, 654 | ;; so we must get the function that is not inside 655 | ;; other function. 656 | (while (setq current-node (treesit-node-parent current-node)) 657 | (when (equal (treesit-node-type current-node) "function_definition") 658 | (setq function-node current-node))) 659 | (throw 'break nil)))) 660 | (dolist (child (treesit-node-children function-node)) 661 | (when (equal (treesit-node-type child) "identifier") 662 | (throw 'return 663 | (cons 664 | ;; Keep a reference to the node that is a 665 | ;; function_definition. We need this 666 | ;; reference because afterwards we need to 667 | ;; move up starting at the current node to 668 | ;; find the node id of the class (if there's 669 | ;; any) in which the function is defined. 670 | function-node 671 | (buffer-substring-no-properties 672 | (treesit-node-start child) 673 | (treesit-node-end child))))))))) 674 | parents) 675 | ;; Move up through the parent nodes to see if the function is 676 | ;; defined inside a class and collect the classes to finally build 677 | ;; the node id of the current function. Remember that the node id 678 | ;; of a function that is defined within nested classes must have 679 | ;; the name of the nested classes. 680 | (let ((current-node (car function))) 681 | (while (setq current-node (treesit-node-parent current-node)) 682 | (when (equal (treesit-node-type current-node) "class_definition") 683 | (dolist (child (treesit-node-children current-node)) 684 | (when (equal (treesit-node-type child) "identifier") 685 | (push (buffer-substring-no-properties 686 | (treesit-node-start child) 687 | (treesit-node-end child)) 688 | parents)))))) 689 | (string-join `(,@parents ,(cdr function)) "::")))) 690 | 691 | (defun python-pytest--node-id-class-at-point-treesit () 692 | "Return the node id of the class at point. 693 | 694 | + If the class is not inside another class, its node id is the name 695 | of the class. 696 | + If the class is defined inside another class, the node id of the 697 | class which is contained would be: TestGroupParent::TestGroupChild, 698 | while the node id of the class which contains the other class would 699 | be TestGroupParent." 700 | (unless (python-pytest--point-is-inside-class-treesit) 701 | (error "The point is not inside a class.")) 702 | (save-restriction 703 | (widen) 704 | (let ((class 705 | ;; Move up to the outermost function 706 | (catch 'return 707 | (let ((current-node (treesit-node-at (point) 'python))) 708 | (catch 'break 709 | (while (setq current-node (treesit-node-parent current-node)) 710 | (when (equal (treesit-node-type current-node) "class_definition") 711 | (throw 'break nil)))) 712 | (dolist (child (treesit-node-children current-node)) 713 | (when (equal (treesit-node-type child) "identifier") 714 | (throw 'return 715 | (cons 716 | ;; Keep a reference to the node that is a 717 | ;; function_definition 718 | current-node 719 | (buffer-substring-no-properties 720 | (treesit-node-start child) 721 | (treesit-node-end child))))))))) 722 | parents) 723 | ;; Move up through the parents to collect the list of classes in 724 | ;; which the class is contained. pytest supports running nested 725 | ;; classes, but it doesn't support runing nested functions. 726 | (let ((current-node (car class))) 727 | (while (setq current-node (treesit-node-parent current-node)) 728 | (when (equal (treesit-node-type current-node) "class_definition") 729 | (dolist (child (treesit-node-children current-node)) 730 | (when (equal (treesit-node-type child) "identifier") 731 | (push (buffer-substring-no-properties 732 | (treesit-node-start child) 733 | (treesit-node-end child)) 734 | parents)))))) 735 | (string-join `(,@parents ,(cdr class)) "::")))) 736 | (defun python-pytest--node-id-def-or-class-at-point () 737 | "Detect the current function/class (if any)." 738 | (let* ((name 739 | (or (python-info-current-defun) 740 | (save-excursion 741 | ;; As a fallback, jumping seems to make it work on empty lines. 742 | (python-nav-beginning-of-defun) 743 | (python-nav-forward-statement) 744 | (python-info-current-defun)) 745 | (user-error "No class/function found"))) 746 | (name 747 | ;; Keep at most two parts, e.g. MyClass.do_something 748 | (s-join "." (-slice (s-split-up-to "\\." name 2) 0 2))) 749 | (name 750 | ;; If the first part starts with a lowercase letter, it is likely 751 | ;; a function, not a class. Keep the first part and discard 752 | ;; nested function names or nested class names, if any. 753 | (if (s-lowercase? (substring name 0 1)) 754 | (car (s-split-up-to "\\." name 1)) 755 | name))) 756 | (s-replace "." "::" name))) 757 | 758 | (defun python-pytest--make-test-name (func) 759 | "Turn function name FUNC into a name (hopefully) matching its test name. 760 | 761 | Example: ‘MyABCThingy.__repr__’ becomes ‘test_my_abc_thingy_repr’." 762 | (--> 763 | func 764 | (s-replace "." "_" it) 765 | (s-snake-case it) 766 | (s-replace-regexp "_\+" "_" it) 767 | (s-chop-suffix "_" it) 768 | (s-chop-prefix "_" it) 769 | (format "test_%s" it))) 770 | 771 | 772 | ;; file/directory helpers 773 | 774 | (defun python-pytest--project-name () 775 | "Find the project name." 776 | (if (python-pytest--using-projectile) 777 | (projectile-project-name) 778 | (if (fboundp 'project-name) 779 | (project-name (project-current)) 780 | ;; older emacs... 781 | (file-name-nondirectory 782 | (directory-file-name (car (project-roots (project-current)))))))) 783 | 784 | (defun python-pytest--project-root () 785 | "Find the project root directory, for project.el can manually set your own 786 | `project-compilation-dir' variable to override `project-root' being used." 787 | (if (python-pytest--using-projectile) 788 | (let ((projectile-require-project-root nil)) 789 | (projectile-compilation-dir)) 790 | (or (and (bound-and-true-p project-compilation-dir) 791 | project-compilation-dir) 792 | (if (fboundp 'project-root) 793 | (project-root (project-current)) 794 | ;; pre-emacs "28.1" 795 | (car (project-roots (project-current))))))) 796 | 797 | (defun python-pytest--relative-file-name (file) 798 | "Make FILE relative to the project root." 799 | ;; Note: setting default-directory gives different results 800 | ;; than providing a second argument to file-relative-name. 801 | (let ((default-directory (python-pytest--project-root))) 802 | (file-relative-name file))) 803 | 804 | (defun python-pytest--test-file-p (file) 805 | "Tell whether FILE is a test file." 806 | (if (python-pytest--using-projectile) 807 | (projectile-test-file-p file) 808 | (let ((base-name (file-name-nondirectory file))) 809 | (or (string-prefix-p "test_" base-name) 810 | (string-suffix-p "_test.py" base-name))))) 811 | 812 | (defun python-pytest--find-test-file (file) 813 | "Find a test file associated to FILE, if any." 814 | (let ((test-file)) 815 | (if (python-pytest--using-projectile) 816 | (setq test-file (projectile-find-matching-test file)) 817 | (let* ((base-name (file-name-sans-extension (file-name-nondirectory file))) 818 | (test-file-regex (concat "\\`test_" 819 | base-name "\\.py\\'\\|\\`" 820 | base-name "_test\\.py\\'"))) 821 | (setq test-file 822 | (car (cl-delete-if 823 | (lambda (full-file) 824 | (let ((file (file-name-nondirectory full-file))) 825 | (not (string-match-p 826 | test-file-regex 827 | file)))) 828 | (project-files (project-current t))))))) 829 | (unless test-file 830 | (user-error "No test file found")) 831 | test-file)) 832 | 833 | (defun python-pytest--sensible-test-file (file) 834 | "Return a sensible test file name for FILE." 835 | (if (python-pytest--test-file-p file) 836 | (python-pytest--relative-file-name file) 837 | (python-pytest--find-test-file file))) 838 | 839 | (cl-defun python-pytest--select-test-files (&key type) 840 | "Interactively choose test files." 841 | (let* ((test-files 842 | (if (python-pytest--using-projectile) 843 | (->> (projectile-project-files (python-pytest--project-root)) 844 | (-sort 'string<) 845 | (projectile-sort-by-recentf-first) 846 | ;; show test files if any found, otherwise show everything 847 | (funcall (-orfn #'projectile-test-files #'identity))) 848 | (let* ((vc-directory-exclusion-list 849 | (append vc-directory-exclusion-list '("venv" ".venv"))) 850 | (sorted-test-files 851 | (sort (cl-delete-if 852 | (lambda (file) 853 | (not (python-pytest--test-file-p file))) 854 | (project-files (project-current t))) 855 | #'string<)) 856 | (recentf-test-files '()) 857 | (test-files-prj 858 | (when (fboundp 'recentf) 859 | (dolist (file recentf-list 860 | (progn 861 | (setq sorted-test-files 862 | (append (nreverse recentf-test-files) 863 | sorted-test-files)) 864 | (cl-delete-duplicates sorted-test-files 865 | :test 'equal ))) 866 | (when (and (file-exists-p file) 867 | (python-pytest--test-file-p file)) 868 | (push (expand-file-name file) recentf-test-files)))))) 869 | test-files-prj))) 870 | (test-directories 871 | (->> test-files 872 | (-map 'file-name-directory) 873 | (-uniq) 874 | (-sort 'string<))) 875 | (candidates (if (eq type 'file) test-files test-directories)) 876 | (prompt (if (eq type 'file) "Choose test files: " "Choose test directories: "))) 877 | (unless candidates 878 | (user-error "No test files or directories found")) 879 | (completing-read-multiple prompt candidates nil t))) 880 | 881 | (defun python-pytest--maybe-save-buffers () 882 | "Maybe save modified buffers." 883 | (cond 884 | ((memq python-pytest-unsaved-buffers-behavior '(ask-current save-current)) 885 | ;; check only current buffer 886 | (when (and (buffer-modified-p) 887 | (or (eq python-pytest-unsaved-buffers-behavior 'save-current) 888 | (y-or-n-p 889 | (format "Save modified buffer (%s)? " (buffer-name))))) 890 | (save-buffer))) 891 | ((memq python-pytest-unsaved-buffers-behavior '(ask-all save-all)) 892 | ;; check all project buffers 893 | (-when-let* 894 | ((buffers 895 | (if (python-pytest--using-projectile) 896 | (projectile-buffers-with-file (projectile-project-buffers)) 897 | (-filter 'buffer-file-name (project-buffers (project-current t))))) 898 | (modified-buffers 899 | (-filter 'buffer-modified-p buffers)) 900 | (confirmed 901 | (or (eq python-pytest-unsaved-buffers-behavior 'save-all) 902 | (y-or-n-p 903 | (format "Save modified project buffers (%d)? " 904 | (length modified-buffers)))))) 905 | (--each modified-buffers 906 | (with-current-buffer it 907 | (save-buffer))))) 908 | (t nil))) 909 | 910 | 911 | ;; third party integration 912 | 913 | (with-eval-after-load 'direnv 914 | (defvar direnv-non-file-modes) 915 | (add-to-list 'direnv-non-file-modes 'python-pytest-mode)) 916 | 917 | 918 | (provide 'python-pytest) 919 | ;;; python-pytest.el ends here 920 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------