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.
Files changed (48) hide show
  1. {redup-0.3.8 → redup-0.3.10}/PKG-INFO +2 -2
  2. {redup-0.3.8 → redup-0.3.10}/README.md +1 -1
  3. {redup-0.3.8 → redup-0.3.10}/pyproject.toml +1 -1
  4. {redup-0.3.8 → redup-0.3.10}/src/redup/__init__.py +1 -1
  5. {redup-0.3.8 → redup-0.3.10}/src/redup/cli_app/main.py +5 -0
  6. redup-0.3.10/src/redup/core/hash_cache.py +104 -0
  7. {redup-0.3.8 → redup-0.3.10}/src/redup/core/scanner.py +7 -3
  8. {redup-0.3.8 → redup-0.3.10}/src/redup.egg-info/PKG-INFO +2 -2
  9. {redup-0.3.8 → redup-0.3.10}/src/redup.egg-info/SOURCES.txt +1 -0
  10. {redup-0.3.8 → redup-0.3.10}/tests/test_scanner.py +3 -3
  11. {redup-0.3.8 → redup-0.3.10}/LICENSE +0 -0
  12. {redup-0.3.8 → redup-0.3.10}/setup.cfg +0 -0
  13. {redup-0.3.8 → redup-0.3.10}/src/redup/__main__.py +0 -0
  14. {redup-0.3.8 → redup-0.3.10}/src/redup/cli_app/__init__.py +0 -0
  15. {redup-0.3.8 → redup-0.3.10}/src/redup/core/__init__.py +0 -0
  16. {redup-0.3.8 → redup-0.3.10}/src/redup/core/config.py +0 -0
  17. {redup-0.3.8 → redup-0.3.10}/src/redup/core/differ.py +0 -0
  18. {redup-0.3.8 → redup-0.3.10}/src/redup/core/hasher.py +0 -0
  19. {redup-0.3.8 → redup-0.3.10}/src/redup/core/lsh_matcher.py +0 -0
  20. {redup-0.3.8 → redup-0.3.10}/src/redup/core/matcher.py +0 -0
  21. {redup-0.3.8 → redup-0.3.10}/src/redup/core/models.py +0 -0
  22. {redup-0.3.8 → redup-0.3.10}/src/redup/core/parallel_scanner.py +0 -0
  23. {redup-0.3.8 → redup-0.3.10}/src/redup/core/pipeline.py +0 -0
  24. {redup-0.3.8 → redup-0.3.10}/src/redup/core/planner.py +0 -0
  25. {redup-0.3.8 → redup-0.3.10}/src/redup/core/ts_extractor.py +0 -0
  26. {redup-0.3.8 → redup-0.3.10}/src/redup/core/utils/__init__.py +0 -0
  27. {redup-0.3.8 → redup-0.3.10}/src/redup/core/utils/duplicate_finders.py +0 -0
  28. {redup-0.3.8 → redup-0.3.10}/src/redup/core/utils/function_extractor.py +0 -0
  29. {redup-0.3.8 → redup-0.3.10}/src/redup/core/utils/hash_utils.py +0 -0
  30. {redup-0.3.8 → redup-0.3.10}/src/redup/reporters/__init__.py +0 -0
  31. {redup-0.3.8 → redup-0.3.10}/src/redup/reporters/code2llm_reporter.py +0 -0
  32. {redup-0.3.8 → redup-0.3.10}/src/redup/reporters/enhanced_reporter.py +0 -0
  33. {redup-0.3.8 → redup-0.3.10}/src/redup/reporters/json_reporter.py +0 -0
  34. {redup-0.3.8 → redup-0.3.10}/src/redup/reporters/markdown_reporter.py +0 -0
  35. {redup-0.3.8 → redup-0.3.10}/src/redup/reporters/toon_reporter.py +0 -0
  36. {redup-0.3.8 → redup-0.3.10}/src/redup/reporters/yaml_reporter.py +0 -0
  37. {redup-0.3.8 → redup-0.3.10}/src/redup.egg-info/dependency_links.txt +0 -0
  38. {redup-0.3.8 → redup-0.3.10}/src/redup.egg-info/entry_points.txt +0 -0
  39. {redup-0.3.8 → redup-0.3.10}/src/redup.egg-info/requires.txt +0 -0
  40. {redup-0.3.8 → redup-0.3.10}/src/redup.egg-info/top_level.txt +0 -0
  41. {redup-0.3.8 → redup-0.3.10}/tests/test_e2e.py +0 -0
  42. {redup-0.3.8 → redup-0.3.10}/tests/test_hasher.py +0 -0
  43. {redup-0.3.8 → redup-0.3.10}/tests/test_matcher.py +0 -0
  44. {redup-0.3.8 → redup-0.3.10}/tests/test_models.py +0 -0
  45. {redup-0.3.8 → redup-0.3.10}/tests/test_pipeline.py +0 -0
  46. {redup-0.3.8 → redup-0.3.10}/tests/test_planner.py +0 -0
  47. {redup-0.3.8 → redup-0.3.10}/tests/test_reporters.py +0 -0
  48. {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.8
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
  [![PyPI](https://img.shields.io/pypi/v/redup)](https://pypi.org/project/redup/)
84
84
  [![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
85
85
  [![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://python.org)
86
- [![Version](https://img.shields.io/badge/version-0.3.8-green.svg)](https://pypi.org/project/redup/)
86
+ [![Version](https://img.shields.io/badge/version-0.3.10-green.svg)](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
  [![PyPI](https://img.shields.io/pypi/v/redup)](https://pypi.org/project/redup/)
6
6
  [![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
7
7
  [![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://python.org)
8
- [![Version](https://img.shields.io/badge/version-0.3.8-green.svg)](https://pypi.org/project/redup/)
8
+ [![Version](https://img.shields.io/badge/version-0.3.10-green.svg)](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
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "redup"
7
- version = "0.3.8"
7
+ version = "0.3.10"
8
8
  description = "Code duplication analyzer and refactoring planner for LLMs"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "0.3.8"
5
+ __version__ = "0.3.10"
6
6
 
7
7
  from redup.core.models import (
8
8
  DuplicateFragment,
@@ -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
- def _should_exclude(path: Path, patterns: list[str]) -> bool:
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
- text = "\n".join(line.strip() for _, line in chunk)
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.8
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
  [![PyPI](https://img.shields.io/pypi/v/redup)](https://pypi.org/project/redup/)
84
84
  [![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
85
85
  [![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://python.org)
86
- [![Version](https://img.shields.io/badge/version-0.3.8-green.svg)](https://pypi.org/project/redup/)
86
+ [![Version](https://img.shields.io/badge/version-0.3.10-green.svg)](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
 
@@ -14,6 +14,7 @@ src/redup/cli_app/main.py
14
14
  src/redup/core/__init__.py
15
15
  src/redup/core/config.py
16
16
  src/redup/core/differ.py
17
+ src/redup/core/hash_cache.py
17
18
  src/redup/core/hasher.py
18
19
  src/redup/core/lsh_matcher.py
19
20
  src/redup/core/matcher.py
@@ -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"), [".git"])
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"), ["venv"])
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"), [".git", "venv"])
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