|
3 | 3 | import json |
4 | 4 | import logging |
5 | 5 | from pathlib import Path |
6 | | - |
7 | | -# Use typing.Tuple for broader compatibility if MyPy struggles with tuple[] |
8 | | -from typing import Dict, List, Set, Tuple |
| 6 | +from typing import Any, Dict, Optional |
9 | 7 |
|
10 | 8 | logger = logging.getLogger(__name__) |
11 | 9 |
|
12 | | -# --- Constants --- |
13 | | - |
14 | | -CONFIG_FILE_NAME = ".codeconcat_config.json" |
15 | | -GITIGNORE_FILE_NAME = ".gitignore" |
16 | | - |
17 | | -# Heuristic: MIME types that are likely binary or not useful text content |
18 | | -# Use typing.Tuple explicitly |
19 | | -EXCLUDED_MIME_PREFIXES: Tuple[str, ...] = ("application/", "image/", "audio/", "video/") |
20 | | - |
21 | | -# Common text/code file extensions (Set for faster lookups) |
22 | | -DEFAULT_WHITELIST_EXTENSIONS: Set[str] = { |
23 | | - # Code |
24 | | - ".py", |
25 | | - ".pyw", |
26 | | - ".pyi", |
27 | | - ".js", |
28 | | - ".mjs", |
29 | | - ".cjs", |
30 | | - ".ts", |
31 | | - ".tsx", |
32 | | - ".java", |
33 | | - ".c", |
34 | | - ".h", |
35 | | - ".cpp", |
36 | | - ".hpp", |
37 | | - ".cs", |
38 | | - ".rb", |
39 | | - ".php", |
40 | | - ".swift", |
41 | | - ".go", |
42 | | - ".rs", |
43 | | - ".kt", |
44 | | - ".kts", |
45 | | - ".scala", |
46 | | - ".pl", |
47 | | - ".pm", |
48 | | - ".sh", |
49 | | - ".bash", |
50 | | - ".zsh", |
51 | | - ".ps1", |
52 | | - ".lua", |
53 | | - ".sql", |
54 | | - ".r", |
55 | | - ".dart", |
56 | | - ".groovy", |
57 | | - ".hs", |
58 | | - ".lhs", |
59 | | - ".ml", |
60 | | - ".mli", |
61 | | - ".fs", |
62 | | - ".fsx", |
63 | | - ".fsi", |
64 | | - ".elm", |
65 | | - ".clj", |
66 | | - ".cljs", |
67 | | - ".edn", |
68 | | - ".ex", |
69 | | - ".exs", |
70 | | - ".erl", |
71 | | - ".hrl", |
72 | | - ".vim", |
73 | | - ".el", |
74 | | - # Markup & Config |
75 | | - ".html", |
76 | | - ".htm", |
77 | | - ".css", |
78 | | - ".scss", |
79 | | - ".sass", |
80 | | - ".less", |
81 | | - ".xml", |
82 | | - ".json", |
83 | | - ".yaml", |
84 | | - ".yml", |
85 | | - ".toml", |
86 | | - ".ini", |
87 | | - ".cfg", |
88 | | - ".conf", |
89 | | - ".properties", |
90 | | - ".md", |
91 | | - ".markdown", |
92 | | - ".rst", |
93 | | - ".adoc", |
94 | | - ".asciidoc", |
95 | | - ".tex", |
96 | | - ".bib", |
97 | | - ".csv", |
98 | | - ".tsv", |
99 | | - ".log", |
100 | | - ".txt", |
101 | | - ".env", |
102 | | - ".dockerfile", |
103 | | - "dockerfile", |
104 | | - ".gitignore", |
105 | | - ".gitattributes", |
106 | | - ".editorconfig", |
107 | | - # Other potentially useful text |
108 | | - ".nfo", |
109 | | - ".readme", |
110 | | - ".inf", |
111 | | - ".url", |
| 10 | +HOME_CONFIG_PATH = Path.home() / ".codeconcat_config.json" |
| 11 | +PROJECT_CONFIG_PATH = Path(".") / ".codeconcat_config.json" |
| 12 | + |
| 13 | +DEFAULT_CONFIG: Dict[str, Any] = { |
| 14 | + "use_gitignore": True, |
| 15 | + "exclude_patterns": [], |
| 16 | + "whitelist_patterns": [], |
| 17 | + # Add other future config options here with defaults |
112 | 18 | } |
113 | 19 |
|
114 | | -# Default patterns to exclude (common build artifacts, caches, envs, etc.) |
115 | | -DEFAULT_EXCLUDE_PATTERNS: List[str] = [ |
116 | | - ".*", |
117 | | - "__pycache__", |
118 | | - "*.pyc", |
119 | | - "*.pyo", |
120 | | - "*.pyd", |
121 | | - "*.so", |
122 | | - "*.o", |
123 | | - "*.a", |
124 | | - "*.dll", |
125 | | - "*.exe", |
126 | | - "node_modules", |
127 | | - "vendor", |
128 | | - "build", |
129 | | - "dist", |
130 | | - "target", |
131 | | - "*.egg-info", |
132 | | - ".venv", |
133 | | - "venv", |
134 | | - "env", |
135 | | - ".env", |
136 | | - ".pytest_cache", |
137 | | - ".mypy_cache", |
138 | | - ".ruff_cache", |
139 | | - "*.lock", |
140 | | - "package-lock.json", |
141 | | - "yarn.lock", |
142 | | - "poetry.lock", |
143 | | - "Pipfile.lock", |
144 | | - "*.swp", |
145 | | - "*.swo", |
146 | | -] |
147 | | - |
148 | | -# --- Functions --- |
149 | | - |
150 | | - |
151 | | -def load_config(config_path: Path) -> Dict[str, List[str]]: |
| 20 | +# Flag to ensure default config creation happens only once per run if needed |
| 21 | +_default_config_created = False |
| 22 | + |
| 23 | + |
| 24 | +def load_config_file(path: Path) -> Optional[Dict[str, Any]]: |
152 | 25 | """Loads configuration from a JSON file.""" |
153 | | - config: Dict[str, List[str]] = {"exclude": [], "whitelist": []} |
154 | | - if config_path.is_file(): |
| 26 | + if path.is_file(): |
155 | 27 | try: |
156 | | - with config_path.open("r", encoding="utf-8") as f: |
157 | | - data = json.load(f) |
158 | | - loaded_exclude = data.get("exclude") |
159 | | - loaded_whitelist = data.get("whitelist") |
160 | | - if isinstance(loaded_exclude, list): |
161 | | - config["exclude"] = [ |
162 | | - str(p) for p in loaded_exclude if isinstance(p, str) |
163 | | - ] |
164 | | - if isinstance(loaded_whitelist, list): |
165 | | - config["whitelist"] = [ |
166 | | - str(p) for p in loaded_whitelist if isinstance(p, str) |
167 | | - ] |
168 | | - logger.info(f"Loaded configuration from {config_path}") |
| 28 | + with open(path, "r", encoding="utf-8") as f: |
| 29 | + return json.load(f) |
169 | 30 | except json.JSONDecodeError: |
170 | | - logger.warning( |
171 | | - f"Could not decode JSON from {config_path}. Using defaults/CLI args." |
172 | | - ) |
173 | | - except Exception as e: |
174 | | - logger.warning( |
175 | | - f"Error reading config file {config_path}: {e}. " |
176 | | - "Using defaults/CLI args." |
177 | | - ) |
178 | | - else: |
179 | | - logger.debug(f"No config file found at {config_path}.") |
| 31 | + logger.warning(f"Could not decode JSON from config file: {path}") |
| 32 | + except OSError as e: |
| 33 | + logger.warning(f"Could not read config file: {path}. Error: {e}") |
| 34 | + return None |
| 35 | + |
| 36 | + |
| 37 | +def merge_configs(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]: |
| 38 | + """Merges two config dictionaries. Override takes precedence.""" |
| 39 | + merged = base.copy() |
| 40 | + for key, value in override.items(): |
| 41 | + if key in merged and isinstance(merged[key], list) and isinstance(value, list): |
| 42 | + # Combine lists for patterns? Or override? Let's override for simplicity. |
| 43 | + # Ensure uniqueness if combining later. |
| 44 | + merged[key] = value |
| 45 | + else: |
| 46 | + merged[key] = value |
| 47 | + return merged |
| 48 | + |
| 49 | + |
| 50 | +def get_config() -> Dict[str, Any]: |
| 51 | + """Loads configuration from home and project files, merging them.""" |
| 52 | + config = DEFAULT_CONFIG.copy() |
| 53 | + |
| 54 | + home_config = load_config_file(HOME_CONFIG_PATH) |
| 55 | + if home_config: |
| 56 | + config = merge_configs(config, home_config) |
| 57 | + logger.info(f"Loaded configuration from {HOME_CONFIG_PATH}") |
| 58 | + |
| 59 | + project_config = load_config_file(PROJECT_CONFIG_PATH) |
| 60 | + if project_config: |
| 61 | + config = merge_configs(config, project_config) |
| 62 | + logger.info(f"Loaded configuration from {PROJECT_CONFIG_PATH} (overrides home config)") |
| 63 | + |
| 64 | + # Create default home config only if neither home nor project config existed |
| 65 | + if not home_config and not project_config: |
| 66 | + create_default_config_if_needed(HOME_CONFIG_PATH) |
| 67 | + |
180 | 68 | return config |
| 69 | + |
| 70 | + |
| 71 | +def create_default_config_if_needed(path: Path) -> None: |
| 72 | + """Creates a default config file at the specified path if it doesn't exist.""" |
| 73 | + global _default_config_created |
| 74 | + if not path.exists() and not _default_config_created: |
| 75 | + try: |
| 76 | + with open(path, "w", encoding="utf-8") as f: |
| 77 | + json.dump(DEFAULT_CONFIG, f, indent=2) |
| 78 | + logger.info(f"Created default configuration file at: {path}") |
| 79 | + _default_config_created = True # Mark as created for this run |
| 80 | + except OSError as e: |
| 81 | + logger.warning(f"Could not create default config file at {path}. Error: {e}") |
0 commit comments