redup 0.4.4__tar.gz → 0.4.6__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.4.4 → redup-0.4.6}/PKG-INFO +3 -2
- {redup-0.4.4 → redup-0.4.6}/README.md +1 -1
- {redup-0.4.4 → redup-0.4.6}/pyproject.toml +2 -1
- {redup-0.4.4 → redup-0.4.6}/src/redup/__init__.py +1 -1
- {redup-0.4.4 → redup-0.4.6}/src/redup/cli_app/main.py +15 -12
- {redup-0.4.4 → redup-0.4.6}/src/redup/cli_app/output_writer.py +4 -3
- {redup-0.4.4 → redup-0.4.6}/src/redup/cli_app/scan_commands.py +2 -2
- redup-0.4.6/src/redup/config.py +172 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup/core/config.py +21 -4
- {redup-0.4.4 → redup-0.4.6}/src/redup.egg-info/PKG-INFO +3 -2
- {redup-0.4.4 → redup-0.4.6}/src/redup.egg-info/SOURCES.txt +1 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup.egg-info/requires.txt +1 -0
- {redup-0.4.4 → redup-0.4.6}/tests/test_e2e.py +6 -6
- {redup-0.4.4 → redup-0.4.6}/LICENSE +0 -0
- {redup-0.4.4 → redup-0.4.6}/setup.cfg +0 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup/__main__.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup/cli_app/__init__.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup/cli_app/config_builder.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup/cli_app/fuzzy_similarity.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup/cli_app/scan_helpers.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup/core/__init__.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup/core/cache.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup/core/differ.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup/core/fuzzy_similarity.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup/core/hash_cache.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup/core/hasher.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup/core/lazy_grouper.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup/core/lsh_matcher.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup/core/matcher.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup/core/models.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup/core/pipeline.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup/core/planner.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup/core/python_parser.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup/core/scanner.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup/core/semantic.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup/core/ts_extractor.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup/core/universal_fuzzy.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup/core/utils/__init__.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup/core/utils/diff_helpers.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup/core/utils/duplicate_finders.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup/core/utils/function_extractor.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup/core/utils/hash_utils.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup/core/utils/language_dispatcher.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup/mcp_server.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup/reporters/__init__.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup/reporters/code2llm_reporter.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup/reporters/enhanced_reporter.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup/reporters/json_reporter.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup/reporters/markdown_reporter.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup/reporters/toon_reporter.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup/reporters/yaml_reporter.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup.egg-info/dependency_links.txt +0 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup.egg-info/entry_points.txt +0 -0
- {redup-0.4.4 → redup-0.4.6}/src/redup.egg-info/top_level.txt +0 -0
- {redup-0.4.4 → redup-0.4.6}/tests/test_hasher.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/tests/test_matcher.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/tests/test_mcp_server.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/tests/test_models.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/tests/test_pipeline.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/tests/test_planner.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/tests/test_reporters.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/tests/test_scanner.py +0 -0
- {redup-0.4.4 → redup-0.4.6}/tests/test_ts_extractor.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: redup
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.6
|
|
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
|
|
@@ -27,6 +27,7 @@ Requires-Dist: typer>=0.12.0
|
|
|
27
27
|
Requires-Dist: rich>=13.0
|
|
28
28
|
Requires-Dist: pydantic>=2.0
|
|
29
29
|
Requires-Dist: tomli>=2.0; python_version < "3.11"
|
|
30
|
+
Requires-Dist: python-dotenv>=1.0
|
|
30
31
|
Provides-Extra: all
|
|
31
32
|
Requires-Dist: redup[fast]; extra == "all"
|
|
32
33
|
Requires-Dist: redup[fuzzy]; extra == "all"
|
|
@@ -92,7 +93,7 @@ Dynamic: license-file
|
|
|
92
93
|
[](https://pypi.org/project/redup/)
|
|
93
94
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
94
95
|
[](https://python.org)
|
|
95
|
-
[](https://pypi.org/project/redup/)
|
|
96
97
|
|
|
97
98
|
reDUP scans codebases for duplicated functions, blocks, and structural patterns — then builds a prioritized refactoring map that LLMs can consume to eliminate redundancy systematically.
|
|
98
99
|
|
|
@@ -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
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "redup"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.6"
|
|
8
8
|
description = "Code duplication analyzer and refactoring planner for LLMs"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -31,6 +31,7 @@ dependencies = [
|
|
|
31
31
|
"rich>=13.0",
|
|
32
32
|
"pydantic>=2.0",
|
|
33
33
|
"tomli>=2.0; python_version<'3.11'",
|
|
34
|
+
"python-dotenv>=1.0",
|
|
34
35
|
]
|
|
35
36
|
|
|
36
37
|
[project.optional-dependencies]
|
|
@@ -13,6 +13,7 @@ import typer
|
|
|
13
13
|
warnings.filterwarnings("ignore", category=SyntaxWarning)
|
|
14
14
|
|
|
15
15
|
import redup # noqa: E402
|
|
16
|
+
from redup.config import config as global_config, reload_config # noqa: E402
|
|
16
17
|
from redup.core.config import create_sample_redup_toml, load_config # noqa: E402
|
|
17
18
|
from redup.core.models import DuplicationMap, ScanConfig # noqa: E402
|
|
18
19
|
|
|
@@ -35,7 +36,8 @@ OutputFormat = Literal["json", "yaml", "toon", "markdown", "all", "enhanced", "c
|
|
|
35
36
|
|
|
36
37
|
|
|
37
38
|
DEFAULT_PATH = Path(".")
|
|
38
|
-
|
|
39
|
+
# Use centralized config for defaults
|
|
40
|
+
DEFAULT_FORMAT: OutputFormat = global_config.DEFAULT_FORMAT # type: ignore
|
|
39
41
|
DEFAULT_OUTPUT: Path | None = None
|
|
40
42
|
|
|
41
43
|
|
|
@@ -78,14 +80,15 @@ def scan(
|
|
|
78
80
|
"--include-tests",
|
|
79
81
|
help="Include test files. Overrides config.",
|
|
80
82
|
),
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
"--
|
|
84
|
-
help="
|
|
83
|
+
no_functions_only: bool = typer.Option(
|
|
84
|
+
False,
|
|
85
|
+
"--no-functions-only",
|
|
86
|
+
help="Analyze all code, not just functions (default: functions-only for speed).",
|
|
85
87
|
),
|
|
86
88
|
parallel: bool = typer.Option(
|
|
87
89
|
False,
|
|
88
|
-
"--parallel
|
|
90
|
+
"--parallel",
|
|
91
|
+
"--no-parallel",
|
|
89
92
|
help="Use parallel scanning for large projects (default: disabled due to issues).",
|
|
90
93
|
),
|
|
91
94
|
max_workers: int | None = typer.Option(
|
|
@@ -98,10 +101,10 @@ def scan(
|
|
|
98
101
|
"--incremental",
|
|
99
102
|
help="Use incremental scanning with caching.",
|
|
100
103
|
),
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
"--
|
|
104
|
-
help="
|
|
104
|
+
no_memory_cache: bool = typer.Option(
|
|
105
|
+
False,
|
|
106
|
+
"--no-memory-cache",
|
|
107
|
+
help="Disable memory cache for scanning.",
|
|
105
108
|
),
|
|
106
109
|
max_cache_mb: int = typer.Option(
|
|
107
110
|
512,
|
|
@@ -122,8 +125,8 @@ def scan(
|
|
|
122
125
|
"""Scan a project for code duplicates."""
|
|
123
126
|
return scan_command(
|
|
124
127
|
path, format, output, extensions, min_lines, min_similarity,
|
|
125
|
-
include_tests,
|
|
126
|
-
incremental,
|
|
128
|
+
include_tests, not no_functions_only, parallel, max_workers,
|
|
129
|
+
incremental, not no_memory_cache, max_cache_mb, fuzzy, fuzzy_threshold
|
|
127
130
|
)
|
|
128
131
|
|
|
129
132
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
+
from redup.config import config, get_default_filename
|
|
6
7
|
from redup.core.models import DuplicationMap
|
|
7
8
|
from redup.reporters.json_reporter import to_json
|
|
8
9
|
from redup.reporters.markdown_reporter import to_markdown
|
|
@@ -15,9 +16,9 @@ from redup.reporters.enhanced_reporter import EnhancedReporter
|
|
|
15
16
|
def write_output(content: str, output: Path | None, suffix: str) -> None:
|
|
16
17
|
"""Write content to file or stdout."""
|
|
17
18
|
import typer
|
|
18
|
-
|
|
19
|
+
|
|
19
20
|
if output:
|
|
20
|
-
target = output if output.suffix else output /
|
|
21
|
+
target = output if output.suffix else output / get_default_filename(suffix)
|
|
21
22
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
22
23
|
target.write_text(content, encoding="utf-8")
|
|
23
24
|
typer.echo(f" → {target}")
|
|
@@ -70,7 +71,7 @@ def write_results(dup_map: DuplicationMap, format: str, output: Any, path: Path)
|
|
|
70
71
|
base_dir = path
|
|
71
72
|
|
|
72
73
|
for fmt, content in formats.items():
|
|
73
|
-
target = base_dir /
|
|
74
|
+
target = base_dir / get_default_filename(fmt)
|
|
74
75
|
target.write_text(content, encoding="utf-8")
|
|
75
76
|
typer.echo(f" → {target}")
|
|
76
77
|
else:
|
|
@@ -21,11 +21,11 @@ def scan_command(
|
|
|
21
21
|
min_lines: Any = typer.Option(None, "--min-lines", help="Minimum block size (lines). Overrides config."),
|
|
22
22
|
min_similarity: Any = typer.Option(None, "--min-sim", help="Minimum similarity score (0.0-1.0). Overrides config."),
|
|
23
23
|
include_tests: bool = typer.Option(False, "--include-tests", help="Include test files in analysis."),
|
|
24
|
-
functions_only: bool = typer.Option(True, "--functions-only
|
|
24
|
+
functions_only: bool = typer.Option(True, "--functions-only", help="Only analyze function-level duplicates (default: enabled for speed)."),
|
|
25
25
|
parallel: bool = typer.Option(False, "--parallel", help="Use parallel processing for faster analysis."),
|
|
26
26
|
max_workers: Any = typer.Option(None, "--max-workers", help="Maximum number of parallel workers."),
|
|
27
27
|
incremental: bool = typer.Option(False, "--incremental", help="Use incremental scanning (cache-based)."),
|
|
28
|
-
memory_cache: bool = typer.Option(True, "--memory-cache
|
|
28
|
+
memory_cache: bool = typer.Option(True, "--memory-cache", help="Use memory cache for faster processing."),
|
|
29
29
|
max_cache_mb: int = typer.Option(512, "--max-cache-mb", help="Maximum memory cache size in MB."),
|
|
30
30
|
fuzzy: bool = typer.Option(False, "--fuzzy", help="Enable fuzzy similarity detection."),
|
|
31
31
|
fuzzy_threshold: float = typer.Option(0.8, "--fuzzy-threshold", help="Fuzzy similarity threshold (0.0-1.0)."),
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Centralized configuration for reDUP with .env file support.
|
|
2
|
+
|
|
3
|
+
This module provides global configuration settings that can be customized via:
|
|
4
|
+
1. Environment variables (REDUP_* prefix)
|
|
5
|
+
2. .env files in the project directory
|
|
6
|
+
3. redup.toml configuration file
|
|
7
|
+
4. Programmatic overrides
|
|
8
|
+
|
|
9
|
+
Example .env file:
|
|
10
|
+
REDUP_DEFAULT_OUTPUT_FILENAME=duplication.toon.yaml
|
|
11
|
+
REDUP_DEFAULT_FORMAT=toon
|
|
12
|
+
REDUP_MIN_LINES=5
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
# Try to load .env file if python-dotenv is available
|
|
22
|
+
try:
|
|
23
|
+
from dotenv import load_dotenv
|
|
24
|
+
|
|
25
|
+
# Load .env from current working directory if it exists
|
|
26
|
+
_env_path = Path.cwd() / ".env"
|
|
27
|
+
if _env_path.exists():
|
|
28
|
+
load_dotenv(_env_path, override=True)
|
|
29
|
+
except ImportError:
|
|
30
|
+
load_dotenv = None # type: ignore
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class RedupConfig:
|
|
34
|
+
"""Global configuration container for reDUP settings.
|
|
35
|
+
|
|
36
|
+
Attributes can be set via:
|
|
37
|
+
- Environment variables (REDUP_* prefix, e.g., REDUP_DEFAULT_OUTPUT_FILENAME)
|
|
38
|
+
- .env files in the project directory (loaded automatically)
|
|
39
|
+
- Direct attribute assignment (e.g., config.DEFAULT_OUTPUT_FILENAME = "...")
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
# Output settings
|
|
43
|
+
DEFAULT_OUTPUT_FILENAME: str = "duplication.toon.yaml"
|
|
44
|
+
DEFAULT_FORMAT: str = "toon"
|
|
45
|
+
DEFAULT_OUTPUT_DIR: str = "."
|
|
46
|
+
|
|
47
|
+
# Scan settings
|
|
48
|
+
DEFAULT_MIN_LINES: int = 3
|
|
49
|
+
DEFAULT_MIN_SIMILARITY: float = 0.85
|
|
50
|
+
DEFAULT_EXTENSIONS: str = ".py"
|
|
51
|
+
DEFAULT_INCLUDE_TESTS: bool = False
|
|
52
|
+
|
|
53
|
+
# Performance settings
|
|
54
|
+
DEFAULT_PARALLEL: bool = False
|
|
55
|
+
DEFAULT_MAX_WORKERS: int | None = None
|
|
56
|
+
DEFAULT_INCREMENTAL: bool = False
|
|
57
|
+
DEFAULT_MEMORY_CACHE: bool = True
|
|
58
|
+
DEFAULT_MAX_CACHE_MB: int = 512
|
|
59
|
+
|
|
60
|
+
# Fuzzy settings
|
|
61
|
+
DEFAULT_FUZZY: bool = False
|
|
62
|
+
DEFAULT_FUZZY_THRESHOLD: float = 0.8
|
|
63
|
+
|
|
64
|
+
# LSH settings
|
|
65
|
+
DEFAULT_LSH_ENABLED: bool = True
|
|
66
|
+
DEFAULT_LSH_MIN_LINES: int = 50
|
|
67
|
+
DEFAULT_LSH_THRESHOLD: float = 0.8
|
|
68
|
+
|
|
69
|
+
# Cache settings
|
|
70
|
+
DEFAULT_CACHE_DIR: str = ".redup/cache"
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def _env_name(cls, attr_name: str) -> str:
|
|
74
|
+
"""Convert attribute name to environment variable name."""
|
|
75
|
+
return f"REDUP_{attr_name}"
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def _load_from_env(cls) -> dict[str, Any]:
|
|
79
|
+
"""Load configuration from environment variables."""
|
|
80
|
+
config = {}
|
|
81
|
+
for attr_name in dir(cls):
|
|
82
|
+
if attr_name.startswith("DEFAULT_"):
|
|
83
|
+
env_name = cls._env_name(attr_name)
|
|
84
|
+
value = os.getenv(env_name)
|
|
85
|
+
if value is not None:
|
|
86
|
+
# Convert to appropriate type based on default
|
|
87
|
+
default_value = getattr(cls, attr_name)
|
|
88
|
+
if isinstance(default_value, bool):
|
|
89
|
+
config[attr_name] = value.lower() in ("true", "1", "yes", "on")
|
|
90
|
+
elif isinstance(default_value, int):
|
|
91
|
+
try:
|
|
92
|
+
config[attr_name] = int(value)
|
|
93
|
+
except ValueError:
|
|
94
|
+
pass
|
|
95
|
+
elif isinstance(default_value, float):
|
|
96
|
+
try:
|
|
97
|
+
config[attr_name] = float(value)
|
|
98
|
+
except ValueError:
|
|
99
|
+
pass
|
|
100
|
+
else:
|
|
101
|
+
config[attr_name] = value
|
|
102
|
+
return config
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
def reload(cls, env_path: Path | None = None) -> None:
|
|
106
|
+
"""Reload configuration from .env file and environment variables.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
env_path: Optional path to .env file. If None, looks for .env in current directory.
|
|
110
|
+
"""
|
|
111
|
+
# Load from .env file if specified or found
|
|
112
|
+
if load_dotenv is not None:
|
|
113
|
+
if env_path is not None:
|
|
114
|
+
if env_path.exists():
|
|
115
|
+
load_dotenv(env_path, override=True)
|
|
116
|
+
else:
|
|
117
|
+
cwd_env = Path.cwd() / ".env"
|
|
118
|
+
if cwd_env.exists():
|
|
119
|
+
load_dotenv(cwd_env, override=True)
|
|
120
|
+
|
|
121
|
+
# Update class attributes from environment variables
|
|
122
|
+
for attr_name, value in cls._load_from_env().items():
|
|
123
|
+
setattr(cls, attr_name, value)
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def get(cls, attr_name: str, default: Any = None) -> Any:
|
|
127
|
+
"""Get configuration value by name."""
|
|
128
|
+
if hasattr(cls, attr_name):
|
|
129
|
+
return getattr(cls, attr_name)
|
|
130
|
+
return default
|
|
131
|
+
|
|
132
|
+
@classmethod
|
|
133
|
+
def set(cls, attr_name: str, value: Any) -> None:
|
|
134
|
+
"""Set configuration value by name."""
|
|
135
|
+
if hasattr(cls, attr_name):
|
|
136
|
+
setattr(cls, attr_name, value)
|
|
137
|
+
else:
|
|
138
|
+
raise AttributeError(f"No such config attribute: {attr_name}")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# Global config instance
|
|
142
|
+
config = RedupConfig()
|
|
143
|
+
|
|
144
|
+
# Load initial configuration from .env if present
|
|
145
|
+
config.reload()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def get_default_filename(suffix: str | None = None) -> str:
|
|
149
|
+
"""Get default output filename.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
suffix: Optional file suffix. If None, uses the suffix from DEFAULT_OUTPUT_FILENAME.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Default filename string.
|
|
156
|
+
"""
|
|
157
|
+
if suffix is None:
|
|
158
|
+
return config.DEFAULT_OUTPUT_FILENAME
|
|
159
|
+
# If suffix is provided, construct filename from base + suffix
|
|
160
|
+
base = config.DEFAULT_OUTPUT_FILENAME.rsplit(".", 1)[0]
|
|
161
|
+
return f"{base}.{suffix}"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def reload_config(env_path: Path | None = None) -> None:
|
|
165
|
+
"""Reload configuration from .env file.
|
|
166
|
+
|
|
167
|
+
This function can be called to reload settings when .env file changes.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
env_path: Optional path to .env file. If None, looks for .env in current directory.
|
|
171
|
+
"""
|
|
172
|
+
config.reload(env_path)
|
|
@@ -14,6 +14,7 @@ except ImportError:
|
|
|
14
14
|
except ImportError:
|
|
15
15
|
tomllib = None
|
|
16
16
|
|
|
17
|
+
from redup.config import config as global_config, reload_config
|
|
17
18
|
from redup.core.models import ScanConfig
|
|
18
19
|
|
|
19
20
|
|
|
@@ -50,11 +51,20 @@ def load_config() -> dict[str, Any]:
|
|
|
50
51
|
|
|
51
52
|
Priority order:
|
|
52
53
|
1. Environment variables (REDUP_*)
|
|
53
|
-
2.
|
|
54
|
-
3.
|
|
55
|
-
4.
|
|
54
|
+
2. .env file in current directory (auto-loaded via redup.config)
|
|
55
|
+
3. redup.toml in current directory
|
|
56
|
+
4. [tool.redup] in pyproject.toml
|
|
57
|
+
5. Global config defaults (redup.config)
|
|
56
58
|
"""
|
|
57
|
-
config
|
|
59
|
+
# Start with global config defaults (already includes .env if present)
|
|
60
|
+
config = {
|
|
61
|
+
"extensions": global_config.DEFAULT_EXTENSIONS,
|
|
62
|
+
"min_lines": global_config.DEFAULT_MIN_LINES,
|
|
63
|
+
"min_similarity": global_config.DEFAULT_MIN_SIMILARITY,
|
|
64
|
+
"include_tests": global_config.DEFAULT_INCLUDE_TESTS,
|
|
65
|
+
"output": global_config.DEFAULT_OUTPUT_DIR,
|
|
66
|
+
"format": global_config.DEFAULT_FORMAT,
|
|
67
|
+
}
|
|
58
68
|
|
|
59
69
|
# Load from redup.toml
|
|
60
70
|
config.update(_get_config_from_redup_toml())
|
|
@@ -116,6 +126,11 @@ def create_sample_redup_toml() -> str:
|
|
|
116
126
|
"""Create a sample redup.toml configuration file content."""
|
|
117
127
|
return """# reDUP Configuration File
|
|
118
128
|
# See https://github.com/semcod/redup for documentation
|
|
129
|
+
#
|
|
130
|
+
# You can also use a .env file in your project directory:
|
|
131
|
+
# REDUP_DEFAULT_OUTPUT_FILENAME=my_duplication.yaml
|
|
132
|
+
# REDUP_DEFAULT_FORMAT=toon
|
|
133
|
+
# REDUP_MIN_LINES=5
|
|
119
134
|
|
|
120
135
|
[scan]
|
|
121
136
|
# File extensions to scan (comma-separated)
|
|
@@ -143,6 +158,8 @@ max_lines = 100
|
|
|
143
158
|
[output]
|
|
144
159
|
# Default output format
|
|
145
160
|
format = "toon"
|
|
161
|
+
# Default output filename (can include suffix)
|
|
162
|
+
filename = "duplication.toon.yaml"
|
|
146
163
|
# Default output directory (relative to project root)
|
|
147
164
|
output = "redup_output"
|
|
148
165
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: redup
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.6
|
|
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
|
|
@@ -27,6 +27,7 @@ Requires-Dist: typer>=0.12.0
|
|
|
27
27
|
Requires-Dist: rich>=13.0
|
|
28
28
|
Requires-Dist: pydantic>=2.0
|
|
29
29
|
Requires-Dist: tomli>=2.0; python_version < "3.11"
|
|
30
|
+
Requires-Dist: python-dotenv>=1.0
|
|
30
31
|
Provides-Extra: all
|
|
31
32
|
Requires-Dist: redup[fast]; extra == "all"
|
|
32
33
|
Requires-Dist: redup[fuzzy]; extra == "all"
|
|
@@ -92,7 +93,7 @@ Dynamic: license-file
|
|
|
92
93
|
[](https://pypi.org/project/redup/)
|
|
93
94
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
94
95
|
[](https://python.org)
|
|
95
|
-
[](https://pypi.org/project/redup/)
|
|
96
97
|
|
|
97
98
|
reDUP scans codebases for duplicated functions, blocks, and structural patterns — then builds a prioritized refactoring map that LLMs can consume to eliminate redundancy systematically.
|
|
98
99
|
|
|
@@ -139,7 +139,7 @@ class TestCLIScanToon:
|
|
|
139
139
|
"scan", str(project_with_duplicates), "-f", "toon", "-o", str(out)
|
|
140
140
|
])
|
|
141
141
|
assert result.exit_code == 0
|
|
142
|
-
toon_file = out / "duplication.toon"
|
|
142
|
+
toon_file = out / "duplication.toon.yaml"
|
|
143
143
|
assert toon_file.exists()
|
|
144
144
|
content = toon_file.read_text()
|
|
145
145
|
assert "DUPLICATES" in content
|
|
@@ -239,7 +239,7 @@ class TestCLIScanAll:
|
|
|
239
239
|
assert result.exit_code == 0
|
|
240
240
|
assert (out / "duplication.json").exists()
|
|
241
241
|
assert (out / "duplication.yaml").exists()
|
|
242
|
-
assert (out / "duplication.toon").exists()
|
|
242
|
+
assert (out / "duplication.toon.yaml").exists()
|
|
243
243
|
|
|
244
244
|
def test_format_all_json_valid(self, project_with_duplicates: Path, tmp_path: Path):
|
|
245
245
|
out = tmp_path / "output"
|
|
@@ -256,7 +256,7 @@ class TestCLIScanAll:
|
|
|
256
256
|
"scan", str(project_with_duplicates), "-f", "all",
|
|
257
257
|
"-o", str(out), "--functions-only"
|
|
258
258
|
])
|
|
259
|
-
toon = (out / "duplication.toon").read_text()
|
|
259
|
+
toon = (out / "duplication.toon.yaml").read_text()
|
|
260
260
|
assert "REFACTOR" in toon
|
|
261
261
|
|
|
262
262
|
def test_all_three_formats_consistent(self, project_with_duplicates: Path, tmp_path: Path):
|
|
@@ -267,7 +267,7 @@ class TestCLIScanAll:
|
|
|
267
267
|
"-o", str(out), "--functions-only"
|
|
268
268
|
])
|
|
269
269
|
json_data = json.loads((out / "duplication.json").read_text())
|
|
270
|
-
toon_text = (out / "duplication.toon").read_text()
|
|
270
|
+
toon_text = (out / "duplication.toon.yaml").read_text()
|
|
271
271
|
yaml_text = (out / "duplication.yaml").read_text()
|
|
272
272
|
|
|
273
273
|
json_groups = json_data["summary"]["total_groups"]
|
|
@@ -423,7 +423,7 @@ class TestFullRoundtrip:
|
|
|
423
423
|
# All files created
|
|
424
424
|
assert (out / "duplication.json").exists()
|
|
425
425
|
assert (out / "duplication.yaml").exists()
|
|
426
|
-
assert (out / "duplication.toon").exists()
|
|
426
|
+
assert (out / "duplication.toon.yaml").exists()
|
|
427
427
|
|
|
428
428
|
# JSON parseable
|
|
429
429
|
json_data = json.loads((out / "duplication.json").read_text())
|
|
@@ -431,7 +431,7 @@ class TestFullRoundtrip:
|
|
|
431
431
|
json_saved = json_data["summary"]["total_saved_lines"]
|
|
432
432
|
|
|
433
433
|
# TOON mentions same numbers
|
|
434
|
-
toon = (out / "duplication.toon").read_text()
|
|
434
|
+
toon = (out / "duplication.toon.yaml").read_text()
|
|
435
435
|
assert f"dup_groups: {json_groups}" in toon
|
|
436
436
|
assert f"saved_lines: {json_saved}" in toon
|
|
437
437
|
|
|
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
|
|
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
|