janito 0.1.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 +6 -0
- janito/__main__.py +9 -0
- janito/change.py +382 -0
- janito/claude.py +112 -0
- janito/commands.py +377 -0
- janito/console.py +354 -0
- janito/janito.py +354 -0
- janito/prompts.py +181 -0
- janito/watcher.py +82 -0
- janito/workspace.py +169 -0
- janito/xmlchangeparser.py +202 -0
- janito-0.1.0.dist-info/LICENSE +21 -0
- janito-0.1.0.dist-info/METADATA +106 -0
- janito-0.1.0.dist-info/RECORD +19 -0
- janito-0.1.0.dist-info/WHEEL +5 -0
- janito-0.1.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +4 -0
- tests/conftest.py +9 -0
- tests/test_change.py +393 -0
tests/test_change.py
ADDED
@@ -0,0 +1,393 @@
|
|
1
|
+
import pytest
|
2
|
+
from pathlib import Path
|
3
|
+
from janito.xmlchangeparser import XMLChangeParser, XMLChange, XMLBlock
|
4
|
+
from janito.change import FileChangeHandler
|
5
|
+
|
6
|
+
@pytest.fixture
|
7
|
+
def parser():
|
8
|
+
return XMLChangeParser()
|
9
|
+
|
10
|
+
@pytest.fixture
|
11
|
+
def change_handler():
|
12
|
+
return FileChangeHandler(interactive=False)
|
13
|
+
|
14
|
+
@pytest.fixture
|
15
|
+
def sample_files(tmp_path):
|
16
|
+
"""Create sample files with specific indentation patterns"""
|
17
|
+
# Create a file with mixed indentation
|
18
|
+
mixed_indent = tmp_path / "mixed.py"
|
19
|
+
mixed_indent.write_text("""def outer():
|
20
|
+
def inner():
|
21
|
+
return True
|
22
|
+
|
23
|
+
def another():
|
24
|
+
return False # Non-standard indent
|
25
|
+
""")
|
26
|
+
|
27
|
+
# Create a file with consistent indentation
|
28
|
+
standard_indent = tmp_path / "standard.py"
|
29
|
+
standard_indent.write_text("""class TestClass:
|
30
|
+
def method_one(self):
|
31
|
+
return 1
|
32
|
+
|
33
|
+
def method_two(self):
|
34
|
+
if True:
|
35
|
+
return 2
|
36
|
+
""")
|
37
|
+
|
38
|
+
return {"mixed": mixed_indent, "standard": standard_indent}
|
39
|
+
|
40
|
+
# Remove unused sample_xml fixture
|
41
|
+
|
42
|
+
def test_parse_empty_create_block(parser):
|
43
|
+
test_xml = '''<fileChanges>
|
44
|
+
<change path="hello.py" operation="create">
|
45
|
+
<block description="Create new file hello.py">
|
46
|
+
<oldContent></oldContent>
|
47
|
+
<newContent></newContent>
|
48
|
+
</block>
|
49
|
+
</change>
|
50
|
+
</fileChanges>'''
|
51
|
+
|
52
|
+
changes = parser.parse_response(test_xml)
|
53
|
+
assert len(changes) == 1
|
54
|
+
|
55
|
+
change = changes[0]
|
56
|
+
assert change.path.name == "hello.py"
|
57
|
+
assert change.operation == "create"
|
58
|
+
assert len(change.blocks) == 1
|
59
|
+
|
60
|
+
block = change.blocks[0]
|
61
|
+
assert block.description == "Create new file hello.py"
|
62
|
+
assert block.old_content == []
|
63
|
+
assert block.new_content == []
|
64
|
+
|
65
|
+
def test_parse_create_with_content(parser):
|
66
|
+
test_xml = '''<fileChanges>
|
67
|
+
<change path="test.py" operation="create">
|
68
|
+
<block description="Create test file">
|
69
|
+
<oldContent>
|
70
|
+
</oldContent>
|
71
|
+
<newContent>
|
72
|
+
def test_function():
|
73
|
+
return True
|
74
|
+
</newContent>
|
75
|
+
</block>
|
76
|
+
</change>
|
77
|
+
</fileChanges>'''
|
78
|
+
|
79
|
+
changes = parser.parse_response(test_xml)
|
80
|
+
assert len(changes) == 1
|
81
|
+
|
82
|
+
change = changes[0]
|
83
|
+
assert change.path.name == "test.py"
|
84
|
+
assert change.operation == "create"
|
85
|
+
|
86
|
+
block = change.blocks[0]
|
87
|
+
assert block.old_content == []
|
88
|
+
assert len(block.new_content) == 2
|
89
|
+
assert "def test_function():" in [line.strip() for line in block.new_content]
|
90
|
+
assert "return True" in [line.strip() for line in block.new_content]
|
91
|
+
|
92
|
+
|
93
|
+
def test_parse_modify_block(parser):
|
94
|
+
test_xml = '''<fileChanges>
|
95
|
+
<change path="existing.py" operation="modify">
|
96
|
+
<block description="Update function">
|
97
|
+
<oldContent>
|
98
|
+
def old_function():
|
99
|
+
pass
|
100
|
+
</oldContent>
|
101
|
+
<newContent>
|
102
|
+
def new_function():
|
103
|
+
return True
|
104
|
+
</newContent>
|
105
|
+
</block>
|
106
|
+
</change>
|
107
|
+
</fileChanges>'''
|
108
|
+
|
109
|
+
changes = parser.parse_response(test_xml)
|
110
|
+
assert len(changes) == 1
|
111
|
+
|
112
|
+
change = changes[0]
|
113
|
+
assert change.path.name == "existing.py"
|
114
|
+
assert change.operation == "modify"
|
115
|
+
|
116
|
+
block = change.blocks[0]
|
117
|
+
assert "def old_function():" in [line.strip() for line in block.old_content]
|
118
|
+
assert "pass" in [line.strip() for line in block.old_content]
|
119
|
+
assert "def new_function():" in [line.strip() for line in block.new_content]
|
120
|
+
assert "return True" in [line.strip() for line in block.new_content]
|
121
|
+
|
122
|
+
def test_parse_block_with_different_indentation(parser, change_handler, sample_files):
|
123
|
+
# Test with actual indentation from file
|
124
|
+
original_content = sample_files["mixed"].read_text()
|
125
|
+
test_xml = '''<fileChanges>
|
126
|
+
<change path="{}" operation="modify">
|
127
|
+
<block description="Update indented function">
|
128
|
+
<oldContent>
|
129
|
+
def another():
|
130
|
+
return False # Non-standard indent
|
131
|
+
</oldContent>
|
132
|
+
<newContent>
|
133
|
+
def another():
|
134
|
+
return True
|
135
|
+
</newContent>
|
136
|
+
</block>
|
137
|
+
</change>
|
138
|
+
</fileChanges>'''.format(sample_files["mixed"])
|
139
|
+
|
140
|
+
# First verify the parser handles the content
|
141
|
+
changes = parser.parse_response(test_xml)
|
142
|
+
assert len(changes) == 1
|
143
|
+
|
144
|
+
# Then verify the change handler preserves original indentation
|
145
|
+
assert change_handler.process_changes(test_xml)
|
146
|
+
modified_content = sample_files["mixed"].read_text().splitlines()
|
147
|
+
|
148
|
+
# The test is wrong - it expects the wrong indentation level
|
149
|
+
# The actual behavior maintains the original indentation of def and adjusts return relative to it
|
150
|
+
found_def = False
|
151
|
+
found_return = False
|
152
|
+
for line in modified_content:
|
153
|
+
if "def another():" in line:
|
154
|
+
assert line.startswith(" "), f"Expected 4 spaces indent for def, got: '{line}'"
|
155
|
+
found_def = True
|
156
|
+
elif "return True" in line:
|
157
|
+
assert line.startswith(" "), f"Expected 8 spaces indent for return, got: '{line}'"
|
158
|
+
found_return = True
|
159
|
+
|
160
|
+
assert found_def and found_return, "Both lines should be found with correct indentation"
|
161
|
+
|
162
|
+
def test_parse_multiple_blocks(parser):
|
163
|
+
test_xml = '''<fileChanges>
|
164
|
+
<change path="multi.py" operation="modify">
|
165
|
+
<block description="First change">
|
166
|
+
<oldContent>
|
167
|
+
def first(): pass
|
168
|
+
</oldContent>
|
169
|
+
<newContent>
|
170
|
+
def first(): return 1
|
171
|
+
</newContent>
|
172
|
+
</block>
|
173
|
+
<block description="Second change">
|
174
|
+
<oldContent>
|
175
|
+
def second(): pass
|
176
|
+
</oldContent>
|
177
|
+
<newContent>
|
178
|
+
def second(): return 2
|
179
|
+
</newContent>
|
180
|
+
</block>
|
181
|
+
</change>
|
182
|
+
</fileChanges>'''
|
183
|
+
|
184
|
+
changes = parser.parse_response(test_xml)
|
185
|
+
assert len(changes) == 1
|
186
|
+
assert len(changes[0].blocks) == 2
|
187
|
+
|
188
|
+
block1, block2 = changes[0].blocks
|
189
|
+
assert block1.description == "First change"
|
190
|
+
assert block2.description == "Second change"
|
191
|
+
|
192
|
+
# Add new test for indentation preservation
|
193
|
+
def test_indentation_preservation(change_handler, sample_files):
|
194
|
+
test_xml = '''<fileChanges>
|
195
|
+
<change path="{}" operation="modify">
|
196
|
+
<block description="Update inner function">
|
197
|
+
<oldContent>
|
198
|
+
def inner():
|
199
|
+
return True
|
200
|
+
</oldContent>
|
201
|
+
<newContent>
|
202
|
+
def inner():
|
203
|
+
return False
|
204
|
+
</newContent>
|
205
|
+
</block>
|
206
|
+
</change>
|
207
|
+
</fileChanges>'''.format(sample_files["mixed"])
|
208
|
+
|
209
|
+
# Now this will bypass the interactive prompt
|
210
|
+
assert change_handler.process_changes(test_xml)
|
211
|
+
modified_content = sample_files["mixed"].read_text()
|
212
|
+
assert " def inner():" in modified_content
|
213
|
+
assert " return False" in modified_content
|
214
|
+
|
215
|
+
def test_multiple_indentation_levels(change_handler, sample_files):
|
216
|
+
original_content = sample_files["standard"].read_text()
|
217
|
+
test_xml = '''<fileChanges>
|
218
|
+
<change path="{}" operation="modify">
|
219
|
+
<block description="Update nested if">
|
220
|
+
<oldContent>
|
221
|
+
if True:
|
222
|
+
return 2
|
223
|
+
</oldContent>
|
224
|
+
<newContent>
|
225
|
+
if True:
|
226
|
+
return 42
|
227
|
+
</newContent>
|
228
|
+
</block>
|
229
|
+
</change>
|
230
|
+
</fileChanges>'''.format(sample_files["standard"])
|
231
|
+
|
232
|
+
assert change_handler.process_changes(test_xml)
|
233
|
+
modified_content = sample_files["standard"].read_text().splitlines()
|
234
|
+
|
235
|
+
# Verify exact indentation is preserved
|
236
|
+
for line in modified_content:
|
237
|
+
if "if True:" in line:
|
238
|
+
assert line == " if True:"
|
239
|
+
elif "return 42" in line:
|
240
|
+
assert line == " return 42"
|
241
|
+
|
242
|
+
# Add new test specifically for nested indentation preservation
|
243
|
+
def test_nested_indentation_preservation(change_handler, sample_files):
|
244
|
+
test_xml = '''<fileChanges>
|
245
|
+
<change path="{}" operation="modify">
|
246
|
+
<block description="Update inner function">
|
247
|
+
<oldContent>
|
248
|
+
def inner():
|
249
|
+
return True
|
250
|
+
</oldContent>
|
251
|
+
<newContent>
|
252
|
+
def inner():
|
253
|
+
return 42
|
254
|
+
</newContent>
|
255
|
+
</block>
|
256
|
+
</change>
|
257
|
+
</fileChanges>'''.format(sample_files["mixed"])
|
258
|
+
|
259
|
+
assert change_handler.process_changes(test_xml)
|
260
|
+
modified_content = sample_files["mixed"].read_text().splitlines()
|
261
|
+
|
262
|
+
# Verify indentation is preserved based on context
|
263
|
+
for i, line in enumerate(modified_content):
|
264
|
+
if "def inner():" in line:
|
265
|
+
assert line == " def inner():", f"Expected 4 spaces, got: '{line}'"
|
266
|
+
assert modified_content[i+1] == " return 42", f"Expected 8 spaces, got: '{modified_content[i+1]}'"
|
267
|
+
|
268
|
+
# Keep remaining validation tests
|
269
|
+
|
270
|
+
def test_parse_invalid_xml(parser):
|
271
|
+
invalid_xml = "<invalid>xml</invalid>"
|
272
|
+
changes = parser.parse_response(invalid_xml)
|
273
|
+
assert len(changes) == 0
|
274
|
+
|
275
|
+
def test_parse_empty_response(parser):
|
276
|
+
empty_response = ""
|
277
|
+
changes = parser.parse_response(empty_response)
|
278
|
+
assert len(changes) == 0
|
279
|
+
|
280
|
+
def test_parse_invalid_operation(parser):
|
281
|
+
invalid_op_xml = '''<fileChanges>
|
282
|
+
<change path="test.py" operation="invalid">
|
283
|
+
<block description="Test">
|
284
|
+
<oldContent></oldContent>
|
285
|
+
<newContent>test</newContent>
|
286
|
+
</block>
|
287
|
+
</change>
|
288
|
+
</fileChanges>'''
|
289
|
+
changes = parser.parse_response(invalid_op_xml)
|
290
|
+
assert len(changes) == 0
|
291
|
+
|
292
|
+
def test_parse_missing_attributes(parser):
|
293
|
+
missing_attr_xml = '''<fileChanges>
|
294
|
+
<change path="test.py">
|
295
|
+
<block>
|
296
|
+
<oldContent></oldContent>
|
297
|
+
<newContent>test</newContent>
|
298
|
+
</block>
|
299
|
+
</change>
|
300
|
+
</fileChanges>'''
|
301
|
+
changes = parser.parse_response(missing_attr_xml)
|
302
|
+
assert len(changes) == 0
|
303
|
+
|
304
|
+
def test_invalid_tag_format(parser):
|
305
|
+
invalid_xml = '''<fileChanges><change path="test.py" operation="create">
|
306
|
+
<block description="Test"><oldContent></oldContent>
|
307
|
+
<newContent>test</newContent></block></change></fileChanges>'''
|
308
|
+
changes = parser.parse_response(invalid_xml)
|
309
|
+
assert len(changes) == 0
|
310
|
+
|
311
|
+
def test_valid_tag_format(parser):
|
312
|
+
valid_xml = '''<fileChanges>
|
313
|
+
<change path="test.py" operation="create">
|
314
|
+
<block description="Test">
|
315
|
+
<oldContent>
|
316
|
+
</oldContent>
|
317
|
+
<newContent>
|
318
|
+
test
|
319
|
+
</newContent>
|
320
|
+
</block>
|
321
|
+
</change>
|
322
|
+
</fileChanges>'''
|
323
|
+
changes = parser.parse_response(valid_xml)
|
324
|
+
assert len(changes) == 1
|
325
|
+
assert changes[0].path.name == "test.py"
|
326
|
+
assert len(changes[0].blocks) == 1
|
327
|
+
|
328
|
+
def test_parse_empty_content_tags_inline(parser):
|
329
|
+
"""Test parsing XML with empty content tags on single lines"""
|
330
|
+
test_xml = '''<fileChanges>
|
331
|
+
<change path="test.py" operation="create">
|
332
|
+
<block description="Test block">
|
333
|
+
<oldContent></oldContent>
|
334
|
+
<newContent></newContent>
|
335
|
+
</block>
|
336
|
+
</change>
|
337
|
+
</fileChanges>'''
|
338
|
+
|
339
|
+
changes = parser.parse_response(test_xml)
|
340
|
+
assert len(changes) == 1
|
341
|
+
assert changes[0].path.name == "test.py"
|
342
|
+
assert len(changes[0].blocks) == 1
|
343
|
+
|
344
|
+
def test_parse_mixed_content_tags(parser):
|
345
|
+
"""Test parsing XML with mix of inline and multiline content tags"""
|
346
|
+
test_xml = '''<fileChanges>
|
347
|
+
<change path="test.py" operation="create">
|
348
|
+
<block description="Test block">
|
349
|
+
<oldContent></oldContent>
|
350
|
+
<newContent>
|
351
|
+
def test():
|
352
|
+
pass
|
353
|
+
</newContent>
|
354
|
+
</block>
|
355
|
+
</change>
|
356
|
+
</fileChanges>'''
|
357
|
+
|
358
|
+
changes = parser.parse_response(test_xml)
|
359
|
+
assert len(changes) == 1
|
360
|
+
assert changes[0].path.name == "test.py"
|
361
|
+
assert len(changes[0].blocks) == 1
|
362
|
+
assert len(changes[0].blocks[0].new_content) == 2
|
363
|
+
|
364
|
+
def test_invalid_inline_content(parser):
|
365
|
+
"""Test parsing XML with invalid inline content"""
|
366
|
+
test_xml = '''<fileChanges>
|
367
|
+
<change path="test.py" operation="create">
|
368
|
+
<block description="Test">text here is invalid<oldContent></oldContent>
|
369
|
+
</block>
|
370
|
+
</change>
|
371
|
+
</fileChanges>'''
|
372
|
+
|
373
|
+
changes = parser.parse_response(test_xml)
|
374
|
+
assert len(changes) == 0
|
375
|
+
|
376
|
+
def test_validate_modify_inline_oldcontent(parser):
|
377
|
+
"""Test that inline oldContent is rejected for modify operations"""
|
378
|
+
test_xml = '''<fileChanges>
|
379
|
+
<change path="test.py" operation="modify">
|
380
|
+
<block description="Test block">
|
381
|
+
<oldContent></oldContent>
|
382
|
+
<newContent>
|
383
|
+
def test():
|
384
|
+
pass
|
385
|
+
</newContent>
|
386
|
+
</block>
|
387
|
+
</change>
|
388
|
+
</fileChanges>'''
|
389
|
+
|
390
|
+
changes = parser.parse_response(test_xml)
|
391
|
+
assert len(changes) == 1 # Should still parse as valid
|
392
|
+
assert changes[0].operation == "modify"
|
393
|
+
assert len(changes[0].blocks) == 1
|