codehound 1.4.1__tar.gz → 1.4.2__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.4.1 → codehound-1.4.2}/PKG-INFO +12 -2
- {codehound-1.4.1 → codehound-1.4.2}/README.md +11 -1
- {codehound-1.4.1 → codehound-1.4.2}/src/codehound/__init__.py +1 -1
- {codehound-1.4.1 → codehound-1.4.2}/src/codehound/checks/lru_cache_on_method.py +71 -0
- {codehound-1.4.1 → codehound-1.4.2}/tests/test_checks.py +86 -0
- {codehound-1.4.1 → codehound-1.4.2}/.gitignore +0 -0
- {codehound-1.4.1 → codehound-1.4.2}/LICENSE +0 -0
- {codehound-1.4.1 → codehound-1.4.2}/pyproject.toml +0 -0
- {codehound-1.4.1 → codehound-1.4.2}/src/codehound/checks/__init__.py +0 -0
- {codehound-1.4.1 → codehound-1.4.2}/src/codehound/checks/async_property.py +0 -0
- {codehound-1.4.1 → codehound-1.4.2}/src/codehound/checks/asyncio_run_in_loop.py +0 -0
- {codehound-1.4.1 → codehound-1.4.2}/src/codehound/checks/bare_except.py +0 -0
- {codehound-1.4.1 → codehound-1.4.2}/src/codehound/checks/blocking_async.py +0 -0
- {codehound-1.4.1 → codehound-1.4.2}/src/codehound/checks/collections_abc_import.py +0 -0
- {codehound-1.4.1 → codehound-1.4.2}/src/codehound/checks/datetime_utcnow.py +0 -0
- {codehound-1.4.1 → codehound-1.4.2}/src/codehound/checks/discarded_future.py +0 -0
- {codehound-1.4.1 → codehound-1.4.2}/src/codehound/checks/floating_process.py +0 -0
- {codehound-1.4.1 → codehound-1.4.2}/src/codehound/checks/floating_task.py +0 -0
- {codehound-1.4.1 → codehound-1.4.2}/src/codehound/checks/floating_thread.py +0 -0
- {codehound-1.4.1 → codehound-1.4.2}/src/codehound/checks/get_event_loop.py +0 -0
- {codehound-1.4.1 → codehound-1.4.2}/src/codehound/checks/loop_closure_capture.py +0 -0
- {codehound-1.4.1 → codehound-1.4.2}/src/codehound/checks/mutable_defaults.py +0 -0
- {codehound-1.4.1 → codehound-1.4.2}/src/codehound/checks/removed_asyncio_task_methods.py +0 -0
- {codehound-1.4.1 → codehound-1.4.2}/src/codehound/checks/removed_getargspec.py +0 -0
- {codehound-1.4.1 → codehound-1.4.2}/src/codehound/checks/resource_leak.py +0 -0
- {codehound-1.4.1 → codehound-1.4.2}/src/codehound/checks/unawaited_coroutine.py +0 -0
- {codehound-1.4.1 → codehound-1.4.2}/src/codehound/checks/unclosed_socket.py +0 -0
- {codehound-1.4.1 → codehound-1.4.2}/src/codehound/checks/unprotected_lock.py +0 -0
- {codehound-1.4.1 → codehound-1.4.2}/src/codehound/cli.py +0 -0
- {codehound-1.4.1 → codehound-1.4.2}/src/codehound/core.py +0 -0
- {codehound-1.4.1 → codehound-1.4.2}/src/codehound/sarif.py +0 -0
- {codehound-1.4.1 → codehound-1.4.2}/src/codehound/terminal.py +0 -0
- {codehound-1.4.1 → codehound-1.4.2}/tests/test_output_formats.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: codehound
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.2
|
|
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
|
|
@@ -132,7 +132,7 @@ Uploads findings to the repo's **Security → Code Scanning** tab via SARIF, in
|
|
|
132
132
|
```yaml
|
|
133
133
|
repos:
|
|
134
134
|
- repo: https://github.com/kratos0718/codehound
|
|
135
|
-
rev: v1.4.
|
|
135
|
+
rev: v1.4.2
|
|
136
136
|
hooks:
|
|
137
137
|
- id: codehound
|
|
138
138
|
```
|
|
@@ -225,6 +225,16 @@ have a regression test verified to fail pre-fix and pass post-fix.
|
|
|
225
225
|
PRs: [optuna/optuna#6859](https://github.com/optuna/optuna/pull/6859),
|
|
226
226
|
[run-llama/llama_index#23089](https://github.com/run-llama/llama_index/pull/23089).
|
|
227
227
|
|
|
228
|
+
**A third CH011 shape needed a guard instead of a PR:** dspy's `Image` (a
|
|
229
|
+
pydantic model) caches `format()` the same way, but `Image` is frozen
|
|
230
|
+
(`model_config = ConfigDict(frozen=True)`), which makes it hashable and
|
|
231
|
+
equal *by field value*, not identity. Checked directly rather than
|
|
232
|
+
assumed: two separately constructed instances with equal fields hash
|
|
233
|
+
equal, and the second one's call is served from the first's cache entry
|
|
234
|
+
without ever being inserted itself — bounded, value-based memoization,
|
|
235
|
+
not a leak. CH011 now recognizes a frozen `@dataclass` or a frozen
|
|
236
|
+
pydantic model and skips it.
|
|
237
|
+
|
|
228
238
|
**CH016 found and fixed its own false positive the day it shipped.** The
|
|
229
239
|
first real-corpus scan of `unclosed-socket` turned up three hits in
|
|
230
240
|
vllm's distributed process-group rendezvous code - all three sockets were
|
|
@@ -112,7 +112,7 @@ Uploads findings to the repo's **Security → Code Scanning** tab via SARIF, in
|
|
|
112
112
|
```yaml
|
|
113
113
|
repos:
|
|
114
114
|
- repo: https://github.com/kratos0718/codehound
|
|
115
|
-
rev: v1.4.
|
|
115
|
+
rev: v1.4.2
|
|
116
116
|
hooks:
|
|
117
117
|
- id: codehound
|
|
118
118
|
```
|
|
@@ -205,6 +205,16 @@ have a regression test verified to fail pre-fix and pass post-fix.
|
|
|
205
205
|
PRs: [optuna/optuna#6859](https://github.com/optuna/optuna/pull/6859),
|
|
206
206
|
[run-llama/llama_index#23089](https://github.com/run-llama/llama_index/pull/23089).
|
|
207
207
|
|
|
208
|
+
**A third CH011 shape needed a guard instead of a PR:** dspy's `Image` (a
|
|
209
|
+
pydantic model) caches `format()` the same way, but `Image` is frozen
|
|
210
|
+
(`model_config = ConfigDict(frozen=True)`), which makes it hashable and
|
|
211
|
+
equal *by field value*, not identity. Checked directly rather than
|
|
212
|
+
assumed: two separately constructed instances with equal fields hash
|
|
213
|
+
equal, and the second one's call is served from the first's cache entry
|
|
214
|
+
without ever being inserted itself — bounded, value-based memoization,
|
|
215
|
+
not a leak. CH011 now recognizes a frozen `@dataclass` or a frozen
|
|
216
|
+
pydantic model and skips it.
|
|
217
|
+
|
|
208
218
|
**CH016 found and fixed its own false positive the day it shipped.** The
|
|
209
219
|
first real-corpus scan of `unclosed-socket` turned up three hits in
|
|
210
220
|
vllm's distributed process-group rendezvous code - all three sockets were
|
|
@@ -12,6 +12,19 @@ instance alive), not a style nit.
|
|
|
12
12
|
Only fires on a method - a plain module-level function decorated the same
|
|
13
13
|
way is exactly what ``lru_cache`` is for, and caches correctly with no
|
|
14
14
|
leak (nothing but the arguments themselves are held).
|
|
15
|
+
|
|
16
|
+
Also doesn't fire on a *frozen* value class (a stdlib ``@dataclass(frozen=
|
|
17
|
+
True)`` or a pydantic model with ``model_config = ConfigDict(frozen=True)``
|
|
18
|
+
/ ``class Config: frozen = True``). A frozen class is hashable and equal by
|
|
19
|
+
its field values, not by identity - verified directly: two separately
|
|
20
|
+
constructed instances with the same field values hash equal, and a second,
|
|
21
|
+
distinct instance's call is served straight from the first instance's
|
|
22
|
+
cache entry without ever being inserted itself (dspy's `Image.format()`,
|
|
23
|
+
checked this way, turned out to be exactly this - real, bounded-by-value
|
|
24
|
+
memoization on an immutable type, not a leak). The cache still only ever
|
|
25
|
+
holds as many distinct instances as there are distinct field-value
|
|
26
|
+
combinations seen, capped at `maxsize`, which is the same bound an
|
|
27
|
+
equivalent free function cached by value would have.
|
|
15
28
|
"""
|
|
16
29
|
|
|
17
30
|
from __future__ import annotations
|
|
@@ -32,6 +45,62 @@ def _is_cache_decorator(node: ast.expr) -> bool:
|
|
|
32
45
|
return False
|
|
33
46
|
|
|
34
47
|
|
|
48
|
+
def _is_frozen_dataclass(decorators: list[ast.expr]) -> bool:
|
|
49
|
+
for d in decorators:
|
|
50
|
+
if not isinstance(d, ast.Call):
|
|
51
|
+
continue
|
|
52
|
+
target = d.func
|
|
53
|
+
name = target.id if isinstance(target, ast.Name) else getattr(target, "attr", None)
|
|
54
|
+
if name != "dataclass":
|
|
55
|
+
continue
|
|
56
|
+
for kw in d.keywords:
|
|
57
|
+
if kw.arg == "frozen" and isinstance(kw.value, ast.Constant) and kw.value.value is True:
|
|
58
|
+
return True
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _is_frozen_pydantic_model(cls: ast.ClassDef) -> bool:
|
|
63
|
+
for stmt in cls.body:
|
|
64
|
+
# model_config = ConfigDict(..., frozen=True, ...) or {"frozen": True}
|
|
65
|
+
if (
|
|
66
|
+
isinstance(stmt, ast.Assign)
|
|
67
|
+
and len(stmt.targets) == 1
|
|
68
|
+
and isinstance(stmt.targets[0], ast.Name)
|
|
69
|
+
and stmt.targets[0].id == "model_config"
|
|
70
|
+
):
|
|
71
|
+
value = stmt.value
|
|
72
|
+
if isinstance(value, ast.Call):
|
|
73
|
+
for kw in value.keywords:
|
|
74
|
+
if kw.arg == "frozen" and isinstance(kw.value, ast.Constant) and kw.value.value is True:
|
|
75
|
+
return True
|
|
76
|
+
elif isinstance(value, ast.Dict):
|
|
77
|
+
for k, v in zip(value.keys, value.values):
|
|
78
|
+
if (
|
|
79
|
+
isinstance(k, ast.Constant)
|
|
80
|
+
and k.value == "frozen"
|
|
81
|
+
and isinstance(v, ast.Constant)
|
|
82
|
+
and v.value is True
|
|
83
|
+
):
|
|
84
|
+
return True
|
|
85
|
+
# Old-style pydantic v1: `class Config: frozen = True`
|
|
86
|
+
if isinstance(stmt, ast.ClassDef) and stmt.name == "Config":
|
|
87
|
+
for inner in stmt.body:
|
|
88
|
+
if (
|
|
89
|
+
isinstance(inner, ast.Assign)
|
|
90
|
+
and len(inner.targets) == 1
|
|
91
|
+
and isinstance(inner.targets[0], ast.Name)
|
|
92
|
+
and inner.targets[0].id == "frozen"
|
|
93
|
+
and isinstance(inner.value, ast.Constant)
|
|
94
|
+
and inner.value.value is True
|
|
95
|
+
):
|
|
96
|
+
return True
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _is_frozen_value_class(cls: ast.ClassDef) -> bool:
|
|
101
|
+
return _is_frozen_dataclass(cls.decorator_list) or _is_frozen_pydantic_model(cls)
|
|
102
|
+
|
|
103
|
+
|
|
35
104
|
def _is_static_or_classmethod(decorators: list[ast.expr]) -> bool:
|
|
36
105
|
for d in decorators:
|
|
37
106
|
target = d.func if isinstance(d, ast.Call) else d
|
|
@@ -56,6 +125,8 @@ class LruCacheOnMethod(Check):
|
|
|
56
125
|
continue
|
|
57
126
|
if _is_static_or_classmethod(node.decorator_list):
|
|
58
127
|
continue
|
|
128
|
+
if _is_frozen_value_class(parent):
|
|
129
|
+
continue
|
|
59
130
|
if not node.args.args or node.args.args[0].arg not in ("self",):
|
|
60
131
|
continue
|
|
61
132
|
for dec in node.decorator_list:
|
|
@@ -517,6 +517,92 @@ def test_ch011_ignores_uncached_instance_method():
|
|
|
517
517
|
assert _run(code, ["CH011"]) == []
|
|
518
518
|
|
|
519
519
|
|
|
520
|
+
def test_ch011_ignores_frozen_dataclass():
|
|
521
|
+
# A frozen dataclass is hashable and equal by field value, not identity -
|
|
522
|
+
# verified empirically: two separately constructed instances with the
|
|
523
|
+
# same field values hash equal, and lru_cache serves the second one
|
|
524
|
+
# straight from the first's cache entry without ever storing it. Bounded
|
|
525
|
+
# by maxsize the same way an equivalent free function cached by value
|
|
526
|
+
# would be - not a leak.
|
|
527
|
+
code = (
|
|
528
|
+
"from dataclasses import dataclass\n"
|
|
529
|
+
"from functools import lru_cache\n"
|
|
530
|
+
"@dataclass(frozen=True)\n"
|
|
531
|
+
"class Point:\n"
|
|
532
|
+
" x: int\n"
|
|
533
|
+
" @lru_cache\n"
|
|
534
|
+
" def scaled(self, factor):\n"
|
|
535
|
+
" return self.x * factor\n"
|
|
536
|
+
)
|
|
537
|
+
assert _run(code, ["CH011"]) == []
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def test_ch011_ignores_frozen_pydantic_model():
|
|
541
|
+
# Real false positive found in dspy: Image (model_config =
|
|
542
|
+
# ConfigDict(frozen=True)) caches format() with @lru_cache(maxsize=32) -
|
|
543
|
+
# confirmed via a REPL check that this is value-based memoization, not a
|
|
544
|
+
# per-instance leak.
|
|
545
|
+
code = (
|
|
546
|
+
"import pydantic\n"
|
|
547
|
+
"from functools import lru_cache\n"
|
|
548
|
+
"class Image(pydantic.BaseModel):\n"
|
|
549
|
+
" url: str\n"
|
|
550
|
+
" model_config = pydantic.ConfigDict(frozen=True)\n"
|
|
551
|
+
" @lru_cache(maxsize=32)\n"
|
|
552
|
+
" def format(self):\n"
|
|
553
|
+
" return self.url.upper()\n"
|
|
554
|
+
)
|
|
555
|
+
assert _run(code, ["CH011"]) == []
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def test_ch011_ignores_frozen_pydantic_model_v1_style_config():
|
|
559
|
+
code = (
|
|
560
|
+
"import pydantic\n"
|
|
561
|
+
"from functools import lru_cache\n"
|
|
562
|
+
"class Image(pydantic.BaseModel):\n"
|
|
563
|
+
" url: str\n"
|
|
564
|
+
" class Config:\n"
|
|
565
|
+
" frozen = True\n"
|
|
566
|
+
" @lru_cache\n"
|
|
567
|
+
" def format(self):\n"
|
|
568
|
+
" return self.url.upper()\n"
|
|
569
|
+
)
|
|
570
|
+
assert _run(code, ["CH011"]) == []
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def test_ch011_still_flags_mutable_dataclass():
|
|
574
|
+
# Guard against the frozen-class check over-suppressing: a plain (not
|
|
575
|
+
# frozen) dataclass is not hashable by value, so this must still flag.
|
|
576
|
+
code = (
|
|
577
|
+
"from dataclasses import dataclass\n"
|
|
578
|
+
"from functools import lru_cache\n"
|
|
579
|
+
"@dataclass\n"
|
|
580
|
+
"class Point:\n"
|
|
581
|
+
" x: int\n"
|
|
582
|
+
" @lru_cache\n"
|
|
583
|
+
" def scaled(self, factor):\n"
|
|
584
|
+
" return self.x * factor\n"
|
|
585
|
+
)
|
|
586
|
+
findings = _run(code, ["CH011"])
|
|
587
|
+
assert len(findings) == 1
|
|
588
|
+
assert findings[0].code == "CH011"
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def test_ch011_still_flags_pydantic_model_without_frozen_config():
|
|
592
|
+
code = (
|
|
593
|
+
"import pydantic\n"
|
|
594
|
+
"from functools import lru_cache\n"
|
|
595
|
+
"class Image(pydantic.BaseModel):\n"
|
|
596
|
+
" url: str\n"
|
|
597
|
+
" @lru_cache\n"
|
|
598
|
+
" def format(self):\n"
|
|
599
|
+
" return self.url.upper()\n"
|
|
600
|
+
)
|
|
601
|
+
findings = _run(code, ["CH011"])
|
|
602
|
+
assert len(findings) == 1
|
|
603
|
+
assert findings[0].code == "CH011"
|
|
604
|
+
|
|
605
|
+
|
|
520
606
|
# --- CH012 floating-process ----------------------------------------------------------
|
|
521
607
|
|
|
522
608
|
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|