redup 0.3.22__tar.gz → 0.3.23__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 (61) hide show
  1. {redup-0.3.22 → redup-0.3.23}/PKG-INFO +3 -2
  2. {redup-0.3.22 → redup-0.3.23}/README.md +1 -1
  3. {redup-0.3.22 → redup-0.3.23}/pyproject.toml +2 -1
  4. {redup-0.3.22 → redup-0.3.23}/src/redup/__init__.py +1 -1
  5. {redup-0.3.22 → redup-0.3.23}/src/redup/cli_app/config_builder.py +7 -1
  6. redup-0.3.23/src/redup/cli_app/fuzzy_similarity.py +160 -0
  7. redup-0.3.23/src/redup/cli_app/main.py +219 -0
  8. {redup-0.3.22 → redup-0.3.23}/src/redup/cli_app/scan_helpers.py +2 -2
  9. {redup-0.3.22 → redup-0.3.23}/src/redup.egg-info/PKG-INFO +3 -2
  10. {redup-0.3.22 → redup-0.3.23}/src/redup.egg-info/SOURCES.txt +1 -0
  11. {redup-0.3.22 → redup-0.3.23}/src/redup.egg-info/requires.txt +3 -0
  12. {redup-0.3.22 → redup-0.3.23}/tests/test_e2e.py +6 -4
  13. redup-0.3.22/src/redup/cli_app/main.py +0 -708
  14. {redup-0.3.22 → redup-0.3.23}/LICENSE +0 -0
  15. {redup-0.3.22 → redup-0.3.23}/setup.cfg +0 -0
  16. {redup-0.3.22 → redup-0.3.23}/src/redup/__main__.py +0 -0
  17. {redup-0.3.22 → redup-0.3.23}/src/redup/cli_app/__init__.py +0 -0
  18. {redup-0.3.22 → redup-0.3.23}/src/redup/cli_app/output_writer.py +0 -0
  19. {redup-0.3.22 → redup-0.3.23}/src/redup/cli_app/scan_commands.py +0 -0
  20. {redup-0.3.22 → redup-0.3.23}/src/redup/core/__init__.py +0 -0
  21. {redup-0.3.22 → redup-0.3.23}/src/redup/core/cache.py +0 -0
  22. {redup-0.3.22 → redup-0.3.23}/src/redup/core/config.py +0 -0
  23. {redup-0.3.22 → redup-0.3.23}/src/redup/core/differ.py +0 -0
  24. {redup-0.3.22 → redup-0.3.23}/src/redup/core/fuzzy_similarity.py +0 -0
  25. {redup-0.3.22 → redup-0.3.23}/src/redup/core/hash_cache.py +0 -0
  26. {redup-0.3.22 → redup-0.3.23}/src/redup/core/hasher.py +0 -0
  27. {redup-0.3.22 → redup-0.3.23}/src/redup/core/lazy_grouper.py +0 -0
  28. {redup-0.3.22 → redup-0.3.23}/src/redup/core/lsh_matcher.py +0 -0
  29. {redup-0.3.22 → redup-0.3.23}/src/redup/core/matcher.py +0 -0
  30. {redup-0.3.22 → redup-0.3.23}/src/redup/core/memory_scanner.py +0 -0
  31. {redup-0.3.22 → redup-0.3.23}/src/redup/core/models.py +0 -0
  32. {redup-0.3.22 → redup-0.3.23}/src/redup/core/parallel_scanner.py +0 -0
  33. {redup-0.3.22 → redup-0.3.23}/src/redup/core/pipeline.py +0 -0
  34. {redup-0.3.22 → redup-0.3.23}/src/redup/core/planner.py +0 -0
  35. {redup-0.3.22 → redup-0.3.23}/src/redup/core/scanner.py +0 -0
  36. {redup-0.3.22 → redup-0.3.23}/src/redup/core/ts_extractor.py +0 -0
  37. {redup-0.3.22 → redup-0.3.23}/src/redup/core/ultra_fast_scanner.py +0 -0
  38. {redup-0.3.22 → redup-0.3.23}/src/redup/core/universal_fuzzy.py +0 -0
  39. {redup-0.3.22 → redup-0.3.23}/src/redup/core/utils/__init__.py +0 -0
  40. {redup-0.3.22 → redup-0.3.23}/src/redup/core/utils/duplicate_finders.py +0 -0
  41. {redup-0.3.22 → redup-0.3.23}/src/redup/core/utils/function_extractor.py +0 -0
  42. {redup-0.3.22 → redup-0.3.23}/src/redup/core/utils/hash_utils.py +0 -0
  43. {redup-0.3.22 → redup-0.3.23}/src/redup/core/utils/language_dispatcher.py +0 -0
  44. {redup-0.3.22 → redup-0.3.23}/src/redup/reporters/__init__.py +0 -0
  45. {redup-0.3.22 → redup-0.3.23}/src/redup/reporters/code2llm_reporter.py +0 -0
  46. {redup-0.3.22 → redup-0.3.23}/src/redup/reporters/enhanced_reporter.py +0 -0
  47. {redup-0.3.22 → redup-0.3.23}/src/redup/reporters/json_reporter.py +0 -0
  48. {redup-0.3.22 → redup-0.3.23}/src/redup/reporters/markdown_reporter.py +0 -0
  49. {redup-0.3.22 → redup-0.3.23}/src/redup/reporters/toon_reporter.py +0 -0
  50. {redup-0.3.22 → redup-0.3.23}/src/redup/reporters/yaml_reporter.py +0 -0
  51. {redup-0.3.22 → redup-0.3.23}/src/redup.egg-info/dependency_links.txt +0 -0
  52. {redup-0.3.22 → redup-0.3.23}/src/redup.egg-info/entry_points.txt +0 -0
  53. {redup-0.3.22 → redup-0.3.23}/src/redup.egg-info/top_level.txt +0 -0
  54. {redup-0.3.22 → redup-0.3.23}/tests/test_hasher.py +0 -0
  55. {redup-0.3.22 → redup-0.3.23}/tests/test_matcher.py +0 -0
  56. {redup-0.3.22 → redup-0.3.23}/tests/test_models.py +0 -0
  57. {redup-0.3.22 → redup-0.3.23}/tests/test_pipeline.py +0 -0
  58. {redup-0.3.22 → redup-0.3.23}/tests/test_planner.py +0 -0
  59. {redup-0.3.22 → redup-0.3.23}/tests/test_reporters.py +0 -0
  60. {redup-0.3.22 → redup-0.3.23}/tests/test_scanner.py +0 -0
  61. {redup-0.3.22 → redup-0.3.23}/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.22
