typemut 0.1.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 (42) hide show
  1. typemut-0.1.0/.github/workflows/ci.yml +32 -0
  2. typemut-0.1.0/.github/workflows/publish.yml +71 -0
  3. typemut-0.1.0/.gitignore +11 -0
  4. typemut-0.1.0/Makefile +38 -0
  5. typemut-0.1.0/PKG-INFO +256 -0
  6. typemut-0.1.0/README.md +227 -0
  7. typemut-0.1.0/pyproject.toml +55 -0
  8. typemut-0.1.0/src/typemut/__init__.py +3 -0
  9. typemut-0.1.0/src/typemut/cli.py +247 -0
  10. typemut-0.1.0/src/typemut/config.py +62 -0
  11. typemut-0.1.0/src/typemut/db.py +156 -0
  12. typemut-0.1.0/src/typemut/discovery.py +201 -0
  13. typemut-0.1.0/src/typemut/engine.py +102 -0
  14. typemut-0.1.0/src/typemut/operators/__init__.py +42 -0
  15. typemut-0.1.0/src/typemut/operators/annotated.py +78 -0
  16. typemut-0.1.0/src/typemut/operators/base.py +39 -0
  17. typemut-0.1.0/src/typemut/operators/container.py +74 -0
  18. typemut-0.1.0/src/typemut/operators/literal.py +93 -0
  19. typemut-0.1.0/src/typemut/operators/optional.py +118 -0
  20. typemut-0.1.0/src/typemut/operators/sibling.py +50 -0
  21. typemut-0.1.0/src/typemut/operators/union.py +78 -0
  22. typemut-0.1.0/src/typemut/registry.py +137 -0
  23. typemut-0.1.0/src/typemut/reporting/__init__.py +0 -0
  24. typemut-0.1.0/src/typemut/reporting/html.py +468 -0
  25. typemut-0.1.0/src/typemut/reporting/terminal.py +74 -0
  26. typemut-0.1.0/tests/__init__.py +0 -0
  27. typemut-0.1.0/tests/conftest.py +14 -0
  28. typemut-0.1.0/tests/fixtures/generics.py +25 -0
  29. typemut-0.1.0/tests/fixtures/pydantic_models.py +29 -0
  30. typemut-0.1.0/tests/fixtures/simple_unions.py +19 -0
  31. typemut-0.1.0/tests/test_config.py +39 -0
  32. typemut-0.1.0/tests/test_db.py +74 -0
  33. typemut-0.1.0/tests/test_discovery.py +70 -0
  34. typemut-0.1.0/tests/test_operators/__init__.py +0 -0
  35. typemut-0.1.0/tests/test_operators/test_annotated.py +32 -0
  36. typemut-0.1.0/tests/test_operators/test_container.py +49 -0
  37. typemut-0.1.0/tests/test_operators/test_literal.py +41 -0
  38. typemut-0.1.0/tests/test_operators/test_optional.py +80 -0
  39. typemut-0.1.0/tests/test_operators/test_sibling.py +41 -0
  40. typemut-0.1.0/tests/test_operators/test_union.py +60 -0
  41. typemut-0.1.0/tests/test_registry.py +32 -0
  42. typemut-0.1.0/uv.lock +284 -0
