testpick 0.2.0__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.
Files changed (44) hide show
  1. testpick-0.2.0/.github/workflows/ci.yml +85 -0
  2. testpick-0.2.0/.gitignore +35 -0
  3. testpick-0.2.0/CHANGELOG.md +32 -0
  4. testpick-0.2.0/LICENSE +21 -0
  5. testpick-0.2.0/PKG-INFO +211 -0
  6. testpick-0.2.0/README.md +173 -0
  7. testpick-0.2.0/pyproject.toml +86 -0
  8. testpick-0.2.0/src/testpick/__init__.py +10 -0
  9. testpick-0.2.0/src/testpick/__main__.py +3 -0
  10. testpick-0.2.0/src/testpick/cli.py +228 -0
  11. testpick-0.2.0/src/testpick/git/__init__.py +21 -0
  12. testpick-0.2.0/src/testpick/git/commit.py +28 -0
  13. testpick-0.2.0/src/testpick/git/diff.py +59 -0
  14. testpick-0.2.0/src/testpick/git/interactive.py +106 -0
  15. testpick-0.2.0/src/testpick/graph/__init__.py +7 -0
  16. testpick-0.2.0/src/testpick/graph/builder.py +201 -0
  17. testpick-0.2.0/src/testpick/graph/edge.py +13 -0
  18. testpick-0.2.0/src/testpick/graph/node.py +16 -0
  19. testpick-0.2.0/src/testpick/parser/__init__.py +18 -0
  20. testpick-0.2.0/src/testpick/parser/base.py +46 -0
  21. testpick-0.2.0/src/testpick/parser/python.py +225 -0
  22. testpick-0.2.0/src/testpick/parser/registry.py +24 -0
  23. testpick-0.2.0/src/testpick/parser/typescript.py +207 -0
  24. testpick-0.2.0/src/testpick/reporter/__init__.py +7 -0
  25. testpick-0.2.0/src/testpick/reporter/github.py +58 -0
  26. testpick-0.2.0/src/testpick/reporter/json_reporter.py +49 -0
  27. testpick-0.2.0/src/testpick/reporter/terminal.py +108 -0
  28. testpick-0.2.0/src/testpick/selector/__init__.py +6 -0
  29. testpick-0.2.0/src/testpick/selector/matcher.py +58 -0
  30. testpick-0.2.0/src/testpick/selector/walker.py +42 -0
  31. testpick-0.2.0/tests/conftest.py +0 -0
  32. testpick-0.2.0/tests/test_cli.py +85 -0
  33. testpick-0.2.0/tests/test_git/__init__.py +0 -0
  34. testpick-0.2.0/tests/test_git/test_commit.py +75 -0
  35. testpick-0.2.0/tests/test_git/test_diff.py +78 -0
  36. testpick-0.2.0/tests/test_git/test_interactive.py +5 -0
  37. testpick-0.2.0/tests/test_graph/__init__.py +0 -0
  38. testpick-0.2.0/tests/test_graph/test_builder.py +221 -0
  39. testpick-0.2.0/tests/test_parser/__init__.py +0 -0
  40. testpick-0.2.0/tests/test_parser/test_python.py +164 -0
  41. testpick-0.2.0/tests/test_parser/test_typescript.py +158 -0
  42. testpick-0.2.0/tests/test_selection_pipeline.py +218 -0
  43. testpick-0.2.0/tests/test_selector/__init__.py +1 -0
  44. testpick-0.2.0/tests/test_selector/test_matcher.py +126 -0