3
+ Version: 0.3.23
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
@@ -26,6 +26,7 @@ Requires-Dist: pyyaml>=6.0
26
26
  Requires-Dist: typer>=0.12.0
27
27
  Requires-Dist: rich>=13.0
28
28
  Requires-Dist: pydantic>=2.0
29
+ Requires-Dist: tomli>=2.0; python_version < "3.11"
29
30
  Provides-Extra: all
30
31
  Requires-Dist: redup[fuzzy]; extra == "all"
31
32
  Requires-Dist: redup[ast]; extra == "all"
@@ -83,7 +84,7 @@ Dynamic: license-file
83
84
  [![PyPI](https://img.shields.io/pypi/v/redup)](https://pypi.org/project/redup/)
84
85
  [![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
85
86
  [![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.22-green.svg)](https://pypi.org/project/redup/)
87
+ [![Version](https://img.shields.io/badge/version-0.3.23-green.svg)](https://pypi.org/project/redup/)
87
88
 
88
89
  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
90
 
@@ -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.22-green.svg)](https://pypi.org/project/redup/)
8
+ [![Version](https://img.shields.io/badge/version-0.3.23-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.22"
7
+ version = "0.3.23"
8
8
  description = "Code duplication analyzer and refactoring planner for LLMs"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -30,6 +30,7 @@ dependencies = [
30
30
  "typer>=0.12.0",
31
31
  "rich>=13.0",
32
32
  "pydantic>=2.0",
33
+ "tomli>=2.0; python_version<'3.11'",
33
34
  ]
34
35
 
35
36
  [project.optional-dependencies]
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "0.3.22"
5
+ __version__ = "0.3.23"
6
6
 
7
7
  from redup.core.models import (
8
8
  DuplicateFragment,
@@ -30,7 +30,9 @@ def build_config_with_file_support(
30
30
  incremental: bool = False,
31
31
  memory_cache: bool = False,
32
32
  max_cache_mb: int = 512,
33
- functions_only: bool = False
33
+ functions_only: bool = False,
34
+ fuzzy: bool = False,
35
+ fuzzy_threshold: float = 0.8
34
36
  ) -> ScanConfig:
35
37
  """Build scan configuration with advanced options."""
36
38
  config = load_config()
@@ -48,4 +50,8 @@ def build_config_with_file_support(
48
50
  if functions_only:
49
51
  scan_config.function_level_only = functions_only
50
52
 
53
+ # Add fuzzy support
54
+ scan_config.fuzzy_enabled = fuzzy
55
+ scan_config.fuzzy_threshold = fuzzy_threshold
56
+
51
57
  return scan_config
@@ -0,0 +1,160 @@
1
+ """Fuzzy similarity detection functions."""
2
+
3
+ from pathlib import Path
4
+ import typer
5
+
6
+ from redup.core.models import DuplicationMap
7
+
8
+
9
+ def _apply_fuzzy_similarity(dup_map: DuplicationMap, threshold: float) -> DuplicationMap:
10
+ """Apply fuzzy similarity detection to find similar components across all languages."""
11
+ validation = _validate_fuzzy_input(dup_map, threshold)
12
+ if not validation.is_valid:
13
+ typer.echo(validation.message)
14
+ return dup_map
15
+
16
+ all_blocks = _extract_all_blocks(dup_map)
17
+ if not all_blocks:
18
+ typer.echo("🔍 No code blocks found for fuzzy analysis")
19
+ return dup_map
20
+
21
+ typer.echo(f"🔍 Analyzing {len(all_blocks)} code blocks for universal fuzzy similarity...")
22
+
23
+ html_css_blocks, other_blocks = _separate_blocks_by_type(all_blocks)
24
+ similar_pairs = _analyze_blocks_with_detectors(html_css_blocks, other_blocks, threshold)
25
+
26
+ _report_fuzzy_results(similar_pairs)
27
+ return dup_map
28
+
29
+
30
+ def _validate_fuzzy_input(dup_map: DuplicationMap, threshold: float) -> 'FuzzyValidationResult':
31
+ """Validate input parameters for fuzzy similarity analysis."""
32
+ from dataclasses import dataclass
33
+
34
+ @dataclass
35
+ class FuzzyValidationResult:
36
+ is_valid: bool
37
+ message: str = ""
38
+
39
+ if threshold <= 0 or threshold > 1:
40
+ return FuzzyValidationResult(False, "❌ Invalid threshold. Must be between 0 and 1.")
41
+
42
+ if not dup_map.groups:
43
+ return FuzzyValidationResult(False, "❌ No duplicate groups found.")
44
+
45
+ return FuzzyValidationResult(True)
46
+
47
+
48
+ def _extract_all_blocks(dup_map: DuplicationMap) -> list:
49
+ """Extract all code blocks from duplicate fragments."""
50
+ from redup.core.scanner import CodeBlock
51
+
52
+ all_blocks = []
53
+ for group in dup_map.groups:
54
+ for fragment in group.fragments:
55
+ # Create a CodeBlock from fragment data
56
+ block = CodeBlock(
57
+ file=fragment.file,
58
+ line_start=fragment.line_start,
59
+ line_end=fragment.line_end,
60
+ text=fragment.text,
61
+ function_name=fragment.function_name,
62
+ class_name=fragment.class_name
63
+ )
64
+ all_blocks.append(block)
65
+ return all_blocks
66
+
67
+
68
+ def _separate_blocks_by_type(all_blocks: list) -> tuple[list, list]:
69
+ """Separate blocks into HTML/CSS and other language categories."""
70
+ html_css_blocks = [block for block in all_blocks
71
+ if Path(block.file).suffix.lower() in ['.html', '.htm', '.css', '.scss', '.sass', '.less']]
72
+
73
+ other_blocks = [block for block in all_blocks if block not in html_css_blocks]
74
+
75
+ return html_css_blocks, other_blocks
76
+
77
+
78
+ def _analyze_blocks_with_detectors(html_css_blocks: list, other_blocks: list, threshold: float) -> list:
79
+ """Analyze blocks using appropriate fuzzy similarity detectors."""
80
+ similar_pairs = []
81
+
82
+ # Analyze HTML/CSS with specialized detector
83
+ if html_css_blocks:
84
+ typer.echo(f" 📝 Analyzing {len(html_css_blocks)} HTML/CSS blocks...")
85
+ html_css_similar = _analyze_html_css_blocks(html_css_blocks, threshold)
86
+ similar_pairs.extend(html_css_similar)
87
+ typer.echo(f" ✨ Found {len(html_css_similar)} similar HTML/CSS component pairs")
88
+
89
+ # Analyze other languages with universal detector
90
+ if other_blocks:
91
+ typer.echo(f" 🔧 Analyzing {len(other_blocks)} blocks from other languages...")
92
+ universal_similar = _analyze_other_language_blocks(other_blocks, threshold)
93
+ similar_pairs.extend(universal_similar)
94
+ typer.echo(f" ✨ Found {len(universal_similar)} similar component pairs from other languages")
95
+
96
+ return similar_pairs
97
+
98
+
99
+ def _analyze_html_css_blocks(html_css_blocks: list, threshold: float) -> list:
100
+ """Analyze HTML/CSS blocks using specialized fuzzy similarity detector."""
101
+ from redup.core.fuzzy_similarity import FuzzySimilarityDetector
102
+
103
+ html_css_detector = FuzzySimilarityDetector(similarity_threshold=threshold)
104
+ return html_css_detector.find_similar_components(html_css_blocks)
105
+
106
+
107
+ def _analyze_other_language_blocks(other_blocks: list, threshold: float) -> list:
108
+ """Analyze non-HTML/CSS blocks using universal fuzzy similarity detector."""
109
+ from redup.core.universal_fuzzy import UniversalFuzzyDetector
110
+
111
+ universal_detector = UniversalFuzzyDetector(similarity_threshold=threshold)
112
+ return universal_detector.find_similar_components(other_blocks)
113
+
114
+
115
+ def _report_fuzzy_results(similar_pairs: list) -> None:
116
+ """Report fuzzy similarity analysis results."""
117
+ if similar_pairs:
118
+ typer.echo(f"🎯 Total fuzzy similar components found: {len(similar_pairs)}")
119
+
120
+ # Simplified reporting to reduce complexity
121
+ typer.echo(f" 📊 Language groups: {len(set(Path(b1.file).suffix + '+' + Path(b2.file).suffix for b1, b2, _ in similar_pairs))}")
122
+
123
+ # Show top examples only
124
+ for block1, block2, similarity in similar_pairs[:5]:
125
+ typer.echo(f" {block1.file} ↔ {block2.file}: {similarity:.2f}")
126
+
127
+ if len(similar_pairs) > 5:
128
+ typer.echo(f" ... and {len(similar_pairs) - 5} more")
129
+
130
+ # Note: In production, you'd integrate these into DuplicationMap
131
+ # For now, we just report the findings
132
+
133
+ else:
134
+ typer.echo("🔍 No fuzzy similar components found")
135
+
136
+
137
+ def _report_similarity_by_groups(similar_pairs):
138
+ """Extracted function to reduce complexity."""
139
+ lang_groups = {}
140
+ for block1, block2, similarity in similar_pairs:
141
+ lang1 = Path(block1.file).suffix
142
+ lang2 = Path(block2.file).suffix
143
+ comp_type = "unknown"
144
+
145
+ # Try to get component type
146
+ try:
147
+ from redup.core.universal_fuzzy import UniversalFuzzyExtractor
148
+ extractor = UniversalFuzzyExtractor()
149
+ sig1 = extractor.extract_universal_signature(block1)
150
+ if sig1:
151
+ comp_type = sig1.component_type
152
+ except:
153
+ pass
154
+
155
+ group_key = f"{lang1}+{lang2}:{comp_type}"
156
+ if group_key not in lang_groups:
157
+ lang_groups[group_key] = []
158
+ lang_groups[group_key].append((block1, block2, similarity))
159
+
160
+ return lang_groups
@@ -0,0 +1,219 @@
1
+ """CLI entry point for reDUP — code duplication analyzer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import warnings
6
+ from collections.abc import Callable
7
+ from typing import Literal
8
+ from pathlib import Path
9
+
10
+ import typer
11
+
12
+ # Suppress SyntaxWarning from external libraries
13
+ warnings.filterwarnings("ignore", category=SyntaxWarning)
14
+
15
+ import redup # noqa: E402
16
+ from redup.core.config import create_sample_redup_toml, load_config # noqa: E402
17
+ from redup.core.models import DuplicationMap, ScanConfig # noqa: E402
18
+
19
+ # Import refactored modules
20
+ from redup.cli_app.config_builder import build_config, build_config_with_file_support # noqa: E402
21
+ from redup.cli_app.output_writer import write_output, write_results # noqa: E402
22
+ from redup.cli_app.scan_commands import ( # noqa: E402
23
+ scan_command, diff_command, check_command, config_command, info_command
24
+ )
25
+ from redup.cli_app.scan_helpers import print_scan_header, print_scan_summary # noqa: E402
26
+
27
+ app = typer.Typer(
28
+ name="redup",
29
+ help="reDUP — Code duplication analyzer and refactoring planner for LLMs.",
30
+ add_completion=False,
31
+ )
32
+
33
+
34
+ OutputFormat = Literal["json", "yaml", "toon", "markdown", "all", "enhanced", "code2llm"]
35
+
36
+
37
+ DEFAULT_PATH = Path(".")
38
+ DEFAULT_FORMAT: OutputFormat = "toon"
39
+ DEFAULT_OUTPUT: Path | None = None
40
+
41
+
42
+ @app.command()
43
+ def scan(
44
+ path: Path = typer.Argument(
45
+ DEFAULT_PATH,
46
+ help="Project root directory to scan.",
47
+ exists=True,
48
+ dir_okay=True,
49
+ file_okay=False,
50
+ ),
51
+ format: str = typer.Option(
52
+ "toon",
53
+ "--format", "-f",
54
+ help="Output format (json, yaml, toon, markdown, code2llm, all, enhanced).",
55
+ ),
56
+ output: Path | None = typer.Option(
57
+ DEFAULT_OUTPUT,
58
+ "--output", "-o",
59
+ help="Output directory or file path. Defaults to stdout.",
60
+ ),
61
+ extensions: str | None = typer.Option(
62
+ None,
63
+ "--ext", "-e",
64
+ help="Comma-separated file extensions to scan. Overrides config.",
65
+ ),
66
+ min_lines: int | None = typer.Option(
67
+ None,
68
+ "--min-lines",
69
+ help="Minimum block size (lines). Overrides config.",
70
+ ),
71
+ min_similarity: float | None = typer.Option(
72
+ None,
73
+ "--min-sim",
74
+ help="Minimum similarity score (0.0-1.0). Overrides config.",
75
+ ),
76
+ include_tests: bool | None = typer.Option(
77
+ None,
78
+ "--include-tests",
79
+ help="Include test files. Overrides config.",
80
+ ),
81
+ functions_only: bool = typer.Option(
82
+ True,
83
+ "--functions-only/--no-functions-only",
84
+ help="Only analyze function-level duplicates (default: enabled for speed).",
85
+ ),
86
+ parallel: bool = typer.Option(
87
+ False,
88
+ "--parallel/--no-parallel",
89
+ help="Use parallel scanning for large projects (default: disabled due to issues).",
90
+ ),
91
+ max_workers: int | None = typer.Option(
92
+ None,
93
+ "--max-workers",
94
+ help="Maximum number of parallel workers.",
95
+ ),
96
+ incremental: bool = typer.Option(
97
+ False,
98
+ "--incremental",
99
+ help="Use incremental scanning with caching.",
100
+ ),
101
+ memory_cache: bool = typer.Option(
102
+ True,
103
+ "--memory-cache/--no-memory-cache",
104
+ help="Use memory cache for faster scanning.",
105
+ ),
106
+ max_cache_mb: int = typer.Option(
107
+ 512,
108
+ "--max-cache-mb",
109
+ help="Maximum memory cache size in MB.",
110
+ ),
111
+ fuzzy: bool = typer.Option(
112
+ False,
113
+ "--fuzzy",
114
+ help="Enable fuzzy similarity detection.",
115
+ ),
116
+ fuzzy_threshold: float = typer.Option(
117
+ 0.8,
118
+ "--fuzzy-threshold",
119
+ help="Fuzzy similarity threshold (0.0-1.0).",
120
+ ),
121
+ ) -> None:
122
+ """Scan a project for code duplicates."""
123
+ return scan_command(
124
+ path, format, output, extensions, min_lines, min_similarity,
125
+ include_tests, functions_only, parallel, max_workers,
126
+ incremental, memory_cache, max_cache_mb, fuzzy, fuzzy_threshold
127
+ )
128
+
129
+
130
+ @app.command()
131
+ def diff(
132
+ before: Path = typer.Argument(
133
+ ...,
134
+ help="Path to the earlier scan JSON file.",
135
+ exists=True,
136
+ dir_okay=False,
137
+ file_okay=True,
138
+ ),
139
+ after: Path = typer.Argument(
140
+ ...,
141
+ help="Path to the later scan JSON file.",
142
+ exists=True,
143
+ dir_okay=False,
144
+ file_okay=True,
145
+ ),
146
+ ) -> None:
147
+ """Compare two reDUP scans and show the differences."""
148
+ return diff_command(before, after)
149
+
150
+
151
+ @app.command()
152
+ def check(
153
+ path: Path = typer.Argument(
154
+ Path("."),
155
+ help="Project root directory to scan.",
156
+ exists=True,
157
+ dir_okay=True,
158
+ file_okay=False,
159
+ ),
160
+ max_groups: int | None = typer.Option(
161
+ None,
162
+ "--max-groups",
163
+ help="Maximum allowed duplicate groups. Overrides config.",
164
+ ),
165
+ max_saved_lines: int | None = typer.Option(
166
+ None,
167
+ "--max-lines",
168
+ help="Maximum allowed recoverable lines. Overrides config.",
169
+ ),
170
+ extensions: str | None = typer.Option(
171
+ None,
172
+ "--ext", "-e",
173
+ help="Comma-separated file extensions to scan. Overrides config.",
174
+ ),
175
+ min_lines: int | None = typer.Option(
176
+ None,
177
+ "--min-lines",
178
+ help="Minimum block size (lines). Overrides config.",
179
+ ),
180
+ min_similarity: float | None = typer.Option(
181
+ None,
182
+ "--min-sim",
183
+ help="Minimum similarity score (0.0-1.0). Overrides config.",
184
+ ),
185
+ include_tests: bool | None = typer.Option(
186
+ None,
187
+ "--include-tests",
188
+ help="Include test files. Overrides config.",
189
+ ),
190
+ ) -> None:
191
+ """Check project for duplicates and exit with non-zero code if thresholds exceeded."""
192
+ return check_command(path, max_groups, max_saved_lines, extensions, min_lines, min_similarity, include_tests)
193
+
194
+
195
+ @app.command()
196
+ def config(
197
+ init: bool = typer.Option(
198
+ False,
199
+ "--init",
200
+ help="Create a sample redup.toml configuration file.",
201
+ ),
202
+ show: bool = typer.Option(
203
+ False,
204
+ "--show",
205
+ help="Show current configuration (files + env vars).",
206
+ ),
207
+ ) -> None:
208
+ """Manage reDUP configuration."""
209
+ return config_command(init, show)
210
+
211
+
212
+ @app.command()
213
+ def info() -> None:
214
+ """Show reDUP version and configuration info."""
215
+ return info_command()
216
+
217
+
218
+ if __name__ == "__main__":
219
+ app()
@@ -6,7 +6,6 @@ from typing import List
6
6
  import typer
7
7
 
8
8
  from redup.core.models import DuplicationMap
9
- from redup.cli_app.main import _apply_fuzzy_similarity
10
9
 
11
10
 
12
11
  def print_scan_header(path: Path, ext_list: List[str], min_lines: int, min_similarity: float) -> None:
@@ -30,5 +29,6 @@ def print_scan_summary(dup_map: DuplicationMap) -> None:
30
29
 
31
30
 
32
31
  def apply_fuzzy_similarity(dup_map: DuplicationMap, threshold: float) -> DuplicationMap:
33
- """Apply fuzzy similarity detection (wrapper for main function)."""
32
+ """Apply fuzzy similarity detection."""
33
+ from redup.cli_app.fuzzy_similarity import _apply_fuzzy_similarity
34
34
  return _apply_fuzzy_similarity(dup_map, threshold)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redup
3
- Version: 0.3.22
3
+ Version: 0.3.23
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
@@ -26,6 +26,7 @@ Requires-Dist: pyyaml>=6.0
26
26
  Requires-Dist: typer>=0.12.0
27
27
  Requires-Dist: rich>=13.0
28
28
  Requires-Dist: pydantic>=2.0
29
+ Requires-Dist: tomli>=2.0; python_version < "3.11"
29
30
  Provides-Extra: all
30
31
  Requires-Dist: redup[fuzzy]; extra == "all"
31
32
  Requires-Dist: redup[ast]; extra == "all"
@@ -83,7 +84,7 @@ Dynamic: license-file
83
84
  [![PyPI](https://img.shields.io/pypi/v/redup)](https://pypi.org/project/redup/)
84
85
  [![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
85
86
  [![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.22-green.svg)](https://pypi.org/project/redup/)
87
+ [![Version](https://img.shields.io/badge/version-0.3.23-green.svg)](https://pypi.org/project/redup/)
87
88
 
88
89
  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
90
 
@@ -11,6 +11,7 @@ src/redup.egg-info/requires.txt
11
11
  src/redup.egg-info/top_level.txt
12
12
  src/redup/cli_app/__init__.py
13
13
  src/redup/cli_app/config_builder.py
14
+ src/redup/cli_app/fuzzy_similarity.py
14
15
  src/redup/cli_app/main.py
15
16
  src/redup/cli_app/output_writer.py
16
17
  src/redup/cli_app/scan_commands.py
@@ -3,6 +3,9 @@ typer>=0.12.0
3
3
  rich>=13.0
4
4
  pydantic>=2.0
5
5
 
6
+ [:python_version < "3.11"]
7
+ tomli>=2.0
8
+
6
9
  [all]
7
10
  redup[fuzzy]
8
11
  redup[ast]
@@ -355,8 +355,11 @@ class TestFullRoundtrip:
355
355
  assert json_file.exists()
356
356
 
357
357
  data = json.loads(json_file.read_text())
358
+ self._verify_json_structure(data)
359
+ self._verify_calculate_tax_group(data)
358
360
 
359
- # Verify structure
361
+ def _verify_json_structure(self, data: dict) -> None:
362
+ """Verify basic JSON structure."""
360
363
  assert "project_path" in data
361
364
  assert "stats" in data
362
365
  assert "summary" in data
@@ -366,11 +369,10 @@ class TestFullRoundtrip:
366
369
  # Verify stats
367
370
  assert data["stats"]["files_scanned"] == 4
368
371
  assert data["stats"]["total_lines"] > 0
369
-
370
- # Verify duplicates found
371
372
  assert data["summary"]["total_groups"] >= 1
372
373
 
373
- # Verify the calculate_tax group
374
+ def _verify_calculate_tax_group(self, data: dict) -> None:
375
+ """Verify the calculate_tax group specifically."""
374
376
  tax_groups = [
375
377
  g for g in data["groups"]
376
378
  if g.get("normalized_name") == "calculate_tax"