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.
@@ -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
+ ![pytest-tui-run in action](https://gitlab.com/clov/pytest-tui-run/-/raw/main/screenshot.png)
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
+ ![pytest-tui-run in action](https://gitlab.com/clov/pytest-tui-run/-/raw/main/screenshot.png)
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,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -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()