codehound 1.0.3__tar.gz → 1.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.
- {codehound-1.0.3 → codehound-1.1.0}/PKG-INFO +26 -6
- {codehound-1.0.3 → codehound-1.1.0}/README.md +25 -5
- {codehound-1.0.3 → codehound-1.1.0}/src/codehound/__init__.py +5 -3
- {codehound-1.0.3 → codehound-1.1.0}/src/codehound/checks/__init__.py +2 -0
- codehound-1.1.0/src/codehound/checks/unawaited_coroutine.py +113 -0
- {codehound-1.0.3 → codehound-1.1.0}/src/codehound/core.py +19 -0
- {codehound-1.0.3 → codehound-1.1.0}/tests/test_checks.py +110 -0
- {codehound-1.0.3 → codehound-1.1.0}/.gitignore +0 -0
- {codehound-1.0.3 → codehound-1.1.0}/LICENSE +0 -0
- {codehound-1.0.3 → codehound-1.1.0}/pyproject.toml +0 -0
- {codehound-1.0.3 → codehound-1.1.0}/src/codehound/checks/blocking_async.py +0 -0
- {codehound-1.0.3 → codehound-1.1.0}/src/codehound/checks/datetime_utcnow.py +0 -0
- {codehound-1.0.3 → codehound-1.1.0}/src/codehound/checks/floating_task.py +0 -0
- {codehound-1.0.3 → codehound-1.1.0}/src/codehound/checks/get_event_loop.py +0 -0
- {codehound-1.0.3 → codehound-1.1.0}/src/codehound/checks/mutable_defaults.py +0 -0
- {codehound-1.0.3 → codehound-1.1.0}/src/codehound/checks/resource_leak.py +0 -0
- {codehound-1.0.3 → codehound-1.1.0}/src/codehound/cli.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: codehound
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: An AST-based static analyzer that hunts real correctness and async-safety bugs in Python code.
|
|
5
5
|
Project-URL: Homepage, https://github.com/kratos0718/codehound
|
|
6
6
|
Project-URL: Issues, https://github.com/kratos0718/codehound/issues
|
|
@@ -20,7 +20,7 @@ Description-Content-Type: text/markdown
|
|
|
20
20
|
|
|
21
21
|
# 🐕 codehound
|
|
22
22
|
|
|
23
|
-
**An AST-based static analyzer that hunts *real* bugs in large Python codebases —
|
|
23
|
+
**An AST-based static analyzer that hunts *real* bugs in large Python codebases — six of the seven rules are backed by a bug that was actually found and merged into a major open-source AI framework; the seventh is a hardening rule verified against real false positives instead.**
|
|
24
24
|
|
|
25
25
|
[](https://github.com/kratos0718/codehound/actions/workflows/ci.yml)
|
|
26
26
|
[](https://pypi.org/project/codehound/)
|
|
@@ -116,9 +116,26 @@ codehound list
|
|
|
116
116
|
| **CH004** | `deprecated-get-event-loop` | `asyncio.get_event_loop()` outside a running loop — deprecated since 3.10. | crewAI structured-tool / Snowflake search tool |
|
|
117
117
|
| **CH005** | `unclosed-file-handle` | `f = open(...)` with no `with` and no matching `.close()` — leaks descriptors until `RLIMIT_NOFILE` is exhausted. | agno `OpenAITools.transcribe_audio` |
|
|
118
118
|
| **CH006** | `floating-task` | `asyncio.create_task(...)` whose result is discarded — the loop keeps only a *weak* reference, so the task can be GC'd before it finishes. (Ruff RUF006) | hardening rule — the most under-caught async bug |
|
|
119
|
+
| **CH007** | `unawaited-coroutine-call` | `foo()` where `foo` is `async def`, called as a bare statement — no `await`, no scheduling. The coroutine object is created and dropped; the body **never runs at all**. | hardening rule — see below |
|
|
119
120
|
|
|
120
121
|
`codehound list` prints this from the source of truth.
|
|
121
122
|
|
|
123
|
+
CH007 doesn't have a found-and-merged bug behind it like the other six -
|
|
124
|
+
it targets a well-known Python correctness gotcha (the
|
|
125
|
+
`RuntimeWarning: coroutine 'foo' was never awaited` you get when a
|
|
126
|
+
coroutine is created and discarded) rather than one this project
|
|
127
|
+
personally tracked down. What it does have is two real false positives
|
|
128
|
+
caught and fixed while building it, both against agno: a bare `self.foo()`
|
|
129
|
+
call matched against an unrelated same-named `async def foo` on a
|
|
130
|
+
*different* class (agno's own sync/async "twin method" convention, e.g.
|
|
131
|
+
`ZepTools`/`ZepAsyncTools`), and a plain callable parameter shadowed by an
|
|
132
|
+
unrelated same-named async function hundreds of lines away in the same
|
|
133
|
+
file. Scanning ~20 major Python AI/ML frameworks after fixing both turned
|
|
134
|
+
up zero real instances - itself a result, not a null: it suggests either
|
|
135
|
+
that mature async test suites catch this before merge, or that most real
|
|
136
|
+
cases are cross-module calls, which this check deliberately doesn't chase
|
|
137
|
+
(same-file name matching only, consistent with every other rule here).
|
|
138
|
+
|
|
122
139
|
---
|
|
123
140
|
|
|
124
141
|
## How it works
|
|
@@ -135,12 +152,13 @@ codehound/
|
|
|
135
152
|
├── datetime_utcnow.py (CH003)
|
|
136
153
|
├── get_event_loop.py (CH004)
|
|
137
154
|
├── resource_leak.py (CH005)
|
|
138
|
-
|
|
155
|
+
├── floating_task.py (CH006)
|
|
156
|
+
└── unawaited_coroutine.py (CH007)
|
|
139
157
|
```
|
|
140
158
|
|
|
141
159
|
Each check receives a parsed `ast` tree plus the precomputed parent map and returns `Finding`s. Adding a rule is one file + one registry line + a test. See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for a full walkthrough of the engine, the parent map, and the design decisions.
|
|
142
160
|
|
|
143
|
-
**False-positive discipline is a feature.** CH005 won't flag a handle that's `return`ed (the caller owns it) or explicitly `.close()`d. CH006 won't flag `TaskGroup.create_task` (the group holds the reference). CH001 only fires when the *enclosing* function is `async`. The test suite asserts both "bad code is flagged" and "correct code is not."
|
|
161
|
+
**False-positive discipline is a feature.** CH005 won't flag a handle that's `return`ed (the caller owns it) or explicitly `.close()`d. CH006 won't flag `TaskGroup.create_task` (the group holds the reference). CH001 only fires when the *enclosing* function is `async`. CH007 scopes `self.foo()` matches to async methods on the *same* class as the call site, and bare `foo()` matches to module-level async functions that aren't shadowed by a same-named parameter - both guards exist because of real false positives caught while building it (see above). The test suite asserts both "bad code is flagged" and "correct code is not."
|
|
144
162
|
|
|
145
163
|
---
|
|
146
164
|
|
|
@@ -157,10 +175,12 @@ Every check has paired tests: the buggy pattern *is* flagged, and the idiomatic
|
|
|
157
175
|
|
|
158
176
|
## Roadmap
|
|
159
177
|
|
|
160
|
-
- [
|
|
178
|
+
- [x] `await` on a non-awaited coroutine (missing-await detection) — CH007
|
|
179
|
+
- [x] PyPI release — `pip install codehound`
|
|
180
|
+
- [ ] Cross-module resolution for CH007 (currently same-file only)
|
|
161
181
|
- [ ] Sync HTTP clients constructed inside async request handlers
|
|
162
182
|
- [ ] `--fix` for the mechanical rules (CH002, CH003, CH004)
|
|
163
|
-
- [ ] Pre-commit hook
|
|
183
|
+
- [ ] Pre-commit hook
|
|
164
184
|
|
|
165
185
|
---
|
|
166
186
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 🐕 codehound
|
|
2
2
|
|
|
3
|
-
**An AST-based static analyzer that hunts *real* bugs in large Python codebases —
|
|
3
|
+
**An AST-based static analyzer that hunts *real* bugs in large Python codebases — six of the seven rules are backed by a bug that was actually found and merged into a major open-source AI framework; the seventh is a hardening rule verified against real false positives instead.**
|
|
4
4
|
|
|
5
5
|
[](https://github.com/kratos0718/codehound/actions/workflows/ci.yml)
|
|
6
6
|
[](https://pypi.org/project/codehound/)
|
|
@@ -96,9 +96,26 @@ codehound list
|
|
|
96
96
|
| **CH004** | `deprecated-get-event-loop` | `asyncio.get_event_loop()` outside a running loop — deprecated since 3.10. | crewAI structured-tool / Snowflake search tool |
|
|
97
97
|
| **CH005** | `unclosed-file-handle` | `f = open(...)` with no `with` and no matching `.close()` — leaks descriptors until `RLIMIT_NOFILE` is exhausted. | agno `OpenAITools.transcribe_audio` |
|
|
98
98
|
| **CH006** | `floating-task` | `asyncio.create_task(...)` whose result is discarded — the loop keeps only a *weak* reference, so the task can be GC'd before it finishes. (Ruff RUF006) | hardening rule — the most under-caught async bug |
|
|
99
|
+
| **CH007** | `unawaited-coroutine-call` | `foo()` where `foo` is `async def`, called as a bare statement — no `await`, no scheduling. The coroutine object is created and dropped; the body **never runs at all**. | hardening rule — see below |
|
|
99
100
|
|
|
100
101
|
`codehound list` prints this from the source of truth.
|
|
101
102
|
|
|
103
|
+
CH007 doesn't have a found-and-merged bug behind it like the other six -
|
|
104
|
+
it targets a well-known Python correctness gotcha (the
|
|
105
|
+
`RuntimeWarning: coroutine 'foo' was never awaited` you get when a
|
|
106
|
+
coroutine is created and discarded) rather than one this project
|
|
107
|
+
personally tracked down. What it does have is two real false positives
|
|
108
|
+
caught and fixed while building it, both against agno: a bare `self.foo()`
|
|
109
|
+
call matched against an unrelated same-named `async def foo` on a
|
|
110
|
+
*different* class (agno's own sync/async "twin method" convention, e.g.
|
|
111
|
+
`ZepTools`/`ZepAsyncTools`), and a plain callable parameter shadowed by an
|
|
112
|
+
unrelated same-named async function hundreds of lines away in the same
|
|
113
|
+
file. Scanning ~20 major Python AI/ML frameworks after fixing both turned
|
|
114
|
+
up zero real instances - itself a result, not a null: it suggests either
|
|
115
|
+
that mature async test suites catch this before merge, or that most real
|
|
116
|
+
cases are cross-module calls, which this check deliberately doesn't chase
|
|
117
|
+
(same-file name matching only, consistent with every other rule here).
|
|
118
|
+
|
|
102
119
|
---
|
|
103
120
|
|
|
104
121
|
## How it works
|
|
@@ -115,12 +132,13 @@ codehound/
|
|
|
115
132
|
├── datetime_utcnow.py (CH003)
|
|
116
133
|
├── get_event_loop.py (CH004)
|
|
117
134
|
├── resource_leak.py (CH005)
|
|
118
|
-
|
|
135
|
+
├── floating_task.py (CH006)
|
|
136
|
+
└── unawaited_coroutine.py (CH007)
|
|
119
137
|
```
|
|
120
138
|
|
|
121
139
|
Each check receives a parsed `ast` tree plus the precomputed parent map and returns `Finding`s. Adding a rule is one file + one registry line + a test. See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for a full walkthrough of the engine, the parent map, and the design decisions.
|
|
122
140
|
|
|
123
|
-
**False-positive discipline is a feature.** CH005 won't flag a handle that's `return`ed (the caller owns it) or explicitly `.close()`d. CH006 won't flag `TaskGroup.create_task` (the group holds the reference). CH001 only fires when the *enclosing* function is `async`. The test suite asserts both "bad code is flagged" and "correct code is not."
|
|
141
|
+
**False-positive discipline is a feature.** CH005 won't flag a handle that's `return`ed (the caller owns it) or explicitly `.close()`d. CH006 won't flag `TaskGroup.create_task` (the group holds the reference). CH001 only fires when the *enclosing* function is `async`. CH007 scopes `self.foo()` matches to async methods on the *same* class as the call site, and bare `foo()` matches to module-level async functions that aren't shadowed by a same-named parameter - both guards exist because of real false positives caught while building it (see above). The test suite asserts both "bad code is flagged" and "correct code is not."
|
|
124
142
|
|
|
125
143
|
---
|
|
126
144
|
|
|
@@ -137,10 +155,12 @@ Every check has paired tests: the buggy pattern *is* flagged, and the idiomatic
|
|
|
137
155
|
|
|
138
156
|
## Roadmap
|
|
139
157
|
|
|
140
|
-
- [
|
|
158
|
+
- [x] `await` on a non-awaited coroutine (missing-await detection) — CH007
|
|
159
|
+
- [x] PyPI release — `pip install codehound`
|
|
160
|
+
- [ ] Cross-module resolution for CH007 (currently same-file only)
|
|
141
161
|
- [ ] Sync HTTP clients constructed inside async request handlers
|
|
142
162
|
- [ ] `--fix` for the mechanical rules (CH002, CH003, CH004)
|
|
143
|
-
- [ ] Pre-commit hook
|
|
163
|
+
- [ ] Pre-commit hook
|
|
144
164
|
|
|
145
165
|
---
|
|
146
166
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"""codehound - an AST-based static analyzer that hunts real bugs in Python code.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
open-source AI framework (agno, crewAI, mem0,
|
|
3
|
+
Seven checks. Six are each backed by a bug that was actually found and
|
|
4
|
+
fixed in a popular open-source AI framework (agno, crewAI, mem0,
|
|
5
|
+
llama_index, huggingface_hub). The seventh (CH007) is a hardening rule
|
|
6
|
+
verified against real false positives instead - see docs/FINDINGS.md.
|
|
5
7
|
"""
|
|
6
8
|
|
|
7
9
|
from __future__ import annotations
|
|
@@ -9,7 +11,7 @@ from __future__ import annotations
|
|
|
9
11
|
from codehound.checks import ALL_CHECKS, get_checks
|
|
10
12
|
from codehound.core import Check, Finding, scan_file, scan_path
|
|
11
13
|
|
|
12
|
-
__version__ = "1.0
|
|
14
|
+
__version__ = "1.1.0"
|
|
13
15
|
|
|
14
16
|
__all__ = [
|
|
15
17
|
"ALL_CHECKS",
|
|
@@ -8,6 +8,7 @@ from codehound.checks.floating_task import FloatingTask
|
|
|
8
8
|
from codehound.checks.get_event_loop import DeprecatedGetEventLoop
|
|
9
9
|
from codehound.checks.mutable_defaults import MutableDefaultArgument
|
|
10
10
|
from codehound.checks.resource_leak import UnclosedFileHandle
|
|
11
|
+
from codehound.checks.unawaited_coroutine import UnawaitedCoroutineCall
|
|
11
12
|
from codehound.core import Check
|
|
12
13
|
|
|
13
14
|
ALL_CHECKS: list[type[Check]] = [
|
|
@@ -17,6 +18,7 @@ ALL_CHECKS: list[type[Check]] = [
|
|
|
17
18
|
DeprecatedGetEventLoop,
|
|
18
19
|
UnclosedFileHandle,
|
|
19
20
|
FloatingTask,
|
|
21
|
+
UnawaitedCoroutineCall,
|
|
20
22
|
]
|
|
21
23
|
|
|
22
24
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""CH007 - Coroutine called without ``await`` (or scheduling).
|
|
2
|
+
|
|
3
|
+
Calling an ``async def`` function produces a coroutine object; it does not run
|
|
4
|
+
any of the function's body until something ``await``s it, wraps it in
|
|
5
|
+
``asyncio.create_task``/``asyncio.gather``, or otherwise drives it. A bare
|
|
6
|
+
``foo()`` where ``foo`` is a coroutine function, used as a standalone
|
|
7
|
+
statement, creates the coroutine and immediately discards it - the work
|
|
8
|
+
inside never executes at all, and Python emits a
|
|
9
|
+
``RuntimeWarning: coroutine 'foo' was never awaited`` (easy to miss if
|
|
10
|
+
warnings aren't surfaced in CI).
|
|
11
|
+
|
|
12
|
+
This is a stricter cousin of CH006 (floating-task): CH006's task is at least
|
|
13
|
+
*scheduled* and can still fail mid-run; here nothing ever starts.
|
|
14
|
+
|
|
15
|
+
Precision matters a lot here specifically because the sync/async "twin
|
|
16
|
+
method" convention (a `def foo` and an `async def foo` on two different
|
|
17
|
+
classes, e.g. `Toolkit` vs `AsyncToolkit`) is common in exactly the
|
|
18
|
+
codebases this tool targets - a naive whole-file name match flags every
|
|
19
|
+
`self.foo()` call against *any* same-named async method anywhere in the
|
|
20
|
+
file, sync twin included. Two real false positives found this way while
|
|
21
|
+
building this check (agno's `ZepTools`/`ZepAsyncTools.initialize`, and a
|
|
22
|
+
`write` call parameter shadowed by an unrelated same-named async closure
|
|
23
|
+
hundreds of lines away) are why the matching below is scoped:
|
|
24
|
+
|
|
25
|
+
- `foo()` (bare name): only matches a module-level `async def foo`, and
|
|
26
|
+
only if `foo` isn't also a parameter of the enclosing function (a
|
|
27
|
+
parameter shadows any outer name with the same spelling).
|
|
28
|
+
- `self.foo()` / `cls.foo()`: only matches an `async def foo` defined
|
|
29
|
+
directly in the *same* enclosing class as the call site - not a
|
|
30
|
+
same-named method on an unrelated class elsewhere in the file.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import ast
|
|
36
|
+
|
|
37
|
+
from codehound.core import Check, Finding, enclosing_class, enclosing_function
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class UnawaitedCoroutineCall(Check):
|
|
41
|
+
code = "CH007"
|
|
42
|
+
name = "unawaited-coroutine-call"
|
|
43
|
+
description = "async function called without await/create_task; the coroutine never runs."
|
|
44
|
+
|
|
45
|
+
def run(self, tree: ast.AST, parents: dict, path: str) -> list[Finding]:
|
|
46
|
+
module_level_async: set[str] = set()
|
|
47
|
+
class_async_methods: dict[int, set[str]] = {}
|
|
48
|
+
|
|
49
|
+
for node in ast.walk(tree):
|
|
50
|
+
if not isinstance(node, ast.AsyncFunctionDef):
|
|
51
|
+
continue
|
|
52
|
+
parent = parents.get(id(node))
|
|
53
|
+
if isinstance(parent, ast.Module):
|
|
54
|
+
module_level_async.add(node.name)
|
|
55
|
+
elif isinstance(parent, ast.ClassDef):
|
|
56
|
+
class_async_methods.setdefault(id(parent), set()).add(node.name)
|
|
57
|
+
|
|
58
|
+
if not module_level_async and not class_async_methods:
|
|
59
|
+
return []
|
|
60
|
+
|
|
61
|
+
findings: list[Finding] = []
|
|
62
|
+
for node in ast.walk(tree):
|
|
63
|
+
if not isinstance(node, ast.Expr) or not isinstance(node.value, ast.Call):
|
|
64
|
+
continue
|
|
65
|
+
call = node.value
|
|
66
|
+
func = call.func
|
|
67
|
+
called_name = None
|
|
68
|
+
|
|
69
|
+
if isinstance(func, ast.Name):
|
|
70
|
+
if func.id in module_level_async and not self._is_shadowed_param(
|
|
71
|
+
func.id, node, parents
|
|
72
|
+
):
|
|
73
|
+
called_name = func.id
|
|
74
|
+
elif isinstance(func, ast.Attribute) and isinstance(func.value, ast.Name):
|
|
75
|
+
if func.value.id in ("self", "cls"):
|
|
76
|
+
owning_class = enclosing_class(node, parents)
|
|
77
|
+
if owning_class is not None and func.attr in class_async_methods.get(
|
|
78
|
+
id(owning_class), set()
|
|
79
|
+
):
|
|
80
|
+
called_name = func.attr
|
|
81
|
+
|
|
82
|
+
if called_name is None:
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
findings.append(
|
|
86
|
+
Finding(
|
|
87
|
+
path=path,
|
|
88
|
+
line=node.lineno,
|
|
89
|
+
col=node.col_offset,
|
|
90
|
+
code=self.code,
|
|
91
|
+
message=(
|
|
92
|
+
f"`{called_name}(...)` is an async function called without `await` "
|
|
93
|
+
f"or scheduling; the coroutine is created and immediately discarded, "
|
|
94
|
+
f"so its body never runs."
|
|
95
|
+
),
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
return findings
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def _is_shadowed_param(name: str, node: ast.AST, parents: dict) -> bool:
|
|
102
|
+
fn = enclosing_function(node, parents)
|
|
103
|
+
if fn is None:
|
|
104
|
+
return False
|
|
105
|
+
args = fn.args
|
|
106
|
+
all_params = (
|
|
107
|
+
args.posonlyargs
|
|
108
|
+
+ args.args
|
|
109
|
+
+ args.kwonlyargs
|
|
110
|
+
+ ([args.vararg] if args.vararg else [])
|
|
111
|
+
+ ([args.kwarg] if args.kwarg else [])
|
|
112
|
+
)
|
|
113
|
+
return any(a.arg == name for a in all_params)
|
|
@@ -110,6 +110,25 @@ def enclosing_function(node: ast.AST, parents: dict):
|
|
|
110
110
|
return None
|
|
111
111
|
|
|
112
112
|
|
|
113
|
+
def enclosing_class(node: ast.AST, parents: dict) -> ast.ClassDef | None:
|
|
114
|
+
"""Return the nearest enclosing ClassDef, or None.
|
|
115
|
+
|
|
116
|
+
Stops at the first FunctionDef/AsyncFunctionDef boundary that isn't
|
|
117
|
+
itself inside the class body being searched for - i.e. this walks up
|
|
118
|
+
through nested functions too, so a method's inner helper still resolves
|
|
119
|
+
to the class it's defined in.
|
|
120
|
+
"""
|
|
121
|
+
cur = node
|
|
122
|
+
while cur is not None:
|
|
123
|
+
p = parents.get(id(cur))
|
|
124
|
+
if p is None:
|
|
125
|
+
return None
|
|
126
|
+
if isinstance(p, ast.ClassDef):
|
|
127
|
+
return p
|
|
128
|
+
cur = p
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
|
|
113
132
|
def is_awaited(node: ast.AST, parents: dict) -> bool:
|
|
114
133
|
"""True if the call ``node`` is the direct operand of an ``await``.
|
|
115
134
|
|
|
@@ -170,3 +170,113 @@ def test_ch006_ignores_taskgroup_create_task():
|
|
|
170
170
|
" tg.create_task(coro)\n"
|
|
171
171
|
)
|
|
172
172
|
assert _run(code, ["CH006"]) == []
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# --- CH007 unawaited-coroutine-call ------------------------------------------------
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def test_ch007_flags_bare_call_to_async_function():
|
|
179
|
+
code = (
|
|
180
|
+
"async def fetch():\n"
|
|
181
|
+
" ...\n"
|
|
182
|
+
"async def f():\n"
|
|
183
|
+
" fetch()\n"
|
|
184
|
+
)
|
|
185
|
+
findings = _run(code, ["CH007"])
|
|
186
|
+
assert len(findings) == 1
|
|
187
|
+
assert findings[0].code == "CH007"
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def test_ch007_ignores_awaited_call():
|
|
191
|
+
code = (
|
|
192
|
+
"async def fetch():\n"
|
|
193
|
+
" ...\n"
|
|
194
|
+
"async def f():\n"
|
|
195
|
+
" await fetch()\n"
|
|
196
|
+
)
|
|
197
|
+
assert _run(code, ["CH007"]) == []
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def test_ch007_ignores_call_wrapped_in_create_task():
|
|
201
|
+
code = (
|
|
202
|
+
"import asyncio\n"
|
|
203
|
+
"async def fetch():\n"
|
|
204
|
+
" ...\n"
|
|
205
|
+
"async def f():\n"
|
|
206
|
+
" asyncio.create_task(fetch())\n"
|
|
207
|
+
)
|
|
208
|
+
assert _run(code, ["CH007"]) == []
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def test_ch007_ignores_assigned_result():
|
|
212
|
+
code = (
|
|
213
|
+
"async def fetch():\n"
|
|
214
|
+
" ...\n"
|
|
215
|
+
"async def f():\n"
|
|
216
|
+
" coro = fetch()\n"
|
|
217
|
+
" await coro\n"
|
|
218
|
+
)
|
|
219
|
+
assert _run(code, ["CH007"]) == []
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def test_ch007_ignores_call_to_sync_function():
|
|
223
|
+
code = (
|
|
224
|
+
"def fetch():\n"
|
|
225
|
+
" ...\n"
|
|
226
|
+
"async def f():\n"
|
|
227
|
+
" fetch()\n"
|
|
228
|
+
)
|
|
229
|
+
assert _run(code, ["CH007"]) == []
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def test_ch007_flags_bare_call_to_async_method_via_self():
|
|
233
|
+
code = (
|
|
234
|
+
"class C:\n"
|
|
235
|
+
" async def fetch(self):\n"
|
|
236
|
+
" ...\n"
|
|
237
|
+
" async def f(self):\n"
|
|
238
|
+
" self.fetch()\n"
|
|
239
|
+
)
|
|
240
|
+
findings = _run(code, ["CH007"])
|
|
241
|
+
assert len(findings) == 1
|
|
242
|
+
assert findings[0].code == "CH007"
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def test_ch007_ignores_returned_coroutine():
|
|
246
|
+
code = (
|
|
247
|
+
"async def fetch():\n"
|
|
248
|
+
" ...\n"
|
|
249
|
+
"def f():\n"
|
|
250
|
+
" return fetch()\n"
|
|
251
|
+
)
|
|
252
|
+
assert _run(code, ["CH007"]) == []
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def test_ch007_ignores_sync_twin_method_on_different_class():
|
|
256
|
+
# Real false positive found in agno: ZepTools.initialize is sync,
|
|
257
|
+
# ZepAsyncTools.initialize (a different class) is async - calling
|
|
258
|
+
# self.initialize() from ZepTools must not match the unrelated twin.
|
|
259
|
+
code = (
|
|
260
|
+
"class ZepTools:\n"
|
|
261
|
+
" def __init__(self):\n"
|
|
262
|
+
" self.initialize()\n"
|
|
263
|
+
" def initialize(self):\n"
|
|
264
|
+
" ...\n"
|
|
265
|
+
"class ZepAsyncTools:\n"
|
|
266
|
+
" async def initialize(self):\n"
|
|
267
|
+
" ...\n"
|
|
268
|
+
)
|
|
269
|
+
assert _run(code, ["CH007"]) == []
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def test_ch007_ignores_name_shadowed_by_parameter():
|
|
273
|
+
# Real false positive found in agno: an unrelated `async def write`
|
|
274
|
+
# exists elsewhere in the file, but `write` here is a plain callable
|
|
275
|
+
# parameter, not a reference to that function.
|
|
276
|
+
code = (
|
|
277
|
+
"def run_with_retry(write):\n"
|
|
278
|
+
" write()\n"
|
|
279
|
+
"async def write():\n"
|
|
280
|
+
" ...\n"
|
|
281
|
+
)
|
|
282
|
+
assert _run(code, ["CH007"]) == []
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|