pytest-tui-run 0.1.0rc3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pytest_tui_run-0.1.0rc3/LICENSE +22 -0
- pytest_tui_run-0.1.0rc3/PKG-INFO +113 -0
- pytest_tui_run-0.1.0rc3/README.md +96 -0
- pytest_tui_run-0.1.0rc3/pyproject.toml +84 -0
- pytest_tui_run-0.1.0rc3/pytest_tui_run/__init__.py +0 -0
- pytest_tui_run-0.1.0rc3/pytest_tui_run/__main__.py +4 -0
- pytest_tui_run-0.1.0rc3/pytest_tui_run/app.py +236 -0
- pytest_tui_run-0.1.0rc3/pytest_tui_run/cli.py +125 -0
- pytest_tui_run-0.1.0rc3/pytest_tui_run/config/pytest-tui-run.schema.json +1468 -0
- pytest_tui_run-0.1.0rc3/pytest_tui_run/config/pytest-tui-run.toml +107 -0
- pytest_tui_run-0.1.0rc3/pytest_tui_run/config.py +274 -0
- pytest_tui_run-0.1.0rc3/pytest_tui_run/constants.py +98 -0
- pytest_tui_run-0.1.0rc3/pytest_tui_run/filter_modal.py +222 -0
- pytest_tui_run-0.1.0rc3/pytest_tui_run/plugin.py +49 -0
- pytest_tui_run-0.1.0rc3/pytest_tui_run/pytest_utils.py +279 -0
- pytest_tui_run-0.1.0rc3/pytest_tui_run/tree.py +572 -0
- pytest_tui_run-0.1.0rc3/pytest_tui_run/tree_node_data.py +128 -0
- pytest_tui_run-0.1.0rc3/pytest_tui_run/types.py +1 -0
- pytest_tui_run-0.1.0rc3/pytest_tui_run/utils.py +202 -0
- pytest_tui_run-0.1.0rc3/pytest_tui_run/vim_log.py +64 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Teemu Peltonen <peltonen.teemu.89@gmail.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
'Software'), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
19
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
20
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
21
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
22
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pytest-tui-run
|
|
3
|
+
Version: 0.1.0rc3
|
|
4
|
+
Summary: TUI runner for `pytest` tests
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Classifier: Environment :: Console
|
|
8
|
+
Classifier: Framework :: Pytest
|
|
9
|
+
Classifier: Operating System :: POSIX
|
|
10
|
+
Classifier: Programming Language :: Python
|
|
11
|
+
Classifier: Topic :: Software Development :: Testing
|
|
12
|
+
Requires-Dist: pydantic>=2.7
|
|
13
|
+
Requires-Dist: pytest
|
|
14
|
+
Requires-Dist: textual
|
|
15
|
+
Requires-Python: >=3.11, <3.15
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# pytest-tui-run
|
|
19
|
+
|
|
20
|
+
TUI test runner for [pytest](https://docs.pytest.org/en/stable/).
|
|
21
|
+
|
|
22
|
+

|
|
23
|
+
|
|
24
|
+
## Motivation
|
|
25
|
+
|
|
26
|
+
Do you like running `pytest` from the terminal, but find it difficult to run a subset
|
|
27
|
+
of tests? Do you like VS Code's test tree view, but hate using the mouse? Then
|
|
28
|
+
`pytest-tui-run` is for you: it combines the best of both worlds by showing you the
|
|
29
|
+
collected test tree, but allows you to use your keyboard to select and run tests.
|
|
30
|
+
Even better, it comes with `vim`-like keybindings by default, which are fully
|
|
31
|
+
configurable.
|
|
32
|
+
|
|
33
|
+
## Alternatives
|
|
34
|
+
|
|
35
|
+
There are similar projects, but none with the same scope.
|
|
36
|
+
- [pytest-explorer](https://github.com/antonguzun/pytest-explorer).
|
|
37
|
+
TUI for exploring `pytest` tests. No tree view.
|
|
38
|
+
- [pytest-tui](https://github.com/jeffwright13/pytest-tui).
|
|
39
|
+
TUI for exploring `pytest` results. No test running.
|
|
40
|
+
- [VS Code Testing View](https://code.visualstudio.com/docs/debugtest/testing).
|
|
41
|
+
GUI for running tests in a tree view. Plugins for multiple languages and test
|
|
42
|
+
frameworks. Mouse centric and works only in VS Code.
|
|
43
|
+
- [Neotest](https://github.com/nvim-neotest/neotest) plugin for
|
|
44
|
+
[Neovim](https://neovim.io/).
|
|
45
|
+
TUI for running tests in a tree view. Plugins for multiple languages and test
|
|
46
|
+
frameworks. Works only in Neovim.
|
|
47
|
+
|
|
48
|
+
## Philosophy
|
|
49
|
+
|
|
50
|
+
Don't reinvent the wheel: everything that can be delegated to `pytest`, is delegated to
|
|
51
|
+
`pytest`. This is to keep the plugin as lightweight as possible to minimize the burden
|
|
52
|
+
of maintaining. In practice this means that the plugin mostly just modifies the
|
|
53
|
+
user-given `pytest` arguments, and then runs `pytest` with those arguments.
|
|
54
|
+
|
|
55
|
+
## Installation
|
|
56
|
+
|
|
57
|
+
The package is distributed through PyPI, so simply use your favorite package manager.
|
|
58
|
+
For example using `pip`:
|
|
59
|
+
```bash
|
|
60
|
+
pip install pytest-tui-run
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
This is a `pytest` plugin, so you can start it from the terminal by
|
|
66
|
+
```bash
|
|
67
|
+
pytest --tui-run
|
|
68
|
+
```
|
|
69
|
+
Just give it the arguments you would normally give to plain `pytest`. Another option
|
|
70
|
+
is to start the app directly:
|
|
71
|
+
```bash
|
|
72
|
+
pytest-tui-run
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
There are two ways to run the (selected) tests:
|
|
76
|
+
|
|
77
|
+
### Run tests in TUI
|
|
78
|
+
|
|
79
|
+
Press `r` to run the (selected) tests directly in the TUI. This shows you the output
|
|
80
|
+
and marks the test results in the tree.
|
|
81
|
+
|
|
82
|
+
### Run tests in CLI
|
|
83
|
+
|
|
84
|
+
Press `R` to run the (selected) tests in CLI. This exits the TUI and prints you the
|
|
85
|
+
proper `pytest` command to run.
|
|
86
|
+
|
|
87
|
+
## Configuration
|
|
88
|
+
|
|
89
|
+
The [default configuration](https://gitlab.com/clov/pytest-tui-run/-/blob/main/pytest-tui-run.toml)
|
|
90
|
+
includes all the available configuration options. A
|
|
91
|
+
[schema](https://gitlab.com/clov/pytest-tui-run/-/blob/main/pytest-tui-run.schema.json)
|
|
92
|
+
is included for validation. Both of these are included with the package to ensure
|
|
93
|
+
compatible versions. To get started, run
|
|
94
|
+
```bash
|
|
95
|
+
pytest --tui-run --init-config
|
|
96
|
+
```
|
|
97
|
+
This will copy the default config to your config directory
|
|
98
|
+
(usually `~/.config/pytest-tui-run/`) and create a symlink to the schema.
|
|
99
|
+
|
|
100
|
+
You may use your favorite validation tool, but a schema directive for
|
|
101
|
+
[Taplo](https://taplo.tamasfe.dev/)
|
|
102
|
+
(in VS Code the
|
|
103
|
+
[Even Better TOML](https://marketplace.visualstudio.com/items?itemName=tamasfe.even-better-toml)
|
|
104
|
+
plugin)
|
|
105
|
+
is included in the config file by default. In other words, if you have a
|
|
106
|
+
Taplo-compatible editor, you should get schema validation out of the box, assuming
|
|
107
|
+
you have properly symlinked the schema file.
|
|
108
|
+
|
|
109
|
+
## Contributing
|
|
110
|
+
|
|
111
|
+
Contributions are welcome. See
|
|
112
|
+
[CONTRIBUTING.md](https://gitlab.com/clov/pytest-tui-run/-/blob/main/CONTRIBUTING.md)
|
|
113
|
+
for details.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# pytest-tui-run
|
|
2
|
+
|
|
3
|
+
TUI test runner for [pytest](https://docs.pytest.org/en/stable/).
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## Motivation
|
|
8
|
+
|
|
9
|
+
Do you like running `pytest` from the terminal, but find it difficult to run a subset
|
|
10
|
+
of tests? Do you like VS Code's test tree view, but hate using the mouse? Then
|
|
11
|
+
`pytest-tui-run` is for you: it combines the best of both worlds by showing you the
|
|
12
|
+
collected test tree, but allows you to use your keyboard to select and run tests.
|
|
13
|
+
Even better, it comes with `vim`-like keybindings by default, which are fully
|
|
14
|
+
configurable.
|
|
15
|
+
|
|
16
|
+
## Alternatives
|
|
17
|
+
|
|
18
|
+
There are similar projects, but none with the same scope.
|
|
19
|
+
- [pytest-explorer](https://github.com/antonguzun/pytest-explorer).
|
|
20
|
+
TUI for exploring `pytest` tests. No tree view.
|
|
21
|
+
- [pytest-tui](https://github.com/jeffwright13/pytest-tui).
|
|
22
|
+
TUI for exploring `pytest` results. No test running.
|
|
23
|
+
- [VS Code Testing View](https://code.visualstudio.com/docs/debugtest/testing).
|
|
24
|
+
GUI for running tests in a tree view. Plugins for multiple languages and test
|
|
25
|
+
frameworks. Mouse centric and works only in VS Code.
|
|
26
|
+
- [Neotest](https://github.com/nvim-neotest/neotest) plugin for
|
|
27
|
+
[Neovim](https://neovim.io/).
|
|
28
|
+
TUI for running tests in a tree view. Plugins for multiple languages and test
|
|
29
|
+
frameworks. Works only in Neovim.
|
|
30
|
+
|
|
31
|
+
## Philosophy
|
|
32
|
+
|
|
33
|
+
Don't reinvent the wheel: everything that can be delegated to `pytest`, is delegated to
|
|
34
|
+
`pytest`. This is to keep the plugin as lightweight as possible to minimize the burden
|
|
35
|
+
of maintaining. In practice this means that the plugin mostly just modifies the
|
|
36
|
+
user-given `pytest` arguments, and then runs `pytest` with those arguments.
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
The package is distributed through PyPI, so simply use your favorite package manager.
|
|
41
|
+
For example using `pip`:
|
|
42
|
+
```bash
|
|
43
|
+
pip install pytest-tui-run
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Usage
|
|
47
|
+
|
|
48
|
+
This is a `pytest` plugin, so you can start it from the terminal by
|
|
49
|
+
```bash
|
|
50
|
+
pytest --tui-run
|
|
51
|
+
```
|
|
52
|
+
Just give it the arguments you would normally give to plain `pytest`. Another option
|
|
53
|
+
is to start the app directly:
|
|
54
|
+
```bash
|
|
55
|
+
pytest-tui-run
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
There are two ways to run the (selected) tests:
|
|
59
|
+
|
|
60
|
+
### Run tests in TUI
|
|
61
|
+
|
|
62
|
+
Press `r` to run the (selected) tests directly in the TUI. This shows you the output
|
|
63
|
+
and marks the test results in the tree.
|
|
64
|
+
|
|
65
|
+
### Run tests in CLI
|
|
66
|
+
|
|
67
|
+
Press `R` to run the (selected) tests in CLI. This exits the TUI and prints you the
|
|
68
|
+
proper `pytest` command to run.
|
|
69
|
+
|
|
70
|
+
## Configuration
|
|
71
|
+
|
|
72
|
+
The [default configuration](https://gitlab.com/clov/pytest-tui-run/-/blob/main/pytest-tui-run.toml)
|
|
73
|
+
includes all the available configuration options. A
|
|
74
|
+
[schema](https://gitlab.com/clov/pytest-tui-run/-/blob/main/pytest-tui-run.schema.json)
|
|
75
|
+
is included for validation. Both of these are included with the package to ensure
|
|
76
|
+
compatible versions. To get started, run
|
|
77
|
+
```bash
|
|
78
|
+
pytest --tui-run --init-config
|
|
79
|
+
```
|
|
80
|
+
This will copy the default config to your config directory
|
|
81
|
+
(usually `~/.config/pytest-tui-run/`) and create a symlink to the schema.
|
|
82
|
+
|
|
83
|
+
You may use your favorite validation tool, but a schema directive for
|
|
84
|
+
[Taplo](https://taplo.tamasfe.dev/)
|
|
85
|
+
(in VS Code the
|
|
86
|
+
[Even Better TOML](https://marketplace.visualstudio.com/items?itemName=tamasfe.even-better-toml)
|
|
87
|
+
plugin)
|
|
88
|
+
is included in the config file by default. In other words, if you have a
|
|
89
|
+
Taplo-compatible editor, you should get schema validation out of the box, assuming
|
|
90
|
+
you have properly symlinked the schema file.
|
|
91
|
+
|
|
92
|
+
## Contributing
|
|
93
|
+
|
|
94
|
+
Contributions are welcome. See
|
|
95
|
+
[CONTRIBUTING.md](https://gitlab.com/clov/pytest-tui-run/-/blob/main/CONTRIBUTING.md)
|
|
96
|
+
for details.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pytest-tui-run"
|
|
3
|
+
version = "0.1.0rc3"
|
|
4
|
+
description = "TUI runner for `pytest` tests"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
requires-python = ">= 3.11, < 3.15"
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Environment :: Console",
|
|
11
|
+
"Framework :: Pytest",
|
|
12
|
+
"Operating System :: POSIX",
|
|
13
|
+
"Programming Language :: Python",
|
|
14
|
+
"Topic :: Software Development :: Testing",
|
|
15
|
+
]
|
|
16
|
+
dependencies = [
|
|
17
|
+
"pydantic >= 2.7", # Data (config) validation
|
|
18
|
+
"pytest",
|
|
19
|
+
"textual", # TUI library
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[dependency-groups]
|
|
23
|
+
test = [
|
|
24
|
+
"pytest-asyncio",
|
|
25
|
+
"pytest-xdist", # Parallel test execution
|
|
26
|
+
"ruff", # Linting and formatting
|
|
27
|
+
"taplo", # Validate and format TOML
|
|
28
|
+
"ty", # Type checking and LSP
|
|
29
|
+
]
|
|
30
|
+
dev = [
|
|
31
|
+
"icecream", # Better debugging
|
|
32
|
+
"textual-dev",
|
|
33
|
+
"tomlkit", # Write TOML files
|
|
34
|
+
{ include-group = "test" },
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
pytest-tui-run = "pytest_tui_run.cli:main"
|
|
39
|
+
|
|
40
|
+
# Register this as a pytest plugin
|
|
41
|
+
[project.entry-points.pytest11]
|
|
42
|
+
tui-run = "pytest_tui_run.plugin"
|
|
43
|
+
|
|
44
|
+
[tool.pytest]
|
|
45
|
+
testpaths = ["pytest_tui_run", "tests"]
|
|
46
|
+
addopts = ["--doctest-modules"]
|
|
47
|
+
doctest_optionflags = ["NORMALIZE_WHITESPACE", "IGNORE_EXCEPTION_DETAIL"]
|
|
48
|
+
|
|
49
|
+
[tool.ruff]
|
|
50
|
+
line-length = 88
|
|
51
|
+
|
|
52
|
+
[tool.ruff.format]
|
|
53
|
+
docstring-code-format = true
|
|
54
|
+
line-ending = "lf"
|
|
55
|
+
|
|
56
|
+
[tool.ruff.lint]
|
|
57
|
+
select = [
|
|
58
|
+
"ANN", # flake8-annotations
|
|
59
|
+
"B", # flake8-bugbear
|
|
60
|
+
"E", # pycodestyle
|
|
61
|
+
"F", # Pyflakes
|
|
62
|
+
"I", # isort
|
|
63
|
+
"SIM", # flake8-simplify
|
|
64
|
+
"UP", # pyupgrade
|
|
65
|
+
]
|
|
66
|
+
ignore = [
|
|
67
|
+
"E501", # line-too-long
|
|
68
|
+
"SIM108", # if-else-block-instead-of-if-exp
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
[build-system]
|
|
72
|
+
requires = ["uv_build>=0.11.11,<0.12"]
|
|
73
|
+
build-backend = "uv_build"
|
|
74
|
+
|
|
75
|
+
[tool.uv.build-backend]
|
|
76
|
+
# Need to configure these for a non-src layout
|
|
77
|
+
module-name = "pytest_tui_run"
|
|
78
|
+
module-root = ""
|
|
79
|
+
|
|
80
|
+
[[tool.uv.index]]
|
|
81
|
+
name = "testpypi"
|
|
82
|
+
url = "https://test.pypi.org/simple/"
|
|
83
|
+
publish-url = "https://test.pypi.org/legacy/"
|
|
84
|
+
explicit = true
|
|
File without changes
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import TYPE_CHECKING, ClassVar
|
|
5
|
+
|
|
6
|
+
from rich.text import Text
|
|
7
|
+
from textual import work
|
|
8
|
+
from textual.app import App, ComposeResult
|
|
9
|
+
from textual.binding import Binding, BindingType
|
|
10
|
+
from textual.containers import Vertical
|
|
11
|
+
from textual.css.query import NoMatches
|
|
12
|
+
from textual.driver import Driver
|
|
13
|
+
from textual.types import CSSPathType
|
|
14
|
+
from textual.widgets import Footer, HelpPanel, RichLog
|
|
15
|
+
|
|
16
|
+
from .constants import WidgetID
|
|
17
|
+
from .filter_modal import (
|
|
18
|
+
FilterModal,
|
|
19
|
+
filter_modal_cls_factory,
|
|
20
|
+
)
|
|
21
|
+
from .pytest_utils import PyTestConfig
|
|
22
|
+
from .tree import PyTestTree, py_test_tree_cls_factory
|
|
23
|
+
from .tree_node_data import PyTestNodeIDParser
|
|
24
|
+
from .types import PyTestNodeID
|
|
25
|
+
from .utils import SubprocessRunner, parse_pytest_collect_output_line
|
|
26
|
+
from .vim_log import vim_log_cls_factory
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from .config import Config
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TreeApp(App, inherit_bindings=False):
|
|
33
|
+
"""TUI for viewing, selecting, and running pytest tests."""
|
|
34
|
+
|
|
35
|
+
BINDINGS: ClassVar[list[BindingType]] = [
|
|
36
|
+
Binding(
|
|
37
|
+
"s", "toggle_output", "Output", tooltip="Show/hide pytest output", show=True
|
|
38
|
+
),
|
|
39
|
+
Binding("f5", "refresh", "Refresh", tooltip="Refresh tests", show=False),
|
|
40
|
+
Binding(
|
|
41
|
+
"q,ctrl+q",
|
|
42
|
+
"quit",
|
|
43
|
+
"Quit",
|
|
44
|
+
tooltip="Quit the app",
|
|
45
|
+
show=True,
|
|
46
|
+
priority=True,
|
|
47
|
+
),
|
|
48
|
+
Binding(
|
|
49
|
+
"question_mark",
|
|
50
|
+
"toggle_help_panel",
|
|
51
|
+
"Help",
|
|
52
|
+
tooltip="Show/hide keybinds",
|
|
53
|
+
show=True,
|
|
54
|
+
),
|
|
55
|
+
Binding(
|
|
56
|
+
"escape",
|
|
57
|
+
"hide_unnecessary_widgets",
|
|
58
|
+
"Hide",
|
|
59
|
+
tooltip="Hide unnecessary widgets",
|
|
60
|
+
show=False,
|
|
61
|
+
),
|
|
62
|
+
]
|
|
63
|
+
DEFAULT_CSS = """
|
|
64
|
+
ScrollableContainer {
|
|
65
|
+
scrollbar-size-horizontal: 1;
|
|
66
|
+
scrollbar-size-vertical: 1;
|
|
67
|
+
}
|
|
68
|
+
"""
|
|
69
|
+
FILTER_MODAL_CLS: ClassVar[type[FilterModal]]
|
|
70
|
+
|
|
71
|
+
CONFIG: ClassVar[Config]
|
|
72
|
+
"""App configuration."""
|
|
73
|
+
|
|
74
|
+
pytest_config: PyTestConfig
|
|
75
|
+
"""Pytest configuration object."""
|
|
76
|
+
pytest_nodeid_parser: PyTestNodeIDParser
|
|
77
|
+
"""Pytest `nodeid` parser."""
|
|
78
|
+
tests: list[PyTestNodeID]
|
|
79
|
+
"""Collected pytest test `nodeid`s."""
|
|
80
|
+
_subprocess_runner: SubprocessRunner
|
|
81
|
+
"""Subprocess runner (for running pytest in a subprocess)."""
|
|
82
|
+
|
|
83
|
+
def __init__(
|
|
84
|
+
self,
|
|
85
|
+
# Custom arguments
|
|
86
|
+
pytest_args: list[str],
|
|
87
|
+
pytest_nodeid_parser: PyTestNodeIDParser,
|
|
88
|
+
# Inherited arguments
|
|
89
|
+
driver_class: type[Driver] | None = None,
|
|
90
|
+
css_path: CSSPathType | None = None,
|
|
91
|
+
watch_css: bool = False,
|
|
92
|
+
ansi_color: bool = False,
|
|
93
|
+
) -> None:
|
|
94
|
+
"""Create an app instance.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
pytest_args: Raw user-given arguments passed to pytest
|
|
98
|
+
pytest_nodeid_parser: Pytest `nodeid` parser
|
|
99
|
+
driver_class: See `textual.app.App` documentation
|
|
100
|
+
css_path: See `textual.app.App` documentation
|
|
101
|
+
watch_css: See `textual.app.App` documentation
|
|
102
|
+
ansi_color: See `textual.app.App` documentation
|
|
103
|
+
|
|
104
|
+
Raises:
|
|
105
|
+
CssPathError: See `textual.app.App` documentation
|
|
106
|
+
"""
|
|
107
|
+
super().__init__(driver_class, css_path, watch_css, ansi_color)
|
|
108
|
+
|
|
109
|
+
self.pytest_config = PyTestConfig.from_args(pytest_args)
|
|
110
|
+
self.pytest_nodeid_parser = pytest_nodeid_parser
|
|
111
|
+
|
|
112
|
+
self.tests = []
|
|
113
|
+
self._pytest_collect_proc = None
|
|
114
|
+
self._subprocess_runner = SubprocessRunner()
|
|
115
|
+
|
|
116
|
+
def _build_tree(self, pytest_nodeids: list[PyTestNodeID]) -> None:
|
|
117
|
+
"""Build the tree."""
|
|
118
|
+
tree = self.query_one(PyTestTree)
|
|
119
|
+
tree.clear()
|
|
120
|
+
tree.build(pytest_nodeids, self.pytest_nodeid_parser)
|
|
121
|
+
tree.root.expand_all()
|
|
122
|
+
|
|
123
|
+
def action_toggle_output(self) -> None:
|
|
124
|
+
"""Toggle output widget visibility."""
|
|
125
|
+
output = self.query_one(WidgetID.output.query)
|
|
126
|
+
output.display = not output.display
|
|
127
|
+
|
|
128
|
+
def _finish_refresh(self) -> None:
|
|
129
|
+
"""Finish refresh action."""
|
|
130
|
+
tree = self.query_one(PyTestTree)
|
|
131
|
+
output = self.query_one(WidgetID.output.query)
|
|
132
|
+
|
|
133
|
+
tree.set_loading(False)
|
|
134
|
+
# Force UI update
|
|
135
|
+
self.refresh()
|
|
136
|
+
output.display = False
|
|
137
|
+
if self.CONFIG.app.notifications.enable_test_collection:
|
|
138
|
+
self.notify("Test collection complete")
|
|
139
|
+
|
|
140
|
+
@work(thread=True)
|
|
141
|
+
def _pytest_collect_worker(self, args: list[str], output: RichLog) -> None:
|
|
142
|
+
"""Worker to collect pytest test `nodeid`s."""
|
|
143
|
+
|
|
144
|
+
def handle_output(output: RichLog, line: str) -> None:
|
|
145
|
+
output.write(Text.from_ansi(line))
|
|
146
|
+
nodeid = parse_pytest_collect_output_line(line)
|
|
147
|
+
if nodeid is not None:
|
|
148
|
+
self.tests.append(nodeid)
|
|
149
|
+
|
|
150
|
+
self.tests = []
|
|
151
|
+
self._subprocess_runner.run(args, output, handle_output)
|
|
152
|
+
|
|
153
|
+
self.call_from_thread(self._build_tree, self.tests)
|
|
154
|
+
self.call_from_thread(self._finish_refresh)
|
|
155
|
+
|
|
156
|
+
def _collect_and_build_tree(self) -> None:
|
|
157
|
+
"""Collect tests and build the tree."""
|
|
158
|
+
tree = self.query_one(PyTestTree)
|
|
159
|
+
output = self.query_one(WidgetID.output.query, RichLog)
|
|
160
|
+
|
|
161
|
+
if self.CONFIG.app.notifications.enable_test_collection:
|
|
162
|
+
self.notify("Collecting tests...")
|
|
163
|
+
tree.set_loading(True)
|
|
164
|
+
|
|
165
|
+
user_args = self.pytest_config.args
|
|
166
|
+
# Make sure we use the needed `-q` flag
|
|
167
|
+
user_args = [arg for arg in user_args if arg not in ["-v", "--verbose", "-vv"]]
|
|
168
|
+
args = ["pytest", "--collect-only", "-q", "--color=yes"] + user_args
|
|
169
|
+
|
|
170
|
+
output.clear()
|
|
171
|
+
output.display = True
|
|
172
|
+
cmd = f"> {' '.join(args)}\n"
|
|
173
|
+
output.write(Text(cmd))
|
|
174
|
+
|
|
175
|
+
self._pytest_collect_worker(args, output)
|
|
176
|
+
|
|
177
|
+
def refresh_tests(self) -> None:
|
|
178
|
+
"""Refresh tests: collect tests and build the tree."""
|
|
179
|
+
self._collect_and_build_tree()
|
|
180
|
+
|
|
181
|
+
def action_refresh(self) -> None:
|
|
182
|
+
"""Refresh tests: collect tests and build the tree."""
|
|
183
|
+
self.refresh_tests()
|
|
184
|
+
|
|
185
|
+
def is_help_panel_open(self) -> bool:
|
|
186
|
+
"""Is the help panel open?"""
|
|
187
|
+
try:
|
|
188
|
+
self.screen.query_one(HelpPanel)
|
|
189
|
+
return True
|
|
190
|
+
except NoMatches:
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
def action_toggle_help_panel(self) -> None:
|
|
194
|
+
"""Toggle help panel visibility."""
|
|
195
|
+
if self.is_help_panel_open():
|
|
196
|
+
self.action_hide_help_panel()
|
|
197
|
+
else:
|
|
198
|
+
self.action_show_help_panel()
|
|
199
|
+
|
|
200
|
+
def action_hide_unnecessary_widgets(self) -> None:
|
|
201
|
+
"""Hide all unnecessary widgets."""
|
|
202
|
+
self.action_hide_help_panel()
|
|
203
|
+
self.query_one(WidgetID.output.query).display = False
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def tree_app_cls_factory(config: Config) -> type[TreeApp]:
|
|
207
|
+
"""Create a `TreeApp` class with the given config."""
|
|
208
|
+
|
|
209
|
+
class ConfiguredTreeApp(
|
|
210
|
+
TreeApp, inherit_bindings=config.app.inherit_default_bindings
|
|
211
|
+
):
|
|
212
|
+
BINDINGS = config.app.bindings
|
|
213
|
+
CONFIG = config
|
|
214
|
+
FILTER_MODAL_CLS = filter_modal_cls_factory(config.filter_modal)
|
|
215
|
+
|
|
216
|
+
def compose(self) -> ComposeResult:
|
|
217
|
+
with Vertical():
|
|
218
|
+
tree_cls = py_test_tree_cls_factory(config.tree)
|
|
219
|
+
tree = tree_cls(label=os.getcwd())
|
|
220
|
+
yield tree
|
|
221
|
+
|
|
222
|
+
log_cls = vim_log_cls_factory(config.output)
|
|
223
|
+
output = log_cls(id=WidgetID.output, markup=True, wrap=True)
|
|
224
|
+
output.border_title = "Output"
|
|
225
|
+
yield output
|
|
226
|
+
yield Footer()
|
|
227
|
+
|
|
228
|
+
def on_mount(self) -> None:
|
|
229
|
+
if config.app.notifications.enable_config_loading:
|
|
230
|
+
if config.found:
|
|
231
|
+
self.notify(f"Config loaded from `{config.path}`")
|
|
232
|
+
else:
|
|
233
|
+
self.notify(f"No config found at `{config.path}`; using defaults")
|
|
234
|
+
self._collect_and_build_tree()
|
|
235
|
+
|
|
236
|
+
return ConfiguredTreeApp
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import shutil
|
|
3
|
+
import sys
|
|
4
|
+
import textwrap
|
|
5
|
+
from importlib.metadata import version
|
|
6
|
+
|
|
7
|
+
from .app import TreeApp, tree_app_cls_factory
|
|
8
|
+
from .config import Config
|
|
9
|
+
from .constants import APP_NAME, PackageConfigPaths, UserConfigPaths
|
|
10
|
+
from .tree_node_data import PyTestNodeIDParser
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def init_user_config(overwrite: bool = False) -> None:
|
|
14
|
+
"""Initialize user config: copy default config and symlink schema."""
|
|
15
|
+
|
|
16
|
+
config_dir = UserConfigPaths.config_dir()
|
|
17
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
18
|
+
|
|
19
|
+
pkg_paths = PackageConfigPaths()
|
|
20
|
+
print(f"Found package installation directory: {pkg_paths.pkg_dir}")
|
|
21
|
+
initialized = False
|
|
22
|
+
|
|
23
|
+
# Copy the default config
|
|
24
|
+
config_src = pkg_paths.config_file
|
|
25
|
+
config_dst = UserConfigPaths.config_file()
|
|
26
|
+
print(f"Copying config to: {config_dst}")
|
|
27
|
+
if not config_dst.exists() or overwrite:
|
|
28
|
+
shutil.copy2(config_src, config_dst)
|
|
29
|
+
initialized = True
|
|
30
|
+
else:
|
|
31
|
+
print("Config already exists, overwrite? [y/N]")
|
|
32
|
+
if input().lower() == "y":
|
|
33
|
+
shutil.copy2(config_src, config_dst)
|
|
34
|
+
initialized = True
|
|
35
|
+
|
|
36
|
+
# Symlink the schema
|
|
37
|
+
schema_src = pkg_paths.schema_file
|
|
38
|
+
schema_dst = UserConfigPaths.schema_file()
|
|
39
|
+
print(f"Symlinking schema to: {schema_dst}")
|
|
40
|
+
if not schema_dst.exists() or overwrite:
|
|
41
|
+
schema_dst.unlink()
|
|
42
|
+
schema_dst.symlink_to(schema_src)
|
|
43
|
+
initialized = True
|
|
44
|
+
else:
|
|
45
|
+
print("Schema already exists, overwrite? [y/N]")
|
|
46
|
+
if input().lower() == "y":
|
|
47
|
+
schema_dst.unlink()
|
|
48
|
+
schema_dst.symlink_to(schema_src)
|
|
49
|
+
initialized = True
|
|
50
|
+
|
|
51
|
+
if initialized:
|
|
52
|
+
print(f"Config/schema initialized at: {config_dir}")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def create_parser() -> argparse.ArgumentParser:
|
|
56
|
+
"""Create the argument parser for the app."""
|
|
57
|
+
parser = argparse.ArgumentParser(
|
|
58
|
+
prog=APP_NAME,
|
|
59
|
+
description="TUI for running pytest tests",
|
|
60
|
+
epilog=textwrap.dedent("""
|
|
61
|
+
All unrecognized arguments are passed through to pytest.
|
|
62
|
+
|
|
63
|
+
Examples:
|
|
64
|
+
%(prog)s -v # Run with verbose pytest output
|
|
65
|
+
%(prog)s tests/test_mod.py # Run specific test file
|
|
66
|
+
%(prog)s -k test_foo -m slow # Filter with pytest -k and -m flags
|
|
67
|
+
|
|
68
|
+
You can also use it as a pytest plugin:
|
|
69
|
+
pytest --tui-run
|
|
70
|
+
|
|
71
|
+
For pytest help:
|
|
72
|
+
pytest --help
|
|
73
|
+
"""),
|
|
74
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
75
|
+
)
|
|
76
|
+
parser.add_argument(
|
|
77
|
+
"-V",
|
|
78
|
+
"--version",
|
|
79
|
+
action="store_true",
|
|
80
|
+
help="Show version and exit",
|
|
81
|
+
)
|
|
82
|
+
parser.add_argument(
|
|
83
|
+
"--init-config",
|
|
84
|
+
action="store_true",
|
|
85
|
+
help="Initialize config: create config directory, and copy default config "
|
|
86
|
+
"and symlink its schema there",
|
|
87
|
+
)
|
|
88
|
+
parser.add_argument(
|
|
89
|
+
"--finit-config",
|
|
90
|
+
action="store_true",
|
|
91
|
+
help="Forced --init-config: overwrites existing config and schema if present",
|
|
92
|
+
)
|
|
93
|
+
return parser
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def main_app(
|
|
97
|
+
config: Config | None = None, argv: list[str] | None = None
|
|
98
|
+
) -> TreeApp | None:
|
|
99
|
+
"""Main (CLI) entry point to the TUI app, without running it."""
|
|
100
|
+
if argv is None:
|
|
101
|
+
argv = sys.argv[1:]
|
|
102
|
+
parser = create_parser()
|
|
103
|
+
args, pytest_args = parser.parse_known_args(argv)
|
|
104
|
+
|
|
105
|
+
if args.version:
|
|
106
|
+
print(APP_NAME, version(APP_NAME))
|
|
107
|
+
return
|
|
108
|
+
if args.init_config or args.finit_config:
|
|
109
|
+
init_user_config(overwrite=args.finit_config)
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
if config is None:
|
|
113
|
+
config = Config.from_file()
|
|
114
|
+
app_cls = tree_app_cls_factory(config)
|
|
115
|
+
nodeid_parser = PyTestNodeIDParser(config.tree.nodes)
|
|
116
|
+
app = app_cls(pytest_args, nodeid_parser)
|
|
117
|
+
return app
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def main(config: Config | None = None, argv: list[str] | None = None) -> None:
|
|
121
|
+
"""Main (CLI) entry point to the TUI app."""
|
|
122
|
+
app = main_app(config, argv)
|
|
123
|
+
if app is None:
|
|
124
|
+
return
|
|
125
|
+
app.run()
|