janito 0.3.0__py3-none-any.whl → 0.4.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/__main__.py +158 -59
- janito/analysis.py +281 -0
- janito/changeapplier.py +436 -0
- janito/changeviewer.py +337 -51
- janito/claude.py +31 -46
- janito/common.py +23 -0
- janito/config.py +8 -3
- janito/console.py +300 -30
- janito/contentchange.py +8 -89
- janito/contextparser.py +113 -0
- janito/fileparser.py +125 -0
- janito/prompts.py +43 -74
- janito/qa.py +36 -5
- janito/scan.py +24 -9
- janito/version.py +23 -0
- {janito-0.3.0.dist-info → janito-0.4.0.dist-info}/METADATA +34 -8
- janito-0.4.0.dist-info/RECORD +21 -0
- janito-0.3.0.dist-info/RECORD +0 -15
- {janito-0.3.0.dist-info → janito-0.4.0.dist-info}/WHEEL +0 -0
- {janito-0.3.0.dist-info → janito-0.4.0.dist-info}/entry_points.txt +0 -0
- {janito-0.3.0.dist-info → janito-0.4.0.dist-info}/licenses/LICENSE +0 -0
janito/changeviewer.py
CHANGED
@@ -1,64 +1,350 @@
|
|
1
1
|
from pathlib import Path
|
2
2
|
from rich.console import Console
|
3
3
|
from rich.text import Text
|
4
|
-
from
|
5
|
-
import
|
4
|
+
from rich.panel import Panel
|
5
|
+
from rich.table import Table
|
6
|
+
from rich.rule import Rule # Add this import
|
7
|
+
from typing import List, Optional, Dict
|
8
|
+
from rich import box
|
9
|
+
from janito.fileparser import FileChange
|
10
|
+
from janito.analysis import AnalysisOption # Add this import
|
11
|
+
from rich.columns import Columns # Add this import at the top with other imports
|
6
12
|
|
7
|
-
|
8
|
-
"""Type definition for a file change"""
|
9
|
-
description: str
|
10
|
-
new_content: str
|
13
|
+
MIN_PANEL_WIDTH = 40 # Minimum width for each panel
|
11
14
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
+
|
16
|
+
def format_sequence_preview(lines: List[str]) -> Text:
|
17
|
+
"""Format a sequence of prefixed lines into rich text with colors"""
|
18
|
+
text = Text()
|
19
|
+
last_was_empty = False
|
15
20
|
|
16
|
-
|
17
|
-
|
18
|
-
|
21
|
+
for line in lines:
|
22
|
+
if not line:
|
23
|
+
# Preserve empty lines but don't duplicate them
|
24
|
+
if not last_was_empty:
|
25
|
+
text.append("\n")
|
26
|
+
last_was_empty = True
|
27
|
+
continue
|
28
|
+
|
29
|
+
last_was_empty = False
|
30
|
+
prefix = line[0] if line[0] in ('=', '>', '<') else ' '
|
31
|
+
content = line[1:] if line[0] in ('=', '>', '<') else line
|
32
|
+
|
33
|
+
if prefix == '=':
|
34
|
+
text.append(f" {content}\n", style="dim")
|
35
|
+
elif prefix == '>':
|
36
|
+
text.append(f"+{content}\n", style="green")
|
37
|
+
elif prefix == '<':
|
38
|
+
text.append(f"-{content}\n", style="red")
|
39
|
+
else:
|
40
|
+
text.append(f" {content}\n", style="yellow dim")
|
19
41
|
|
20
|
-
|
21
|
-
|
22
|
-
|
42
|
+
return text
|
43
|
+
|
44
|
+
|
45
|
+
|
46
|
+
|
47
|
+
|
48
|
+
|
49
|
+
def show_changes_legend(console: Console) -> None:
|
50
|
+
"""Display a legend explaining the colors and symbols used in change previews in a horizontal layout"""
|
51
|
+
# Create a list of colored text objects
|
52
|
+
legend_items = [
|
53
|
+
Text("Unchanged", style="#98C379"),
|
54
|
+
Text(" • ", style="dim"),
|
55
|
+
Text("Removed", style="#E06C75"),
|
56
|
+
Text(" • ", style="dim"),
|
57
|
+
Text("Relocated", style="#61AFEF"),
|
58
|
+
Text(" • ", style="dim"),
|
59
|
+
Text("New", style="#C678DD")
|
60
|
+
]
|
23
61
|
|
24
|
-
|
25
|
-
|
62
|
+
# Combine all items into a single text object
|
63
|
+
legend_text = Text()
|
64
|
+
for item in legend_items:
|
65
|
+
legend_text.append_text(item)
|
26
66
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
def show_diff_changes(console: Console, filepath: Path, original: str, new_content: str, description: str) -> None:
|
36
|
-
"""Display file changes using unified diff format"""
|
37
|
-
# Show header
|
38
|
-
console.print(f"\n[bold blue]Changes for {filepath}[/bold blue]")
|
39
|
-
console.print(f"[dim]{description}[/dim]\n")
|
40
|
-
|
41
|
-
# Generate diff
|
42
|
-
diff = difflib.unified_diff(
|
43
|
-
original.splitlines(keepends=True),
|
44
|
-
new_content.splitlines(keepends=True),
|
45
|
-
fromfile='old',
|
46
|
-
tofile='new',
|
47
|
-
lineterm=''
|
67
|
+
# Create a simple panel with the horizontal legend
|
68
|
+
legend_panel = Panel(
|
69
|
+
legend_text,
|
70
|
+
title="Changes Legend",
|
71
|
+
title_align="left",
|
72
|
+
border_style="white",
|
73
|
+
box=box.ROUNDED,
|
74
|
+
padding=(0, 1)
|
48
75
|
)
|
49
76
|
|
50
|
-
#
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
77
|
+
# Center the legend panel horizontally
|
78
|
+
console.print(Columns([legend_panel], align="center"))
|
79
|
+
console.print() # Add extra line for spacing
|
80
|
+
|
81
|
+
|
82
|
+
def show_change_preview(console: Console, filepath: Path, change: FileChange) -> None:
|
83
|
+
"""Display a preview of changes for a single file with side-by-side comparison"""
|
84
|
+
# Show changes legend first
|
85
|
+
show_changes_legend(console)
|
86
|
+
|
87
|
+
# Create main file panel content
|
88
|
+
main_content = []
|
89
|
+
|
90
|
+
# Handle new file preview
|
91
|
+
if change.is_new_file:
|
92
|
+
new_file_panel = Panel(
|
93
|
+
Text(change.content),
|
94
|
+
title="New File Content",
|
95
|
+
title_align="left",
|
96
|
+
border_style="green",
|
97
|
+
box=box.ROUNDED
|
98
|
+
)
|
99
|
+
main_content.append(new_file_panel)
|
100
|
+
|
101
|
+
# Create and display main file panel
|
102
|
+
file_panel = Panel(
|
103
|
+
Columns(main_content),
|
104
|
+
title=str(filepath),
|
105
|
+
title_align="left",
|
106
|
+
border_style="white",
|
107
|
+
box=box.ROUNDED )
|
108
|
+
return
|
109
|
+
|
110
|
+
|
111
|
+
|
112
|
+
# For modifications, create side-by-side comparison for each change
|
113
|
+
for i, (search, replace, description) in enumerate(change.search_blocks, 1):
|
114
|
+
# Show change header with description
|
115
|
+
header = f"Change {i}"
|
116
|
+
if description:
|
117
|
+
header += f": {description}"
|
118
|
+
|
119
|
+
if replace is None:
|
120
|
+
# For deletions, show single panel with content to be deleted
|
121
|
+
change_panel = Panel(
|
122
|
+
Text(search, style="red"),
|
123
|
+
title=f"Content to Delete{' - ' + description if description else ''}",
|
124
|
+
title_align="left",
|
125
|
+
border_style="#E06C75", # Brighter red
|
126
|
+
box=box.ROUNDED
|
127
|
+
)
|
128
|
+
main_content.append(change_panel)
|
62
129
|
else:
|
63
|
-
|
130
|
+
# For replacements, show side-by-side panels
|
131
|
+
|
132
|
+
|
133
|
+
# Find common content between search and replace
|
134
|
+
search_lines = search.splitlines()
|
135
|
+
replace_lines = replace.splitlines()
|
136
|
+
|
137
|
+
# Find common lines from top
|
138
|
+
common_top = []
|
139
|
+
for s, r in zip(search_lines, replace_lines):
|
140
|
+
if s == r:
|
141
|
+
common_top.append(s)
|
142
|
+
else:
|
143
|
+
break
|
144
|
+
|
145
|
+
# Find common lines from bottom
|
146
|
+
search_remaining = search_lines[len(common_top):]
|
147
|
+
replace_remaining = replace_lines[len(common_top):]
|
148
|
+
|
149
|
+
common_bottom = []
|
150
|
+
for s, r in zip(reversed(search_remaining), reversed(replace_remaining)):
|
151
|
+
if s == r:
|
152
|
+
common_bottom.insert(0, s)
|
153
|
+
else:
|
154
|
+
break
|
155
|
+
|
156
|
+
# Get the unique middle sections
|
157
|
+
search_middle = search_remaining[:-len(common_bottom)] if common_bottom else search_remaining
|
158
|
+
replace_middle = replace_remaining[:-len(common_bottom)] if common_bottom else replace_remaining
|
159
|
+
|
160
|
+
|
161
|
+
|
162
|
+
|
163
|
+
# Format content with highlighting using consistent colors and line numbers
|
164
|
+
|
165
|
+
|
166
|
+
def format_content(lines: List[str], is_search: bool) -> Text:
|
167
|
+
text = Text()
|
168
|
+
|
169
|
+
COLORS = {
|
170
|
+
'unchanged': '#98C379', # Brighter green for unchanged lines
|
171
|
+
'removed': '#E06C75', # Clearer red for removed lines
|
172
|
+
'added': '#61AFEF', # Bright blue for added lines
|
173
|
+
'new': '#C678DD', # Purple for completely new lines
|
174
|
+
'relocated': '#61AFEF' # Use same blue for relocated lines
|
175
|
+
}
|
176
|
+
|
177
|
+
# Create sets of lines for comparison
|
178
|
+
search_set = set(search_lines)
|
179
|
+
replace_set = set(replace_lines)
|
180
|
+
common_lines = search_set & replace_set
|
181
|
+
new_lines = replace_set - search_set
|
182
|
+
relocated_lines = common_lines - set(common_top) - set(common_bottom)
|
183
|
+
|
184
|
+
def add_line(line: str, style: str, prefix: str = " "):
|
185
|
+
# Special handling for icons
|
186
|
+
if style == COLORS['relocated']:
|
187
|
+
prefix = "⇄"
|
188
|
+
elif style == COLORS['removed'] and prefix == "-":
|
189
|
+
prefix = "✕"
|
190
|
+
elif style == COLORS['new'] or (style == COLORS['added'] and prefix == "+"):
|
191
|
+
prefix = "✚"
|
192
|
+
text.append(prefix, style=style)
|
193
|
+
text.append(f" {line}\n", style=style)
|
194
|
+
|
195
|
+
# Format common top section
|
196
|
+
for line in common_top:
|
197
|
+
add_line(line, COLORS['unchanged'], "=")
|
198
|
+
|
199
|
+
# Format changed middle section
|
200
|
+
for line in (search_middle if is_search else replace_middle):
|
201
|
+
if line in relocated_lines:
|
202
|
+
add_line(line, COLORS['relocated'], "⇄")
|
203
|
+
elif not is_search and line in new_lines:
|
204
|
+
add_line(line, COLORS['new'], "+")
|
205
|
+
else:
|
206
|
+
style = COLORS['removed'] if is_search else COLORS['added']
|
207
|
+
prefix = "✕" if is_search else "+"
|
208
|
+
add_line(line, style, prefix)
|
209
|
+
|
210
|
+
# Format common bottom section
|
211
|
+
for line in common_bottom:
|
212
|
+
add_line(line, COLORS['unchanged'], "=")
|
213
|
+
|
214
|
+
return text
|
215
|
+
|
216
|
+
|
217
|
+
|
218
|
+
# Create panels for old and new content without width constraints
|
219
|
+
old_panel = Panel(
|
220
|
+
format_content(search_lines, True),
|
221
|
+
title="Current Content",
|
222
|
+
title_align="left",
|
223
|
+
border_style="#E06C75",
|
224
|
+
box=box.ROUNDED
|
225
|
+
)
|
226
|
+
|
227
|
+
new_panel = Panel(
|
228
|
+
format_content(replace_lines, False),
|
229
|
+
title="New Content",
|
230
|
+
title_align="left",
|
231
|
+
border_style="#61AFEF",
|
232
|
+
box=box.ROUNDED
|
233
|
+
)
|
234
|
+
|
235
|
+
# Add change panels to main content with auto-fitting columns
|
236
|
+
change_columns = Columns([old_panel, new_panel], equal=True, align="center")
|
237
|
+
change_panel = Panel(
|
238
|
+
change_columns,
|
239
|
+
title=header,
|
240
|
+
title_align="left",
|
241
|
+
border_style="cyan",
|
242
|
+
box=box.ROUNDED
|
243
|
+
)
|
244
|
+
main_content.append(change_panel)
|
245
|
+
|
246
|
+
# Create and display main file panel
|
247
|
+
file_panel = Panel(
|
248
|
+
Columns(main_content, align="center"),
|
249
|
+
title=f"Modifying {filepath}",
|
250
|
+
title_align="left",
|
251
|
+
border_style="white",
|
252
|
+
box=box.ROUNDED
|
253
|
+
)
|
254
|
+
console.print(file_panel)
|
255
|
+
console.print()
|
256
|
+
|
257
|
+
# Remove or comment out the unused unified panel code since we're using direct column display
|
258
|
+
|
259
|
+
def preview_all_changes(console: Console, changes: Dict[Path, FileChange]) -> None:
|
260
|
+
"""Show preview for all file changes"""
|
261
|
+
console.print("\n[bold blue]Change Preview[/bold blue]")
|
262
|
+
|
263
|
+
for filepath, change in changes.items():
|
264
|
+
show_change_preview(console, filepath, change)
|
265
|
+
|
266
|
+
|
267
|
+
def _display_options(options: Dict[str, AnalysisOption]) -> None:
|
268
|
+
"""Display available options in a centered, responsive layout with consistent spacing."""
|
269
|
+
console = Console()
|
270
|
+
|
271
|
+
# Display centered header with decorative rule
|
272
|
+
console.print()
|
273
|
+
console.print(Rule(" Available Options ", style="bold cyan", align="center"))
|
274
|
+
console.print()
|
275
|
+
|
276
|
+
# Safety check for empty options
|
277
|
+
if not options:
|
278
|
+
console.print(Panel("[yellow]No options available[/yellow]", border_style="yellow"))
|
279
|
+
return
|
280
|
+
|
281
|
+
# Calculate optimal layout dimensions based on terminal width
|
282
|
+
terminal_width = console.width or 100
|
283
|
+
panel_padding = (1, 2) # Consistent padding for all panels
|
284
|
+
available_width = terminal_width - 4 # Account for margins
|
285
|
+
|
286
|
+
# Determine optimal panel width and number of columns
|
287
|
+
min_panels_per_row = 1
|
288
|
+
max_panels_per_row = 3
|
289
|
+
optimal_panel_width = min(
|
290
|
+
available_width // max_panels_per_row,
|
291
|
+
available_width // min_panels_per_row
|
292
|
+
)
|
293
|
+
|
294
|
+
if optimal_panel_width < MIN_PANEL_WIDTH:
|
295
|
+
optimal_panel_width = MIN_PANEL_WIDTH
|
296
|
+
|
297
|
+
# Create panels with consistent styling and spacing
|
298
|
+
panels = []
|
299
|
+
for letter, option in options.items():
|
300
|
+
# Build content with consistent formatting
|
301
|
+
content = Text()
|
302
|
+
|
303
|
+
# Add description section
|
304
|
+
content.append("Description:\n", style="bold cyan")
|
305
|
+
for item in option.description_items:
|
306
|
+
content.append(f"• {item}\n", style="white")
|
307
|
+
content.append("\n")
|
308
|
+
|
309
|
+
# Add affected files section if present
|
310
|
+
if option.affected_files:
|
311
|
+
content.append("Affected files:\n", style="bold cyan")
|
312
|
+
for file in option.affected_files:
|
313
|
+
content.append(f"• {file}\n", style="yellow")
|
314
|
+
|
315
|
+
# Create panel with consistent styling
|
316
|
+
panel = Panel(
|
317
|
+
content,
|
318
|
+
box=box.ROUNDED,
|
319
|
+
border_style="cyan",
|
320
|
+
title=f"Option {letter}: {option.summary}",
|
321
|
+
title_align="center",
|
322
|
+
padding=panel_padding,
|
323
|
+
width=optimal_panel_width
|
324
|
+
)
|
325
|
+
panels.append(panel)
|
326
|
+
|
327
|
+
# Calculate optimal number of columns based on available width
|
328
|
+
num_columns = max(1, min(
|
329
|
+
len(panels), # Don't exceed number of panels
|
330
|
+
available_width // optimal_panel_width, # Width-based limit
|
331
|
+
max_panels_per_row # Maximum columns limit
|
332
|
+
))
|
333
|
+
|
334
|
+
# Create a centered container panel for all options
|
335
|
+
container = Panel(
|
336
|
+
Columns(
|
337
|
+
panels,
|
338
|
+
num_columns=num_columns,
|
339
|
+
equal=True,
|
340
|
+
align="center",
|
341
|
+
padding=(0, 2) # Consistent spacing between columns
|
342
|
+
),
|
343
|
+
box=box.SIMPLE,
|
344
|
+
padding=(1, 4), # Add padding around the columns for better centering
|
345
|
+
width=min(terminal_width - 4, num_columns * optimal_panel_width + (num_columns - 1) * 4)
|
346
|
+
)
|
64
347
|
|
348
|
+
# Display the centered container
|
349
|
+
console.print(Columns([container], align="center"))
|
350
|
+
console.print()
|
janito/claude.py
CHANGED
@@ -1,13 +1,8 @@
|
|
1
|
-
from rich.traceback import install
|
2
1
|
import anthropic
|
3
2
|
import os
|
4
3
|
from typing import Optional
|
5
|
-
from rich.progress import Progress, SpinnerColumn, TextColumn
|
6
4
|
from threading import Event
|
7
5
|
|
8
|
-
# Install rich traceback handler
|
9
|
-
install(show_locals=True)
|
10
|
-
|
11
6
|
class ClaudeAPIAgent:
|
12
7
|
"""Handles interaction with Claude API, including message handling"""
|
13
8
|
def __init__(self, api_key: Optional[str] = None, system_prompt: str = None):
|
@@ -18,7 +13,6 @@ class ClaudeAPIAgent:
|
|
18
13
|
raise ValueError("ANTHROPIC_API_KEY environment variable is required")
|
19
14
|
self.client = anthropic.Client(api_key=self.api_key)
|
20
15
|
self.model = "claude-3-5-sonnet-20241022"
|
21
|
-
self.stop_progress = Event()
|
22
16
|
self.system_message = system_prompt
|
23
17
|
self.last_prompt = None
|
24
18
|
self.last_full_message = None
|
@@ -29,46 +23,37 @@ class ClaudeAPIAgent:
|
|
29
23
|
|
30
24
|
def send_message(self, message: str, stop_event: Event = None) -> str:
|
31
25
|
"""Send message to Claude API and return response"""
|
26
|
+
self.messages_history.append(("user", message))
|
27
|
+
# Store the full message
|
28
|
+
self.last_full_message = message
|
29
|
+
|
32
30
|
try:
|
33
|
-
|
34
|
-
# Store the full message
|
35
|
-
self.last_full_message = message
|
36
|
-
|
37
|
-
try:
|
38
|
-
# Check if already cancelled
|
39
|
-
if stop_event and stop_event.is_set():
|
40
|
-
return ""
|
41
|
-
|
42
|
-
# Start API request
|
43
|
-
response = self.client.messages.create(
|
44
|
-
model=self.model, # Use discovered model
|
45
|
-
system=self.system_message,
|
46
|
-
max_tokens=4000,
|
47
|
-
messages=[
|
48
|
-
{"role": "user", "content": message}
|
49
|
-
],
|
50
|
-
temperature=0,
|
51
|
-
)
|
52
|
-
|
53
|
-
# Handle response
|
54
|
-
response_text = response.content[0].text
|
55
|
-
|
56
|
-
# Only store and process response if not cancelled
|
57
|
-
if not (stop_event and stop_event.is_set()):
|
58
|
-
self.last_response = response_text
|
59
|
-
self.messages_history.append(("assistant", response_text))
|
60
|
-
|
61
|
-
# Always return the response, let caller handle cancellation
|
62
|
-
return response_text
|
63
|
-
|
64
|
-
except KeyboardInterrupt:
|
65
|
-
if stop_event:
|
66
|
-
stop_event.set()
|
67
|
-
return ""
|
68
|
-
|
69
|
-
except Exception as e:
|
70
|
-
error_msg = f"Error: {str(e)}"
|
71
|
-
self.messages_history.append(("error", error_msg))
|
31
|
+
# Check if already cancelled
|
72
32
|
if stop_event and stop_event.is_set():
|
73
33
|
return ""
|
74
|
-
|
34
|
+
|
35
|
+
response = self.client.messages.create(
|
36
|
+
model=self.model, # Use discovered model
|
37
|
+
system=self.system_message,
|
38
|
+
max_tokens=4000,
|
39
|
+
messages=[
|
40
|
+
{"role": "user", "content": message}
|
41
|
+
],
|
42
|
+
temperature=0,
|
43
|
+
)
|
44
|
+
|
45
|
+
# Handle response
|
46
|
+
response_text = response.content[0].text
|
47
|
+
|
48
|
+
# Only store and process response if not cancelled
|
49
|
+
if not (stop_event and stop_event.is_set()):
|
50
|
+
self.last_response = response_text
|
51
|
+
self.messages_history.append(("assistant", response_text))
|
52
|
+
|
53
|
+
# Always return the response, let caller handle cancellation
|
54
|
+
return response_text
|
55
|
+
|
56
|
+
except KeyboardInterrupt:
|
57
|
+
if stop_event:
|
58
|
+
stop_event.set()
|
59
|
+
return ""
|
janito/common.py
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TimeElapsedColumn
|
2
|
+
from janito.claude import ClaudeAPIAgent
|
3
|
+
|
4
|
+
def progress_send_message(claude: ClaudeAPIAgent, message: str) -> str:
|
5
|
+
"""
|
6
|
+
Send a message to Claude with a progress indicator and elapsed time.
|
7
|
+
|
8
|
+
Args:
|
9
|
+
claude: The Claude API agent instance
|
10
|
+
message: The message to send
|
11
|
+
|
12
|
+
Returns:
|
13
|
+
The response from Claude
|
14
|
+
"""
|
15
|
+
with Progress(
|
16
|
+
SpinnerColumn(),
|
17
|
+
TextColumn("[progress.description]{task.description}"),
|
18
|
+
TimeElapsedColumn(),
|
19
|
+
) as progress:
|
20
|
+
task = progress.add_task("Waiting for response from Claude...", total=None)
|
21
|
+
response = claude.send_message(message)
|
22
|
+
progress.update(task, completed=True)
|
23
|
+
return response
|
janito/config.py
CHANGED
@@ -7,7 +7,8 @@ class ConfigManager:
|
|
7
7
|
def __init__(self):
|
8
8
|
self.debug = False
|
9
9
|
self.verbose = False
|
10
|
-
self.debug_line = None
|
10
|
+
self.debug_line = None
|
11
|
+
self.test_cmd = os.getenv('JANITO_TEST_CMD')
|
11
12
|
|
12
13
|
@classmethod
|
13
14
|
def get_instance(cls) -> "ConfigManager":
|
@@ -21,12 +22,16 @@ class ConfigManager:
|
|
21
22
|
def set_verbose(self, enabled: bool) -> None:
|
22
23
|
self.verbose = enabled
|
23
24
|
|
24
|
-
def set_debug_line(self, line: Optional[int]) -> None:
|
25
|
+
def set_debug_line(self, line: Optional[int]) -> None:
|
25
26
|
self.debug_line = line
|
26
27
|
|
27
|
-
def should_debug_line(self, line: int) -> bool:
|
28
|
+
def should_debug_line(self, line: int) -> bool:
|
28
29
|
"""Return True if we should show debug for this line number"""
|
29
30
|
return self.debug and (self.debug_line is None or self.debug_line == line)
|
30
31
|
|
32
|
+
def set_test_cmd(self, cmd: Optional[str]) -> None:
|
33
|
+
"""Set the test command, overriding environment variable"""
|
34
|
+
self.test_cmd = cmd if cmd is not None else os.getenv('JANITO_TEST_CMD')
|
35
|
+
|
31
36
|
# Create a singleton instance
|
32
37
|
config = ConfigManager.get_instance()
|