redup 0.1.1__tar.gz → 0.1.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.
Files changed (34) hide show
  1. {redup-0.1.1 → redup-0.1.2}/PKG-INFO +1 -1
  2. {redup-0.1.1 → redup-0.1.2}/pyproject.toml +2 -1
  3. {redup-0.1.1 → redup-0.1.2}/src/redup/__init__.py +2 -2
  4. {redup-0.1.1 → redup-0.1.2}/src/redup/cli_app/main.py +12 -8
  5. {redup-0.1.1 → redup-0.1.2}/src/redup/core/hasher.py +19 -14
  6. {redup-0.1.1 → redup-0.1.2}/src/redup/core/models.py +6 -7
  7. {redup-0.1.1 → redup-0.1.2}/src/redup/core/pipeline.py +2 -4
  8. {redup-0.1.1 → redup-0.1.2}/src/redup/core/planner.py +1 -2
  9. {redup-0.1.1 → redup-0.1.2}/src/redup/core/scanner.py +1 -1
  10. {redup-0.1.1 → redup-0.1.2}/src/redup/reporters/yaml_reporter.py +1 -1
  11. {redup-0.1.1 → redup-0.1.2}/src/redup.egg-info/PKG-INFO +1 -1
  12. {redup-0.1.1 → redup-0.1.2}/LICENSE +0 -0
  13. {redup-0.1.1 → redup-0.1.2}/README.md +0 -0
  14. {redup-0.1.1 → redup-0.1.2}/setup.cfg +0 -0
  15. {redup-0.1.1 → redup-0.1.2}/src/redup/__main__.py +0 -0
  16. {redup-0.1.1 → redup-0.1.2}/src/redup/cli_app/__init__.py +0 -0
  17. {redup-0.1.1 → redup-0.1.2}/src/redup/core/__init__.py +0 -0
  18. {redup-0.1.1 → redup-0.1.2}/src/redup/core/matcher.py +0 -0
  19. {redup-0.1.1 → redup-0.1.2}/src/redup/reporters/__init__.py +0 -0
  20. {redup-0.1.1 → redup-0.1.2}/src/redup/reporters/json_reporter.py +0 -0
  21. {redup-0.1.1 → redup-0.1.2}/src/redup/reporters/toon_reporter.py +0 -0
  22. {redup-0.1.1 → redup-0.1.2}/src/redup.egg-info/SOURCES.txt +0 -0
  23. {redup-0.1.1 → redup-0.1.2}/src/redup.egg-info/dependency_links.txt +0 -0
  24. {redup-0.1.1 → redup-0.1.2}/src/redup.egg-info/entry_points.txt +0 -0
  25. {redup-0.1.1 → redup-0.1.2}/src/redup.egg-info/requires.txt +0 -0
  26. {redup-0.1.1 → redup-0.1.2}/src/redup.egg-info/top_level.txt +0 -0
  27. {redup-0.1.1 → redup-0.1.2}/tests/test_e2e.py +0 -0
  28. {redup-0.1.1 → redup-0.1.2}/tests/test_hasher.py +0 -0
  29. {redup-0.1.1 → redup-0.1.2}/tests/test_matcher.py +0 -0
  30. {redup-0.1.1 → redup-0.1.2}/tests/test_models.py +0 -0
  31. {redup-0.1.1 → redup-0.1.2}/tests/test_pipeline.py +0 -0
  32. {redup-0.1.1 → redup-0.1.2}/tests/test_planner.py +0 -0
  33. {redup-0.1.1 → redup-0.1.2}/tests/test_reporters.py +0 -0
  34. {redup-0.1.1 → redup-0.1.2}/tests/test_scanner.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redup
3
- Version: 0.1.1
3
+ Version: 0.1.2
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "redup"
7
- version = "0.1.1"
7
+ version = "0.1.2"
8
8
  description = "Code duplication analyzer and refactoring planner for LLMs"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -66,6 +66,7 @@ line-length = 100
