redup 0.3.16__tar.gz → 0.3.18__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 (53) hide show
  1. {redup-0.3.16 → redup-0.3.18}/PKG-INFO +2 -2
  2. {redup-0.3.16 → redup-0.3.18}/README.md +1 -1
  3. {redup-0.3.16 → redup-0.3.18}/pyproject.toml +1 -1
  4. {redup-0.3.16 → redup-0.3.18}/src/redup/__init__.py +1 -1
  5. {redup-0.3.16 → redup-0.3.18}/src/redup/cli_app/main.py +18 -13
  6. {redup-0.3.16 → redup-0.3.18}/src/redup/core/hasher.py +1 -1
  7. {redup-0.3.16 → redup-0.3.18}/src/redup/core/lazy_grouper.py +4 -0
  8. {redup-0.3.16 → redup-0.3.18}/src/redup/core/memory_scanner.py +34 -30
  9. {redup-0.3.16 → redup-0.3.18}/src/redup/core/pipeline.py +13 -6
  10. {redup-0.3.16 → redup-0.3.18}/src/redup/core/scanner.py +1 -3
  11. {redup-0.3.16 → redup-0.3.18}/src/redup/core/ultra_fast_scanner.py +1 -5
  12. {redup-0.3.16 → redup-0.3.18}/src/redup.egg-info/PKG-INFO +2 -2
  13. {redup-0.3.16 → redup-0.3.18}/LICENSE +0 -0
  14. {redup-0.3.16 → redup-0.3.18}/setup.cfg +0 -0
  15. {redup-0.3.16 → redup-0.3.18}/src/redup/__main__.py +0 -0
  16. {redup-0.3.16 → redup-0.3.18}/src/redup/cli_app/__init__.py +0 -0
  17. {redup-0.3.16 → redup-0.3.18}/src/redup/core/__init__.py +0 -0
  18. {redup-0.3.16 → redup-0.3.18}/src/redup/core/cache.py +0 -0
  19. {redup-0.3.16 → redup-0.3.18}/src/redup/core/config.py +0 -0
  20. {redup-0.3.16 → redup-0.3.18}/src/redup/core/differ.py +0 -0
  21. {redup-0.3.16 → redup-0.3.18}/src/redup/core/hash_cache.py +0 -0
  22. {redup-0.3.16 → redup-0.3.18}/src/redup/core/lsh_matcher.py +0 -0
  23. {redup-0.3.16 → redup-0.3.18}/src/redup/core/matcher.py +0 -0
  24. {redup-0.3.16 → redup-0.3.18}/src/redup/core/models.py +0 -0
  25. {redup-0.3.16 → redup-0.3.18}/src/redup/core/parallel_scanner.py +0 -0
  26. {redup-0.3.16 → redup-0.3.18}/src/redup/core/planner.py +0 -0
  27. {redup-0.3.16 → redup-0.3.18}/src/redup/core/ts_extractor.py +0 -0
  28. {redup-0.3.16 → redup-0.3.18}/src/redup/core/utils/__init__.py +0 -0
  29. {redup-0.3.16 → redup-0.3.18}/src/redup/core/utils/duplicate_finders.py +0 -0
  30. {redup-0.3.16 → redup-0.3.18}/src/redup/core/utils/function_extractor.py +0 -0
  31. {redup-0.3.16 → redup-0.3.18}/src/redup/core/utils/hash_utils.py +0 -0
  32. {redup-0.3.16 → redup-0.3.18}/src/redup/core/utils/language_dispatcher.py +0 -0
  33. {redup-0.3.16 → redup-0.3.18}/src/redup/reporters/__init__.py +0 -0
  34. {redup-0.3.16 → redup-0.3.18}/src/redup/reporters/code2llm_reporter.py +0 -0
  35. {redup-0.3.16 → redup-0.3.18}/src/redup/reporters/enhanced_reporter.py +0 -0
  36. {redup-0.3.16 → redup-0.3.18}/src/redup/reporters/json_reporter.py +0 -0
  37. {redup-0.3.16 → redup-0.3.18}/src/redup/reporters/markdown_reporter.py +0 -0
  38. {redup-0.3.16 → redup-0.3.18}/src/redup/reporters/toon_reporter.py +0 -0
  39. {redup-0.3.16 → redup-0.3.18}/src/redup/reporters/yaml_reporter.py +0 -0
  40. {redup-0.3.16 → redup-0.3.18}/src/redup.egg-info/SOURCES.txt +0 -0
  41. {redup-0.3.16 → redup-0.3.18}/src/redup.egg-info/dependency_links.txt +0 -0
  42. {redup-0.3.16 → redup-0.3.18}/src/redup.egg-info/entry_points.txt +0 -0
  43. {redup-0.3.16 → redup-0.3.18}/src/redup.egg-info/requires.txt +0 -0
  44. {redup-0.3.16 → redup-0.3.18}/src/redup.egg-info/top_level.txt +0 -0
  45. {redup-0.3.16 → redup-0.3.18}/tests/test_e2e.py +0 -0
  46. {redup-0.3.16 → redup-0.3.18}/tests/test_hasher.py +0 -0
  47. {redup-0.3.16 → redup-0.3.18}/tests/test_matcher.py +0 -0
  48. {redup-0.3.16 → redup-0.3.18}/tests/test_models.py +0 -0
  49. {redup-0.3.16 → redup-0.3.18}/tests/test_pipeline.py +0 -0
  50. {redup-0.3.16 → redup-0.3.18}/tests/test_planner.py +0 -0
  51. {redup-0.3.16 → redup-0.3.18}/tests/test_reporters.py +0 -0
  52. {redup-0.3.16 → redup-0.3.18}/tests/test_scanner.py +0 -0
  53. {redup-0.3.16 → redup-0.3.18}/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.16
