threadcheck 0.0.1__py3-none-any.whl

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,324 @@
1
+ import ast
2
+ from pathlib import Path
3
+
4
+ from .models import RaceWarning, Severity, WarningCategory, Confidence
5
+
6
+
7
+ def _calc_confidence(context, func_name: str | None) -> Confidence:
8
+ if func_name and context.is_thread_target(func_name):
9
+ return Confidence.HIGH
10
+ if context.has_any_thread():
11
+ return Confidence.MEDIUM
12
+ return Confidence.LOW
13
+
14
+
15
+ class GlobalVisitor(ast.NodeVisitor):
16
+ def __init__(self, filepath: Path, context):
17
+ self.filepath = filepath
18
+ self.context = context
19
+ self.warnings: list[RaceWarning] = []
20
+ self._globals_in_function: set[str] = set()
21
+ self._current_func: str | None = None
22
+
23
+ def visit_FunctionDef(self, node):
24
+ old = self._globals_in_function, self._current_func
25
+ self._globals_in_function = set()
26
+ self._current_func = node.name
27
+ self.generic_visit(node)
28
+ self._globals_in_function, self._current_func = old
29
+
30
+ visit_AsyncFunctionDef = visit_FunctionDef
31
+
32
+ def visit_Global(self, node):
33
+ for name in node.names:
34
+ self._globals_in_function.add(name)
35
+
36
+ def _check_name(self, node):
37
+ if isinstance(node, ast.Name) and node.id in self._globals_in_function:
38
+ if self.context.is_protected(node.lineno):
39
+ return
40
+ confidence = _calc_confidence(self.context, self._current_func)
41
+ self.warnings.append(
42
+ RaceWarning(
43
+ file=self.filepath,
44
+ line=node.lineno,
45
+ col=node.col_offset,
46
+ severity=Severity.WARNING,
47
+ category=WarningCategory.UNSAFE_GLOBAL,
48
+ message=f"Global variable `{node.id}` modified without lock",
49
+ suggestion=f"Use `threading.Lock()` to protect `{node.id}`",
50
+ confidence=confidence,
51
+ )
52
+ )
53
+
54
+ def visit_Assign(self, node):
55
+ for target in node.targets:
56
+ if isinstance(target, ast.Name):
57
+ self._check_name(target)
58
+ elif isinstance(target, (ast.Tuple, ast.List)):
59
+ for elt in target.elts:
60
+ if isinstance(elt, ast.Name):
61
+ self._check_name(elt)
62
+ self.generic_visit(node)
63
+
64
+ def visit_AugAssign(self, node):
65
+ self._check_name(node.target)
66
+ self.generic_visit(node)
67
+
68
+ def visit_Delete(self, node):
69
+ for target in node.targets:
70
+ if isinstance(target, ast.Name):
71
+ self._check_name(target)
72
+ self.generic_visit(node)
73
+
74
+
75
+ class NonlocalVisitor(ast.NodeVisitor):
76
+ def __init__(self, filepath: Path, context):
77
+ self.filepath = filepath
78
+ self.context = context
79
+ self.warnings: list[RaceWarning] = []
80
+ self._nonlocals_in_function: set[str] = set()
81
+ self._current_func: str | None = None
82
+
83
+ def visit_FunctionDef(self, node):
84
+ old = self._nonlocals_in_function, self._current_func
85
+ self._nonlocals_in_function = set()
86
+ self._current_func = node.name
87
+ self.generic_visit(node)
88
+ self._nonlocals_in_function, self._current_func = old
89
+
90
+ visit_AsyncFunctionDef = visit_FunctionDef
91
+
92
+ def visit_Nonlocal(self, node):
93
+ for name in node.names:
94
+ self._nonlocals_in_function.add(name)
95
+
96
+ def _check_name(self, node):
97
+ if isinstance(node, ast.Name) and node.id in self._nonlocals_in_function:
98
+ if self.context.is_protected(node.lineno):
99
+ return
100
+ confidence = _calc_confidence(self.context, self._current_func)
101
+ self.warnings.append(
102
+ RaceWarning(
103
+ file=self.filepath,
104
+ line=node.lineno,
105
+ col=node.col_offset,
106
+ severity=Severity.WARNING,
107
+ category=WarningCategory.UNSAFE_NONLOCAL,
108
+ message=f"Nonlocal variable `{node.id}` modified without lock",
109
+ suggestion=f"Use `threading.Lock()` to protect `{node.id}`",
110
+ confidence=confidence,
111
+ )
112
+ )
113
+
114
+ def visit_Assign(self, node):
115
+ for target in node.targets:
116
+ if isinstance(target, ast.Name):
117
+ self._check_name(target)
118
+ elif isinstance(target, (ast.Tuple, ast.List)):
119
+ for elt in target.elts:
120
+ if isinstance(elt, ast.Name):
121
+ self._check_name(elt)
122
+ self.generic_visit(node)
123
+
124
+ def visit_AugAssign(self, node):
125
+ self._check_name(node.target)
126
+ self.generic_visit(node)
127
+
128
+ def visit_Delete(self, node):
129
+ for target in node.targets:
130
+ if isinstance(target, ast.Name):
131
+ self._check_name(target)
132
+ self.generic_visit(node)
133
+
134
+
135
+ class ThreadVisitor(ast.NodeVisitor):
136
+ def __init__(self, filepath: Path, context):
137
+ self.filepath = filepath
138
+ self.context = context
139
+ self.warnings: list[RaceWarning] = []
140
+
141
+ def visit_Call(self, node):
142
+ if isinstance(node.func, ast.Attribute) and node.func.attr == "Thread":
143
+ if isinstance(node.func.value, ast.Name) and node.func.value.id == "threading":
144
+ target = None
145
+ for kw in node.keywords:
146
+ if kw.arg == "target":
147
+ if isinstance(kw.value, ast.Name):
148
+ target = kw.value.id
149
+ elif isinstance(kw.value, (ast.Lambda, ast.FunctionDef)):
150
+ target = "<lambda>"
151
+ elif isinstance(kw.value, ast.Attribute):
152
+ target = ast.unparse(kw.value)
153
+ label = f" (target={target})" if target else ""
154
+ self.warnings.append(
155
+ RaceWarning(
156
+ file=self.filepath,
157
+ line=node.lineno,
158
+ col=node.col_offset,
159
+ severity=Severity.INFO,
160
+ category=WarningCategory.THREAD_USAGE,
161
+ message=f"Thread creation detected{label}",
162
+ suggestion="Ensure shared variable access in thread functions is lock-protected",
163
+ confidence=Confidence.LOW,
164
+ )
165
+ )
166
+ self.generic_visit(node)
167
+
168
+
169
+ class SharedMutableVisitor(ast.NodeVisitor):
170
+ def __init__(self, filepath: Path, context):
171
+ self.filepath = filepath
172
+ self.context = context
173
+ self.warnings: list[RaceWarning] = []
174
+ self._module_level_assigns: set[str] = set()
175
+ self._in_function = False
176
+ self._current_func: str | None = None
177
+
178
+ def visit_Module(self, node):
179
+ for item in node.body:
180
+ if isinstance(item, ast.Assign):
181
+ for target in item.targets:
182
+ if isinstance(target, ast.Name) and _is_mutable_literal(item.value):
183
+ self._module_level_assigns.add(target.id)
184
+ self.generic_visit(node)
185
+
186
+ def visit_FunctionDef(self, node):
187
+ self._in_function = True
188
+ self._current_func = node.name
189
+ self.generic_visit(node)
190
+ self._current_func = None
191
+ self._in_function = False
192
+
193
+ visit_AsyncFunctionDef = visit_FunctionDef
194
+
195
+ def _add_warning(self, node, var_name: str, detail: str = ""):
196
+ if self.context.is_protected(node.lineno):
197
+ return
198
+ confidence = _calc_confidence(self.context, self._current_func)
199
+ msg = f"Module-level mutable object `{var_name}` modified inside function"
200
+ if detail:
201
+ msg = f"Module-level mutable object `{detail}` called from multiple threads"
202
+ self.warnings.append(
203
+ RaceWarning(
204
+ file=self.filepath,
205
+ line=node.lineno,
206
+ col=node.col_offset,
207
+ severity=Severity.WARNING,
208
+ category=WarningCategory.SHARED_MUTABLE,
209
+ message=msg,
210
+ suggestion="Consider using thread-safe data structures or add lock protection",
211
+ confidence=confidence,
212
+ )
213
+ )
214
+
215
+ def visit_Assign(self, node):
216
+ if self._in_function:
217
+ for target in node.targets:
218
+ if isinstance(target, ast.Name) and target.id in self._module_level_assigns:
219
+ self._add_warning(node, target.id)
220
+ self.generic_visit(node)
221
+
222
+ def visit_AugAssign(self, node):
223
+ if self._in_function and isinstance(node.target, ast.Name):
224
+ if node.target.id in self._module_level_assigns:
225
+ self._add_warning(node, node.target.id)
226
+ self.generic_visit(node)
227
+
228
+ def visit_Call(self, node):
229
+ if self._in_function:
230
+ if isinstance(node.func, ast.Attribute) and node.func.attr in (
231
+ "append", "extend", "pop", "remove", "clear",
232
+ "insert", "sort", "reverse", "update", "add", "discard",
233
+ ):
234
+ if isinstance(node.func.value, ast.Name) and node.func.value.id in self._module_level_assigns:
235
+ detail = f"{node.func.value.id}.{node.func.attr}()"
236
+ self._add_warning(node, node.func.value.id, detail)
237
+ self.generic_visit(node)
238
+
239
+
240
+ class ClassAttributeVisitor(ast.NodeVisitor):
241
+ def __init__(self, filepath: Path, context):
242
+ self.filepath = filepath
243
+ self.context = context
244
+ self.warnings: list[RaceWarning] = []
245
+ self._current_class: str | None = None
246
+ self._current_method: str | None = None
247
+ self._class_is_thread = False
248
+
249
+ def visit_ClassDef(self, node):
250
+ old = self._current_class, self._class_is_thread
251
+ self._current_class = node.name
252
+ self._class_is_thread = any(
253
+ _is_name_or_attr(base, "Thread") for base in node.bases
254
+ )
255
+ self.generic_visit(node)
256
+ self._current_class, self._class_is_thread = old
257
+
258
+ def visit_FunctionDef(self, node):
259
+ old = self._current_method
260
+ if node.name in ("__init__", "__new__", "__class_getitem__"):
261
+ self._current_method = None
262
+ self.generic_visit(node)
263
+ self._current_method = old
264
+ return
265
+ self._current_method = node.name
266
+ self.generic_visit(node)
267
+ self._current_method = old
268
+
269
+ visit_AsyncFunctionDef = visit_FunctionDef
270
+
271
+ def _check_attr(self, node, attr_name: str):
272
+ if self.context.is_protected(node.lineno):
273
+ return
274
+ in_thread_target = self._current_method and self.context.is_thread_target(
275
+ self._current_method
276
+ )
277
+ if in_thread_target or self._class_is_thread:
278
+ confidence = Confidence.HIGH
279
+ elif self.context.has_any_thread():
280
+ confidence = Confidence.MEDIUM
281
+ else:
282
+ confidence = Confidence.LOW
283
+ self.warnings.append(
284
+ RaceWarning(
285
+ file=self.filepath,
286
+ line=node.lineno,
287
+ col=node.col_offset,
288
+ severity=Severity.WARNING,
289
+ category=WarningCategory.CLASS_ATTRIBUTE,
290
+ message=(
291
+ f"Attribute `{attr_name}` of class `{self._current_class}` "
292
+ f"modified without lock in method `{self._current_method}`"
293
+ ),
294
+ suggestion="Use `threading.Lock()` to protect class attribute access",
295
+ confidence=confidence,
296
+ )
297
+ )
298
+
299
+ def visit_Assign(self, node):
300
+ if self._current_class and self._current_method:
301
+ for target in node.targets:
302
+ if isinstance(target, ast.Attribute):
303
+ if isinstance(target.value, ast.Name) and target.value.id == "self":
304
+ self._check_attr(node, target.attr)
305
+ self.generic_visit(node)
306
+
307
+ def visit_AugAssign(self, node):
308
+ if self._current_class and self._current_method:
309
+ if isinstance(node.target, ast.Attribute):
310
+ if isinstance(node.target.value, ast.Name) and node.target.value.id == "self":
311
+ self._check_attr(node, node.target.attr)
312
+ self.generic_visit(node)
313
+
314
+
315
+ def _is_mutable_literal(node):
316
+ return isinstance(node, (ast.List, ast.Dict, ast.Set, ast.ListComp, ast.SetComp, ast.DictComp))
317
+
318
+
319
+ def _is_name_or_attr(node, name: str) -> bool:
320
+ if isinstance(node, ast.Name):
321
+ return node.id == name
322
+ if isinstance(node, ast.Attribute):
323
+ return node.attr == name
324
+ return False
@@ -0,0 +1,248 @@
1
+ Metadata-Version: 2.4
2
+ Name: threadcheck
3
+ Version: 0.0.1
4
+ Summary: Data Race Detector for Free-Threading Python
5
+ Project-URL: Homepage, https://github.com/ChidcGithub/Threadcheck
6
+ Project-URL: Source, https://github.com/ChidcGithub/Threadcheck
7
+ Project-URL: BugTracker, https://github.com/ChidcGithub/Threadcheck/issues
8
+ Author: threadcheck contributors
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Software Development :: Debuggers
18
+ Classifier: Topic :: Software Development :: Testing
19
+ Requires-Python: >=3.12
20
+ Provides-Extra: test
21
+ Requires-Dist: pytest; extra == 'test'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # threadcheck
25
+
26
+ [![Python](https://img.shields.io/badge/python-3.12%2B-blue)](https://www.python.org)
27
+ [![License](https://img.shields.io/badge/license-MIT-green)](LICENSE)
28
+ [![PyPI](https://img.shields.io/badge/pypi-v0.0.0-orange)](https://pypi.org/project/threadcheck/)
29
+ [![Tests](https://img.shields.io/badge/tests-21%2F21-passing-brightgreen)]()
30
+
31
+ [**中文文档**](README_CN.md)
32
+
33
+ Python data race detector for the free-threading (no-GIL) era. Detects concurrent access to shared mutable state in multi-threaded Python programs through static analysis and runtime instrumentation.
34
+
35
+ ---
36
+
37
+ ## Problem
38
+
39
+ Python 3.14 (2026) introduces free-threading, removing the Global Interpreter Lock (GIL). This enables true parallel execution of multi-threaded code, but the ecosystem lacks debugging tools for concurrency bugs. Go has `-race`, C++ has ThreadSanitizer, Java has SpotBugs. Python has nothing comparable without recompiling the interpreter with Clang and TSan.
40
+
41
+ threadcheck is a pure-Python race detector that installs with `pip` and works out of the box.
42
+
43
+ ---
44
+
45
+ ## Features
46
+
47
+ - **Static analysis** -- scans AST for shared mutable state (global, nonlocal, class attributes) and missing lock protection
48
+ - **Runtime detection** -- instruments code via AST transformation at import time; tracks memory accesses with vector clocks and detects happens-before violations
49
+ - **Lock-aware suppression** -- understands `threading.Lock`, `threading.RLock`, and `with`-based synchronization; raises confidence when locks are missing and suppresses warnings when they are present
50
+ - **Confidence scoring** -- each warning tagged HIGH / MEDIUM / LOW based on thread context and lock coverage
51
+ - **CLI tool** -- single-command static scan or instrumented execution
52
+ - **JSON and SARIF output** -- suitable for CI/CD pipeline integration
53
+
54
+ ---
55
+
56
+ ## Installation
57
+
58
+ ```bash
59
+ pip install threadcheck
60
+ ```
61
+
62
+ Requires Python 3.12+. Python 3.14+ is recommended for free-threading features.
63
+
64
+ ---
65
+
66
+ ## Quick Start
67
+
68
+ ### Static Analysis
69
+
70
+ Scan a file or directory for potential race conditions without running any code:
71
+
72
+ ```bash
73
+ threadcheck scan my_project/
74
+ ```
75
+
76
+ Output:
77
+
78
+ ```
79
+ [WARNING] [HIGH] [unsafe_global] my_project/counter.py:8:8
80
+ Global variable `counter` modified without lock in thread
81
+ Suggestion: use `threading.Lock()` to protect access
82
+ ```
83
+
84
+ JSON output:
85
+
86
+ ```bash
87
+ threadcheck scan my_project/ --json -o report.json
88
+ ```
89
+
90
+ ### Runtime Detection
91
+
92
+ Execute a script with instrumentation to detect actual data races:
93
+
94
+ ```bash
95
+ threadcheck run my_script.py
96
+ ```
97
+
98
+ Output for a racing script:
99
+
100
+ ```
101
+ Data races detected:
102
+ [!] `counter`
103
+ Thread-28928 (write) at my_script.py:8
104
+ Thread-9888 (write) at my_script.py:8
105
+ ```
106
+
107
+ A script protected with locks reports:
108
+
109
+ ```
110
+ No data races detected
111
+ ```
112
+
113
+ ### CI Integration
114
+
115
+ ```bash
116
+ pip install threadcheck
117
+ threadcheck scan src/ --json -o threadcheck_report.json
118
+ ```
119
+
120
+ ---
121
+
122
+ ## Commands
123
+
124
+ | Command | Description | Status |
125
+ |---|---|---|
126
+ | `scan <path>` | Static race analysis of file or directory | Stable |
127
+ | `run <script>` | Execute script with runtime race detection | Beta |
128
+ | `check-compat <path>` | Free-threading compatibility check | Planned |
129
+
130
+ ---
131
+
132
+ ## Library Usage
133
+
134
+ ```python
135
+ from threadcheck import analyze_file, analyze_path
136
+
137
+ warnings = analyze_file("my_module.py")
138
+ warnings = analyze_path("src/")
139
+
140
+ for w in warnings:
141
+ print(f"{w.file}:{w.line} [{w.confidence.value}] {w.message}")
142
+ ```
143
+
144
+ ```python
145
+ from threadcheck.dynamic.tracker import ThreadCheckTracker
146
+ from threadcheck.dynamic.transform import transform_and_compile
147
+
148
+ code = transform_and_compile(source, "script.py")
149
+ ThreadCheckTracker.start()
150
+ exec(code, {"_threadcheck_tracker": ThreadCheckTracker})
151
+ ThreadCheckTracker.stop()
152
+
153
+ print(ThreadCheckTracker.format_races())
154
+ ThreadCheckTracker.reset()
155
+ ```
156
+
157
+ ---
158
+
159
+ ## Architecture
160
+
161
+ ### Static Analysis Pipeline
162
+
163
+ 1. Parse source into AST
164
+ 2. Identify shared mutable state: globals, nonlocals, class attributes (`self.x`), module-level mutable objects
165
+ 3. Detect thread creation sites (`threading.Thread`)
166
+ 4. Cross-reference with lock usage (`with lock:`, `lock.acquire()`)
167
+ 5. Assign confidence: HIGH (thread target, no lock), MEDIUM (thread present, no lock), LOW (suspicious pattern, no thread context)
168
+ 6. Report findings with repair suggestions
169
+
170
+ ### Runtime Detection Pipeline
171
+
172
+ 1. Parse source into AST
173
+ 2. Identify shared variables per function scope
174
+ 3. Transform AST: inject `write_before()`, `lock_acquire()`, `lock_release()` calls around shared variable accesses
175
+ 4. Compile and execute transformed code under a tracker that maintains per-thread vector clocks
176
+ 5. On lock acquire, synchronize clocks (happens-before merge)
177
+ 6. After execution, scan access log for conflicting operations (concurrent writes or write-read pairs with no happens-before relationship)
178
+ 7. Report detected races with thread IDs and source locations
179
+
180
+ ---
181
+
182
+ ## Project Structure
183
+
184
+ ```
185
+ threadcheck/
186
+ ├── pyproject.toml
187
+ ├── src/
188
+ │ └── threadcheck/
189
+ │ ├── __init__.py
190
+ │ ├── __main__.py
191
+ │ ├── _version.py # single version source
192
+ │ ├── cli.py # argument parsing + dispatch
193
+ │ ├── static/
194
+ │ │ ├── analyzer.py # static analysis entry point
195
+ │ │ ├── visitors.py # AST visitors (global, nonlocal, class attr, shared mutable)
196
+ │ │ ├── lock_tracker.py # lock usage analysis
197
+ │ │ └── models.py # RaceWarning, Severity, Confidence models
198
+ │ ├── dynamic/
199
+ │ │ ├── __main__.py # run_script entry point
200
+ │ │ ├── transform.py # AST transformation engine
201
+ │ │ ├── tracker.py # runtime tracker with vector clocks
202
+ │ │ ├── clock.py # vector clock implementation
203
+ │ │ └── hook.py # sys.meta_path import hook
204
+ │ ├── reporting/
205
+ │ │ ├── formatter.py # terminal output formatting
206
+ │ │ └── types.py # type re-exports
207
+ │ └── pytest_plugin.py # pytest integration (planned)
208
+ ├── tests/
209
+ │ ├── fixtures/ # sample code with known races
210
+ │ ├── test_static_analyzer.py
211
+ │ └── test_dynamic_detector.py
212
+ └── README.md / README_CN.md
213
+ ```
214
+
215
+ ---
216
+
217
+ ## Roadmap
218
+
219
+ | Phase | Feature | Status |
220
+ |---|---|---|
221
+ | 1 | CLI, static analysis (globals/nonlocals) | Done |
222
+ | 2 | Class attributes, lock suppression, confidence scoring | Done |
223
+ | 3 | AST import hook, runtime instrumentation, vector clocks | Done |
224
+ | 4 | Race report deduplication, enhanced happens-before analysis | Planned |
225
+ | 5 | SARIF output, JSON reporting | Planned |
226
+ | 6 | pytest plugin | Planned |
227
+ | 7 | Free-threading compatibility checker | Planned |
228
+
229
+ ---
230
+
231
+ ## Limitations
232
+
233
+ - Static analysis may produce false positives (reports race that cannot occur at runtime) and false negatives (misses races that involve indirect sharing through aliases or containers)
234
+ - Runtime detection modifies the AST before execution; code that introspects its own source or frame objects may behave differently
235
+ - Runtime instrumentation incurs overhead (approximately 2-5x slowdown for typical code)
236
+ - Lock tracking supports `threading.Lock`, `threading.RLock`, and standard `with`-based patterns; other synchronization primitives (`threading.Event`, `threading.Condition`, third-party libraries) are not tracked
237
+
238
+ ---
239
+
240
+ ## License
241
+
242
+ MIT
243
+
244
+ ---
245
+
246
+ ## Contributing
247
+
248
+ Contributions are welcome. Please open an issue or submit a pull request on GitHub.
@@ -0,0 +1,25 @@
1
+ threadcheck/__init__.py,sha256=ySY4Q9AN1WXp1sDMrOd0SotrC1PwTNqBeDTt55wa8Z4,374
2
+ threadcheck/__main__.py,sha256=bYt9eEaoRQWdejEHFD8REx9jxVEdZptECFsV7F49Ink,30
3
+ threadcheck/_version.py,sha256=sXLh7g3KC4QCFxcZGBTpG2scR7hmmBsMjq6LqRptkRg,22
4
+ threadcheck/cli.py,sha256=RzbJWXtvjGBfZkdlxGIxYYA9DqajCEWnpQHFwGP6S8g,2834
5
+ threadcheck/pytest_plugin.py,sha256=5AvZWl_CR7sZAmvcG8QqG7za50nn87GSKkCALnpvyjc,1918
6
+ threadcheck/dynamic/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ threadcheck/dynamic/__main__.py,sha256=9_Q8impnDtwP2SfvYCPCgTS0kmw1UQZhPCsDU42gI3I,962
8
+ threadcheck/dynamic/clock.py,sha256=1zbHTHNFgwxCvP9yYA25UwFgCUnydkNOzcH1ogEisfY,907
9
+ threadcheck/dynamic/hook.py,sha256=fg_OR4p9rlCs5ES09VPteq9riss-avd-PR1uMZEsxZk,3032
10
+ threadcheck/dynamic/tracker.py,sha256=Ty8Fm2zWQxU8PP2MLb8tzhyExy40aNQdjgX29Vu5oEM,6215
11
+ threadcheck/dynamic/transform.py,sha256=_ldNaih-q4U0OJcxBH11LwIKcT-AVKB3zI9JcxBr-zs,6556
12
+ threadcheck/reporting/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ threadcheck/reporting/formatter.py,sha256=OZUdeOfcMq54J4b9jFQBhXgD_KSjVYsLCY9EGDu1Q_o,865
14
+ threadcheck/reporting/sarif.py,sha256=cXih48-HhUXfIFcfG5GVa7xBiPVvidvLfnpMESCVR1o,3572
15
+ threadcheck/reporting/types.py,sha256=KG8lwBtpGIEfi5YYb2xp-uHWxxXR71SwDWKWxnIeqq0,97
16
+ threadcheck/static/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ threadcheck/static/analyzer.py,sha256=MvQDdGwAV_c8Gc4ms1R7sCrjA38jPcvU2Rf5SvJgaiA,3087
18
+ threadcheck/static/lock_tracker.py,sha256=NbuR5zWKZ7jTdT2KRV06Sq1xPNCqeuvjnCnorQAIPUE,1476
19
+ threadcheck/static/models.py,sha256=iUaQjvrfoocqBHf7nTAa493EZFr0RDxGcqb4g5xeOeo,1113
20
+ threadcheck/static/visitors.py,sha256=yJCxZ_7K8c9DzDnQL4H1U85ViUAPhfZIujlkU1P3zf4,12538
21
+ threadcheck-0.0.1.dist-info/METADATA,sha256=IrMM0wHwuvMjwBdOt7fHDLfIlhWtGM_LtZ7Vrs_bULM,8504
22
+ threadcheck-0.0.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
23
+ threadcheck-0.0.1.dist-info/entry_points.txt,sha256=ZmGa6T6j2vSEq5-vjMGtunc55yiQDRGuyZG0W9PJDSU,105
24
+ threadcheck-0.0.1.dist-info/licenses/LICENSE,sha256=wqDapPVwjdlXZPEEEoRLLST4Pw4vKcoFCjTPXW-VeXE,1062
25
+ threadcheck-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,5 @@
1
+ [console_scripts]
2
+ threadcheck = threadcheck.cli:main
3
+
4
+ [pytest11]
5
+ threadcheck = threadcheck.pytest_plugin
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chidc
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.