janito 1.9.0__py3-none-any.whl → 1.10.0__py3-none-any.whl
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.
- janito/__init__.py +1 -1
- janito/agent/api_exceptions.py +4 -0
- janito/agent/config.py +1 -1
- janito/agent/config_defaults.py +2 -26
- janito/agent/conversation.py +163 -122
- janito/agent/conversation_api.py +149 -159
- janito/agent/{conversation_history.py → llm_conversation_history.py} +18 -1
- janito/agent/openai_client.py +38 -23
- janito/agent/openai_schema_generator.py +162 -129
- janito/agent/platform_discovery.py +134 -77
- janito/agent/profile_manager.py +5 -5
- janito/agent/rich_message_handler.py +80 -31
- janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +5 -4
- janito/agent/test_openai_schema_generator.py +93 -0
- janito/agent/tool_base.py +7 -2
- janito/agent/tool_executor.py +54 -49
- janito/agent/tool_registry.py +5 -2
- janito/agent/tool_use_tracker.py +26 -5
- janito/agent/tools/__init__.py +6 -3
- janito/agent/tools/create_directory.py +3 -1
- janito/agent/tools/create_file.py +7 -1
- janito/agent/tools/fetch_url.py +40 -3
- janito/agent/tools/find_files.py +3 -1
- janito/agent/tools/get_file_outline/core.py +6 -7
- janito/agent/tools/get_file_outline/search_outline.py +3 -1
- janito/agent/tools/get_lines.py +7 -2
- janito/agent/tools/move_file.py +3 -1
- janito/agent/tools/present_choices.py +3 -1
- janito/agent/tools/python_command_runner.py +150 -0
- janito/agent/tools/python_file_runner.py +148 -0
- janito/agent/tools/python_stdin_runner.py +154 -0
- janito/agent/tools/remove_directory.py +3 -1
- janito/agent/tools/remove_file.py +5 -1
- janito/agent/tools/replace_file.py +12 -2
- janito/agent/tools/replace_text_in_file.py +4 -2
- janito/agent/tools/run_bash_command.py +30 -69
- janito/agent/tools/run_powershell_command.py +134 -105
- janito/agent/tools/search_text.py +172 -122
- janito/agent/tools/validate_file_syntax/core.py +3 -1
- janito/agent/tools_utils/action_type.py +7 -0
- janito/agent/tools_utils/dir_walk_utils.py +3 -2
- janito/agent/tools_utils/formatting.py +47 -21
- janito/agent/tools_utils/gitignore_utils.py +66 -40
- janito/agent/tools_utils/test_gitignore_utils.py +46 -0
- janito/cli/_print_config.py +63 -61
- janito/cli/arg_parser.py +13 -12
- janito/cli/cli_main.py +137 -147
- janito/cli/main.py +152 -174
- janito/cli/one_shot.py +40 -26
- janito/i18n/__init__.py +1 -1
- janito/rich_utils.py +46 -8
- janito/shell/commands/__init__.py +2 -4
- janito/shell/commands/conversation_restart.py +3 -1
- janito/shell/commands/edit.py +3 -0
- janito/shell/commands/history_view.py +3 -3
- janito/shell/commands/lang.py +3 -0
- janito/shell/commands/livelogs.py +5 -3
- janito/shell/commands/prompt.py +6 -0
- janito/shell/commands/session.py +3 -0
- janito/shell/commands/session_control.py +3 -0
- janito/shell/commands/termweb_log.py +8 -0
- janito/shell/commands/tools.py +3 -0
- janito/shell/commands/track.py +36 -0
- janito/shell/commands/utility.py +13 -18
- janito/shell/commands/verbose.py +3 -4
- janito/shell/input_history.py +62 -0
- janito/shell/main.py +117 -181
- janito/shell/session/manager.py +0 -21
- janito/shell/ui/interactive.py +0 -2
- janito/termweb/static/editor.css +0 -4
- janito/tests/test_rich_utils.py +44 -0
- janito/web/app.py +0 -75
- {janito-1.9.0.dist-info → janito-1.10.0.dist-info}/METADATA +61 -42
- {janito-1.9.0.dist-info → janito-1.10.0.dist-info}/RECORD +78 -71
- {janito-1.9.0.dist-info → janito-1.10.0.dist-info}/WHEEL +1 -1
- janito/agent/providers.py +0 -77
- janito/agent/tools/run_python_command.py +0 -161
- janito/shell/commands/sum.py +0 -49
- {janito-1.9.0.dist-info → janito-1.10.0.dist-info}/entry_points.txt +0 -0
- {janito-1.9.0.dist-info → janito-1.10.0.dist-info}/licenses/LICENSE +0 -0
- {janito-1.9.0.dist-info → janito-1.10.0.dist-info}/top_level.txt +0 -0
@@ -1,10 +1,11 @@
|
|
1
1
|
from janito.agent.tool_base import ToolBase
|
2
|
+
from janito.agent.tools_utils.action_type import ActionType
|
2
3
|
from janito.agent.tool_registry import register_tool
|
3
4
|
from janito.agent.tools_utils.utils import pluralize
|
4
5
|
from janito.i18n import tr
|
5
6
|
import os
|
6
7
|
import re
|
7
|
-
from janito.agent.tools_utils.gitignore_utils import
|
8
|
+
from janito.agent.tools_utils.gitignore_utils import GitignoreFilter
|
8
9
|
|
9
10
|
|
10
11
|
def is_binary_file(path, blocksize=1024):
|
@@ -28,12 +29,13 @@ def is_binary_file(path, blocksize=1024):
|
|
28
29
|
class SearchTextTool(ToolBase):
|
29
30
|
"""
|
30
31
|
Search for a text pattern (regex or plain string) in all files within one or more directories or file paths and return matching lines. Respects .gitignore.
|
31
|
-
|
32
32
|
Args:
|
33
33
|
paths (str): String of one or more paths (space-separated) to search in. Each path can be a directory or a file.
|
34
34
|
pattern (str): Regex pattern or plain text substring to search for in files. Must not be empty. Tries regex first, falls back to substring if regex is invalid.
|
35
35
|
Note: When using regex mode, special characters (such as [, ], ., *, etc.) must be escaped if you want to match them literally (e.g., use '\\[DEBUG\\]' to match the literal string '[DEBUG]').
|
36
|
-
is_regex (bool): If True, treat pattern as
|
36
|
+
is_regex (bool): If True, treat pattern as a regular expression. If False, treat as plain text (default).
|
37
|
+
Only set is_regex=True if your pattern is a valid regular expression. Do NOT set is_regex=True for plain text patterns, as regex special characters (such as ., *, [, ], etc.) will be interpreted and may cause unexpected results.
|
38
|
+
For plain text substring search, leave is_regex as False or omit it.
|
37
39
|
max_depth (int, optional): Maximum directory depth to search. If 0 (default), search is recursive with no depth limit. If >0, limits recursion to that depth. Setting max_depth=1 disables recursion (only top-level directory). Ignored for file paths.
|
38
40
|
max_results (int): Maximum number of results to return. 0 means no limit (default).
|
39
41
|
ignore_utf8_errors (bool): If True, ignore utf-8 decode errors. Defaults to True.
|
@@ -42,20 +44,16 @@ class SearchTextTool(ToolBase):
|
|
42
44
|
If max_results is reached, appends a note to the output.
|
43
45
|
"""
|
44
46
|
|
45
|
-
def
|
46
|
-
self,
|
47
|
-
paths: str,
|
48
|
-
pattern: str,
|
49
|
-
is_regex: bool = False,
|
50
|
-
max_depth: int = 0,
|
51
|
-
max_results: int = 0,
|
52
|
-
ignore_utf8_errors: bool = True,
|
53
|
-
) -> str:
|
47
|
+
def _prepare_pattern(self, pattern, is_regex):
|
54
48
|
if not pattern:
|
55
49
|
self.report_error(
|
56
50
|
tr("Error: Empty search pattern provided. Operation aborted.")
|
57
51
|
)
|
58
|
-
return
|
52
|
+
return (
|
53
|
+
None,
|
54
|
+
False,
|
55
|
+
tr("Error: Empty search pattern provided. Operation aborted."),
|
56
|
+
)
|
59
57
|
regex = None
|
60
58
|
use_regex = False
|
61
59
|
if is_regex:
|
@@ -63,9 +61,11 @@ class SearchTextTool(ToolBase):
|
|
63
61
|
regex = re.compile(pattern)
|
64
62
|
use_regex = True
|
65
63
|
except re.error as e:
|
66
|
-
self.report_warning(tr("
|
67
|
-
return
|
68
|
-
|
64
|
+
self.report_warning(tr("\u26a0\ufe0f Invalid regex pattern."))
|
65
|
+
return (
|
66
|
+
None,
|
67
|
+
False,
|
68
|
+
tr("Warning: Invalid regex pattern: {error}. No results.", error=e),
|
69
69
|
)
|
70
70
|
else:
|
71
71
|
try:
|
@@ -74,116 +74,100 @@ class SearchTextTool(ToolBase):
|
|
74
74
|
except re.error:
|
75
75
|
regex = None
|
76
76
|
use_regex = False
|
77
|
-
|
78
|
-
limit_reached = False
|
79
|
-
total_results = 0
|
80
|
-
paths_list = paths.split()
|
81
|
-
for search_path in paths_list:
|
82
|
-
from janito.agent.tools_utils.utils import display_path
|
77
|
+
return regex, use_regex, None
|
83
78
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
dirs, files = filter_ignored(search_path, dirs, files)
|
137
|
-
walker = [(search_path, dirs, files)]
|
79
|
+
def _search_file(
|
80
|
+
self,
|
81
|
+
path,
|
82
|
+
pattern,
|
83
|
+
regex,
|
84
|
+
use_regex,
|
85
|
+
max_results,
|
86
|
+
total_results,
|
87
|
+
ignore_utf8_errors,
|
88
|
+
):
|
89
|
+
dir_output = []
|
90
|
+
dir_limit_reached = False
|
91
|
+
if not is_binary_file(path):
|
92
|
+
try:
|
93
|
+
open_kwargs = {"mode": "r", "encoding": "utf-8"}
|
94
|
+
if ignore_utf8_errors:
|
95
|
+
open_kwargs["errors"] = "ignore"
|
96
|
+
with open(path, **open_kwargs) as f:
|
97
|
+
for lineno, line in enumerate(f, 1):
|
98
|
+
if use_regex:
|
99
|
+
if regex.search(line):
|
100
|
+
dir_output.append(f"{path}:{lineno}: {line.strip()}")
|
101
|
+
else:
|
102
|
+
if pattern in line:
|
103
|
+
dir_output.append(f"{path}:{lineno}: {line.strip()}")
|
104
|
+
if (
|
105
|
+
max_results > 0
|
106
|
+
and (total_results + len(dir_output)) >= max_results
|
107
|
+
):
|
108
|
+
dir_limit_reached = True
|
109
|
+
break
|
110
|
+
except Exception:
|
111
|
+
pass
|
112
|
+
return dir_output, dir_limit_reached
|
113
|
+
|
114
|
+
def _search_directory(
|
115
|
+
self,
|
116
|
+
search_path,
|
117
|
+
pattern,
|
118
|
+
regex,
|
119
|
+
use_regex,
|
120
|
+
max_depth,
|
121
|
+
max_results,
|
122
|
+
total_results,
|
123
|
+
ignore_utf8_errors,
|
124
|
+
):
|
125
|
+
dir_output = []
|
126
|
+
dir_limit_reached = False
|
127
|
+
if max_depth == 1:
|
128
|
+
walk_result = next(os.walk(search_path), None)
|
129
|
+
if walk_result is None:
|
130
|
+
walker = [(search_path, [], [])]
|
138
131
|
else:
|
139
|
-
|
140
|
-
|
141
|
-
|
132
|
+
_, dirs, files = walk_result
|
133
|
+
gitignore = GitignoreFilter()
|
134
|
+
dirs, files = gitignore.filter_ignored(search_path, dirs, files)
|
135
|
+
walker = [(search_path, dirs, files)]
|
136
|
+
else:
|
137
|
+
gitignore = GitignoreFilter()
|
138
|
+
walker = os.walk(search_path)
|
139
|
+
stop_search = False
|
140
|
+
for root, dirs, files in walker:
|
141
|
+
if stop_search:
|
142
|
+
break
|
143
|
+
rel_path = os.path.relpath(root, search_path)
|
144
|
+
depth = 0 if rel_path == "." else rel_path.count(os.sep) + 1
|
145
|
+
if max_depth == 1 and depth > 0:
|
146
|
+
break
|
147
|
+
if max_depth > 0 and depth > max_depth:
|
148
|
+
continue
|
149
|
+
dirs, files = gitignore.filter_ignored(root, dirs, files)
|
150
|
+
for filename in files:
|
142
151
|
if stop_search:
|
143
152
|
break
|
144
|
-
|
145
|
-
|
146
|
-
|
153
|
+
path = os.path.join(root, filename)
|
154
|
+
file_output, file_limit_reached = self._search_file(
|
155
|
+
path,
|
156
|
+
pattern,
|
157
|
+
regex,
|
158
|
+
use_regex,
|
159
|
+
max_results,
|
160
|
+
total_results + len(dir_output),
|
161
|
+
ignore_utf8_errors,
|
162
|
+
)
|
163
|
+
dir_output.extend(file_output)
|
164
|
+
if file_limit_reached:
|
165
|
+
dir_limit_reached = True
|
166
|
+
stop_search = True
|
147
167
|
break
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
for filename in files:
|
152
|
-
if stop_search:
|
153
|
-
break
|
154
|
-
path = os.path.join(root, filename)
|
155
|
-
if is_binary_file(path):
|
156
|
-
continue
|
157
|
-
try:
|
158
|
-
open_kwargs = {"mode": "r", "encoding": "utf-8"}
|
159
|
-
if ignore_utf8_errors:
|
160
|
-
open_kwargs["errors"] = "ignore"
|
161
|
-
with open(path, **open_kwargs) as f:
|
162
|
-
for lineno, line in enumerate(f, 1):
|
163
|
-
if use_regex:
|
164
|
-
if regex.search(line):
|
165
|
-
dir_output.append(
|
166
|
-
f"{path}:{lineno}: {line.strip()}"
|
167
|
-
)
|
168
|
-
else:
|
169
|
-
if pattern in line:
|
170
|
-
dir_output.append(
|
171
|
-
f"{path}:{lineno}: {line.strip()}"
|
172
|
-
)
|
173
|
-
if (
|
174
|
-
max_results > 0
|
175
|
-
and (total_results + len(dir_output)) >= max_results
|
176
|
-
):
|
177
|
-
dir_limit_reached = True
|
178
|
-
stop_search = True
|
179
|
-
break
|
180
|
-
except Exception:
|
181
|
-
continue
|
182
|
-
output.extend(dir_output)
|
183
|
-
total_results += len(dir_output)
|
184
|
-
if dir_limit_reached:
|
185
|
-
limit_reached = True
|
186
|
-
break
|
168
|
+
return dir_output, dir_limit_reached
|
169
|
+
|
170
|
+
def _format_result(self, pattern, use_regex, output, limit_reached):
|
187
171
|
header = tr(
|
188
172
|
"[search_text] Pattern: '{pattern}' | Regex: {use_regex} | Results: {count}",
|
189
173
|
pattern=pattern,
|
@@ -195,10 +179,76 @@ class SearchTextTool(ToolBase):
|
|
195
179
|
result += tr("\n[Note: max_results limit reached, output truncated.]")
|
196
180
|
self.report_success(
|
197
181
|
tr(
|
198
|
-
"
|
182
|
+
" \u2705 {count} {line_word}{limit}",
|
199
183
|
count=len(output),
|
200
184
|
line_word=pluralize("line", len(output)),
|
201
185
|
limit=(" (limit reached)" if limit_reached else ""),
|
202
186
|
)
|
203
187
|
)
|
204
188
|
return result
|
189
|
+
|
190
|
+
def run(
|
191
|
+
self,
|
192
|
+
paths: str,
|
193
|
+
pattern: str,
|
194
|
+
is_regex: bool = False,
|
195
|
+
max_depth: int = 0,
|
196
|
+
max_results: int = 0,
|
197
|
+
ignore_utf8_errors: bool = True,
|
198
|
+
) -> str:
|
199
|
+
regex, use_regex, error_msg = self._prepare_pattern(pattern, is_regex)
|
200
|
+
if error_msg:
|
201
|
+
return error_msg
|
202
|
+
paths_list = paths.split()
|
203
|
+
results = []
|
204
|
+
total_results = 0
|
205
|
+
limit_reached = False
|
206
|
+
for search_path in paths_list:
|
207
|
+
from janito.agent.tools_utils.utils import display_path
|
208
|
+
|
209
|
+
info_str = tr(
|
210
|
+
"\U0001f50d Searching for {search_type} '{pattern}' in '{disp_path}'",
|
211
|
+
search_type=("regex" if use_regex else "text"),
|
212
|
+
pattern=pattern,
|
213
|
+
disp_path=display_path(search_path),
|
214
|
+
)
|
215
|
+
if max_depth > 0:
|
216
|
+
info_str += tr(" [max_depth={max_depth}]", max_depth=max_depth)
|
217
|
+
self.report_info(ActionType.READ, info_str)
|
218
|
+
dir_output = []
|
219
|
+
dir_limit_reached = False
|
220
|
+
if os.path.isfile(search_path):
|
221
|
+
dir_output, dir_limit_reached = self._search_file(
|
222
|
+
search_path,
|
223
|
+
pattern,
|
224
|
+
regex,
|
225
|
+
use_regex,
|
226
|
+
max_results,
|
227
|
+
total_results,
|
228
|
+
ignore_utf8_errors,
|
229
|
+
)
|
230
|
+
total_results += len(dir_output)
|
231
|
+
if dir_limit_reached:
|
232
|
+
limit_reached = True
|
233
|
+
else:
|
234
|
+
dir_output, dir_limit_reached = self._search_directory(
|
235
|
+
search_path,
|
236
|
+
pattern,
|
237
|
+
regex,
|
238
|
+
use_regex,
|
239
|
+
max_depth,
|
240
|
+
max_results,
|
241
|
+
total_results,
|
242
|
+
ignore_utf8_errors,
|
243
|
+
)
|
244
|
+
total_results += len(dir_output)
|
245
|
+
if dir_limit_reached:
|
246
|
+
limit_reached = True
|
247
|
+
# Format and append result for this path
|
248
|
+
result_str = self._format_result(
|
249
|
+
pattern, use_regex, dir_output, dir_limit_reached
|
250
|
+
)
|
251
|
+
results.append(info_str + "\n" + result_str)
|
252
|
+
if limit_reached:
|
253
|
+
break
|
254
|
+
return "\n\n".join(results)
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import os
|
2
2
|
from janito.i18n import tr
|
3
3
|
from janito.agent.tool_base import ToolBase
|
4
|
+
from janito.agent.tools_utils.action_type import ActionType
|
4
5
|
from janito.agent.tool_registry import register_tool
|
5
6
|
from janito.agent.tools_utils.utils import display_path
|
6
7
|
|
@@ -77,7 +78,8 @@ class ValidateFileSyntaxTool(ToolBase):
|
|
77
78
|
def run(self, file_path: str) -> str:
|
78
79
|
disp_path = display_path(file_path)
|
79
80
|
self.report_info(
|
80
|
-
|
81
|
+
ActionType.READ,
|
82
|
+
tr("🔎 Validating syntax for file '{disp_path}' ...", disp_path=disp_path),
|
81
83
|
)
|
82
84
|
result = validate_file_syntax(
|
83
85
|
file_path,
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import os
|
2
|
-
from .gitignore_utils import
|
2
|
+
from .gitignore_utils import GitignoreFilter
|
3
3
|
|
4
4
|
|
5
5
|
def walk_dir_with_gitignore(root_dir, max_depth=None):
|
@@ -11,6 +11,7 @@ def walk_dir_with_gitignore(root_dir, max_depth=None):
|
|
11
11
|
- If max_depth=1, only the root directory (matches 'find . -maxdepth 1').
|
12
12
|
- If max_depth=N (N>1), yields files in root and up to N-1 levels below root (matches 'find . -maxdepth N').
|
13
13
|
"""
|
14
|
+
gitignore = GitignoreFilter()
|
14
15
|
for root, dirs, files in os.walk(root_dir):
|
15
16
|
rel_path = os.path.relpath(root, root_dir)
|
16
17
|
depth = 0 if rel_path == "." else rel_path.count(os.sep) + 1
|
@@ -19,5 +20,5 @@ def walk_dir_with_gitignore(root_dir, max_depth=None):
|
|
19
20
|
# For max_depth=1, only root (depth=0). For max_depth=2, root and one level below (depth=0,1).
|
20
21
|
if depth > 0:
|
21
22
|
continue
|
22
|
-
dirs, files = filter_ignored(root, dirs, files)
|
23
|
+
dirs, files = gitignore.filter_ignored(root, dirs, files)
|
23
24
|
yield root, dirs, files
|
@@ -1,23 +1,49 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
rows = []
|
6
|
-
for item in outline_items:
|
7
|
-
docstring = item.get("docstring", "").replace("\n", " ")
|
8
|
-
if len(docstring) > 24:
|
9
|
-
docstring = docstring[:21] + "..."
|
10
|
-
rows.append(
|
11
|
-
f"| {item['type']:<7} | {item['name']:<11} | {item['start']:<5} | {item['end']:<3} | {item['parent']:<8} | {docstring:<24} |"
|
12
|
-
)
|
13
|
-
return header + "\n" + "\n".join(rows)
|
1
|
+
class OutlineFormatter:
|
2
|
+
"""
|
3
|
+
Utility class for formatting code and markdown outlines into human-readable tables.
|
4
|
+
"""
|
14
5
|
|
6
|
+
@staticmethod
|
7
|
+
def format_outline_table(outline_items):
|
8
|
+
"""
|
9
|
+
Format a list of code outline items (classes, functions, variables) into a table.
|
15
10
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
11
|
+
Args:
|
12
|
+
outline_items (list of dict): Each dict should contain keys: 'type', 'name', 'start', 'end', 'parent', 'docstring'.
|
13
|
+
|
14
|
+
Returns:
|
15
|
+
str: Formatted table as a string.
|
16
|
+
"""
|
17
|
+
if not outline_items:
|
18
|
+
return "No classes, functions, or variables found."
|
19
|
+
header = "| Type | Name | Start | End | Parent | Docstring |\n|---------|-------------|-------|-----|----------|--------------------------|"
|
20
|
+
rows = []
|
21
|
+
for item in outline_items:
|
22
|
+
docstring = item.get("docstring", "").replace("\n", " ")
|
23
|
+
if len(docstring) > 24:
|
24
|
+
docstring = docstring[:21] + "..."
|
25
|
+
rows.append(
|
26
|
+
f"| {item['type']:<7} | {item['name']:<11} | {item['start']:<5} | {item['end']:<3} | {item['parent']:<8} | {docstring:<24} |"
|
27
|
+
)
|
28
|
+
return header + "\n" + "\n".join(rows)
|
29
|
+
|
30
|
+
@staticmethod
|
31
|
+
def format_markdown_outline_table(outline_items):
|
32
|
+
"""
|
33
|
+
Format a list of markdown outline items (headers) into a table.
|
34
|
+
|
35
|
+
Args:
|
36
|
+
outline_items (list of dict): Each dict should contain keys: 'level', 'title', 'line'.
|
37
|
+
|
38
|
+
Returns:
|
39
|
+
str: Formatted table as a string.
|
40
|
+
"""
|
41
|
+
if not outline_items:
|
42
|
+
return "No headers found."
|
43
|
+
header = "| Level | Header | Line |\n|-------|----------------------------------|------|"
|
44
|
+
rows = []
|
45
|
+
for item in outline_items:
|
46
|
+
rows.append(
|
47
|
+
f"| {item['level']:<5} | {item['title']:<32} | {item['line']:<4} |"
|
48
|
+
)
|
49
|
+
return header + "\n" + "\n".join(rows)
|
@@ -1,43 +1,69 @@
|
|
1
1
|
import os
|
2
2
|
import pathspec
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
4
|
+
|
5
|
+
class GitignoreFilter:
|
6
|
+
"""
|
7
|
+
Utility class for loading, interpreting, and applying .gitignore patterns to file and directory paths.
|
8
|
+
|
9
|
+
Methods
|
10
|
+
-------
|
11
|
+
__init__(self, gitignore_path: str = ".gitignore")
|
12
|
+
Loads and parses .gitignore patterns from the specified path.
|
13
|
+
|
14
|
+
is_ignored(self, path: str) -> bool
|
15
|
+
Returns True if the given path matches any of the loaded .gitignore patterns.
|
16
|
+
|
17
|
+
filter_ignored(self, root: str, dirs: list, files: list) -> tuple[list, list]
|
18
|
+
Filters out ignored directories and files from the provided lists, returning only those not ignored.
|
19
|
+
"""
|
20
|
+
|
21
|
+
def __init__(self, gitignore_path: str = ".gitignore"):
|
22
|
+
self.gitignore_path = os.path.abspath(gitignore_path)
|
23
|
+
self.base_dir = os.path.dirname(self.gitignore_path)
|
24
|
+
lines = []
|
25
|
+
if not os.path.exists(self.gitignore_path):
|
26
|
+
self._spec = pathspec.PathSpec.from_lines("gitwildmatch", [])
|
27
|
+
else:
|
28
|
+
with open(
|
29
|
+
self.gitignore_path, "r", encoding="utf-8", errors="replace"
|
30
|
+
) as f:
|
31
|
+
lines = f.readlines()
|
32
|
+
self._spec = pathspec.PathSpec.from_lines("gitwildmatch", lines)
|
33
|
+
# Collect directory patterns (ending with /)
|
34
|
+
self.dir_patterns = [
|
35
|
+
line.strip() for line in lines if line.strip().endswith("/")
|
36
|
+
]
|
37
|
+
|
38
|
+
def is_ignored(self, path: str) -> bool:
|
39
|
+
"""Return True if the given path is ignored by the loaded .gitignore patterns."""
|
40
|
+
abs_path = os.path.abspath(path)
|
41
|
+
rel_path = os.path.relpath(abs_path, self.base_dir).replace(os.sep, "/")
|
42
|
+
return self._spec.match_file(rel_path)
|
43
|
+
|
44
|
+
def filter_ignored(self, root: str, dirs: list, files: list) -> tuple[list, list]:
|
45
|
+
"""
|
46
|
+
Filter out ignored directories and files from the provided lists.
|
47
|
+
Always ignores the .git directory (like git does).
|
48
|
+
"""
|
49
|
+
|
50
|
+
def dir_is_ignored(d):
|
51
|
+
abs_path = os.path.abspath(os.path.join(root, d))
|
52
|
+
rel_path = os.path.relpath(abs_path, self.base_dir).replace(os.sep, "/")
|
53
|
+
if rel_path == ".git" or rel_path.startswith(".git/"):
|
54
|
+
return True
|
55
|
+
# Remove directory if it matches a directory pattern
|
56
|
+
for pat in self.dir_patterns:
|
57
|
+
pat_clean = pat.rstrip("/")
|
58
|
+
if rel_path == pat_clean or rel_path.startswith(pat_clean + "/"):
|
59
|
+
return True
|
60
|
+
return self._spec.match_file(rel_path)
|
61
|
+
|
62
|
+
def file_is_ignored(f):
|
63
|
+
abs_path = os.path.abspath(os.path.join(root, f))
|
64
|
+
rel_path = os.path.relpath(abs_path, self.base_dir).replace(os.sep, "/")
|
65
|
+
return self._spec.match_file(rel_path)
|
66
|
+
|
67
|
+
dirs[:] = [d for d in dirs if not dir_is_ignored(d)]
|
68
|
+
files = [f for f in files if not file_is_ignored(f)]
|
69
|
+
return dirs, files
|
@@ -0,0 +1,46 @@
|
|
1
|
+
import os
|
2
|
+
import tempfile
|
3
|
+
import shutil
|
4
|
+
import pytest
|
5
|
+
from janito.agent.tools_utils.gitignore_utils import GitignoreFilter
|
6
|
+
|
7
|
+
|
8
|
+
def test_gitignore_filter_basic(tmp_path):
|
9
|
+
# Create a .gitignore file
|
10
|
+
gitignore_content = """
|
11
|
+
ignored_file.txt
|
12
|
+
ignored_dir/
|
13
|
+
*.log
|
14
|
+
"""
|
15
|
+
gitignore_path = tmp_path / ".gitignore"
|
16
|
+
gitignore_path.write_text(gitignore_content)
|
17
|
+
|
18
|
+
# Create files and directories
|
19
|
+
(tmp_path / "ignored_file.txt").write_text("should be ignored")
|
20
|
+
(tmp_path / "not_ignored.txt").write_text("should not be ignored")
|
21
|
+
(tmp_path / "ignored_dir").mkdir()
|
22
|
+
(tmp_path / "ignored_dir" / "file.txt").write_text("should be ignored")
|
23
|
+
(tmp_path / "not_ignored_dir").mkdir()
|
24
|
+
(tmp_path / "not_ignored_dir" / "file.txt").write_text("should not be ignored")
|
25
|
+
(tmp_path / "file.log").write_text("should be ignored")
|
26
|
+
|
27
|
+
gi = GitignoreFilter(str(gitignore_path))
|
28
|
+
|
29
|
+
assert gi.is_ignored(str(tmp_path / "ignored_file.txt"))
|
30
|
+
assert not gi.is_ignored(str(tmp_path / "not_ignored.txt"))
|
31
|
+
# Directory itself is not ignored, only its contents
|
32
|
+
assert not gi.is_ignored(str(tmp_path / "ignored_dir"))
|
33
|
+
assert gi.is_ignored(str(tmp_path / "ignored_dir" / "file.txt"))
|
34
|
+
assert not gi.is_ignored(str(tmp_path / "not_ignored_dir"))
|
35
|
+
assert not gi.is_ignored(str(tmp_path / "not_ignored_dir" / "file.txt"))
|
36
|
+
assert gi.is_ignored(str(tmp_path / "file.log"))
|
37
|
+
|
38
|
+
# Test filter_ignored
|
39
|
+
dirs = ["ignored_dir", "not_ignored_dir"]
|
40
|
+
files = ["ignored_file.txt", "not_ignored.txt", "file.log"]
|
41
|
+
filtered_dirs, filtered_files = gi.filter_ignored(str(tmp_path), dirs, files)
|
42
|
+
assert "ignored_dir" not in filtered_dirs
|
43
|
+
assert "not_ignored_dir" in filtered_dirs
|
44
|
+
assert "ignored_file.txt" not in filtered_files
|
45
|
+
assert "file.log" not in filtered_files
|
46
|
+
assert "not_ignored.txt" in filtered_files
|