@@ -0,0 +1,85 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ tags:
7
+ - 'v*'
8
+ pull_request:
9
+ workflow_dispatch:
10
+ inputs:
11
+ tag:
12
+ description: 'Tag to release (e.g., v0.3.0)'
13
+ required: true
14
+
15
+ jobs:
16
+ lint:
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ - uses: actions/setup-python@v5
21
+ with:
22
+ python-version: "3.12"
23
+ - run: pip install -e ".[dev,tree-sitter]"
24
+ - run: ruff check .
25
+ - run: mypy
26
+
27
+ test:
28
+ runs-on: ${{ matrix.os }}
29
+ strategy:
30
+ fail-fast: false
31
+ matrix:
32
+ os: [ubuntu-latest]
33
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
34
+ include:
35
+ - os: windows-latest
36
+ python-version: "3.12"
37
+ - os: macos-latest
38
+ python-version: "3.12"
39
+ steps:
40
+ - uses: actions/checkout@v4
41
+ - uses: actions/setup-python@v5
42
+ with:
43
+ python-version: ${{ matrix.python-version }}
44
+ - run: pip install -e ".[dev,tree-sitter]"
45
+ - run: pytest tests/ --cov=testpick --cov-report=term-missing
46
+
47
+ build:
48
+ runs-on: ubuntu-latest
49
+ steps:
50
+ - uses: actions/checkout@v4
51
+ - uses: actions/setup-python@v5
52
+ with:
53
+ python-version: "3.12"
54
+ - run: pip install build
55
+ - run: python -m build
56
+
57
+ - name: Upload Build Artifacts
58
+ uses: actions/upload-artifact@v4
59
+ with:
60
+ name: python-package-distributions
61
+ path: dist/
62
+
63
+ release:
64
+ needs: [lint, test, build]
65
+ if: startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && github.event.inputs.tag)
66
+ runs-on: ubuntu-latest
67
+ permissions:
68
+ contents: write
69
+ id-token: write
70
+ steps:
71
+ - name: Download Build Artifacts
72
+ uses: actions/download-artifact@v4
73
+ with:
74
+ name: python-package-distributions
75
+ path: dist/
76
+
77
+ - name: Create GitHub Release
78
+ uses: softprops/action-gh-release@v2
79
+ with:
80
+ tag_name: ${{ github.event.inputs.tag || github.ref_name }}
81
+ files: dist/*
82
+ generate_release_notes: true
83
+
84
+ - name: Publish to PyPI
85
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,35 @@
1
+ # Python
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ *.egg-info/
6
+ .eggs/
7
+ dist/
8
+ build/
9
+ *.whl
10
+
11
+ # Virtual environments
12
+ .venv/
13
+ venv/
14
+ ENV/
15
+
16
+ # IDE
17
+ .vscode/
18
+ .idea/
19
+
20
+ # OS
21
+ .DS_Store
22
+ Thumbs.db
23
+
24
+ # Test
25
+ .pytest_cache/
26
+ .mypy_cache/
27
+ .coverage
28
+ htmlcov/
29
+
30
+ # Local tool state
31
+ .freebuff/
32
+
33
+ # Misc
34
+ *.log
35
+ *.tgz
@@ -0,0 +1,32 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0
4
+
5
+ ### Fixed
6
+
7
+ - TypeScript and JavaScript imports now produce real graph edges. Previously only
8
+ Python files were ever linked, so chains came back empty on JS/TS projects.
9
+ - Relative specifiers starting with `./` or `../` no longer resolve against the
10
+ filesystem root.
11
+ - `--commit` analyzes the commit you pass; `--base <branch>` compares against the
12
+ branch point instead of the working tree.
13
+ - The `tree-sitter` extra installs the grammars the parsers actually import, so AST
14
+ parsing activates instead of silently falling back to regex.
15
+ - Dependency chains in terminal, JSON and GitHub output are real paths through the
16
+ graph rather than a copy of the changed-file list.
17
+ - Renamed files no longer report a mangled `old<TAB>new` path, and deleted tests are
18
+ never selected.
19
+ - JSON and GitHub Actions output use forward slashes, so paths work on Windows
20
+ runners.
21
+ - Everything resolves from the repository root, so the command works from any
22
+ subdirectory.
23
+
24
+ ### Added
25
+
26
+ - CI: ruff, mypy and pytest on Python 3.10-3.13 across Linux, macOS and Windows,
27
+ plus a package build check.
28
+ - End-to-end tests for the selection pipeline (77 tests, 82% coverage).
29
+
30
+ ### Removed
31
+
32
+ - Unused `--dry-run` option and the unwired `SymbolTracker`.
testpick-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 krauzX
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,211 @@
1
+ Metadata-Version: 2.5
2
+ Name: testpick
3
+ Version: 0.2.0
4
+ Summary: Pick the tests affected by your git diff before you run them
5
+ Project-URL: Homepage, https://github.com/krauzX/testpick
6
+ Project-URL: Repository, https://github.com/krauzX/testpick
7
+ Project-URL: Issues, https://github.com/krauzX/testpick/issues
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: ci,git,pytest,static-analysis,testing
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development :: Testing
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: click>=8.0
24
+ Requires-Dist: gitpython>=3.1
25
+ Requires-Dist: readchar>=4.0
26
+ Requires-Dist: rich>=13.0
27
+ Provides-Extra: dev
28
+ Requires-Dist: mypy>=1.10; extra == 'dev'
29
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
30
+ Requires-Dist: pytest>=8.0; extra == 'dev'
31
+ Requires-Dist: ruff>=0.5; extra == 'dev'
32
+ Provides-Extra: tree-sitter
33
+ Requires-Dist: tree-sitter-javascript>=0.23; extra == 'tree-sitter'
34
+ Requires-Dist: tree-sitter-python>=0.23; extra == 'tree-sitter'
35
+ Requires-Dist: tree-sitter-typescript>=0.23; extra == 'tree-sitter'
36
+ Requires-Dist: tree-sitter>=0.25; extra == 'tree-sitter'
37
+ Description-Content-Type: text/markdown
38
+
39
+ # testpick
40
+
41
+ [![CI](https://github.com/krauzX/testpick/actions/workflows/ci.yml/badge.svg)](https://github.com/krauzX/testpick/actions/workflows/ci.yml)
42
+
43
+ A Python CLI that looks at your git diff and tells you which tests are going to break. Before you run them.
44
+
45
+ ## Why this exists
46
+
47
+ You change a function in `utils.py`. Now what? You could run the full test suite and wait, or you could guess
48
+ which tests matter and hope you're right. `pytest --lf` reruns failures only after they've already failed.
49
+ Jest's `--changedSince` follows direct file changes.
50
+
51
+ testpick parses your git diff, builds an import graph of your source files, and walks it backwards from what
52
+ you changed to find every test that depends on it — transitively. Each selected test comes with the chain that
53
+ explains why, so you can sanity-check the answer instead of trusting it.
54
+
55
+ ## Getting started
56
+
57
+ ```bash
58
+ git clone https://github.com/krauzX/testpick.git
59
+ cd testpick
60
+ pip install -e .
61
+
62
+ # optional but recommended: real AST parsing instead of the regex fallback
63
+ pip install -e ".[tree-sitter]"
64
+
65
+ # for development (tests, linter, type checker)
66
+ pip install -e ".[dev]"
67
+ ```
68
+
69
+ Python 3.10+, runs on Linux, macOS and Windows.
70
+
71
+ ## Usage
72
+
73
+ ```bash
74
+ # what changed since your last commit, including uncommitted work?
75
+ testpick
76
+
77
+ # committed changes on your branch since it diverged from main
78
+ testpick --base main
79
+
80
+ # a single commit
81
+ testpick --commit abc123
82
+
83
+ # output for scripts and CI
84
+ testpick --format json
85
+ testpick --format github # GitHub Actions annotations
86
+
87
+ # only analyze Python and TypeScript files
88
+ testpick --languages python,typescript
89
+ ```
90
+
91
+ Everything resolves from the repository root, so it works from any directory inside the repo.
92
+
93
+ ### Example output
94
+
95
+ ```
96
+ ┌─────────────────────────────────────────────────────────────────────────────┐
97
+ │ testpick - Test Selection Results │
98
+ └─────────────────────────────────────────────────────────────────────────────┘
99
+
100
+ Changed Files
101
+ ┌────────────┬────────────────────────────────────────┐
102
+ │ Status │ File │
103
+ ├────────────┼────────────────────────────────────────┤
104
+ │ Modified │ src/testpick/graph/builder.py │
105
+ │ Modified │ src/testpick/reporter/github.py │
106
+ │ Modified │ src/testpick/reporter/json_reporter.py │
107
+ │ Added │ tests/test_selection_pipeline.py │
108
+ └────────────┴────────────────────────────────────────┘
109
+
110
+ Selected Tests
111
+ ┌───────┬──────────────────────────────────┬──────────────────────────────────┐
112
+ │ # │ Test File │ Dependency Path │
113
+ ├───────┼──────────────────────────────────┼──────────────────────────────────┤
114
+ │ 1 │ tests/test_cli.py │ tests/test_cli.py -> │
115
+ │ │ │ src/testpick/cli.py -> │
116
+ │ │ │ src/testpick/graph/__init__.py │
117
+ │ │ │ -> src/testpick/graph/builder.py │
118
+ │ 2 │ tests/test_graph/test_builder.py │ tests/test_graph/test_builder.py │
119
+ │ │ │ -> │
120
+ │ │ │ src/testpick/graph/__init__.py │
121
+ │ │ │ -> src/testpick/graph/builder.py │
122
+ │ 3 │ tests/test_selection_pipeline.py │ direct │
123
+ │ 4 │ tests/test_selector/test_matche… │ tests/test_selector/test_matche… │
124
+ │ │ │ -> │
125
+ │ │ │ src/testpick/graph/__init__.py │
126
+ │ │ │ -> src/testpick/graph/builder.py │
127
+ └───────┴──────────────────────────────────┴──────────────────────────────────┘
128
+
129
+ Statistics
130
+ ┌────────────────┬─────┐
131
+ │ Files Changed │ 4 │
132
+ │ Tests Selected │ 4 │
133
+ │ Insertions │ 258 │
134
+ │ Deletions │ 25 │
135
+ └────────────────┴─────┘
136
+ ```
137
+
138
+ `direct` means the test file itself changed. The JSON format carries the same
139
+ information, with chains as arrays:
140
+
141
+ ```json
142
+ {
143
+ "changed_files": [
144
+ { "status": "M", "path": "src/utils.ts" }
145
+ ],
146
+ "test_files": [
147
+ {
148
+ "path": "tests/test_page.ts",
149
+ "dependency_chain": ["tests/test_page.ts", "src/page.ts", "src/utils.ts"]
150
+ }
151
+ ],
152
+ "stats": {
153
+ "files_changed": 1,
154
+ "insertions": 6,
155
+ "deletions": 2,
156
+ "tests_selected": 1
157
+ }
158
+ }
159
+ ```
160
+
161
+ ## How it works
162
+
163
+ 1. Read the diff with `git diff --name-status` and resolve every path against the repository root.
164
+ 2. Parse each source file for imports. tree-sitter grammars are used when installed, otherwise a regex pass
165
+ handles Python, TypeScript and JavaScript.
166
+ 3. Resolve each import to a file: relative specifiers against the importing file's directory, absolute ones
167
+ against `src/`, the project root, then the file's own directory — preferring the importing file's language
168
+ when both a `.py` and a `.ts` file would match.
169
+ 4. Walk reverse edges from the changed files with a breadth-first search, recording the parent of every hit so
170
+ the chain can be explained afterwards.
171
+ 5. Report the files that are test files, direct changes first.
172
+
173
+ ## How it compares
174
+
175
+ Other tools pick tests from a diff too; they differ in how far they can see:
176
+
177
+ - **pytest-testmon** records which code each test actually executed, so it catches runtime coupling a static
178
+ graph misses — but it needs a baseline run and is pytest-only.
179
+ - **Jest `--changedSince`** maps changed files to tests, but inside a Jest project and mostly direct
180
+ relationships.
181
+ - **Nx and Turborepo affected tasks** pick packages to rebuild, which is coarser than picking individual tests.
182
+ - **snob, pytest-impacted, pytest-git-selector** do static graph selection like this one, but for Python only.
183
+
184
+ testpick's corner: one CLI covering Python and TypeScript/JavaScript, no baseline run required, and every
185
+ selection arrives with the chain that produced it.
186
+
187
+ ## Limitations
188
+
189
+ Static analysis only sees imports it can read:
190
+
191
+ - Dynamic imports, dependency injection, plugin registries and other runtime wiring are invisible to it.
192
+ - A file deleted from the working tree is gone from the parse tree, so testpick reports the deletion but
193
+ cannot trace what used to import it.
194
+ - `--languages` scopes which files are analyzed for dependencies; changed files are always reported.
195
+
196
+ Treat the output as a starting point. A full suite run still belongs in CI before a release.
197
+
198
+ ## Running tests
199
+
200
+ ```bash
201
+ pytest tests/ -v
202
+ pytest tests/ --cov=testpick
203
+ ```
204
+
205
+ 77 tests covering diff parsing, import resolution, graph building, the selection pipeline and all three
206
+ reporters, with 82% branch coverage. CI runs ruff, mypy and the suite on Python 3.10-3.13 across Linux, macOS
207
+ and Windows.
208
+
209
+ ## License
210
+
211
+ MIT
@@ -0,0 +1,173 @@
1
+ # testpick
2
+
3
+ [![CI](https://github.com/krauzX/testpick/actions/workflows/ci.yml/badge.svg)](https://github.com/krauzX/testpick/actions/workflows/ci.yml)
4
+
5
+ A Python CLI that looks at your git diff and tells you which tests are going to break. Before you run them.
6
+
7
+ ## Why this exists
8
+
9
+ You change a function in `utils.py`. Now what? You could run the full test suite and wait, or you could guess
10
+ which tests matter and hope you're right. `pytest --lf` reruns failures only after they've already failed.
11
+ Jest's `--changedSince` follows direct file changes.
12
+
13
+ testpick parses your git diff, builds an import graph of your source files, and walks it backwards from what
14
+ you changed to find every test that depends on it — transitively. Each selected test comes with the chain that
15
+ explains why, so you can sanity-check the answer instead of trusting it.
16
+
17
+ ## Getting started
18
+
19
+ ```bash
20
+ git clone https://github.com/krauzX/testpick.git
21
+ cd testpick
22
+ pip install -e .
23
+
24
+ # optional but recommended: real AST parsing instead of the regex fallback
25
+ pip install -e ".[tree-sitter]"
26
+
27
+ # for development (tests, linter, type checker)
28
+ pip install -e ".[dev]"
29
+ ```
30
+
31
+ Python 3.10+, runs on Linux, macOS and Windows.
32
+
33
+ ## Usage
34
+
35
+ ```bash
36
+ # what changed since your last commit, including uncommitted work?
37
+ testpick
38
+
39
+ # committed changes on your branch since it diverged from main
40
+ testpick --base main
41
+
42
+ # a single commit
43
+ testpick --commit abc123
44
+
45
+ # output for scripts and CI
46
+ testpick --format json
47
+ testpick --format github # GitHub Actions annotations
48
+
49
+ # only analyze Python and TypeScript files
50
+ testpick --languages python,typescript
51
+ ```
52
+
53
+ Everything resolves from the repository root, so it works from any directory inside the repo.
54
+
55
+ ### Example output
56
+
57
+ ```
58
+ ┌─────────────────────────────────────────────────────────────────────────────┐
59
+ │ testpick - Test Selection Results │
60
+ └─────────────────────────────────────────────────────────────────────────────┘
61
+
62
+ Changed Files
63
+ ┌────────────┬────────────────────────────────────────┐
64
+ │ Status │ File │
65
+ ├────────────┼────────────────────────────────────────┤
66
+ │ Modified │ src/testpick/graph/builder.py │
67
+ │ Modified │ src/testpick/reporter/github.py │
68
+ │ Modified │ src/testpick/reporter/json_reporter.py │
69
+ │ Added │ tests/test_selection_pipeline.py │
70
+ └────────────┴────────────────────────────────────────┘
71
+
72
+ Selected Tests
73
+ ┌───────┬──────────────────────────────────┬──────────────────────────────────┐
74
+ │ # │ Test File │ Dependency Path │
75
+ ├───────┼──────────────────────────────────┼──────────────────────────────────┤
76
+ │ 1 │ tests/test_cli.py │ tests/test_cli.py -> │
77
+ │ │ │ src/testpick/cli.py -> │
78
+ │ │ │ src/testpick/graph/__init__.py │
79
+ │ │ │ -> src/testpick/graph/builder.py │
80
+ │ 2 │ tests/test_graph/test_builder.py │ tests/test_graph/test_builder.py │
81
+ │ │ │ -> │
82
+ │ │ │ src/testpick/graph/__init__.py │
83
+ │ │ │ -> src/testpick/graph/builder.py │
84
+ │ 3 │ tests/test_selection_pipeline.py │ direct │
85
+ │ 4 │ tests/test_selector/test_matche… │ tests/test_selector/test_matche… │
86
+ │ │ │ -> │
87
+ │ │ │ src/testpick/graph/__init__.py │
88
+ │ │ │ -> src/testpick/graph/builder.py │
89
+ └───────┴──────────────────────────────────┴──────────────────────────────────┘
90
+
91
+ Statistics
92
+ ┌────────────────┬─────┐
93
+ │ Files Changed │ 4 │
94
+ │ Tests Selected │ 4 │
95
+ │ Insertions │ 258 │
96
+ │ Deletions │ 25 │
97
+ └────────────────┴─────┘
98
+ ```
99
+
100
+ `direct` means the test file itself changed. The JSON format carries the same
101
+ information, with chains as arrays:
102
+
103
+ ```json
104
+ {
105
+ "changed_files": [
106
+ { "status": "M", "path": "src/utils.ts" }
107
+ ],
108
+ "test_files": [
109
+ {
110
+ "path": "tests/test_page.ts",
111
+ "dependency_chain": ["tests/test_page.ts", "src/page.ts", "src/utils.ts"]
112
+ }
113
+ ],
114
+ "stats": {
115
+ "files_changed": 1,
116
+ "insertions": 6,
117
+ "deletions": 2,
118
+ "tests_selected": 1
119
+ }
120
+ }
121
+ ```
122
+
123
+ ## How it works
124
+
125
+ 1. Read the diff with `git diff --name-status` and resolve every path against the repository root.
126
+ 2. Parse each source file for imports. tree-sitter grammars are used when installed, otherwise a regex pass
127
+ handles Python, TypeScript and JavaScript.
128
+ 3. Resolve each import to a file: relative specifiers against the importing file's directory, absolute ones
129
+ against `src/`, the project root, then the file's own directory — preferring the importing file's language
130
+ when both a `.py` and a `.ts` file would match.
131
+ 4. Walk reverse edges from the changed files with a breadth-first search, recording the parent of every hit so
132
+ the chain can be explained afterwards.
133
+ 5. Report the files that are test files, direct changes first.
134
+
135
+ ## How it compares
136
+
137
+ Other tools pick tests from a diff too; they differ in how far they can see:
138
+
139
+ - **pytest-testmon** records which code each test actually executed, so it catches runtime coupling a static
140
+ graph misses — but it needs a baseline run and is pytest-only.
141
+ - **Jest `--changedSince`** maps changed files to tests, but inside a Jest project and mostly direct
142
+ relationships.
143
+ - **Nx and Turborepo affected tasks** pick packages to rebuild, which is coarser than picking individual tests.
144
+ - **snob, pytest-impacted, pytest-git-selector** do static graph selection like this one, but for Python only.
145
+
146
+ testpick's corner: one CLI covering Python and TypeScript/JavaScript, no baseline run required, and every
147
+ selection arrives with the chain that produced it.
148
+
149
+ ## Limitations
150
+
151
+ Static analysis only sees imports it can read:
152
+
153
+ - Dynamic imports, dependency injection, plugin registries and other runtime wiring are invisible to it.
154
+ - A file deleted from the working tree is gone from the parse tree, so testpick reports the deletion but
155
+ cannot trace what used to import it.
156
+ - `--languages` scopes which files are analyzed for dependencies; changed files are always reported.
157
+
158
+ Treat the output as a starting point. A full suite run still belongs in CI before a release.
159
+
160
+ ## Running tests
161
+
162
+ ```bash
163
+ pytest tests/ -v
164
+ pytest tests/ --cov=testpick
165
+ ```
166
+
167
+ 77 tests covering diff parsing, import resolution, graph building, the selection pipeline and all three
168
+ reporters, with 82% branch coverage. CI runs ruff, mypy and the suite on Python 3.10-3.13 across Linux, macOS
169
+ and Windows.
170
+
171
+ ## License
172
+
173
+ MIT
@@ -0,0 +1,86 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "testpick"
7
+ version = "0.2.0"
8
+ description = "Pick the tests affected by your git diff before you run them"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.10"
12
+ keywords = ["testing", "pytest", "git", "static-analysis", "ci"]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Environment :: Console",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Software Development :: Testing",
25
+ ]
26
+ dependencies = [
27
+ "click>=8.0",
28
+ "gitpython>=3.1",
29
+ "rich>=13.0",
30
+ "readchar>=4.0",
31
+ ]
32
+
33
+ [project.optional-dependencies]
34
+ tree-sitter = [
35
+ "tree-sitter>=0.25",
36
+ "tree-sitter-python>=0.23",
37
+ "tree-sitter-typescript>=0.23",
38
+ "tree-sitter-javascript>=0.23",
39
+ ]
40
+ dev = [
41
+ "pytest>=8.0",
42
+ "pytest-cov>=5.0",
43
+ "ruff>=0.5",
44
+ "mypy>=1.10",
45
+ ]
46
+
47
+ [project.urls]
48
+ Homepage = "https://github.com/krauzX/testpick"
49
+ Repository = "https://github.com/krauzX/testpick"
50
+ Issues = "https://github.com/krauzX/testpick/issues"
51
+
52
+ [project.scripts]
53
+ testpick = "testpick.cli:main"
54
+
55
+ [tool.hatch.build.targets.wheel]
56
+ packages = ["src/testpick"]
57
+
58
+ [tool.ruff]
59
+ line-length = 120
60
+ target-version = "py310"
61
+
62
+ [tool.ruff.lint]
63
+ select = ["E", "F", "W", "B"]
64
+
65
+ [tool.mypy]
66
+ python_version = "3.10"
67
+ files = ["src/testpick"]
68
+ check_untyped_defs = true
69
+ no_implicit_optional = true
70
+ warn_unused_ignores = true
71
+
72
+ # rich imports IPython, whose numpy stubs use syntax newer than our target
73
+ [[tool.mypy.overrides]]
74
+ module = ["IPython", "IPython.*", "numpy", "numpy.*"]
75
+ follow_imports = "skip"
76
+
77
+ [tool.pytest.ini_options]
78
+ testpaths = ["tests"]
79
+
80
+ [tool.coverage.run]
81
+ source = ["testpick"]
82
+ branch = true
83
+
84
+ [tool.coverage.report]
85
+ show_missing = true
86
+ skip_covered = true
@@ -0,0 +1,10 @@
1
+ """testpick - Select which tests to run based on code changes."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("testpick")
7
+ except PackageNotFoundError:
8
+ __version__ = "0.0.0"
9
+
10
+ __all__ = ["__version__"]
@@ -0,0 +1,3 @@
1
+ from testpick.cli import main
2
+
3
+ main()