3
+ Version: 0.3.18
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.16-green.svg)](https://pypi.org/project/redup/)
86
+ [![Version](https://img.shields.io/badge/version-0.3.18-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.16-green.svg)](https://pypi.org/project/redup/)
8
+ [![Version](https://img.shields.io/badge/version-0.3.18-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.16"
7
+ version = "0.3.18"
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.16"
5
+ __version__ = "0.3.18"
6
6
 
7
7
  from redup.core.models import (
8
8
  DuplicateFragment,
@@ -100,8 +100,10 @@ def _build_config_with_file_support(
100
100
  # Apply performance options
101
101
  if parallel:
102
102
  config.parallel_workers = max_workers
103
+ config._parallel_enabled = True
103
104
  elif max_workers is not None:
104
105
  config.parallel_workers = max_workers
106
+ config._parallel_enabled = True
105
107
 
106
108
  if incremental:
107
109
  config.enable_cache = True
@@ -241,9 +243,9 @@ def scan(
241
243
  help="Only analyze function-level duplicates (default: enabled for speed).",
242
244
  ),
243
245
  parallel: bool = typer.Option(
244
- True,
246
+ False,
245
247
  "--parallel/--no-parallel",
246
- help="Use parallel scanning for large projects (default: enabled).",
248
+ help="Use parallel scanning for large projects (default: disabled due to issues).",
247
249
  ),
248
250
  max_workers: int = typer.Option(
249
251
  None,
@@ -251,9 +253,9 @@ def scan(
251
253
  help="Number of parallel workers (default: CPU count).",
252
254
  ),
253
255
  incremental: bool = typer.Option(
254
- True,
256
+ False,
255
257
  "--incremental/--no-incremental",
256
- help="Use hash cache to skip unchanged files (default: enabled).",
258
+ help="Use hash cache to skip unchanged files (default: disabled due to issues).",
257
259
  ),
258
260
  memory_cache: bool = typer.Option(
259
261
  True,
@@ -268,13 +270,13 @@ def scan(
268
270
  ) -> None:
269
271
  """Scan a project for code duplicates and generate a refactoring map.
270
272
 
271
- Performance optimizations are enabled by default:
272
- - ✅ --functions-only: Faster function-level analysis
273
- - --parallel: Multi-core processing
274
- - --incremental: Skip unchanged files
275
- - ✅ --memory-cache: RAM preload for speed
273
+ Performance optimizations:
274
+ - ✅ --functions-only: Faster function-level analysis (enabled)
275
+ - ⚠️ --parallel: Multi-core processing (disabled due to issues)
276
+ - ⚠️ --incremental: Skip unchanged files (disabled due to issues)
277
+ - ✅ --memory-cache: RAM preload for speed (enabled)
276
278
 
277
- Use --no-* flags to disable specific optimizations.
279
+ Use --parallel and/or --incremental when needed.
278
280
  """
279
281
  try:
280
282
  config = _build_config_with_file_support(
@@ -285,16 +287,19 @@ def scan(
285
287
  _print_scan_header(path, config.extensions, config.min_block_lines, config.min_similarity)
286
288
 
287
289
  # Choose analysis method based on optimization flags
288
- if parallel or incremental:
290
+ if parallel and not incremental:
291
+ # Pure parallel mode (no cache)
292
+ dup_map = analyze_parallel(config=config, function_level_only=functions_only, max_workers=max_workers)
293
+ elif parallel or incremental:
294
+ # Optimized mode with cache and/or parallel
289
295
  dup_map = analyze_optimized(
290
296
  config=config,
291
297
  function_level_only=functions_only,
292
298
  use_memory_cache=getattr(config, '_memory_cache', True),
293
299
  max_cache_mb=getattr(config, '_max_cache_mb', 512)
294
300
  )
295
- elif parallel:
296
- dup_map = analyze_parallel(config=config, function_level_only=functions_only, max_workers=max_workers)
297
301
  else:
302
+ # Standard mode
298
303
  dup_map = analyze(config=config, function_level_only=functions_only)
299
304
 
300
305
  _print_scan_summary(dup_map)
@@ -39,7 +39,7 @@ def _normalize_text(text: str) -> str:
39
39
  # Vectorized: single list comprehension pass instead of loop
40
40
  lines = text.splitlines()
41
41
  result_lines = [
42
- stripped for line in lines
42
+ processed for line in lines
43
43
  if (line.strip() and not _MULTILINE_STRING_RE.match(line))
44
44
  for processed in [_COMMENT_RE.sub("", line).strip()]
45
45
  if processed # Skip empty lines (only comments)
@@ -74,11 +74,15 @@ def _create_duplicate_group(
74
74
  for hb in blocks
75
75
  ]
76
76
 
77
+ # Extract function name from first fragment if available
78
+ normalized_name = fragments[0].function_name if fragments and fragments[0].function_name else None
79
+
77
80
  return DuplicateGroup(
78
81
  id=hash_val,
79
82
  duplicate_type=duplicate_type,
80
83
  fragments=fragments,
81
84
  normalized_hash=hash_val,
85
+ normalized_name=normalized_name,
82
86
  )
83
87
 
84
88
 
@@ -144,16 +144,18 @@ def scan_project_memory_optimized(
144
144
  func_blocks = []
145
145
  class_blocks = []
146
146
 
147
- # Also extract sliding-window blocks
148
- from redup.core.scanner import _extract_blocks_sliding
149
- lines = content.splitlines()
150
- sliding = _extract_blocks_sliding(lines, config.min_block_lines)
151
- line_blocks = [
152
- CodeBlock(file=rel_path, line_start=s, line_end=e, text=t)
153
- for s, e, t in sliding
154
- ]
155
-
156
- all_blocks = func_blocks + class_blocks + line_blocks
147
+ # Add sliding window blocks only if not functions_only
148
+ all_blocks = func_blocks + class_blocks
149
+ if not (config.functions_only or getattr(config, 'functions_only', False)):
150
+ # Also extract sliding-window blocks
151
+ from redup.core.scanner import _extract_blocks_sliding
152
+ lines = content.splitlines()
153
+ sliding = _extract_blocks_sliding(lines, config.min_block_lines)
154
+ line_blocks = [
155
+ CodeBlock(file=rel_path, line_start=s, line_end=e, text=t)
156
+ for s, e, t in sliding
157
+ ]
158
+ all_blocks.extend(line_blocks)
157
159
 
158
160
  sf = ScannedFile(path=rel_path, lines=lines, blocks=all_blocks)
159
161
  scanned.append(sf)
@@ -202,22 +204,18 @@ def scan_project_parallel_memory_optimized(
202
204
  if len(files) < 10 or workers == 1:
203
205
  return scan_project_memory_optimized(config, max_cache_mb)
204
206
 
205
- # Initialize shared memory cache for workers
207
+ # Initialize memory cache and preload files in main process
206
208
  cache = MemoryFileCache(max_cache_mb)
207
-
208
- # Preload files into cache
209
209
  cache.preload_files(files)
210
210
  print(f"📁 Preloaded {cache.get_stats()['cached_files']} files for parallel processing")
211
211
 
212
212
  scanned: list[ScannedFile] = []
213
213
 
214
- def process_file_cached(filepath: Path) -> ScannedFile:
215
- """Worker function that uses cached content."""
214
+ def process_file_with_content(args: tuple[Path, bytes]) -> ScannedFile:
215
+ """Worker function that receives content directly."""
216
+ filepath, content_bytes = args
216
217
  try:
217
- content_bytes = cache.get_file_content(filepath)
218
218
  content = content_bytes.decode('utf-8', errors='replace')
219
-
220
- # Use existing _process_file logic but with cached content
221
219
  rel_path = str(filepath.relative_to(config.root.resolve()))
222
220
 
223
221
  # Extract blocks
@@ -226,6 +224,7 @@ def scan_project_parallel_memory_optimized(
226
224
  func_blocks = _extract_function_blocks_python(content, rel_path)
227
225
  class_blocks = _extract_class_blocks_python(content, rel_path)
228
226
  else:
227
+ # Try tree-sitter for other languages
229
228
  try:
230
229
  from redup.core.ts_extractor import extract_functions_treesitter, is_language_supported
231
230
  if is_language_supported(rel_path):
@@ -236,27 +235,32 @@ def scan_project_parallel_memory_optimized(
236
235
  func_blocks = []
237
236
  class_blocks = []
238
237
 
239
- # Sliding window blocks
240
- from redup.core.scanner import _extract_blocks_sliding
238
+ # Add sliding window blocks only if not functions_only
239
+ all_blocks = func_blocks + class_blocks
241
240
  lines = content.splitlines()
242
- sliding = _extract_blocks_sliding(lines, config.min_block_lines)
243
- line_blocks = [
244
- CodeBlock(file=rel_path, line_start=s, line_end=e, text=t)
245
- for s, e, t in sliding
246
- ]
247
-
248
- all_blocks = func_blocks + class_blocks + line_blocks
241
+ if not (config.functions_only or getattr(config, 'functions_only', False)):
242
+ # Sliding window blocks for line-level duplicates
243
+ from redup.core.scanner import _extract_blocks_sliding
244
+ sliding = _extract_blocks_sliding(lines, config.min_block_lines)
245
+ line_blocks = [
246
+ CodeBlock(file=rel_path, line_start=s, line_end=e, text=t)
247
+ for s, e, t in sliding
248
+ ]
249
+ all_blocks.extend(line_blocks)
249
250
 
250
251
  return ScannedFile(path=rel_path, lines=lines, blocks=all_blocks)
251
252
 
252
253
  except (OSError, PermissionError):
253
254
  return ScannedFile(path=str(filepath), lines=[], blocks=[])
254
255
 
255
- # Process files in parallel using cached content
256
+ # Prepare (filepath, content) pairs from cache
257
+ file_content_pairs = [(f, cache.get_file_content(f)) for f in files]
258
+
259
+ # Process files in parallel using cached content passed as arguments
256
260
  with ProcessPoolExecutor(max_workers=workers) as executor:
257
261
  future_to_file = {
258
- executor.submit(process_file_cached, filepath): filepath
259
- for filepath in files
262
+ executor.submit(process_file_with_content, pair): pair[0]
263
+ for pair in file_content_pairs
260
264
  }
261
265
 
262
266
  for future in as_completed(future_to_file):
@@ -30,7 +30,6 @@ from redup.core.scanner import CodeBlock, ScannedFile, scan_project
30
30
  from redup.core.cache import HashCache, build_hash_index_with_cache
31
31
  from redup.core.lazy_grouper import find_all_duplicates_lazy, DuplicateGroupCollector
32
32
  from redup.core.memory_scanner import scan_project_memory_optimized, scan_project_parallel_memory_optimized
33
- from redup.core.ultra_fast_scanner import scan_project_ultra_fast
34
33
 
35
34
 
36
35
  def _blocks_to_group(
@@ -184,12 +183,23 @@ def analyze_optimized(
184
183
  try:
185
184
  # Phase 1: Optimized scanning with memory cache
186
185
  if config.parallel_workers and config.parallel_workers > 1:
186
+ # Explicit worker count specified
187
187
  if use_memory_cache:
188
188
  scanned_files, stats = scan_project_parallel_memory_optimized(
189
189
  config, config.parallel_workers, max_cache_mb
190
190
  )
191
191
  else:
192
192
  scanned_files, stats = _scan_phase_parallel(config, config.parallel_workers)
193
+ elif config.parallel_workers is None and (getattr(config, '_parallel_enabled', False) or config.parallel_workers is None):
194
+ # Auto-detect CPU count when parallel_workers is None (default behavior)
195
+ import multiprocessing as mp
196
+ auto_workers = mp.cpu_count()
197
+ if use_memory_cache:
198
+ scanned_files, stats = scan_project_parallel_memory_optimized(
199
+ config, auto_workers, max_cache_mb
200
+ )
201
+ else:
202
+ scanned_files, stats = _scan_phase_parallel(config, auto_workers)
193
203
  elif use_memory_cache:
194
204
  scanned_files, stats = scan_project_memory_optimized(config, max_cache_mb)
195
205
  else:
@@ -243,11 +253,8 @@ def _ensure_config(config: ScanConfig | None) -> ScanConfig:
243
253
 
244
254
 
245
255
  def _scan_phase(config: ScanConfig, function_level_only: bool = False) -> tuple[list[ScannedFile], ScanStats]:
246
- """Phase 1: Scan project files with ultra-fast optimization."""
247
- # Use ultra-fast scanner by default for 4x speedup
248
- if getattr(config, 'use_ultra_fast_scanner', False):
249
- return scan_project_ultra_fast(config)
250
- # Use optimized scan_project with RAM preload
256
+ """Phase 1: Scan project files."""
257
+ # Use standard scan_project (ultra_fast_scanner disabled - doesn't extract function names)
251
258
  return scan_project(config, function_level_only=function_level_only)
252
259
 
253
260
 
@@ -63,7 +63,7 @@ def _should_exclude(path: Path, patterns: tuple[str, ...]) -> bool:
63
63
 
64
64
  def _collect_files(config: ScanConfig) -> Iterator[Path]:
65
65
  """Yield all files matching the scan configuration."""
66
- root = config.root.resolve()
66
+ root = Path(config.root).resolve() if isinstance(config.root, str) else config.root.resolve()
67
67
  for ext in config.extensions:
68
68
  for path in root.rglob(f"*{ext}"):
69
69
  if _should_exclude(path, tuple(config.exclude_patterns)):
@@ -249,7 +249,6 @@ def _preload_files_to_ram(config: ScanConfig) -> list[tuple[Path, str]]:
249
249
  Returns list of (path, content) tuples. Filters out files exceeding size limit.
250
250
  """
251
251
  files_content: list[tuple[Path, str]] = []
252
- total_size = 0
253
252
 
254
253
  for filepath in _collect_files(config):
255
254
  # Check size first to avoid loading huge files
@@ -263,7 +262,6 @@ def _preload_files_to_ram(config: ScanConfig) -> list[tuple[Path, str]]:
263
262
  source = _read_file_optimized(filepath, config.max_file_size_kb)
264
263
  if source is not None:
265
264
  files_content.append((filepath, source))
266
- total_size += len(source)
267
265
 
268
266
  return files_content
269
267
 
@@ -83,15 +83,11 @@ def preload_to_ram(config: ScanConfig, show_progress: bool = True) -> dict[Path,
83
83
  size_mb = total_size / 1e6
84
84
  print(f"📁 Preloaded {len(sources)} files to RAM ({size_mb:.1f}MB) in {load_time:.2f}s")
85
85
 
86
- # Early exit: if no files or too small, return immediately
86
+ # Early exit: if no files, return immediately
87
87
  if len(sources) == 0:
88
88
  print("🔍 No files to scan")
89
89
  return {}
90
90
 
91
- if total_size < 1000: # Less than 1KB total
92
- print("🔍 Project too small for meaningful analysis")
93
- return {}
94
-
95
91
  return sources
96
92
 
97
93
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redup
3
- Version: 0.3.16
3
+ Version: 0.3.18
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.16-green.svg)](https://pypi.org/project/redup/)
86
+ [![Version](https://img.shields.io/badge/version-0.3.18-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
 
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