redup 0.3.8__tar.gz → 0.3.10__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.
- {redup-0.3.8 → redup-0.3.10}/PKG-INFO +2 -2
- {redup-0.3.8 → redup-0.3.10}/README.md +1 -1
- {redup-0.3.8 → redup-0.3.10}/pyproject.toml +1 -1
- {redup-0.3.8 → redup-0.3.10}/src/redup/__init__.py +1 -1
- {redup-0.3.8 → redup-0.3.10}/src/redup/cli_app/main.py +5 -0
- redup-0.3.10/src/redup/core/hash_cache.py +104 -0
- {redup-0.3.8 → redup-0.3.10}/src/redup/core/scanner.py +7 -3
- {redup-0.3.8 → redup-0.3.10}/src/redup.egg-info/PKG-INFO +2 -2
- {redup-0.3.8 → redup-0.3.10}/src/redup.egg-info/SOURCES.txt +1 -0
- {redup-0.3.8 → redup-0.3.10}/tests/test_scanner.py +3 -3
- {redup-0.3.8 → redup-0.3.10}/LICENSE +0 -0
- {redup-0.3.8 → redup-0.3.10}/setup.cfg +0 -0
- {redup-0.3.8 → redup-0.3.10}/src/redup/__main__.py +0 -0
- {redup-0.3.8 → redup-0.3.10}/src/redup/cli_app/__init__.py +0 -0
- {redup-0.3.8 → redup-0.3.10}/src/redup/core/__init__.py +0 -0
- {redup-0.3.8 → redup-0.3.10}/src/redup/core/config.py +0 -0
- {redup-0.3.8 → redup-0.3.10}/src/redup/core/differ.py +0 -0
- {redup-0.3.8 → redup-0.3.10}/src/redup/core/hasher.py +0 -0
- {redup-0.3.8 → redup-0.3.10}/src/redup/core/lsh_matcher.py +0 -0
- {redup-0.3.8 → redup-0.3.10}/src/redup/core/matcher.py +0 -0
- {redup-0.3.8 → redup-0.3.10}/src/redup/core/models.py +0 -0
- {redup-0.3.8 → redup-0.3.10}/src/redup/core/parallel_scanner.py +0 -0
- {redup-0.3.8 → redup-0.3.10}/src/redup/core/pipeline.py +0 -0
- {redup-0.3.8 → redup-0.3.10}/src/redup/core/planner.py +0 -0
- {redup-0.3.8 → redup-0.3.10}/src/redup/core/ts_extractor.py +0 -0
- {redup-0.3.8 → redup-0.3.10}/src/redup/core/utils/__init__.py +0 -0
- {redup-0.3.8 → redup-0.3.10}/src/redup/core/utils/duplicate_finders.py +0 -0
- {redup-0.3.8 → redup-0.3.10}/src/redup/core/utils/function_extractor.py +0 -0
- {redup-0.3.8 → redup-0.3.10}/src/redup/core/utils/hash_utils.py +0 -0
- {redup-0.3.8 → redup-0.3.10}/src/redup/reporters/__init__.py +0 -0
- {redup-0.3.8 → redup-0.3.10}/src/redup/reporters/code2llm_reporter.py +0 -0
- {redup-0.3.8 → redup-0.3.10}/src/redup/reporters/enhanced_reporter.py +0 -0
- {redup-0.3.8 → redup-0.3.10}/src/redup/reporters/json_reporter.py +0 -0
- {redup-0.3.8 → redup-0.3.10}/src/redup/reporters/markdown_reporter.py +0 -0
- {redup-0.3.8 → redup-0.3.10}/src/redup/reporters/toon_reporter.py +0 -0
- {redup-0.3.8 → redup-0.3.10}/src/redup/reporters/yaml_reporter.py +0 -0
- {redup-0.3.8 → redup-0.3.10}/src/redup.egg-info/dependency_links.txt +0 -0
- {redup-0.3.8 → redup-0.3.10}/src/redup.egg-info/entry_points.txt +0 -0
- {redup-0.3.8 → redup-0.3.10}/src/redup.egg-info/requires.txt +0 -0
- {redup-0.3.8 → redup-0.3.10}/src/redup.egg-info/top_level.txt +0 -0
- {redup-0.3.8 → redup-0.3.10}/tests/test_e2e.py +0 -0
- {redup-0.3.8 → redup-0.3.10}/tests/test_hasher.py +0 -0
- {redup-0.3.8 → redup-0.3.10}/tests/test_matcher.py +0 -0
- {redup-0.3.8 → redup-0.3.10}/tests/test_models.py +0 -0
- {redup-0.3.8 → redup-0.3.10}/tests/test_pipeline.py +0 -0
- {redup-0.3.8 → redup-0.3.10}/tests/test_planner.py +0 -0
- {redup-0.3.8 → redup-0.3.10}/tests/test_reporters.py +0 -0
- {redup-0.3.8 → redup-0.3.10}/tests/test_ts_extractor.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: redup
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.10
|
|
4
4
|
Summary: Code duplication analyzer and refactoring planner for LLMs
|
|
5
5
|
Author-email: Tom Sapletta <tom@sapletta.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -83,7 +83,7 @@ Dynamic: license-file
|
|
|
83
83
|
[](https://pypi.org/project/redup/)
|
|
84
84
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
85
85
|
[](https://python.org)
|
|
86
|
-
[](https://pypi.org/project/redup/)
|
|
87
87
|
|
|
88
88
|
reDUP scans codebases for duplicated functions, blocks, and structural patterns — then builds a prioritized refactoring map that LLMs can consume to eliminate redundancy systematically.
|
|
89
89
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://pypi.org/project/redup/)
|
|
6
6
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
7
7
|
[](https://python.org)
|
|
8
|
-
[](https://pypi.org/project/redup/)
|
|
9
9
|
|
|
10
10
|
reDUP scans codebases for duplicated functions, blocks, and structural patterns — then builds a prioritized refactoring map that LLMs can consume to eliminate redundancy systematically.
|
|
11
11
|
|
|
@@ -226,6 +226,11 @@ def scan(
|
|
|
226
226
|
"--max-workers",
|
|
227
227
|
help="Number of parallel workers (default: CPU count).",
|
|
228
228
|
),
|
|
229
|
+
incremental: bool = typer.Option(
|
|
230
|
+
False,
|
|
231
|
+
"--incremental",
|
|
232
|
+
help="Use hash cache to skip unchanged files (creates .redup_cache.json).",
|
|
233
|
+
),
|
|
229
234
|
) -> None:
|
|
230
235
|
"""Scan a project for code duplicates and generate a refactoring map."""
|
|
231
236
|
config = _build_config_with_file_support(path, extensions, min_lines, min_similarity, include_tests)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Hash cache for incremental reDUP scans.
|
|
2
|
+
|
|
3
|
+
Stores SHA-256 hashes of files to skip unchanged files in subsequent scans.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import hashlib
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class HashCache:
|
|
15
|
+
"""Cache for file hashes to enable incremental scanning."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, cache_path: Path | None = None):
|
|
18
|
+
if cache_path is None:
|
|
19
|
+
cache_path = Path(".redup_cache.json")
|
|
20
|
+
self.cache_path = cache_path
|
|
21
|
+
self._cache: dict[str, dict[str, Any]] = {}
|
|
22
|
+
self._load()
|
|
23
|
+
|
|
24
|
+
def _load(self) -> None:
|
|
25
|
+
"""Load cache from disk."""
|
|
26
|
+
if self.cache_path.exists():
|
|
27
|
+
try:
|
|
28
|
+
with open(self.cache_path, 'r', encoding='utf-8') as f:
|
|
29
|
+
self._cache = json.load(f)
|
|
30
|
+
except (json.JSONDecodeError, IOError):
|
|
31
|
+
self._cache = {}
|
|
32
|
+
|
|
33
|
+
def save(self) -> None:
|
|
34
|
+
"""Save cache to disk."""
|
|
35
|
+
try:
|
|
36
|
+
with open(self.cache_path, 'w', encoding='utf-8') as f:
|
|
37
|
+
json.dump(self._cache, f, indent=2)
|
|
38
|
+
except IOError:
|
|
39
|
+
pass # Cache save is best-effort
|
|
40
|
+
|
|
41
|
+
def get_file_hash(self, file_path: Path) -> str:
|
|
42
|
+
"""Calculate hash of file content."""
|
|
43
|
+
try:
|
|
44
|
+
with open(file_path, 'rb') as f:
|
|
45
|
+
return hashlib.sha256(f.read()).hexdigest()[:16]
|
|
46
|
+
except (IOError, OSError):
|
|
47
|
+
return ""
|
|
48
|
+
|
|
49
|
+
def is_unchanged(self, file_path: Path, config_hash: str = "") -> bool:
|
|
50
|
+
"""Check if file is unchanged since last scan."""
|
|
51
|
+
path_str = str(file_path)
|
|
52
|
+
|
|
53
|
+
if path_str not in self._cache:
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
entry = self._cache[path_str]
|
|
57
|
+
current_hash = self.get_file_hash(file_path)
|
|
58
|
+
|
|
59
|
+
# Check content hash and config
|
|
60
|
+
return (
|
|
61
|
+
entry.get("content_hash") == current_hash and
|
|
62
|
+
entry.get("config_hash") == config_hash
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def update(self, file_path: Path, file_results: dict[str, Any], config_hash: str = "") -> None:
|
|
66
|
+
"""Update cache entry for a file."""
|
|
67
|
+
path_str = str(file_path)
|
|
68
|
+
content_hash = self.get_file_hash(file_path)
|
|
69
|
+
|
|
70
|
+
self._cache[path_str] = {
|
|
71
|
+
"content_hash": content_hash,
|
|
72
|
+
"config_hash": config_hash,
|
|
73
|
+
"mtime": file_path.stat().st_mtime if file_path.exists() else 0,
|
|
74
|
+
"results": file_results,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
def invalidate(self, file_path: Path | None = None) -> None:
|
|
78
|
+
"""Invalidate cache for a file or entire cache."""
|
|
79
|
+
if file_path is None:
|
|
80
|
+
self._cache = {}
|
|
81
|
+
else:
|
|
82
|
+
path_str = str(file_path)
|
|
83
|
+
self._cache.pop(path_str, None)
|
|
84
|
+
|
|
85
|
+
def get_cached_results(self, file_path: Path) -> dict[str, Any] | None:
|
|
86
|
+
"""Get cached results for unchanged file."""
|
|
87
|
+
path_str = str(file_path)
|
|
88
|
+
|
|
89
|
+
if path_str in self._cache:
|
|
90
|
+
return self._cache[path_str].get("results")
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
def clear(self) -> None:
|
|
94
|
+
"""Clear entire cache."""
|
|
95
|
+
self._cache = {}
|
|
96
|
+
if self.cache_path.exists():
|
|
97
|
+
self.cache_path.unlink()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _config_to_hash(config: Any) -> str:
|
|
101
|
+
"""Convert config to a hash string for cache validation."""
|
|
102
|
+
import json
|
|
103
|
+
config_str = json.dumps(config, sort_keys=True, default=str)
|
|
104
|
+
return hashlib.sha256(config_str.encode()).hexdigest()[:8]
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import fnmatch
|
|
6
|
+
import functools
|
|
6
7
|
import time
|
|
7
8
|
from collections.abc import Iterator
|
|
8
9
|
from dataclasses import dataclass, field
|
|
@@ -40,7 +41,8 @@ class ScannedFile:
|
|
|
40
41
|
return len(self.lines)
|
|
41
42
|
|
|
42
43
|
|
|
43
|
-
|
|
44
|
+
@functools.lru_cache(maxsize=1000)
|
|
45
|
+
def _should_exclude(path: Path, patterns: tuple[str, ...]) -> bool:
|
|
44
46
|
"""Check if a path matches any exclusion pattern."""
|
|
45
47
|
name = path.name
|
|
46
48
|
str_path = str(path)
|
|
@@ -63,7 +65,7 @@ def _collect_files(config: ScanConfig) -> Iterator[Path]:
|
|
|
63
65
|
root = config.root.resolve()
|
|
64
66
|
for ext in config.extensions:
|
|
65
67
|
for path in root.rglob(f"*{ext}"):
|
|
66
|
-
if _should_exclude(path, config.exclude_patterns):
|
|
68
|
+
if _should_exclude(path, tuple(config.exclude_patterns)):
|
|
67
69
|
continue
|
|
68
70
|
if not config.include_tests and _is_test_file(path):
|
|
69
71
|
continue
|
|
@@ -100,7 +102,9 @@ def _extract_blocks_sliding(lines: list[str], min_lines: int) -> list[tuple[int,
|
|
|
100
102
|
chunk = non_empty[start_idx : start_idx + window_size]
|
|
101
103
|
first_line = chunk[0][0] + 1
|
|
102
104
|
last_line = chunk[-1][0] + 1
|
|
103
|
-
|
|
105
|
+
# Optimized: reduce string operations
|
|
106
|
+
lines = [line.strip() for _, line in chunk]
|
|
107
|
+
text = "\n".join(lines)
|
|
104
108
|
blocks.append((first_line, last_line, text))
|
|
105
109
|
|
|
106
110
|
return blocks
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: redup
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.10
|
|
4
4
|
Summary: Code duplication analyzer and refactoring planner for LLMs
|
|
5
5
|
Author-email: Tom Sapletta <tom@sapletta.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -83,7 +83,7 @@ Dynamic: license-file
|
|
|
83
83
|
[](https://pypi.org/project/redup/)
|
|
84
84
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
85
85
|
[](https://python.org)
|
|
86
|
-
[](https://pypi.org/project/redup/)
|
|
87
87
|
|
|
88
88
|
reDUP scans codebases for duplicated functions, blocks, and structural patterns — then builds a prioritized refactoring map that LLMs can consume to eliminate redundancy systematically.
|
|
89
89
|
|
|
@@ -13,15 +13,15 @@ from redup.core.scanner import (
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def test_should_exclude_git():
|
|
16
|
-
assert _should_exclude(Path(".git/config"),
|
|
16
|
+
assert _should_exclude(Path(".git/config"), (".git",))
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def test_should_exclude_venv():
|
|
20
|
-
assert _should_exclude(Path("project/venv/lib/site.py"),
|
|
20
|
+
assert _should_exclude(Path("project/venv/lib/site.py"), ("venv",))
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def test_should_not_exclude_normal():
|
|
24
|
-
assert not _should_exclude(Path("src/main.py"),
|
|
24
|
+
assert not _should_exclude(Path("src/main.py"), (".git", "venv"))
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
def test_is_test_file():
|
|
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
|
|
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
|