From a138b34e573c54f6ff9ff7145a08556d05d8f7ff Mon Sep 17 00:00:00 2001 From: Rodrigo Morales Date: Mon, 12 Aug 2024 13:59:20 -0500 Subject: [PATCH 01/20] rename parameter "func" with "node-id" in function python-pytest--run We rename the parameter to improve the readability of the code because the function python-pytest--run will be used for executing specific functions and classes in future commits and each function or class is identified by their node id. The term "node id" is used in the official pytest documentation: https://docs.pytest.org/en/7.1.x/how-to/usage.html#nodeids --- python-pytest.el | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/python-pytest.el b/python-pytest.el index 11b11a9..b8b7f99 100644 --- a/python-pytest.el +++ b/python-pytest.el @@ -271,7 +271,7 @@ With a prefix argument, allow editing." (python-pytest--run :args args :file file - :func func + :node-id func :edit current-prefix-arg)) ;;;###autoload @@ -313,7 +313,7 @@ With a prefix argument, allow editing." (python-pytest--run :args args :file file - :func func + :node-id func :edit current-prefix-arg)) ;;;###autoload @@ -360,16 +360,22 @@ With a prefix ARG, allow editing." map) "Keymap for `python-pytest-mode' major mode.") -(cl-defun python-pytest--run (&key args file func edit) - "Run pytest for the given arguments." +(cl-defun python-pytest--run (&key args file node-id edit) + "Run pytest for the given arguments. + +NODE-ID should be the node id of the test to run. pytest uses +double colon \"::\" for separating components in node ids. For +example, the node-id for a function outside a class is the +function name, the node-id for a function inside a class is +TestClass::test_my_function, the node-id for a function inside a +class that is inside another class is +TestClassParent::TestClassChild::test_my_function." (setq args (python-pytest--transform-arguments args)) (when (and file (file-name-absolute-p file)) (setq file (python-pytest--relative-file-name file))) - (when func - (setq func (s-replace "." "::" func))) (let ((command) (thing (cond - ((and file func) (format "%s::%s" file func)) + ((and file node-id) (format "%s::%s" file node-id)) (file file)))) (when thing (setq args (-snoc args (python-pytest--shell-quote thing)))) @@ -565,7 +571,7 @@ When present ON-REPLACEMENT is substituted, else OFF-REPLACEMENT is appended." (if (s-lowercase? (substring name 0 1)) (car (s-split-up-to "\\." name 1)) name))) - name)) + (s-replace "." "::" name))) (defun python-pytest--make-test-name (func) "Turn function name FUNC into a name (hopefully) matching its test name. From c283ae78e37b2e45a4010ec3c82d55976076045b Mon Sep 17 00:00:00 2001 From: Rodrigo Morales Date: Mon, 12 Aug 2024 14:23:04 -0500 Subject: [PATCH 02/20] Add functions python-pytest-run-(def|class)-at-point and related helpers --- python-pytest.el | 128 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 127 insertions(+), 1 deletion(-) diff --git a/python-pytest.el b/python-pytest.el index b8b7f99..a02a4f1 100644 --- a/python-pytest.el +++ b/python-pytest.el @@ -179,7 +179,8 @@ When non-nil only ‘test_foo()’ will match, and nothing else." [("m" "files" python-pytest-files) ("M" "directories" python-pytest-directories)] [("d" "def/class (dwim)" python-pytest-function-dwim) - ("D" "def/class (this)" python-pytest-function)]]) + ("D" "def at point" python-pytest-run-def-at-point) + ("c" "class at point" python-pytest-run-class-at-point)]]) (define-obsolete-function-alias 'python-pytest-popup 'python-pytest-dispatch "2.0.0") @@ -257,6 +258,26 @@ With a prefix argument, allow editing." :args args :edit current-prefix-arg)) +;;;###autoload +(defun python-pytest-run-def-at-point () + "Run def at point." + (interactive) + (python-pytest--run + :args (transient-args 'python-pytest-dispatch) + :file (buffer-file-name) + :node-id (python-pytest--path-def-at-point) + :edit current-prefix-arg)) + +;;;###autoload +(defun python-pytest-run-class-at-point () + "Run class at point." + (interactive) + (python-pytest--run + :args (transient-args 'python-pytest-dispatch) + :file (buffer-file-name) + :node-id (python-pytest--path-class-at-point) + :edit current-prefix-arg)) + ;;;###autoload (defun python-pytest-function (file func args) "Run pytest on FILE with FUNC (or class). @@ -551,6 +572,111 @@ When present ON-REPLACEMENT is substituted, else OFF-REPLACEMENT is appended." ;; python helpers +(defun python-pytest--point-is-inside-def () + (unless (treesit-language-available-p 'python) + (error "This function requires tree-sitter support for python, but it is not available.")) + (catch 'return + (let ((current-node (treesit-node-at (point) 'python))) + (while (setq current-node (treesit-node-parent current-node)) + (when (equal (treesit-node-type current-node) "function_definition") + (throw 'return t)))))) + +(defun python-pytest--point-is-inside-class () + (unless (treesit-language-available-p 'python) + (error "This function requires tree-sitter support for python, but it is not available.")) + (catch 'return + (let ((current-node (treesit-node-at (point) 'python))) + (while (setq current-node (treesit-node-parent current-node)) + (when (equal (treesit-node-type current-node) "class_definition") + (throw 'return t)))))) + +(defun python-pytest--path-def-at-point () + (unless (python-pytest--point-is-inside-def) + (error "The point is not inside a def.")) + (let ((function + ;; Move up to the outermost function + (catch 'return + (let ((current-node (treesit-node-at (point) 'python)) + function-node) + (catch 'break + (while (setq current-node (treesit-node-parent current-node)) + (when (equal (treesit-node-type current-node) "function_definition") + (setq function-node current-node) + ;; At this point, we know that we are on a + ;; function. We need to move up to see if the + ;; function is inside a function. If that's the + ;; case, we move up. This way, we find the + ;; outermost function. We need to do this because + ;; pytest can't execute functions inside functions, + ;; so we must get the function that is not inside + ;; other function. + (while (setq current-node (treesit-node-parent current-node)) + (when (equal (treesit-node-type current-node) "function_definition") + (setq function-node current-node))) + (throw 'break nil)))) + (dolist (child (treesit-node-children function-node)) + (when (equal (treesit-node-type child) "identifier") + (throw 'return + (cons + ;; Keep a reference to the node that is a + ;; function_definition. We need this + ;; reference because we need to move up + ;; through the class in which the function is + ;; located to make up the entire path. + function-node + (buffer-substring-no-properties + (treesit-node-start child) + (treesit-node-end child))))))))) + parents) + ;; Move up through the parents to collect the chain of classes + ;; in which the function is contained. + (let ((current-node (car function))) + (while (setq current-node (treesit-node-parent current-node)) + (when (equal (treesit-node-type current-node) "class_definition") + (dolist (child (treesit-node-children current-node)) + (when (equal (treesit-node-type child) "identifier") + (push (buffer-substring-no-properties + (treesit-node-start child) + (treesit-node-end child)) + parents)))))) + (string-join `(,@parents ,(cdr function)) "::"))) + +(defun python-pytest--path-class-at-point () + (unless (python-pytest--point-is-inside-class) + (error "The point is not inside a class.")) + (let ((class + ;; Move up to the outermost function + (catch 'return + (let ((current-node (treesit-node-at (point) 'python))) + (catch 'break + (while (setq current-node (treesit-node-parent current-node)) + (when (equal (treesit-node-type current-node) "class_definition") + (throw 'break nil)))) + (dolist (child (treesit-node-children current-node)) + (when (equal (treesit-node-type child) "identifier") + (throw 'return + (cons + ;; Keep a reference to the node that is a + ;; function_definition + current-node + (buffer-substring-no-properties + (treesit-node-start child) + (treesit-node-end child))))))))) + parents) + ;; Move up through the parents to collect the list of classes in + ;; which the class is contained. pytest supports running nested + ;; classes, but it doesn't support runing nested functions. + (let ((current-node (car class))) + (while (setq current-node (treesit-node-parent current-node)) + (when (equal (treesit-node-type current-node) "class_definition") + (dolist (child (treesit-node-children current-node)) + (when (equal (treesit-node-type child) "identifier") + (push (buffer-substring-no-properties + (treesit-node-start child) + (treesit-node-end child)) + parents)))))) + (string-join `(,@parents ,(cdr class)) "::"))) + (defun python-pytest--current-defun () "Detect the current function/class (if any)." (let* ((name From 1725d8199116c2bbb38d72624f1c4f6b23528e4a Mon Sep 17 00:00:00 2001 From: Rodrigo Morales Date: Mon, 12 Aug 2024 14:23:29 -0500 Subject: [PATCH 03/20] Update version from 3.3.0 to 3.5.0 in docstring We need to create a new version because in future commits we will mark a function as obsolete since this new version. --- python-pytest.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python-pytest.el b/python-pytest.el index a02a4f1..846faf4 100644 --- a/python-pytest.el +++ b/python-pytest.el @@ -1,7 +1,7 @@ ;;; python-pytest.el --- helpers to run pytest -*- lexical-binding: t; -*- ;; Author: wouter bolsterlee -;; Version: 3.3.0 +;; Version: 3.5.0 ;; Package-Requires: ((emacs "24.4") (dash "2.18.0") (transient "0.3.7") (s "1.12.0")) ;; Keywords: pytest, test, python, languages, processes, tools ;; URL: https://github.com/wbolster/emacs-python-pytest From 702d52d7c41a4e0af9c4a7e3f7a047c6c9219dfd Mon Sep 17 00:00:00 2001 From: Rodrigo Morales Date: Mon, 12 Aug 2024 14:24:14 -0500 Subject: [PATCH 04/20] Declare python-pytest--current-defun obsolete The new functions that should be used python-pytest--path-def-at-point are python-pytest--path-class-at-point. python-pytest--current-defun could obtain the identifiers for functions and classes. If the current point was inside a function, there was no way to get the test id of the current class. By using python-pytest--path-def-at-point and python-pytest--path-class-at-point, there is no such limitation. --- python-pytest.el | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/python-pytest.el b/python-pytest.el index 846faf4..8cf7ba9 100644 --- a/python-pytest.el +++ b/python-pytest.el @@ -287,7 +287,7 @@ With a prefix argument, allow editing." (interactive (list (buffer-file-name) - (python-pytest--current-defun) + (python-pytest--path-def-at-point) (transient-args 'python-pytest-dispatch))) (python-pytest--run :args args @@ -307,7 +307,7 @@ With a prefix argument, allow editing." (interactive (list (buffer-file-name) - (python-pytest--current-defun) + (python-pytest--path-def-at-point) (transient-args 'python-pytest-dispatch))) (unless (python-pytest--test-file-p file) (setq @@ -679,6 +679,7 @@ When present ON-REPLACEMENT is substituted, else OFF-REPLACEMENT is appended." (defun python-pytest--current-defun () "Detect the current function/class (if any)." + (declare (obsolete 'python-pytest--path-def-at-point "python-pytest 3.5.0")) (let* ((name (or (python-info-current-defun) (save-excursion From 0a88630b8431bbcf8f5645c48606ea4ef421beb4 Mon Sep 17 00:00:00 2001 From: Rodrigo Morales Date: Mon, 12 Aug 2024 14:31:22 -0500 Subject: [PATCH 05/20] Declare python-pytest-function obsolete The new functions that should be used are python-pytest-run-def-at-point and python-pytest-run-class-at-point. --- python-pytest.el | 1 + 1 file changed, 1 insertion(+) diff --git a/python-pytest.el b/python-pytest.el index 8cf7ba9..ae84312 100644 --- a/python-pytest.el +++ b/python-pytest.el @@ -284,6 +284,7 @@ With a prefix argument, allow editing." Additional ARGS are passed along to pytest. With a prefix argument, allow editing." + (declare (obsolete 'python-pytest-run-def-at-point "python-pytest 3.5.0")) (interactive (list (buffer-file-name) From 885fb4acb0cc7856da669f7d14a86305dc325eb8 Mon Sep 17 00:00:00 2001 From: Rodrigo Morales Date: Mon, 12 Aug 2024 14:32:25 -0500 Subject: [PATCH 06/20] Rename function python-pytest-function-dwim and add info in docstring --- python-pytest.el | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/python-pytest.el b/python-pytest.el index ae84312..6f529d5 100644 --- a/python-pytest.el +++ b/python-pytest.el @@ -178,7 +178,7 @@ When non-nil only ‘test_foo()’ will match, and nothing else." ("F" "file (this)" python-pytest-file)] [("m" "files" python-pytest-files) ("M" "directories" python-pytest-directories)] - [("d" "def/class (dwim)" python-pytest-function-dwim) + [("d" "def at point (dwim)" python-pytest-run-def-at-point-dwim) ("D" "def at point" python-pytest-run-def-at-point) ("c" "class at point" python-pytest-run-class-at-point)]]) @@ -297,11 +297,16 @@ With a prefix argument, allow editing." :edit current-prefix-arg)) ;;;###autoload -(defun python-pytest-function-dwim (file func args) - "Run pytest on FILE with FUNC (or class). - -When run interactively, this tries to work sensibly using -the current file and function around point. +(defun python-pytest-run-def-at-point-dwim (file func args) + "Run pytest on FILE using FUNC at point as the node-id. + +If `python-pytest--test-file-p' returns t for FILE (i.e. the file +is a test file), then this function results in the same behavior +as calling `python-pytest-run-def-at-point'. If +`python-pytest--test-file-p' returns nil for FILE (i.e. the +current file is not a test file), then this function will try to +find related test files and test defs (i.e. sensible match) for +the current file and the def at point. Additional ARGS are passed along to pytest. With a prefix argument, allow editing." From 42cc3d9aa4033b304749dc1470da0d6e4d90d99b Mon Sep 17 00:00:00 2001 From: Rodrigo Morales Date: Mon, 12 Aug 2024 14:34:18 -0500 Subject: [PATCH 07/20] Add tests for some Python helper functions --- tests/README.org | 9 +++ tests/test-python-helpers.el | 134 +++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 tests/README.org create mode 100644 tests/test-python-helpers.el diff --git a/tests/README.org b/tests/README.org new file mode 100644 index 0000000..0bede2f --- /dev/null +++ b/tests/README.org @@ -0,0 +1,9 @@ +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.) + +#+BEGIN_SRC sh +emacs \ + --batch \ + --eval '(load-file "./python-pytest.el")' \ + --eval '(dolist (file (directory-files-recursively "tests" "\\`[^.].*\\.el\\'\''")) (load-file file))' \ + --eval '(ert-run-tests-batch-and-exit)' +#+END_SRC diff --git a/tests/test-python-helpers.el b/tests/test-python-helpers.el new file mode 100644 index 0000000..b1a249a --- /dev/null +++ b/tests/test-python-helpers.el @@ -0,0 +1,134 @@ +(defmacro pytest-test-with-temp-text (text &rest body) + (declare (indent 1) (debug t)) + `(let ((inside-text (if (stringp ,text) ,text (eval ,text)))) + (with-temp-buffer + (setq python-indent-offset 2 + python-indent-guess-indent-offset nil) + (python-mode) + (let ((point (string-match "" inside-text))) + (if point + (progn + (insert (replace-match "" nil nil inside-text)) + (goto-char (1+ (match-beginning 0)))) + (insert inside-text) + (goto-char (point-min)))) + (font-lock-ensure (point-min) (point-max)) + ,@body))) + +(ert-deftest get-current-def-outside-class () + (pytest-test-with-temp-text (concat + "def foo():\n" + " pass\n" + "def bar():\n" + " pass\n") + (should (equal (python-pytest--path-def-at-point) "foo")) + (forward-line 1) + (should (equal (python-pytest--path-def-at-point) "foo")) + (forward-line 1) + (should (equal (python-pytest--path-def-at-point) "bar")) + (forward-line 1) + (should (equal (python-pytest--path-def-at-point) "bar")))) + +(ert-deftest get-current-def-inside-class () + (pytest-test-with-temp-text (concat + "class TestGroup:\n" + " def foo():\n" + " pass\n" + " def bar():\n" + " pass\n") + (should (equal (python-pytest--path-def-at-point) "TestGroup::foo")) + (forward-line 1) + (should (equal (python-pytest--path-def-at-point) "TestGroup::foo")) + (forward-line 1) + (should (equal (python-pytest--path-def-at-point) "TestGroup::bar")) + (forward-line 1) + (should (equal (python-pytest--path-def-at-point) "TestGroup::bar")))) + +(ert-deftest get-current-def-inside-multiple-classes () + (pytest-test-with-temp-text (string-join + '("class TestDepthOne:" + " class TestDepthTwo:" + " class TestDepthThree:" + " def foo():" + " pass" + " def bar():" + " pass") + "\n") + (should (equal (python-pytest--path-def-at-point) "TestDepthOne::TestDepthTwo::TestDepthThree::foo")) + (forward-line 1) + (should (equal (python-pytest--path-def-at-point) "TestDepthOne::TestDepthTwo::TestDepthThree::foo")) + (forward-line 1) + (should (equal (python-pytest--path-def-at-point) "TestDepthOne::TestDepthTwo::TestDepthThree::bar")) + (forward-line 1) + (should (equal (python-pytest--path-def-at-point) "TestDepthOne::TestDepthTwo::TestDepthThree::bar")) + (forward-line 1)) + (pytest-test-with-temp-text (string-join + '("class TestDepthOne:" + " def test_depth_one():" + " pass" + " class TestDepthTwo:" + " def test_depth_two():" + " pass" + " class TestDepthThree:" + " def test_depth_three():" + " pass") + "\n") + (should (equal (python-pytest--path-def-at-point) "TestDepthOne::test_depth_one")) + (search-forward "test_depth_two") + (should (equal (python-pytest--path-def-at-point) "TestDepthOne::TestDepthTwo::test_depth_two")) + (search-forward "test_depth_three") + (should (equal (python-pytest--path-def-at-point) "TestDepthOne::TestDepthTwo::TestDepthThree::test_depth_three")))) + +(ert-deftest get-current-def-inside-def () + (pytest-test-with-temp-text (string-join + '("def foo():" + " def bar():" + " pass") + "\n") + (should (equal (python-pytest--path-def-at-point) "foo"))) + (pytest-test-with-temp-text (string-join + '("class TestDepthOne:" + " class TestDepthTwo:" + " class TestDepthThree:" + " def foo():" + " def bar():" + " pass") + "\n") + ;; We want to get the outermost def because pytest can't + ;; identify defs inside defs. In other words, pytest can + ;; only identify those defs that are not contained within + ;; other defs. + (should (equal (python-pytest--path-def-at-point) "TestDepthOne::TestDepthTwo::TestDepthThree::foo")))) + +(ert-deftest get-current-class-outside-class () + (pytest-test-with-temp-text (string-join + '("class Test:" + " def foo():" + " pass") + "\n") + (should (equal (python-pytest--path-class-at-point) "Test")))) + +(ert-deftest get-current-class-inside-class () + (pytest-test-with-temp-text (string-join + '("class TestDepthOne:" + " class TestDepthTwo:" + " def foo():" + " pass") + "\n") + (should (equal + (python-pytest--path-class-at-point) + "TestDepthOne::TestDepthTwo")))) + +(ert-deftest get-current-class-inside-multiple-classes () + (pytest-test-with-temp-text (string-join + '("class TestDepthOne:" + " class TestDepthTwo:" + " class TestDepthThree:" + " class TestDepthFour:" + " class TestDepthFive:" + " def foo():" + " pass") + "\n") + (should (equal + (python-pytest--path-class-at-point) + "TestDepthOne::TestDepthTwo::TestDepthThree::TestDepthFour::TestDepthFive")))) From c8fab0ac879d9737e208763e143f2630f241a5cd Mon Sep 17 00:00:00 2001 From: Rodrigo Morales Date: Mon, 12 Aug 2024 14:53:52 -0500 Subject: [PATCH 08/20] Add docstring to functions python-pytest--path-(def|class)-at-point --- python-pytest.el | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/python-pytest.el b/python-pytest.el index 6f529d5..243533f 100644 --- a/python-pytest.el +++ b/python-pytest.el @@ -597,6 +597,15 @@ When present ON-REPLACEMENT is substituted, else OFF-REPLACEMENT is appended." (throw 'return t)))))) (defun python-pytest--path-def-at-point () + "Return the node id of the def at point. + ++ If the test function is not inside a class, its node id is the name + of the function. ++ If the test function is defined inside a class, its node id would + look like: TestGroup::test_my_function. ++ If the test function is defined inside a class that is defined + inside another class, its node id would look like: + TestGroupParent::TestGroupChild::test_my_function." (unless (python-pytest--point-is-inside-def) (error "The point is not inside a def.")) (let ((function @@ -648,6 +657,14 @@ When present ON-REPLACEMENT is substituted, else OFF-REPLACEMENT is appended." (string-join `(,@parents ,(cdr function)) "::"))) (defun python-pytest--path-class-at-point () + "Return the node id of the class at point. + ++ If the class is not inside another class, its node id is the name + of the class. ++ If the class is defined inside another class, the node id of the + class which is contained would be: TestGroupParent::TestGroupChild, + while the node id of the class which contains the other class would + be TestGroupParent." (unless (python-pytest--point-is-inside-class) (error "The point is not inside a class.")) (let ((class From af9b22f73c58b9faf0205c8d7c932dd2c7c31ab8 Mon Sep 17 00:00:00 2001 From: Rodrigo Morales Date: Mon, 12 Aug 2024 15:02:00 -0500 Subject: [PATCH 09/20] Rename functions that have "path" in their names with "node-id" --- python-pytest.el | 28 ++++++++++++++----------- tests/test-python-helpers.el | 40 ++++++++++++++++++------------------ 2 files changed, 36 insertions(+), 32 deletions(-) diff --git a/python-pytest.el b/python-pytest.el index 243533f..4390c30 100644 --- a/python-pytest.el +++ b/python-pytest.el @@ -265,7 +265,7 @@ With a prefix argument, allow editing." (python-pytest--run :args (transient-args 'python-pytest-dispatch) :file (buffer-file-name) - :node-id (python-pytest--path-def-at-point) + :node-id (python-pytest--node-id-def-at-point) :edit current-prefix-arg)) ;;;###autoload @@ -275,7 +275,7 @@ With a prefix argument, allow editing." (python-pytest--run :args (transient-args 'python-pytest-dispatch) :file (buffer-file-name) - :node-id (python-pytest--path-class-at-point) + :node-id (python-pytest--node-id-class-at-point) :edit current-prefix-arg)) ;;;###autoload @@ -288,7 +288,7 @@ With a prefix argument, allow editing." (interactive (list (buffer-file-name) - (python-pytest--path-def-at-point) + (python-pytest--node-id-def-at-point) (transient-args 'python-pytest-dispatch))) (python-pytest--run :args args @@ -313,7 +313,7 @@ With a prefix argument, allow editing." (interactive (list (buffer-file-name) - (python-pytest--path-def-at-point) + (python-pytest--node-id-def-at-point) (transient-args 'python-pytest-dispatch))) (unless (python-pytest--test-file-p file) (setq @@ -596,7 +596,7 @@ When present ON-REPLACEMENT is substituted, else OFF-REPLACEMENT is appended." (when (equal (treesit-node-type current-node) "class_definition") (throw 'return t)))))) -(defun python-pytest--path-def-at-point () +(defun python-pytest--node-id-def-at-point () "Return the node id of the def at point. + If the test function is not inside a class, its node id is the name @@ -635,16 +635,20 @@ When present ON-REPLACEMENT is substituted, else OFF-REPLACEMENT is appended." (cons ;; Keep a reference to the node that is a ;; function_definition. We need this - ;; reference because we need to move up - ;; through the class in which the function is - ;; located to make up the entire path. + ;; reference because afterwards we need to + ;; move up starting at the current node to + ;; find the node id of the class (if there's + ;; any) in which the function is defined. function-node (buffer-substring-no-properties (treesit-node-start child) (treesit-node-end child))))))))) parents) - ;; Move up through the parents to collect the chain of classes - ;; in which the function is contained. + ;; Move up through the parent nodes to see if the function is + ;; defined inside a class and collect the classes to finally build + ;; the node id of the current function. Remember that the node id + ;; of a function that is defined within nested classes must have + ;; the name of the nested classes. (let ((current-node (car function))) (while (setq current-node (treesit-node-parent current-node)) (when (equal (treesit-node-type current-node) "class_definition") @@ -656,7 +660,7 @@ When present ON-REPLACEMENT is substituted, else OFF-REPLACEMENT is appended." parents)))))) (string-join `(,@parents ,(cdr function)) "::"))) -(defun python-pytest--path-class-at-point () +(defun python-pytest--node-id-class-at-point () "Return the node id of the class at point. + If the class is not inside another class, its node id is the name @@ -702,7 +706,7 @@ When present ON-REPLACEMENT is substituted, else OFF-REPLACEMENT is appended." (defun python-pytest--current-defun () "Detect the current function/class (if any)." - (declare (obsolete 'python-pytest--path-def-at-point "python-pytest 3.5.0")) + (declare (obsolete 'python-pytest--node-id-def-at-point "python-pytest 3.5.0")) (let* ((name (or (python-info-current-defun) (save-excursion diff --git a/tests/test-python-helpers.el b/tests/test-python-helpers.el index b1a249a..19fe76e 100644 --- a/tests/test-python-helpers.el +++ b/tests/test-python-helpers.el @@ -21,13 +21,13 @@ " pass\n" "def bar():\n" " pass\n") - (should (equal (python-pytest--path-def-at-point) "foo")) + (should (equal (python-pytest--node-id-def-at-point) "foo")) (forward-line 1) - (should (equal (python-pytest--path-def-at-point) "foo")) + (should (equal (python-pytest--node-id-def-at-point) "foo")) (forward-line 1) - (should (equal (python-pytest--path-def-at-point) "bar")) + (should (equal (python-pytest--node-id-def-at-point) "bar")) (forward-line 1) - (should (equal (python-pytest--path-def-at-point) "bar")))) + (should (equal (python-pytest--node-id-def-at-point) "bar")))) (ert-deftest get-current-def-inside-class () (pytest-test-with-temp-text (concat @@ -36,13 +36,13 @@ " pass\n" " def bar():\n" " pass\n") - (should (equal (python-pytest--path-def-at-point) "TestGroup::foo")) + (should (equal (python-pytest--node-id-def-at-point) "TestGroup::foo")) (forward-line 1) - (should (equal (python-pytest--path-def-at-point) "TestGroup::foo")) + (should (equal (python-pytest--node-id-def-at-point) "TestGroup::foo")) (forward-line 1) - (should (equal (python-pytest--path-def-at-point) "TestGroup::bar")) + (should (equal (python-pytest--node-id-def-at-point) "TestGroup::bar")) (forward-line 1) - (should (equal (python-pytest--path-def-at-point) "TestGroup::bar")))) + (should (equal (python-pytest--node-id-def-at-point) "TestGroup::bar")))) (ert-deftest get-current-def-inside-multiple-classes () (pytest-test-with-temp-text (string-join @@ -54,13 +54,13 @@ " def bar():" " pass") "\n") - (should (equal (python-pytest--path-def-at-point) "TestDepthOne::TestDepthTwo::TestDepthThree::foo")) + (should (equal (python-pytest--node-id-def-at-point) "TestDepthOne::TestDepthTwo::TestDepthThree::foo")) (forward-line 1) - (should (equal (python-pytest--path-def-at-point) "TestDepthOne::TestDepthTwo::TestDepthThree::foo")) + (should (equal (python-pytest--node-id-def-at-point) "TestDepthOne::TestDepthTwo::TestDepthThree::foo")) (forward-line 1) - (should (equal (python-pytest--path-def-at-point) "TestDepthOne::TestDepthTwo::TestDepthThree::bar")) + (should (equal (python-pytest--node-id-def-at-point) "TestDepthOne::TestDepthTwo::TestDepthThree::bar")) (forward-line 1) - (should (equal (python-pytest--path-def-at-point) "TestDepthOne::TestDepthTwo::TestDepthThree::bar")) + (should (equal (python-pytest--node-id-def-at-point) "TestDepthOne::TestDepthTwo::TestDepthThree::bar")) (forward-line 1)) (pytest-test-with-temp-text (string-join '("class TestDepthOne:" @@ -73,11 +73,11 @@ " def test_depth_three():" " pass") "\n") - (should (equal (python-pytest--path-def-at-point) "TestDepthOne::test_depth_one")) + (should (equal (python-pytest--node-id-def-at-point) "TestDepthOne::test_depth_one")) (search-forward "test_depth_two") - (should (equal (python-pytest--path-def-at-point) "TestDepthOne::TestDepthTwo::test_depth_two")) + (should (equal (python-pytest--node-id-def-at-point) "TestDepthOne::TestDepthTwo::test_depth_two")) (search-forward "test_depth_three") - (should (equal (python-pytest--path-def-at-point) "TestDepthOne::TestDepthTwo::TestDepthThree::test_depth_three")))) + (should (equal (python-pytest--node-id-def-at-point) "TestDepthOne::TestDepthTwo::TestDepthThree::test_depth_three")))) (ert-deftest get-current-def-inside-def () (pytest-test-with-temp-text (string-join @@ -85,7 +85,7 @@ " def bar():" " pass") "\n") - (should (equal (python-pytest--path-def-at-point) "foo"))) + (should (equal (python-pytest--node-id-def-at-point) "foo"))) (pytest-test-with-temp-text (string-join '("class TestDepthOne:" " class TestDepthTwo:" @@ -98,7 +98,7 @@ ;; identify defs inside defs. In other words, pytest can ;; only identify those defs that are not contained within ;; other defs. - (should (equal (python-pytest--path-def-at-point) "TestDepthOne::TestDepthTwo::TestDepthThree::foo")))) + (should (equal (python-pytest--node-id-def-at-point) "TestDepthOne::TestDepthTwo::TestDepthThree::foo")))) (ert-deftest get-current-class-outside-class () (pytest-test-with-temp-text (string-join @@ -106,7 +106,7 @@ " def foo():" " pass") "\n") - (should (equal (python-pytest--path-class-at-point) "Test")))) + (should (equal (python-pytest--node-id-class-at-point) "Test")))) (ert-deftest get-current-class-inside-class () (pytest-test-with-temp-text (string-join @@ -116,7 +116,7 @@ " pass") "\n") (should (equal - (python-pytest--path-class-at-point) + (python-pytest--node-id-class-at-point) "TestDepthOne::TestDepthTwo")))) (ert-deftest get-current-class-inside-multiple-classes () @@ -130,5 +130,5 @@ " pass") "\n") (should (equal - (python-pytest--path-class-at-point) + (python-pytest--node-id-class-at-point) "TestDepthOne::TestDepthTwo::TestDepthThree::TestDepthFour::TestDepthFive")))) From ac353d2419235bc7aaa0083eebe9a85421b2c77e Mon Sep 17 00:00:00 2001 From: Rodrigo Morales Date: Fri, 16 Aug 2024 10:14:56 -0500 Subject: [PATCH 10/20] add suffix "-treesit" to functions that require treesit --- python-pytest.el | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/python-pytest.el b/python-pytest.el index 4390c30..ef026e8 100644 --- a/python-pytest.el +++ b/python-pytest.el @@ -179,8 +179,8 @@ When non-nil only ‘test_foo()’ will match, and nothing else." [("m" "files" python-pytest-files) ("M" "directories" python-pytest-directories)] [("d" "def at point (dwim)" python-pytest-run-def-at-point-dwim) - ("D" "def at point" python-pytest-run-def-at-point) - ("c" "class at point" python-pytest-run-class-at-point)]]) + ("D" "def at point" python-pytest-run-def-at-point-treesit) + ("c" "class at point" python-pytest-run-class-at-point-treesit)]]) (define-obsolete-function-alias 'python-pytest-popup 'python-pytest-dispatch "2.0.0") @@ -259,23 +259,23 @@ With a prefix argument, allow editing." :edit current-prefix-arg)) ;;;###autoload -(defun python-pytest-run-def-at-point () +(defun python-pytest-run-def-at-point-treesit () "Run def at point." (interactive) (python-pytest--run :args (transient-args 'python-pytest-dispatch) :file (buffer-file-name) - :node-id (python-pytest--node-id-def-at-point) + :node-id (python-pytest--node-id-def-at-point-treesit) :edit current-prefix-arg)) ;;;###autoload -(defun python-pytest-run-class-at-point () +(defun python-pytest-run-class-at-point-treesit () "Run class at point." (interactive) (python-pytest--run :args (transient-args 'python-pytest-dispatch) :file (buffer-file-name) - :node-id (python-pytest--node-id-class-at-point) + :node-id (python-pytest--node-id-class-at-point-treesit) :edit current-prefix-arg)) ;;;###autoload @@ -596,7 +596,7 @@ When present ON-REPLACEMENT is substituted, else OFF-REPLACEMENT is appended." (when (equal (treesit-node-type current-node) "class_definition") (throw 'return t)))))) -(defun python-pytest--node-id-def-at-point () +(defun python-pytest--node-id-def-at-point-treesit () "Return the node id of the def at point. + If the test function is not inside a class, its node id is the name @@ -660,7 +660,7 @@ When present ON-REPLACEMENT is substituted, else OFF-REPLACEMENT is appended." parents)))))) (string-join `(,@parents ,(cdr function)) "::"))) -(defun python-pytest--node-id-class-at-point () +(defun python-pytest--node-id-class-at-point-treesit () "Return the node id of the class at point. + If the class is not inside another class, its node id is the name From f2c8e98bacb223780728b8f893b6a8e6f51861e6 Mon Sep 17 00:00:00 2001 From: Rodrigo Morales Date: Fri, 16 Aug 2024 10:19:33 -0500 Subject: [PATCH 11/20] call python-pytest--current-defun where it was previously called In a recent previous commit, python-pytest--current-defun had been marked as obsolete and all function calls have been replaced in favour of python-pytest--node-id-def-at-point. However, we should continue supporting python-pytest--current-defun so that users that use a GNU Emacs version that don't have tree-sitter support can continue using this package. See comment: https://github.com/wbolster/emacs-python-pytest/pull/75#discussion_r1719328468 To make it clear, Emacs users that have tree-sitter support should use python-pytest--node-id-def-at-point and python-pytest--node-id-class-at-point. Emacs users that don't have tree-sitter support should use python-pytest--current-defun and those functions that call that function (i.e. python-pytest-function and python-pytest-run-def-at-point-dwim). --- python-pytest.el | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/python-pytest.el b/python-pytest.el index ef026e8..7e11b47 100644 --- a/python-pytest.el +++ b/python-pytest.el @@ -284,11 +284,10 @@ With a prefix argument, allow editing." Additional ARGS are passed along to pytest. With a prefix argument, allow editing." - (declare (obsolete 'python-pytest-run-def-at-point "python-pytest 3.5.0")) (interactive (list (buffer-file-name) - (python-pytest--node-id-def-at-point) + (python-pytest--current-defun) (transient-args 'python-pytest-dispatch))) (python-pytest--run :args args @@ -313,7 +312,7 @@ With a prefix argument, allow editing." (interactive (list (buffer-file-name) - (python-pytest--node-id-def-at-point) + (python-pytest--current-defun) (transient-args 'python-pytest-dispatch))) (unless (python-pytest--test-file-p file) (setq @@ -706,7 +705,6 @@ When present ON-REPLACEMENT is substituted, else OFF-REPLACEMENT is appended." (defun python-pytest--current-defun () "Detect the current function/class (if any)." - (declare (obsolete 'python-pytest--node-id-def-at-point "python-pytest 3.5.0")) (let* ((name (or (python-info-current-defun) (save-excursion From 1db5a14e4dd51fc1c0bcdea46de3d780fc2661e8 Mon Sep 17 00:00:00 2001 From: Rodrigo Morales Date: Fri, 16 Aug 2024 10:32:19 -0500 Subject: [PATCH 12/20] rename function python-pytest--current-defun for better readibility In a recent previous commit, we have added 2 functions: python-pytest--node-id-def-at-point-treesit and python-pytest--node-id-class-at-point-treesit. These two function names clearly explain that they either get the node id of the def at or class at point. The name python-pytest--current-defun can be confusing for some readers because the reader might think that it can only get the def at point, not the class at point. The new name python-pytest--node-id-def-or-class-at-point makes it clear that it can get both the def or class at point. --- python-pytest.el | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/python-pytest.el b/python-pytest.el index 7e11b47..1a0da37 100644 --- a/python-pytest.el +++ b/python-pytest.el @@ -287,7 +287,7 @@ With a prefix argument, allow editing." (interactive (list (buffer-file-name) - (python-pytest--current-defun) + (python-pytest--node-id-def-or-class-at-point) (transient-args 'python-pytest-dispatch))) (python-pytest--run :args args @@ -312,7 +312,7 @@ With a prefix argument, allow editing." (interactive (list (buffer-file-name) - (python-pytest--current-defun) + (python-pytest--node-id-def-or-class-at-point) (transient-args 'python-pytest-dispatch))) (unless (python-pytest--test-file-p file) (setq @@ -703,7 +703,7 @@ When present ON-REPLACEMENT is substituted, else OFF-REPLACEMENT is appended." parents)))))) (string-join `(,@parents ,(cdr class)) "::"))) -(defun python-pytest--current-defun () +(defun python-pytest--node-id-def-or-class-at-point () "Detect the current function/class (if any)." (let* ((name (or (python-info-current-defun) From ec50a7504af2e8fa5983e9ccec9eb4c3fcb32ea9 Mon Sep 17 00:00:00 2001 From: Rodrigo Morales Date: Fri, 16 Aug 2024 10:43:01 -0500 Subject: [PATCH 13/20] rename functions that call python-pytest--node-id-def-or-class-at-point The functions python-pytest-function and python-pytest-run-def-at-point-dwim call python-pytest--node-id-def-or-class-at-point. We rename those 2 functions for better readibility. --- python-pytest.el | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/python-pytest.el b/python-pytest.el index 1a0da37..9db0983 100644 --- a/python-pytest.el +++ b/python-pytest.el @@ -178,7 +178,7 @@ When non-nil only ‘test_foo()’ will match, and nothing else." ("F" "file (this)" python-pytest-file)] [("m" "files" python-pytest-files) ("M" "directories" python-pytest-directories)] - [("d" "def at point (dwim)" python-pytest-run-def-at-point-dwim) + [("d" "def at point (dwim)" python-pytest-run-def-or-class-at-point-dwim) ("D" "def at point" python-pytest-run-def-at-point-treesit) ("c" "class at point" python-pytest-run-class-at-point-treesit)]]) @@ -279,7 +279,7 @@ With a prefix argument, allow editing." :edit current-prefix-arg)) ;;;###autoload -(defun python-pytest-function (file func args) +(defun python-pytest-run-def-or-class-at-point (file func args) "Run pytest on FILE with FUNC (or class). Additional ARGS are passed along to pytest. @@ -296,7 +296,7 @@ With a prefix argument, allow editing." :edit current-prefix-arg)) ;;;###autoload -(defun python-pytest-run-def-at-point-dwim (file func args) +(defun python-pytest-run-def-or-class-at-point-dwim (file func args) "Run pytest on FILE using FUNC at point as the node-id. If `python-pytest--test-file-p' returns t for FILE (i.e. the file From a104cf85b6acf1e3f37f5faf4a9a9060d92b305e Mon Sep 17 00:00:00 2001 From: Rodrigo Morales Date: Fri, 16 Aug 2024 10:56:04 -0500 Subject: [PATCH 14/20] add defcustom python-pytest-use-treesit and use it in python-pytest-dispatch --- python-pytest.el | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/python-pytest.el b/python-pytest.el index 9db0983..6b3ea1c 100644 --- a/python-pytest.el +++ b/python-pytest.el @@ -127,6 +127,16 @@ When non-nil only ‘test_foo()’ will match, and nothing else." (set-default symbol value) value)))) +(defcustom python-pytest-use-treesit nil + "Whether to use treesit for getting the node ids of things at point. + +Users that are running a version of Emacs that supports treesit +and have the Python language grammar for treesit should set this +variable to t. Users that are running a version of Emacs that +don't support treesit should set this variable to nil." + :group 'python-pytest + :type 'boolean) + (defvar python-pytest--history nil "History for pytest invocations.") @@ -178,9 +188,10 @@ When non-nil only ‘test_foo()’ will match, and nothing else." ("F" "file (this)" python-pytest-file)] [("m" "files" python-pytest-files) ("M" "directories" python-pytest-directories)] - [("d" "def at point (dwim)" python-pytest-run-def-or-class-at-point-dwim) - ("D" "def at point" python-pytest-run-def-at-point-treesit) - ("c" "class at point" python-pytest-run-class-at-point-treesit)]]) + [("d" "def at point (dwim)" python-pytest-run-def-or-class-at-point-dwim :if-not python-pytest-use-treesit-p) + ("D" "def at point" python-pytest-run-def-or-class-at-point :if-not python-pytest-use-treesit-p) + ("d" "def at point" python-pytest-run-def-at-point-treesit :if python-pytest-use-treesit-p) + ("c" "class at point" python-pytest-run-class-at-point-treesit :if python-pytest-use-treesit-p)]]) (define-obsolete-function-alias 'python-pytest-popup 'python-pytest-dispatch "2.0.0") @@ -461,6 +472,17 @@ TestClassParent::TestClassChild::test_my_function." (setq process (get-buffer-process buffer)) (set-process-sentinel process #'python-pytest--process-sentinel)))) +(defun python-pytest-use-treesit-p () + "Return t if python-pytest-use-treesit is t. Otherwise, return nil. + +This function is passed to the parameter :if in +`python-pytest-dispatch'. + +Although this function might look useless, the main reason why it +was defined was that the parameter that is provided to the +transient keyword :if must be a function." + python-pytest-use-treesit) + (defun python-pytest--shell-quote (s) "Quote S for use in a shell command. Like `shell-quote-argument', but prettier." (if (s-equals-p s (shell-quote-argument s)) From e09e8f86cf19199b0cfd2e5525bb393526836a73 Mon Sep 17 00:00:00 2001 From: Rodrigo Morales Date: Mon, 19 Aug 2024 07:57:05 -0500 Subject: [PATCH 15/20] set python-pytest-use-treesit to t when treesit is an available feature --- python-pytest.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python-pytest.el b/python-pytest.el index 6b3ea1c..3649e56 100644 --- a/python-pytest.el +++ b/python-pytest.el @@ -127,7 +127,7 @@ When non-nil only ‘test_foo()’ will match, and nothing else." (set-default symbol value) value)))) -(defcustom python-pytest-use-treesit nil +(defcustom python-pytest-use-treesit (featurep 'treesit) "Whether to use treesit for getting the node ids of things at point. Users that are running a version of Emacs that supports treesit From 1f29d556458b899ae13c53250357d395511658b7 Mon Sep 17 00:00:00 2001 From: Rodrigo Morales Date: Mon, 19 Aug 2024 07:57:53 -0500 Subject: [PATCH 16/20] add (require 'treesit nil t) to the top of python-pytest.el --- python-pytest.el | 1 + 1 file changed, 1 insertion(+) diff --git a/python-pytest.el b/python-pytest.el index 3649e56..6139359 100644 --- a/python-pytest.el +++ b/python-pytest.el @@ -29,6 +29,7 @@ (require 'projectile nil t) (require 'project nil t) +(require 'treesit nil t) (defgroup python-pytest nil "pytest integration" From 498eb8fe0bd602efcf0230cb6b75f950ea786593 Mon Sep 17 00:00:00 2001 From: Rodrigo Morales Date: Mon, 19 Aug 2024 08:33:18 -0500 Subject: [PATCH 17/20] fix tests: rename functions --- tests/test-python-helpers.el | 40 ++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/tests/test-python-helpers.el b/tests/test-python-helpers.el index 19fe76e..8225b49 100644 --- a/tests/test-python-helpers.el +++ b/tests/test-python-helpers.el @@ -21,13 +21,13 @@ " pass\n" "def bar():\n" " pass\n") - (should (equal (python-pytest--node-id-def-at-point) "foo")) + (should (equal (python-pytest--node-id-def-at-point-treesit) "foo")) (forward-line 1) - (should (equal (python-pytest--node-id-def-at-point) "foo")) + (should (equal (python-pytest--node-id-def-at-point-treesit) "foo")) (forward-line 1) - (should (equal (python-pytest--node-id-def-at-point) "bar")) + (should (equal (python-pytest--node-id-def-at-point-treesit) "bar")) (forward-line 1) - (should (equal (python-pytest--node-id-def-at-point) "bar")))) + (should (equal (python-pytest--node-id-def-at-point-treesit) "bar")))) (ert-deftest get-current-def-inside-class () (pytest-test-with-temp-text (concat @@ -36,13 +36,13 @@ " pass\n" " def bar():\n" " pass\n") - (should (equal (python-pytest--node-id-def-at-point) "TestGroup::foo")) + (should (equal (python-pytest--node-id-def-at-point-treesit) "TestGroup::foo")) (forward-line 1) - (should (equal (python-pytest--node-id-def-at-point) "TestGroup::foo")) + (should (equal (python-pytest--node-id-def-at-point-treesit) "TestGroup::foo")) (forward-line 1) - (should (equal (python-pytest--node-id-def-at-point) "TestGroup::bar")) + (should (equal (python-pytest--node-id-def-at-point-treesit) "TestGroup::bar")) (forward-line 1) - (should (equal (python-pytest--node-id-def-at-point) "TestGroup::bar")))) + (should (equal (python-pytest--node-id-def-at-point-treesit) "TestGroup::bar")))) (ert-deftest get-current-def-inside-multiple-classes () (pytest-test-with-temp-text (string-join @@ -54,13 +54,13 @@ " def bar():" " pass") "\n") - (should (equal (python-pytest--node-id-def-at-point) "TestDepthOne::TestDepthTwo::TestDepthThree::foo")) + (should (equal (python-pytest--node-id-def-at-point-treesit) "TestDepthOne::TestDepthTwo::TestDepthThree::foo")) (forward-line 1) - (should (equal (python-pytest--node-id-def-at-point) "TestDepthOne::TestDepthTwo::TestDepthThree::foo")) + (should (equal (python-pytest--node-id-def-at-point-treesit) "TestDepthOne::TestDepthTwo::TestDepthThree::foo")) (forward-line 1) - (should (equal (python-pytest--node-id-def-at-point) "TestDepthOne::TestDepthTwo::TestDepthThree::bar")) + (should (equal (python-pytest--node-id-def-at-point-treesit) "TestDepthOne::TestDepthTwo::TestDepthThree::bar")) (forward-line 1) - (should (equal (python-pytest--node-id-def-at-point) "TestDepthOne::TestDepthTwo::TestDepthThree::bar")) + (should (equal (python-pytest--node-id-def-at-point-treesit) "TestDepthOne::TestDepthTwo::TestDepthThree::bar")) (forward-line 1)) (pytest-test-with-temp-text (string-join '("class TestDepthOne:" @@ -73,11 +73,11 @@ " def test_depth_three():" " pass") "\n") - (should (equal (python-pytest--node-id-def-at-point) "TestDepthOne::test_depth_one")) + (should (equal (python-pytest--node-id-def-at-point-treesit) "TestDepthOne::test_depth_one")) (search-forward "test_depth_two") - (should (equal (python-pytest--node-id-def-at-point) "TestDepthOne::TestDepthTwo::test_depth_two")) + (should (equal (python-pytest--node-id-def-at-point-treesit) "TestDepthOne::TestDepthTwo::test_depth_two")) (search-forward "test_depth_three") - (should (equal (python-pytest--node-id-def-at-point) "TestDepthOne::TestDepthTwo::TestDepthThree::test_depth_three")))) + (should (equal (python-pytest--node-id-def-at-point-treesit) "TestDepthOne::TestDepthTwo::TestDepthThree::test_depth_three")))) (ert-deftest get-current-def-inside-def () (pytest-test-with-temp-text (string-join @@ -85,7 +85,7 @@ " def bar():" " pass") "\n") - (should (equal (python-pytest--node-id-def-at-point) "foo"))) + (should (equal (python-pytest--node-id-def-at-point-treesit) "foo"))) (pytest-test-with-temp-text (string-join '("class TestDepthOne:" " class TestDepthTwo:" @@ -98,7 +98,7 @@ ;; identify defs inside defs. In other words, pytest can ;; only identify those defs that are not contained within ;; other defs. - (should (equal (python-pytest--node-id-def-at-point) "TestDepthOne::TestDepthTwo::TestDepthThree::foo")))) + (should (equal (python-pytest--node-id-def-at-point-treesit) "TestDepthOne::TestDepthTwo::TestDepthThree::foo")))) (ert-deftest get-current-class-outside-class () (pytest-test-with-temp-text (string-join @@ -106,7 +106,7 @@ " def foo():" " pass") "\n") - (should (equal (python-pytest--node-id-class-at-point) "Test")))) + (should (equal (python-pytest--node-id-class-at-point-treesit) "Test")))) (ert-deftest get-current-class-inside-class () (pytest-test-with-temp-text (string-join @@ -116,7 +116,7 @@ " pass") "\n") (should (equal - (python-pytest--node-id-class-at-point) + (python-pytest--node-id-class-at-point-treesit) "TestDepthOne::TestDepthTwo")))) (ert-deftest get-current-class-inside-multiple-classes () @@ -130,5 +130,5 @@ " pass") "\n") (should (equal - (python-pytest--node-id-class-at-point) + (python-pytest--node-id-class-at-point-treesit) "TestDepthOne::TestDepthTwo::TestDepthThree::TestDepthFour::TestDepthFive")))) From 7ab96723c6eab1ceb6d7ba9e4a5a60641cc4d03e Mon Sep 17 00:00:00 2001 From: Rodrigo Morales Date: Mon, 19 Aug 2024 08:33:46 -0500 Subject: [PATCH 18/20] add suffix -treesit to functions that use treesit features --- python-pytest.el | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/python-pytest.el b/python-pytest.el index 6139359..d031608 100644 --- a/python-pytest.el +++ b/python-pytest.el @@ -600,7 +600,7 @@ When present ON-REPLACEMENT is substituted, else OFF-REPLACEMENT is appended." ;; python helpers -(defun python-pytest--point-is-inside-def () +(defun python-pytest--point-is-inside-def-treesit () (unless (treesit-language-available-p 'python) (error "This function requires tree-sitter support for python, but it is not available.")) (catch 'return @@ -609,7 +609,7 @@ When present ON-REPLACEMENT is substituted, else OFF-REPLACEMENT is appended." (when (equal (treesit-node-type current-node) "function_definition") (throw 'return t)))))) -(defun python-pytest--point-is-inside-class () +(defun python-pytest--point-is-inside-class-treesit () (unless (treesit-language-available-p 'python) (error "This function requires tree-sitter support for python, but it is not available.")) (catch 'return @@ -628,7 +628,7 @@ When present ON-REPLACEMENT is substituted, else OFF-REPLACEMENT is appended." + If the test function is defined inside a class that is defined inside another class, its node id would look like: TestGroupParent::TestGroupChild::test_my_function." - (unless (python-pytest--point-is-inside-def) + (unless (python-pytest--point-is-inside-def-treesit) (error "The point is not inside a def.")) (let ((function ;; Move up to the outermost function @@ -691,7 +691,7 @@ When present ON-REPLACEMENT is substituted, else OFF-REPLACEMENT is appended." class which is contained would be: TestGroupParent::TestGroupChild, while the node id of the class which contains the other class would be TestGroupParent." - (unless (python-pytest--point-is-inside-class) + (unless (python-pytest--point-is-inside-class-treesit) (error "The point is not inside a class.")) (let ((class ;; Move up to the outermost function From b6b746526da0c89470158bdc6160e602a8eb7e76 Mon Sep 17 00:00:00 2001 From: Rodrigo Morales Date: Fri, 23 Aug 2024 10:32:53 -0500 Subject: [PATCH 19/20] make *-treesit defun's correctly handle narrowed buffers --- python-pytest.el | 195 ++++++++++++++++++----------------- tests/test-python-helpers.el | 95 ++++++++++++++++- 2 files changed, 192 insertions(+), 98 deletions(-) diff --git a/python-pytest.el b/python-pytest.el index d031608..bd6392d 100644 --- a/python-pytest.el +++ b/python-pytest.el @@ -603,20 +603,24 @@ When present ON-REPLACEMENT is substituted, else OFF-REPLACEMENT is appended." (defun python-pytest--point-is-inside-def-treesit () (unless (treesit-language-available-p 'python) (error "This function requires tree-sitter support for python, but it is not available.")) - (catch 'return - (let ((current-node (treesit-node-at (point) 'python))) - (while (setq current-node (treesit-node-parent current-node)) - (when (equal (treesit-node-type current-node) "function_definition") - (throw 'return t)))))) + (save-restriction + (widen) + (catch 'return + (let ((current-node (treesit-node-at (point) 'python))) + (while (setq current-node (treesit-node-parent current-node)) + (when (equal (treesit-node-type current-node) "function_definition") + (throw 'return t))))))) (defun python-pytest--point-is-inside-class-treesit () (unless (treesit-language-available-p 'python) (error "This function requires tree-sitter support for python, but it is not available.")) - (catch 'return - (let ((current-node (treesit-node-at (point) 'python))) - (while (setq current-node (treesit-node-parent current-node)) - (when (equal (treesit-node-type current-node) "class_definition") - (throw 'return t)))))) + (save-restriction + (widen) + (catch 'return + (let ((current-node (treesit-node-at (point) 'python))) + (while (setq current-node (treesit-node-parent current-node)) + (when (equal (treesit-node-type current-node) "class_definition") + (throw 'return t))))))) (defun python-pytest--node-id-def-at-point-treesit () "Return the node id of the def at point. @@ -630,57 +634,59 @@ When present ON-REPLACEMENT is substituted, else OFF-REPLACEMENT is appended." TestGroupParent::TestGroupChild::test_my_function." (unless (python-pytest--point-is-inside-def-treesit) (error "The point is not inside a def.")) - (let ((function - ;; Move up to the outermost function - (catch 'return - (let ((current-node (treesit-node-at (point) 'python)) - function-node) - (catch 'break - (while (setq current-node (treesit-node-parent current-node)) - (when (equal (treesit-node-type current-node) "function_definition") - (setq function-node current-node) - ;; At this point, we know that we are on a - ;; function. We need to move up to see if the - ;; function is inside a function. If that's the - ;; case, we move up. This way, we find the - ;; outermost function. We need to do this because - ;; pytest can't execute functions inside functions, - ;; so we must get the function that is not inside - ;; other function. - (while (setq current-node (treesit-node-parent current-node)) - (when (equal (treesit-node-type current-node) "function_definition") - (setq function-node current-node))) - (throw 'break nil)))) - (dolist (child (treesit-node-children function-node)) - (when (equal (treesit-node-type child) "identifier") - (throw 'return - (cons - ;; Keep a reference to the node that is a - ;; function_definition. We need this - ;; reference because afterwards we need to - ;; move up starting at the current node to - ;; find the node id of the class (if there's - ;; any) in which the function is defined. - function-node - (buffer-substring-no-properties - (treesit-node-start child) - (treesit-node-end child))))))))) - parents) - ;; Move up through the parent nodes to see if the function is - ;; defined inside a class and collect the classes to finally build - ;; the node id of the current function. Remember that the node id - ;; of a function that is defined within nested classes must have - ;; the name of the nested classes. - (let ((current-node (car function))) - (while (setq current-node (treesit-node-parent current-node)) - (when (equal (treesit-node-type current-node) "class_definition") - (dolist (child (treesit-node-children current-node)) - (when (equal (treesit-node-type child) "identifier") - (push (buffer-substring-no-properties - (treesit-node-start child) - (treesit-node-end child)) - parents)))))) - (string-join `(,@parents ,(cdr function)) "::"))) + (save-restriction + (widen) + (let ((function + ;; Move up to the outermost function + (catch 'return + (let ((current-node (treesit-node-at (point) 'python)) + function-node) + (catch 'break + (while (setq current-node (treesit-node-parent current-node)) + (when (equal (treesit-node-type current-node) "function_definition") + (setq function-node current-node) + ;; At this point, we know that we are on a + ;; function. We need to move up to see if the + ;; function is inside a function. If that's the + ;; case, we move up. This way, we find the + ;; outermost function. We need to do this because + ;; pytest can't execute functions inside functions, + ;; so we must get the function that is not inside + ;; other function. + (while (setq current-node (treesit-node-parent current-node)) + (when (equal (treesit-node-type current-node) "function_definition") + (setq function-node current-node))) + (throw 'break nil)))) + (dolist (child (treesit-node-children function-node)) + (when (equal (treesit-node-type child) "identifier") + (throw 'return + (cons + ;; Keep a reference to the node that is a + ;; function_definition. We need this + ;; reference because afterwards we need to + ;; move up starting at the current node to + ;; find the node id of the class (if there's + ;; any) in which the function is defined. + function-node + (buffer-substring-no-properties + (treesit-node-start child) + (treesit-node-end child))))))))) + parents) + ;; Move up through the parent nodes to see if the function is + ;; defined inside a class and collect the classes to finally build + ;; the node id of the current function. Remember that the node id + ;; of a function that is defined within nested classes must have + ;; the name of the nested classes. + (let ((current-node (car function))) + (while (setq current-node (treesit-node-parent current-node)) + (when (equal (treesit-node-type current-node) "class_definition") + (dolist (child (treesit-node-children current-node)) + (when (equal (treesit-node-type child) "identifier") + (push (buffer-substring-no-properties + (treesit-node-start child) + (treesit-node-end child)) + parents)))))) + (string-join `(,@parents ,(cdr function)) "::")))) (defun python-pytest--node-id-class-at-point-treesit () "Return the node id of the class at point. @@ -693,39 +699,40 @@ When present ON-REPLACEMENT is substituted, else OFF-REPLACEMENT is appended." be TestGroupParent." (unless (python-pytest--point-is-inside-class-treesit) (error "The point is not inside a class.")) - (let ((class - ;; Move up to the outermost function - (catch 'return - (let ((current-node (treesit-node-at (point) 'python))) - (catch 'break - (while (setq current-node (treesit-node-parent current-node)) - (when (equal (treesit-node-type current-node) "class_definition") - (throw 'break nil)))) - (dolist (child (treesit-node-children current-node)) - (when (equal (treesit-node-type child) "identifier") - (throw 'return - (cons - ;; Keep a reference to the node that is a - ;; function_definition - current-node - (buffer-substring-no-properties - (treesit-node-start child) - (treesit-node-end child))))))))) - parents) - ;; Move up through the parents to collect the list of classes in - ;; which the class is contained. pytest supports running nested - ;; classes, but it doesn't support runing nested functions. - (let ((current-node (car class))) - (while (setq current-node (treesit-node-parent current-node)) - (when (equal (treesit-node-type current-node) "class_definition") - (dolist (child (treesit-node-children current-node)) - (when (equal (treesit-node-type child) "identifier") - (push (buffer-substring-no-properties - (treesit-node-start child) - (treesit-node-end child)) - parents)))))) - (string-join `(,@parents ,(cdr class)) "::"))) - + (save-restriction + (widen) + (let ((class + ;; Move up to the outermost function + (catch 'return + (let ((current-node (treesit-node-at (point) 'python))) + (catch 'break + (while (setq current-node (treesit-node-parent current-node)) + (when (equal (treesit-node-type current-node) "class_definition") + (throw 'break nil)))) + (dolist (child (treesit-node-children current-node)) + (when (equal (treesit-node-type child) "identifier") + (throw 'return + (cons + ;; Keep a reference to the node that is a + ;; function_definition + current-node + (buffer-substring-no-properties + (treesit-node-start child) + (treesit-node-end child))))))))) + parents) + ;; Move up through the parents to collect the list of classes in + ;; which the class is contained. pytest supports running nested + ;; classes, but it doesn't support runing nested functions. + (let ((current-node (car class))) + (while (setq current-node (treesit-node-parent current-node)) + (when (equal (treesit-node-type current-node) "class_definition") + (dolist (child (treesit-node-children current-node)) + (when (equal (treesit-node-type child) "identifier") + (push (buffer-substring-no-properties + (treesit-node-start child) + (treesit-node-end child)) + parents)))))) + (string-join `(,@parents ,(cdr class)) "::")))) (defun python-pytest--node-id-def-or-class-at-point () "Detect the current function/class (if any)." (let* ((name diff --git a/tests/test-python-helpers.el b/tests/test-python-helpers.el index 8225b49..1f21af5 100644 --- a/tests/test-python-helpers.el +++ b/tests/test-python-helpers.el @@ -27,7 +27,25 @@ (forward-line 1) (should (equal (python-pytest--node-id-def-at-point-treesit) "bar")) (forward-line 1) - (should (equal (python-pytest--node-id-def-at-point-treesit) "bar")))) + (should (equal (python-pytest--node-id-def-at-point-treesit) "bar")) + ;; when the buffer is narrowed, we should get the same result. + (goto-char (point-min)) + (search-forward "foo") + (save-restriction + (narrow-to-defun) + (should (equal (python-pytest--node-id-def-at-point-treesit) "foo"))) + (forward-line 1) + (save-restriction + (narrow-to-defun) + (should (equal (python-pytest--node-id-def-at-point-treesit) "foo"))) + (forward-line 1) + (save-restriction + (narrow-to-defun) + (should (equal (python-pytest--node-id-def-at-point-treesit) "bar"))) + (forward-line 1) + (save-restriction + (narrow-to-defun) + (should (equal (python-pytest--node-id-def-at-point-treesit) "bar"))))) (ert-deftest get-current-def-inside-class () (pytest-test-with-temp-text (concat @@ -42,7 +60,25 @@ (forward-line 1) (should (equal (python-pytest--node-id-def-at-point-treesit) "TestGroup::bar")) (forward-line 1) - (should (equal (python-pytest--node-id-def-at-point-treesit) "TestGroup::bar")))) + (should (equal (python-pytest--node-id-def-at-point-treesit) "TestGroup::bar")) + ;; when the buffer is narrowed, we should get the same result + (goto-char (point-min)) + (search-forward "foo") + (save-restriction + (narrow-to-defun) + (should (equal (python-pytest--node-id-def-at-point-treesit) "TestGroup::foo"))) + (forward-line 1) + (save-restriction + (narrow-to-defun) + (should (equal (python-pytest--node-id-def-at-point-treesit) "TestGroup::foo"))) + (forward-line 1) + (save-restriction + (narrow-to-defun) + (should (equal (python-pytest--node-id-def-at-point-treesit) "TestGroup::bar"))) + (forward-line 1) + (save-restriction + (narrow-to-defun) + (should (equal (python-pytest--node-id-def-at-point-treesit) "TestGroup::bar"))))) (ert-deftest get-current-def-inside-multiple-classes () (pytest-test-with-temp-text (string-join @@ -61,7 +97,25 @@ (should (equal (python-pytest--node-id-def-at-point-treesit) "TestDepthOne::TestDepthTwo::TestDepthThree::bar")) (forward-line 1) (should (equal (python-pytest--node-id-def-at-point-treesit) "TestDepthOne::TestDepthTwo::TestDepthThree::bar")) - (forward-line 1)) + (forward-line 1) + ;; when the buffer is narrowed, we should get the same result. + (goto-char (point-min)) + (save-restriction + (search-forward "foo") + (narrow-to-defun) + (should (equal (python-pytest--node-id-def-at-point-treesit) "TestDepthOne::TestDepthTwo::TestDepthThree::foo"))) + (save-restriction + (forward-line 1) + (narrow-to-defun) + (should (equal (python-pytest--node-id-def-at-point-treesit) "TestDepthOne::TestDepthTwo::TestDepthThree::foo"))) + (save-restriction + (forward-line 1) + (narrow-to-defun) + (should (equal (python-pytest--node-id-def-at-point-treesit) "TestDepthOne::TestDepthTwo::TestDepthThree::bar"))) + (save-restriction + (forward-line 1) + (narrow-to-defun) + (should (equal (python-pytest--node-id-def-at-point-treesit) "TestDepthOne::TestDepthTwo::TestDepthThree::bar")))) (pytest-test-with-temp-text (string-join '("class TestDepthOne:" " def test_depth_one():" @@ -77,7 +131,21 @@ (search-forward "test_depth_two") (should (equal (python-pytest--node-id-def-at-point-treesit) "TestDepthOne::TestDepthTwo::test_depth_two")) (search-forward "test_depth_three") - (should (equal (python-pytest--node-id-def-at-point-treesit) "TestDepthOne::TestDepthTwo::TestDepthThree::test_depth_three")))) + (should (equal (python-pytest--node-id-def-at-point-treesit) "TestDepthOne::TestDepthTwo::TestDepthThree::test_depth_three")) + ;; when the buffer is narrowed, we should get the same result. + (goto-char (point-min)) + (save-restriction + (search-forward "test_depth_one") + (narrow-to-defun) + (should (equal (python-pytest--node-id-def-at-point-treesit) "TestDepthOne::test_depth_one"))) + (save-restriction + (search-forward "test_depth_two") + (narrow-to-defun) + (should (equal (python-pytest--node-id-def-at-point-treesit) "TestDepthOne::TestDepthTwo::test_depth_two"))) + (save-restriction + (search-forward "test_depth_three") + (narrow-to-defun) + (should (equal (python-pytest--node-id-def-at-point-treesit) "TestDepthOne::TestDepthTwo::TestDepthThree::test_depth_three"))))) (ert-deftest get-current-def-inside-def () (pytest-test-with-temp-text (string-join @@ -85,6 +153,8 @@ " def bar():" " pass") "\n") + (should (equal (python-pytest--node-id-def-at-point-treesit) "foo")) + (narrow-to-defun) (should (equal (python-pytest--node-id-def-at-point-treesit) "foo"))) (pytest-test-with-temp-text (string-join '("class TestDepthOne:" @@ -98,6 +168,9 @@ ;; identify defs inside defs. In other words, pytest can ;; only identify those defs that are not contained within ;; other defs. + (should (equal (python-pytest--node-id-def-at-point-treesit) "TestDepthOne::TestDepthTwo::TestDepthThree::foo")) + ;; when the buffer is narrowed, we should get the same result. + (narrow-to-defun) (should (equal (python-pytest--node-id-def-at-point-treesit) "TestDepthOne::TestDepthTwo::TestDepthThree::foo")))) (ert-deftest get-current-class-outside-class () @@ -106,15 +179,24 @@ " def foo():" " pass") "\n") + (should (equal (python-pytest--node-id-class-at-point-treesit) "Test")) + ;; when the buffer is narrowed, we should get the same result. + (narrow-to-defun) (should (equal (python-pytest--node-id-class-at-point-treesit) "Test")))) (ert-deftest get-current-class-inside-class () + ;; when the buffer is not narrowed (pytest-test-with-temp-text (string-join '("class TestDepthOne:" " class TestDepthTwo:" " def foo():" " pass") "\n") + (should (equal + (python-pytest--node-id-class-at-point-treesit) + "TestDepthOne::TestDepthTwo")) + ;; when the buffer is narrowed, we should get the same result. + (narrow-to-defun) (should (equal (python-pytest--node-id-class-at-point-treesit) "TestDepthOne::TestDepthTwo")))) @@ -129,6 +211,11 @@ " def foo():" " pass") "\n") + (should (equal + (python-pytest--node-id-class-at-point-treesit) + "TestDepthOne::TestDepthTwo::TestDepthThree::TestDepthFour::TestDepthFive")) + ;; when the buffer is narrowed, we should get the same result. + (narrow-to-defun) (should (equal (python-pytest--node-id-class-at-point-treesit) "TestDepthOne::TestDepthTwo::TestDepthThree::TestDepthFour::TestDepthFive")))) From 2f8a35e5754bfb27d402ba9a9477557da761b2a6 Mon Sep 17 00:00:00 2001 From: wouter bolsterlee Date: Mon, 26 Aug 2024 11:44:32 +0200 Subject: [PATCH 20/20] mark internal helper as such --- python-pytest.el | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/python-pytest.el b/python-pytest.el index bd6392d..93664eb 100644 --- a/python-pytest.el +++ b/python-pytest.el @@ -189,10 +189,10 @@ don't support treesit should set this variable to nil." ("F" "file (this)" python-pytest-file)] [("m" "files" python-pytest-files) ("M" "directories" python-pytest-directories)] - [("d" "def at point (dwim)" python-pytest-run-def-or-class-at-point-dwim :if-not python-pytest-use-treesit-p) - ("D" "def at point" python-pytest-run-def-or-class-at-point :if-not python-pytest-use-treesit-p) - ("d" "def at point" python-pytest-run-def-at-point-treesit :if python-pytest-use-treesit-p) - ("c" "class at point" python-pytest-run-class-at-point-treesit :if python-pytest-use-treesit-p)]]) + [("d" "def at point (dwim)" python-pytest-run-def-or-class-at-point-dwim :if-not python-pytest--use-treesit-p) + ("D" "def at point" python-pytest-run-def-or-class-at-point :if-not python-pytest--use-treesit-p) + ("d" "def at point" python-pytest-run-def-at-point-treesit :if python-pytest--use-treesit-p) + ("c" "class at point" python-pytest-run-class-at-point-treesit :if python-pytest--use-treesit-p)]]) (define-obsolete-function-alias 'python-pytest-popup 'python-pytest-dispatch "2.0.0") @@ -473,7 +473,7 @@ TestClassParent::TestClassChild::test_my_function." (setq process (get-buffer-process buffer)) (set-process-sentinel process #'python-pytest--process-sentinel)))) -(defun python-pytest-use-treesit-p () +(defun python-pytest--use-treesit-p () "Return t if python-pytest-use-treesit is t. Otherwise, return nil. This function is passed to the parameter :if in