@@ -0,0 +1,32 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python-version: ["3.11", "3.12", "3.13"]
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Install uv
20
+ uses: astral-sh/setup-uv@v4
21
+
22
+ - name: Set up Python ${{ matrix.python-version }}
23
+ run: uv python install ${{ matrix.python-version }}
24
+
25
+ - name: Install
26
+ run: make install
27
+
28
+ - name: Run tests
29
+ run: make test
30
+
31
+ - name: Run mypy
32
+ run: make lint
@@ -0,0 +1,71 @@
1
+ name: Publish
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+ inputs:
8
+ target:
9
+ description: "Publish target"
10
+ required: true
11
+ type: choice
12
+ options:
13
+ - testpypi
14
+ - pypi
15
+
16
+ permissions:
17
+ id-token: write
18
+
19
+ jobs:
20
+ build:
21
+ runs-on: ubuntu-latest
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+
25
+ - name: Install uv
26
+ uses: astral-sh/setup-uv@v4
27
+
28
+ - name: Build package
29
+ run: uv build
30
+
31
+ - name: Upload dist
32
+ uses: actions/upload-artifact@v4
33
+ with:
34
+ name: dist
35
+ path: dist/
36
+
37
+ publish-testpypi:
38
+ needs: build
39
+ runs-on: ubuntu-latest
40
+ if: >-
41
+ (github.event_name == 'workflow_dispatch' && github.event.inputs.target == 'testpypi') ||
42
+ (github.event_name == 'release' && github.event.release.prerelease)
43
+ environment: testpypi
44
+ steps:
45
+ - name: Download dist
46
+ uses: actions/download-artifact@v4
47
+ with:
48
+ name: dist
49
+ path: dist/
50
+
51
+ - name: Publish to TestPyPI
52
+ uses: pypa/gh-action-pypi-publish@release/v1
53
+ with:
54
+ repository-url: https://test.pypi.org/legacy/
55
+
56
+ publish-pypi:
57
+ needs: build
58
+ runs-on: ubuntu-latest
59
+ if: >-
60
+ (github.event_name == 'workflow_dispatch' && github.event.inputs.target == 'pypi') ||
61
+ (github.event_name == 'release' && !github.event.release.prerelease)
62
+ environment: pypi
63
+ steps:
64
+ - name: Download dist
65
+ uses: actions/download-artifact@v4
66
+ with:
67
+ name: dist
68
+ path: dist/
69
+
70
+ - name: Publish to PyPI
71
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.egg-info/
4
+ .venv/
5
+ .idea/
6
+ .pytest_cache/
7
+ .mypy_cache/
8
+ *.sqlite
9
+ typemut-report.html
10
+ dist/
11
+ build/
typemut-0.1.0/Makefile ADDED
@@ -0,0 +1,38 @@
1
+ .PHONY: install test lint clean run
2
+
3
+ install:
4
+ uv sync --all-extras --all-groups
5
+
6
+ test:
7
+ uv run pytest tests/ -v
8
+
9
+ lint:
10
+ uv run mypy src/typemut/
11
+
12
+ clean:
13
+ rm -f typemut.sqlite
14
+ rm -rf .pytest_cache __pycache__ src/typemut/__pycache__
15
+ find . -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
16
+ find . -name "*.pyc" -delete 2>/dev/null || true
17
+
18
+ # Run typemut on an external project:
19
+ # make run PROJECT=/path/to/project
20
+ # make run PROJECT=/path/to/project CONFIG=custom.toml
21
+ PROJECT ?= .
22
+ CONFIG ?= typemut.toml
23
+ DB ?= typemut.sqlite
24
+
25
+ run:
26
+ uv run typemut -C $(PROJECT) run --config $(CONFIG) --db $(DB)
27
+
28
+ init:
29
+ uv run typemut -C $(PROJECT) init --config $(CONFIG) --db $(DB)
30
+
31
+ exec:
32
+ uv run typemut -C $(PROJECT) exec --config $(CONFIG) --db $(DB)
33
+
34
+ report:
35
+ uv run typemut -C $(PROJECT) report --db $(DB)
36
+
37
+ html:
38
+ uv run typemut -C $(PROJECT) html --db $(DB)
typemut-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,256 @@
1
+ Metadata-Version: 2.4
2
+ Name: typemut
3
+ Version: 0.1.0
4
+ Summary: Mutation testing for type annotations
5
+ Project-URL: Homepage, https://github.com/nkhitrov/typemut
6
+ Project-URL: Repository, https://github.com/nkhitrov/typemut
7
+ Project-URL: Issues, https://github.com/nkhitrov/typemut/issues
8
+ Author-email: Niсл Khitrov <khitrov34@gmail.com>
9
+ License-Expression: MIT
10
+ Keywords: mutation-testing,mypy,pyright,testing,type-annotations,type-checking
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Software Development :: Quality Assurance
19
+ Classifier: Topic :: Software Development :: Testing
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: click>=8.0
23
+ Requires-Dist: parso>=0.8
24
+ Requires-Dist: rich>=13.0
25
+ Provides-Extra: dev
26
+ Requires-Dist: mypy>=1.0; extra == 'dev'
27
+ Requires-Dist: pytest>=7.0; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # typemut
31
+
32
+ Mutation testing for Python type annotations.
33
+
34
+ Standard mutation testing tools (cosmic-ray, mutmut) mutate runtime code and check if tests catch it. **typemut** mutates only type annotations and checks if type checkers (mypy, pyright) catch the change.
35
+
36
+ - **Mutant killed** = type checker reports an error (types are strict enough)
37
+ - **Mutant survived** = no type error (types are too loose or type checker coverage is weak)
38
+
39
+ ## Installation
40
+
41
+ ```bash
42
+ pip install typemut
43
+ # or
44
+ uv add typemut
45
+ ```
46
+
47
+ ## Quick Start
48
+
49
+ 1. Create `typemut.toml` in your project root:
50
+
51
+ ```toml
52
+ [typemut]
53
+ module-path = "src/myproject"
54
+ test-command = "make typecheck" # must exit non-zero on type errors
55
+ timeout = 30
56
+
57
+ [typemut.operators]
58
+ remove-union-member = true
59
+ swap-literal-value = true
60
+ swap-sibling-type = true
61
+ strip-annotated = true
62
+ remove-optional = true
63
+ add-optional = true
64
+ swap-container-type = true
65
+ ```
66
+
67
+ 2. Run:
68
+
69
+ ```bash
70
+ typemut run # full pipeline: discover + execute + report
71
+ typemut html --open # generate HTML report and open in browser
72
+ ```
73
+
74
+ Or from another directory:
75
+
76
+ ```bash
77
+ typemut -C /path/to/project run
78
+ ```
79
+
80
+ ## Commands
81
+
82
+ | Command | Description |
83
+ |---------|-------------|
84
+ | `typemut run` | Full pipeline: discover mutations, run type checker, show report |
85
+ | `typemut init` | Discover mutations and store in SQLite |
86
+ | `typemut exec` | Run type checker against each pending mutation |
87
+ | `typemut report` | Show terminal report |
88
+ | `typemut html` | Generate HTML report with diffs |
89
+
90
+ ## What It Finds
91
+
92
+ typemut generates mutations of type annotations and checks whether the type checker catches them. Each mutation operator targets a specific class of type safety issues.
93
+
94
+ ### RemoveUnionMember
95
+
96
+ Removes one member from a union type.
97
+
98
+ ```python
99
+ # Original
100
+ def handle(value: int | str | float) -> None: ...
101
+
102
+ # Mutant: remove str
103
+ def handle(value: int | float) -> None: ...
104
+ ```
105
+
106
+ **Survived = your code doesn't distinguish between union members.** If removing `str` from the union causes no type error, it means no code path relies on `value` being a `str`. The union may be overly broad, or the type checker doesn't see the code that handles `str` specifically.
107
+
108
+ ### RemoveOptional
109
+
110
+ Removes `None` from `X | None`.
111
+
112
+ ```python
113
+ # Original
114
+ def find_user(id: int) -> User | None: ...
115
+
116
+ # Mutant
117
+ def find_user(id: int) -> User: ...
118
+ ```
119
+
120
+ **Survived = callers don't check for `None`.** The return type says "might be None" but no consumer's type annotations actually require a None-check. Either the None case is dead code, or callers use `# type: ignore`.
121
+
122
+ ### AddOptional
123
+
124
+ Adds `| None` to return types and class fields (parameters are excluded — callers simply won't pass None, making those mutations uninformative).
125
+
126
+ ```python
127
+ # Original
128
+ class Config:
129
+ name: str
130
+
131
+ # Mutant
132
+ class Config:
133
+ name: str | None
134
+ ```
135
+
136
+ **Survived = consumers don't rely on non-None guarantee.** The field claims to always have a value, but no typed code would break if it could be `None`. This often reveals missing type coverage in code that reads the field.
137
+
138
+ ### SwapSiblingType
139
+
140
+ Replaces a class with its sibling (same base class).
141
+
142
+ ```python
143
+ class LoanState: ...
144
+ class ActiveLoan(LoanState): ...
145
+ class ClosedLoan(LoanState): ...
146
+
147
+ # Original
148
+ def process(loan: ActiveLoan) -> None: ...
149
+
150
+ # Mutant
151
+ def process(loan: ClosedLoan) -> None: ...
152
+ ```
153
+
154
+ **Survived = the type system doesn't distinguish between sibling states.** Common in state machine patterns where the base class is typed as `Any` or all siblings share the same interface.
155
+
156
+ ### SwapLiteralValue
157
+
158
+ Swaps values inside `Literal[...]` with other literal values from the same file.
159
+
160
+ ```python
161
+ # Original
162
+ status: Literal["active"]
163
+
164
+ # Mutant (if "closed" exists in the same file)
165
+ status: Literal["closed"]
166
+ ```
167
+
168
+ **Survived = literal values are interchangeable from the type checker's perspective.** The code doesn't use literal narrowing or overloads to distinguish between the values.
169
+
170
+ ### StripAnnotated
171
+
172
+ Removes metadata from `Annotated[X, ...]`, leaving just the base type.
173
+
174
+ ```python
175
+ # Original
176
+ age: Annotated[int, Gt(0)]
177
+
178
+ # Mutant
179
+ age: int
180
+ ```
181
+
182
+ **Survived = expected in most cases.** `Annotated` metadata is typically runtime-only (Pydantic validators, etc.). Survived mutants here are normal unless you use mypy plugins that understand the metadata.
183
+
184
+ ### SwapContainerType
185
+
186
+ Swaps between compatible container types.
187
+
188
+ ```python
189
+ # Original
190
+ items: list[int]
191
+
192
+ # Mutant
193
+ items: tuple[int]
194
+ ```
195
+
196
+ **Swap groups:** `list` <-> `tuple`, `set` <-> `frozenset`. Dict has no swap target.
197
+
198
+ **Survived = code doesn't rely on container-specific behavior at the type level.** For example, if code only iterates over items, both `list` and `tuple` work equally.
199
+
200
+ ## Filtering
201
+
202
+ Annotations are automatically skipped when:
203
+
204
+ - The line contains `# type: ignore` or `# pragma: no mutate`
205
+ - The annotation is `Any` (mutations are meaningless — Any absorbs all types)
206
+ - `AddOptional` targets a function parameter (low signal — callers won't pass None)
207
+
208
+ ## Config Reference
209
+
210
+ ```toml
211
+ [typemut]
212
+ module-path = "src/myproject" # directory to scan for annotations
213
+ test-command = "make typecheck" # command to run type checker
214
+ timeout = 30 # seconds per mutation
215
+ excluded-modules = ["src/vendor/*.py"] # glob patterns to skip
216
+ skip-comments = ["type: ignore", "pragma: no mutate"]
217
+ db = "typemut.sqlite" # database file
218
+
219
+ [typemut.operators]
220
+ # all enabled by default, disable selectively
221
+ remove-union-member = true
222
+ swap-literal-value = true
223
+ swap-sibling-type = true
224
+ strip-annotated = true
225
+ remove-optional = true
226
+ add-optional = true
227
+ swap-container-type = true
228
+ ```
229
+
230
+ ## HTML Report
231
+
232
+ The HTML report shows:
233
+ - Summary stats and per-module mutation scores
234
+ - Each mutant as a collapsible card with unified diff
235
+ - Color-coded status: killed (green), survived (red), error (orange)
236
+ - Full type checker output per mutant
237
+ - Expand/Collapse All controls
238
+
239
+ ```bash
240
+ typemut html --open # save and open in browser
241
+ typemut html -o report.html # save to specific file
242
+ ```
243
+
244
+ ## Development
245
+
246
+ ```bash
247
+ make install # create venv and install with dev deps
248
+ make test # run tests
249
+ make lint # run mypy
250
+ ```
251
+
252
+ ## Dependencies
253
+
254
+ - **parso** — CST parsing (preserves formatting and whitespace)
255
+ - **rich** — terminal reporting
256
+ - **click** — CLI framework
@@ -0,0 +1,227 @@
1
+ # typemut
2
+
3
+ Mutation testing for Python type annotations.
4
+
5
+ Standard mutation testing tools (cosmic-ray, mutmut) mutate runtime code and check if tests catch it. **typemut** mutates only type annotations and checks if type checkers (mypy, pyright) catch the change.
6
+
7
+ - **Mutant killed** = type checker reports an error (types are strict enough)
8
+ - **Mutant survived** = no type error (types are too loose or type checker coverage is weak)
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ pip install typemut
14
+ # or
15
+ uv add typemut
16
+ ```
17
+
18
+ ## Quick Start
19
+
20
+ 1. Create `typemut.toml` in your project root:
21
+
22
+ ```toml
23
+ [typemut]
24
+ module-path = "src/myproject"
25
+ test-command = "make typecheck" # must exit non-zero on type errors
26
+ timeout = 30
27
+
28
+ [typemut.operators]
29
+ remove-union-member = true
30
+ swap-literal-value = true
31
+ swap-sibling-type = true
32
+ strip-annotated = true
33
+ remove-optional = true
34
+ add-optional = true
35
+ swap-container-type = true
36
+ ```
37
+
38
+ 2. Run:
39
+
40
+ ```bash
41
+ typemut run # full pipeline: discover + execute + report
42
+ typemut html --open # generate HTML report and open in browser
43
+ ```
44
+
45
+ Or from another directory:
46
+
47
+ ```bash
48
+ typemut -C /path/to/project run
49
+ ```
50
+
51
+ ## Commands
52
+
53
+ | Command | Description |
54
+ |---------|-------------|
55
+ | `typemut run` | Full pipeline: discover mutations, run type checker, show report |
56
+ | `typemut init` | Discover mutations and store in SQLite |
57
+ | `typemut exec` | Run type checker against each pending mutation |
58
+ | `typemut report` | Show terminal report |
59
+ | `typemut html` | Generate HTML report with diffs |
60
+
61
+ ## What It Finds
62
+
63
+ typemut generates mutations of type annotations and checks whether the type checker catches them. Each mutation operator targets a specific class of type safety issues.
64
+
65
+ ### RemoveUnionMember
66
+
67
+ Removes one member from a union type.
68
+
69
+ ```python
70
+ # Original
71
+ def handle(value: int | str | float) -> None: ...
72
+
73
+ # Mutant: remove str
74
+ def handle(value: int | float) -> None: ...
75
+ ```
76
+
77
+ **Survived = your code doesn't distinguish between union members.** If removing `str` from the union causes no type error, it means no code path relies on `value` being a `str`. The union may be overly broad, or the type checker doesn't see the code that handles `str` specifically.
78
+
79
+ ### RemoveOptional
80
+
81
+ Removes `None` from `X | None`.
82
+
83
+ ```python
84
+ # Original
85
+ def find_user(id: int) -> User | None: ...
86
+
87
+ # Mutant
88
+ def find_user(id: int) -> User: ...
89
+ ```
90
+
91
+ **Survived = callers don't check for `None`.** The return type says "might be None" but no consumer's type annotations actually require a None-check. Either the None case is dead code, or callers use `# type: ignore`.
92
+
93
+ ### AddOptional
94
+
95
+ Adds `| None` to return types and class fields (parameters are excluded — callers simply won't pass None, making those mutations uninformative).
96
+
97
+ ```python
98
+ # Original
99
+ class Config:
100
+ name: str
101
+
102
+ # Mutant
103
+ class Config:
104
+ name: str | None
105
+ ```
106
+
107
+ **Survived = consumers don't rely on non-None guarantee.** The field claims to always have a value, but no typed code would break if it could be `None`. This often reveals missing type coverage in code that reads the field.
108
+
109
+ ### SwapSiblingType
110
+
111
+ Replaces a class with its sibling (same base class).
112
+
113
+ ```python
114
+ class LoanState: ...
115
+ class ActiveLoan(LoanState): ...
116
+ class ClosedLoan(LoanState): ...
117
+
118
+ # Original
119
+ def process(loan: ActiveLoan) -> None: ...
120
+
121
+ # Mutant
122
+ def process(loan: ClosedLoan) -> None: ...
123
+ ```
124
+
125
+ **Survived = the type system doesn't distinguish between sibling states.** Common in state machine patterns where the base class is typed as `Any` or all siblings share the same interface.
126
+
127
+ ### SwapLiteralValue
128
+
129
+ Swaps values inside `Literal[...]` with other literal values from the same file.
130
+
131
+ ```python
132
+ # Original
133
+ status: Literal["active"]
134
+
135
+ # Mutant (if "closed" exists in the same file)
136
+ status: Literal["closed"]
137
+ ```
138
+
139
+ **Survived = literal values are interchangeable from the type checker's perspective.** The code doesn't use literal narrowing or overloads to distinguish between the values.
140
+
141
+ ### StripAnnotated
142
+
143
+ Removes metadata from `Annotated[X, ...]`, leaving just the base type.
144
+
145
+ ```python
146
+ # Original
147
+ age: Annotated[int, Gt(0)]
148
+
149
+ # Mutant
150
+ age: int
151
+ ```
152
+
153
+ **Survived = expected in most cases.** `Annotated` metadata is typically runtime-only (Pydantic validators, etc.). Survived mutants here are normal unless you use mypy plugins that understand the metadata.
154
+
155
+ ### SwapContainerType
156
+
157
+ Swaps between compatible container types.
158
+
159
+ ```python
160
+ # Original
161
+ items: list[int]
162
+
163
+ # Mutant
164
+ items: tuple[int]
165
+ ```
166
+
167
+ **Swap groups:** `list` <-> `tuple`, `set` <-> `frozenset`. Dict has no swap target.
168
+
169
+ **Survived = code doesn't rely on container-specific behavior at the type level.** For example, if code only iterates over items, both `list` and `tuple` work equally.
170
+
171
+ ## Filtering
172
+
173
+ Annotations are automatically skipped when:
174
+
175
+ - The line contains `# type: ignore` or `# pragma: no mutate`
176
+ - The annotation is `Any` (mutations are meaningless — Any absorbs all types)
177
+ - `AddOptional` targets a function parameter (low signal — callers won't pass None)
178
+
179
+ ## Config Reference
180
+
181
+ ```toml
182
+ [typemut]
183
+ module-path = "src/myproject" # directory to scan for annotations
184
+ test-command = "make typecheck" # command to run type checker
185
+ timeout = 30 # seconds per mutation
186
+ excluded-modules = ["src/vendor/*.py"] # glob patterns to skip
187
+ skip-comments = ["type: ignore", "pragma: no mutate"]
188
+ db = "typemut.sqlite" # database file
189
+
190
+ [typemut.operators]
191
+ # all enabled by default, disable selectively
192
+ remove-union-member = true
193
+ swap-literal-value = true
194
+ swap-sibling-type = true
195
+ strip-annotated = true
196
+ remove-optional = true
197
+ add-optional = true
198
+ swap-container-type = true
199
+ ```
200
+
201
+ ## HTML Report
202
+
203
+ The HTML report shows:
204
+ - Summary stats and per-module mutation scores
205
+ - Each mutant as a collapsible card with unified diff
206
+ - Color-coded status: killed (green), survived (red), error (orange)
207
+ - Full type checker output per mutant
208
+ - Expand/Collapse All controls
209
+
210
+ ```bash
211
+ typemut html --open # save and open in browser
212
+ typemut html -o report.html # save to specific file
213
+ ```
214
+
215
+ ## Development
216
+
217
+ ```bash
218
+ make install # create venv and install with dev deps
219
+ make test # run tests
220
+ make lint # run mypy
221
+ ```
222
+
223
+ ## Dependencies
224
+
225
+ - **parso** — CST parsing (preserves formatting and whitespace)
226
+ - **rich** — terminal reporting
227
+ - **click** — CLI framework
@@ -0,0 +1,55 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "typemut"
7
+ dynamic = ["version"]
8
+ description = "Mutation testing for type annotations"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "Niсл Khitrov", email = "khitrov34@gmail.com" },
14
+ ]
15
+ keywords = ["mutation-testing", "type-annotations", "mypy", "pyright", "type-checking", "testing"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Software Development :: Testing",
25
+ "Topic :: Software Development :: Quality Assurance",
26
+ "Typing :: Typed",
27
+ ]
28
+ dependencies = [
29
+ "parso>=0.8",
30
+ "rich>=13.0",
31
+ "click>=8.0",
32
+ ]
33
+
34
+ [project.urls]
35
+ Homepage = "https://github.com/nkhitrov/typemut"
36
+ Repository = "https://github.com/nkhitrov/typemut"
37
+ Issues = "https://github.com/nkhitrov/typemut/issues"
38
+
39
+ [project.scripts]
40
+ typemut = "typemut.cli:main"
41
+
42
+ [tool.hatch.version]
43
+ path = "src/typemut/__init__.py"
44
+
45
+ [tool.hatch.build.targets.wheel]
46
+ packages = ["src/typemut"]
47
+
48
+ [tool.pytest.ini_options]
49
+ testpaths = ["tests"]
50
+
51
+ [project.optional-dependencies]
52
+ dev = [
53
+ "pytest>=7.0",
54
+ "mypy>=1.0",
55
+ ]
@@ -0,0 +1,3 @@
1
+ """typemut — Mutation testing for type annotations."""
2
+
3
+ __version__ = "0.1.0"