66
66
 
67
67
  [tool.ruff.lint]
68
68
  select = ["E", "F", "W", "I", "UP", "B", "SIM"]
69
+ ignore = ["B008"]
69
70
 
70
71
  [tool.mypy]
71
72
  python_version = "3.10"
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "0.1.1"
5
+ __version__ = "0.1.2"
6
6
 
7
7
  from redup.core.models import (
8
8
  DuplicateFragment,
@@ -11,8 +11,8 @@ from redup.core.models import (
11
11
  RefactorSuggestion,
12
12
  ScanConfig,
13
13
  )
14
- from redup.core.scanner import scan_project
15
14
  from redup.core.pipeline import analyze
15
+ from redup.core.scanner import scan_project
16
16
 
17
17
  __all__ = [
18
18
  "analyze",
@@ -4,7 +4,6 @@ from __future__ import annotations
4
4
 
5
5
  from enum import Enum
6
6
  from pathlib import Path
7
- from typing import Optional
8
7
 
9
8
  import typer
10
9
 
@@ -29,6 +28,11 @@ class OutputFormat(str, Enum):
29
28
  all = "all"
30
29
 
31
30
 
31
+ DEFAULT_PATH = Path(".")
32
+ DEFAULT_FORMAT = OutputFormat.toon
33
+ DEFAULT_OUTPUT: Path | None = None
34
+
35
+
32
36
  def _write_output(content: str, output: Path | None, suffix: str) -> None:
33
37
  """Write content to file or stdout."""
34
38
  if output:
@@ -43,19 +47,19 @@ def _write_output(content: str, output: Path | None, suffix: str) -> None:
43
47
  @app.command()
44
48
  def scan(
45
49
  path: Path = typer.Argument(
46
- Path("."),
50
+ DEFAULT_PATH,
47
51
  help="Project root directory to scan.",
48
52
  exists=True,
49
53
  dir_okay=True,
50
54
  file_okay=False,
51
55
  ),
52
56
  format: OutputFormat = typer.Option(
53
- OutputFormat.toon,
57
+ DEFAULT_FORMAT,
54
58
  "--format", "-f",
55
59
  help="Output format.",
56
60
  ),
57
- output: Optional[Path] = typer.Option(
58
- None,
61
+ output: Path | None = typer.Option(
62
+ DEFAULT_OUTPUT,
59
63
  "--output", "-o",
60
64
  help="Output directory or file path. Defaults to stdout.",
61
65
  ),
@@ -118,7 +122,7 @@ def scan(
118
122
  output_dir = path / "redup_output"
119
123
  output_dir.mkdir(parents=True, exist_ok=True)
120
124
 
121
- for fmt, renderer, suffix in [
125
+ for _fmt, renderer, suffix in [
122
126
  ("JSON", to_json, "json"),
123
127
  ("YAML", to_yaml, "yaml"),
124
128
  ("TOON", to_toon, "toon"),
@@ -139,8 +143,8 @@ def scan(
139
143
  def info() -> None:
140
144
  """Show reDUP version and configuration info."""
141
145
  typer.echo(f"reDUP v{redup.__version__}")
142
- typer.echo(f" Python package: redup")
143
- typer.echo(f" Repo: https://github.com/semcod/redup")
146
+ typer.echo(" Python package: redup")
147
+ typer.echo(" Repo: https://github.com/semcod/redup")
144
148
  typer.echo("")
145
149
  typer.echo("Optional dependencies:")
146
150
 
@@ -23,7 +23,7 @@ def _normalize_text(text: str) -> str:
23
23
  continue
24
24
  if stripped.startswith('"""') or stripped.startswith("'''"):
25
25
  continue
26
- stripped = re.sub(r"#.*$", "", stripped)
26
+ stripped = re.sub(r'#.*$', "", stripped)
27
27
  stripped = stripped.strip()
28
28
  if stripped:
29
29
  lines.append(stripped)
@@ -49,7 +49,7 @@ def _normalize_ast_text(text: str) -> str:
49
49
  normalized = _normalize_text(text)
50
50
  normalized = re.sub(r'"[^"]*"', '"__STR__"', normalized)
51
51
  normalized = re.sub(r"'[^']*'", "'__STR__'", normalized)
52
- normalized = re.sub(r"\b\d+\.?\d*\b", "__NUM__", normalized)
52
+ normalized = re.sub(r'\b\d+\.?\d*\b', "__NUM__", normalized)
53
53
  return normalized
54
54
 
55
55
 
@@ -122,16 +122,20 @@ def _ast_to_normalized_string(tree: object) -> str:
122
122
  return " ".join(parts)
123
123
 
124
124
 
125
+ def _hash_text(text: str, normalizer: callable) -> str:
126
+ """Generic SHA-256 hash function with configurable normalizer."""
127
+ normalized = normalizer(text)
128
+ return hashlib.sha256(normalized.encode("utf-8")).hexdigest()[:16]
129
+
130
+
125
131
  def hash_block(text: str) -> str:
126
132
  """SHA-256 hash of normalized text."""
127
- normalized = _normalize_text(text)
128
- return hashlib.sha256(normalized.encode("utf-8")).hexdigest()[:16]
133
+ return _hash_text(text, _normalize_text)
129
134
 
130
135
 
131
136
  def hash_block_structural(text: str) -> str:
132
137
  """SHA-256 hash of deeply normalized text (variable names replaced)."""
133
- normalized = _normalize_ast_text(text)
134
- return hashlib.sha256(normalized.encode("utf-8")).hexdigest()[:16]
138
+ return _hash_text(text, _normalize_ast_text)
135
139
 
136
140
 
137
141
  @dataclass
@@ -175,22 +179,23 @@ def build_hash_index(blocks: list[CodeBlock], min_lines: int = 3) -> HashIndex:
175
179
  return index
176
180
 
177
181
 
178
- def find_exact_duplicates(index: HashIndex) -> dict[str, list[HashedBlock]]:
179
- """Find groups of blocks with identical normalized text."""
182
+ def _find_duplicates(hash_dict: dict[str, list[HashedBlock]]) -> dict[str, list[HashedBlock]]:
183
+ """Generic duplicate finder for any hash dictionary."""
180
184
  return {
181
185
  h: blocks
182
- for h, blocks in index.exact.items()
186
+ for h, blocks in hash_dict.items()
183
187
  if len(blocks) > 1 and _blocks_from_different_locations(blocks)
184
188
  }
185
189
 
186
190
 
191
+ def find_exact_duplicates(index: HashIndex) -> dict[str, list[HashedBlock]]:
192
+ """Find groups of blocks with identical normalized text."""
193
+ return _find_duplicates(index.exact)
194
+
195
+
187
196
  def find_structural_duplicates(index: HashIndex) -> dict[str, list[HashedBlock]]:
188
197
  """Find groups of blocks with identical structure (names may differ)."""
189
- return {
190
- h: blocks
191
- for h, blocks in index.structural.items()
192
- if len(blocks) > 1 and _blocks_from_different_locations(blocks)
193
- }
198
+ return _find_duplicates(index.structural)
194
199
 
195
200
 
196
201
  def _blocks_from_different_locations(blocks: list[HashedBlock]) -> bool:
@@ -5,7 +5,6 @@ from __future__ import annotations
5
5
  from dataclasses import dataclass, field
6
6
  from enum import Enum
7
7
  from pathlib import Path
8
- from typing import Optional
9
8
 
10
9
 
11
10
  class DuplicateType(str, Enum):
@@ -59,8 +58,8 @@ class DuplicateFragment:
59
58
  line_start: int
60
59
  line_end: int
61
60
  text: str = ""
62
- function_name: Optional[str] = None
63
- class_name: Optional[str] = None
61
+ function_name: str | None = None
62
+ class_name: str | None = None
64
63
 
65
64
  @property
66
65
  def line_count(self) -> int:
@@ -76,7 +75,7 @@ class DuplicateGroup:
76
75
  fragments: list[DuplicateFragment] = field(default_factory=list)
77
76
  similarity_score: float = 1.0
78
77
  normalized_hash: str = ""
79
- normalized_name: Optional[str] = None
78
+ normalized_name: str | None = None
80
79
 
81
80
  @property
82
81
  def occurrences(self) -> int:
@@ -107,8 +106,8 @@ class RefactorSuggestion:
107
106
  group_id: str
108
107
  action: RefactorAction
109
108
  new_module: str
110
- function_name: Optional[str] = None
111
- class_name: Optional[str] = None
109
+ function_name: str | None = None
110
+ class_name: str | None = None
112
111
  original_files: list[str] = field(default_factory=list)
113
112
  risk_level: RiskLevel = RiskLevel.LOW
114
113
  priority: int = 0
@@ -131,7 +130,7 @@ class DuplicationMap:
131
130
  """Complete result of a reDUP analysis run."""
132
131
 
133
132
  project_path: str = ""
134
- config: Optional[ScanConfig] = None
133
+ config: ScanConfig | None = None
135
134
  groups: list[DuplicateGroup] = field(default_factory=list)
136
135
  suggestions: list[RefactorSuggestion] = field(default_factory=list)
137
136
  stats: ScanStats = field(default_factory=ScanStats)
@@ -2,15 +2,13 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from collections import defaultdict
6
-
7
5
  from redup.core.hasher import (
8
6
  HashedBlock,
9
7
  build_hash_index,
10
8
  find_exact_duplicates,
11
9
  find_structural_duplicates,
12
10
  )
13
- from redup.core.matcher import match_candidates, refine_structural_matches
11
+ from redup.core.matcher import refine_structural_matches
14
12
  from redup.core.models import (
15
13
  DuplicateFragment,
16
14
  DuplicateGroup,
@@ -19,7 +17,7 @@ from redup.core.models import (
19
17
  ScanConfig,
20
18
  )
21
19
  from redup.core.planner import generate_suggestions
22
- from redup.core.scanner import CodeBlock, ScannedFile, scan_project
20
+ from redup.core.scanner import CodeBlock, scan_project
23
21
 
24
22
 
25
23
  def _blocks_to_group(
@@ -3,7 +3,6 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import os
6
- from collections import Counter
7
6
 
8
7
  from redup.core.models import (
9
8
  DuplicateGroup,
@@ -20,7 +19,7 @@ def _common_prefix(paths: list[str]) -> str:
20
19
  return ""
21
20
  parts_list = [p.split(os.sep) for p in paths]
22
21
  prefix: list[str] = []
23
- for segments in zip(*parts_list):
22
+ for segments in zip(*parts_list, strict=True):
24
23
  if len(set(segments)) == 1:
25
24
  prefix.append(segments[0])
26
25
  else:
@@ -4,9 +4,9 @@ from __future__ import annotations
4
4
 
5
5
  import fnmatch
6
6
  import time
7
+ from collections.abc import Iterator
7
8
  from dataclasses import dataclass, field
8
9
  from pathlib import Path
9
- from typing import Iterator
10
10
 
11
11
  from redup.core.models import ScanConfig, ScanStats
12
12
 
@@ -10,7 +10,7 @@ def to_yaml(dup_map: DuplicationMap) -> str:
10
10
  try:
11
11
  import yaml
12
12
  except ImportError:
13
- raise ImportError("pyyaml is required for YAML output: pip install pyyaml")
13
+ raise ImportError("pyyaml is required for YAML output: pip install pyyaml") from None
14
14
 
15
15
  from redup.reporters.json_reporter import _group_to_dict, _suggestion_to_dict
16
16
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redup
3
- Version: 0.1.1
3
+ Version: 0.1.2
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
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