Skip to content

Commit cf198a7

Browse files
committed
fixes #737
1 parent e84bade commit cf198a7

3 files changed

Lines changed: 251 additions & 13 deletions

File tree

fastcore/_modidx.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,7 +584,9 @@
584584
'fastcore.test.test_stdout': ('test.html#test_stdout', 'fastcore/test.py'),
585585
'fastcore.test.test_warns': ('test.html#test_warns', 'fastcore/test.py')},
586586
'fastcore.tools': { 'fastcore.tools.create': ('tools.html#create', 'fastcore/tools.py'),
587+
'fastcore.tools.get_callable': ('tools.html#get_callable', 'fastcore/tools.py'),
587588
'fastcore.tools.insert': ('tools.html#insert', 'fastcore/tools.py'),
589+
'fastcore.tools.move_lines': ('tools.html#move_lines', 'fastcore/tools.py'),
588590
'fastcore.tools.replace_lines': ('tools.html#replace_lines', 'fastcore/tools.py'),
589591
'fastcore.tools.rg': ('tools.html#rg', 'fastcore/tools.py'),
590592
'fastcore.tools.run_cmd': ('tools.html#run_cmd', 'fastcore/tools.py'),

fastcore/tools.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/12_tools.ipynb.
44

55
# %% auto 0
6-
__all__ = ['run_cmd', 'rg', 'sed', 'view', 'create', 'insert', 'str_replace', 'strs_replace', 'replace_lines']
6+
__all__ = ['run_cmd', 'rg', 'sed', 'view', 'create', 'insert', 'str_replace', 'strs_replace', 'replace_lines', 'move_lines',
7+
'get_callable']
78

89
# %% ../nbs/12_tools.ipynb
9-
from .utils import *
10+
from .imports import *
1011
from shlex import split
1112
from subprocess import run, DEVNULL
1213

@@ -149,3 +150,35 @@ def replace_lines(
149150
content[start_line-1:end_line] = [new_content]
150151
p.write_text(''.join(content))
151152
return f"Replaced lines {start_line} to {end_line}."
153+
154+
# %% ../nbs/12_tools.ipynb
155+
def move_lines(
156+
path: str, # Path to the file to modify
157+
start_line: int, # Starting line number to move (1-based)
158+
end_line: int, # Ending line number to move (1-based, inclusive)
159+
dest_line: int, # Destination line number (1-based, where lines will be inserted before)
160+
) -> str:
161+
"Move lines from start_line:end_line to before dest_line"
162+
if not (p := Path(path)).exists(): return f"Error: File not found: {p}"
163+
lines = p.read_text().splitlines()
164+
if not (1 <= start_line <= end_line <= len(lines)): return f"Error: Invalid range {start_line}-{end_line}"
165+
if not (1 <= dest_line <= len(lines) + 1): return f"Error: Invalid destination {dest_line}"
166+
if start_line <= dest_line <= end_line + 1: return "Error: Destination within source range"
167+
168+
chunk = lines[start_line-1:end_line]
169+
del lines[start_line-1:end_line]
170+
# Adjust dest if it was after the removed chunk
171+
if dest_line > end_line: dest_line -= len(chunk)
172+
lines[dest_line-1:dest_line-1] = chunk
173+
p.write_text('\n'.join(lines) + '\n')
174+
return f"Moved lines {start_line}-{end_line} to line {dest_line}"
175+
176+
# %% ../nbs/12_tools.ipynb
177+
def get_callable():
178+
"Return callable objects defined in caller's module"
179+
import inspect
180+
g = inspect.currentframe().f_back.f_globals
181+
return {
182+
f:o for f,o in g.items()
183+
if callable(o) and hasattr(o, '__module__') and o.__module__ == '__main__' and not f.startswith('_')
184+
}

nbs/12_tools.ipynb

Lines changed: 214 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"outputs": [],
2828
"source": [
2929
"#| export\n",
30-
"from fastcore.utils import *\n",
30+
"from fastcore.imports import *\n",
3131
"from shlex import split\n",
3232
"from subprocess import run, DEVNULL"
3333
]
@@ -95,14 +95,14 @@
9595
"name": "stdout",
9696
"output_type": "stream",
9797
"text": [
98-
"000_tour.ipynb\n",
98+
"\u001b[34m__pycache__\u001b[m\u001b[m\n",
99+
"_parallel_win.ipynb\n",
100+
"_quarto.yml\n",
99101
"00_test.ipynb\n",
102+
"000_tour.ipynb\n",
100103
"01_basics.ipynb\n",
101104
"02_foundation.ipynb\n",
102-
"03_xtras.ipynb\n",
103-
"03a_parallel.ipynb\n",
104-
"03b_net.ipynb\n",
105-
"04_docments.ipy\n"
105+
"03_xtras\n"
106106
]
107107
}
108108
],
@@ -364,7 +364,7 @@
364364
"id": "d3cd7681",
365365
"metadata": {},
366366
"source": [
367-
"Python implementations of the text editor tools from [Anthropic](https://docs.claude.com/en/docs/agents-and-tools/tool-use/text-editor-tool). These tools are especially useful in an AI's tool loop. See [`claudette`](https://claudette.answer.ai/text_editor.html) for examples."
367+
"Python implementations of the text editor tools from [Anthropic](https://docs.claude.com/en/docs/agents-and-tools/tool-use/text-editor-tool), plus more. These tools are especially useful in an AI's tool loop. See [`claudette`](https://claudette.answer.ai/text_editor.html) for examples."
368368
]
369369
},
370370
{
@@ -691,6 +691,173 @@
691691
"print(view('test.txt', nums=True))"
692692
]
693693
},
694+
{
695+
"cell_type": "code",
696+
"execution_count": null,
697+
"id": "b136abf8",
698+
"metadata": {},
699+
"outputs": [],
700+
"source": [
701+
"#| export\n",
702+
"def move_lines(\n",
703+
" path: str, # Path to the file to modify\n",
704+
" start_line: int, # Starting line number to move (1-based)\n",
705+
" end_line: int, # Ending line number to move (1-based, inclusive)\n",
706+
" dest_line: int, # Destination line number (1-based, where lines will be inserted before)\n",
707+
") -> str:\n",
708+
" \"Move lines from start_line:end_line to before dest_line\"\n",
709+
" if not (p := Path(path)).exists(): return f\"Error: File not found: {p}\"\n",
710+
" lines = p.read_text().splitlines()\n",
711+
" if not (1 <= start_line <= end_line <= len(lines)): return f\"Error: Invalid range {start_line}-{end_line}\"\n",
712+
" if not (1 <= dest_line <= len(lines) + 1): return f\"Error: Invalid destination {dest_line}\"\n",
713+
" if start_line <= dest_line <= end_line + 1: return \"Error: Destination within source range\"\n",
714+
" \n",
715+
" chunk = lines[start_line-1:end_line]\n",
716+
" del lines[start_line-1:end_line]\n",
717+
" # Adjust dest if it was after the removed chunk\n",
718+
" if dest_line > end_line: dest_line -= len(chunk)\n",
719+
" lines[dest_line-1:dest_line-1] = chunk\n",
720+
" p.write_text('\\n'.join(lines) + '\\n')\n",
721+
" return f\"Moved lines {start_line}-{end_line} to line {dest_line}\""
722+
]
723+
},
724+
{
725+
"cell_type": "markdown",
726+
"id": "770d2dde",
727+
"metadata": {},
728+
"source": [
729+
"The `move_lines` function relocates a range of lines within a file to a new position. It handles the tricky index adjustment when the destination is after the removed chunk.\n",
730+
"\n",
731+
"Let's test it by creating a simple 5-line file:"
732+
]
733+
},
734+
{
735+
"cell_type": "code",
736+
"execution_count": null,
737+
"id": "feca8699",
738+
"metadata": {},
739+
"outputs": [
740+
{
741+
"name": "stdout",
742+
"output_type": "stream",
743+
"text": [
744+
" 1 │ Line 1\n",
745+
" 2 │ Line 2\n",
746+
" 3 │ Line 3\n",
747+
" 4 │ Line 4\n",
748+
" 5 │ Line 5\n"
749+
]
750+
}
751+
],
752+
"source": [
753+
"create('move_test.txt', 'Line 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5', overwrite=True)\n",
754+
"print(view('move_test.txt', nums=True))"
755+
]
756+
},
757+
{
758+
"cell_type": "markdown",
759+
"id": "dd9b01ef",
760+
"metadata": {},
761+
"source": [
762+
"Move lines 4-5 up to before line 2:"
763+
]
764+
},
765+
{
766+
"cell_type": "code",
767+
"execution_count": null,
768+
"id": "a1cf1f48",
769+
"metadata": {},
770+
"outputs": [
771+
{
772+
"name": "stdout",
773+
"output_type": "stream",
774+
"text": [
775+
"Moved lines 4-5 to line 2\n",
776+
" 1 │ Line 1\n",
777+
" 2 │ Line 4\n",
778+
" 3 │ Line 5\n",
779+
" 4 │ Line 2\n",
780+
" 5 │ Line 3\n"
781+
]
782+
}
783+
],
784+
"source": [
785+
"print(move_lines('move_test.txt', 4, 5, 2))\n",
786+
"print(view('move_test.txt', nums=True))"
787+
]
788+
},
789+
{
790+
"cell_type": "markdown",
791+
"id": "2bcd9008",
792+
"metadata": {},
793+
"source": [
794+
"Move lines down — moving lines 1-2 to the end (line 6) correctly adjusts the destination index after removal:"
795+
]
796+
},
797+
{
798+
"cell_type": "code",
799+
"execution_count": null,
800+
"id": "ef06f4f2",
801+
"metadata": {},
802+
"outputs": [
803+
{
804+
"name": "stdout",
805+
"output_type": "stream",
806+
"text": [
807+
"Moved lines 1-2 to line 4\n",
808+
" 1 │ Line 5\n",
809+
" 2 │ Line 2\n",
810+
" 3 │ Line 3\n",
811+
" 4 │ Line 1\n",
812+
" 5 │ Line 4\n"
813+
]
814+
}
815+
],
816+
"source": [
817+
"print(move_lines('move_test.txt', 1, 2, 6))\n",
818+
"print(view('move_test.txt', nums=True))"
819+
]
820+
},
821+
{
822+
"cell_type": "markdown",
823+
"id": "6e1d8181",
824+
"metadata": {},
825+
"source": [
826+
"Error handling — destination within source range, invalid line ranges, and invalid destinations are all caught:"
827+
]
828+
},
829+
{
830+
"cell_type": "code",
831+
"execution_count": null,
832+
"id": "918bb808",
833+
"metadata": {},
834+
"outputs": [
835+
{
836+
"name": "stdout",
837+
"output_type": "stream",
838+
"text": [
839+
"Error: Destination within source range\n",
840+
"Error: Invalid range 10-12\n",
841+
"Error: Invalid destination 99\n"
842+
]
843+
}
844+
],
845+
"source": [
846+
"print(move_lines('move_test.txt', 2, 3, 3)) # dest within source range\n",
847+
"print(move_lines('move_test.txt', 10, 12, 1)) # invalid range\n",
848+
"print(move_lines('move_test.txt', 1, 2, 99)) # invalid destination"
849+
]
850+
},
851+
{
852+
"cell_type": "code",
853+
"execution_count": null,
854+
"id": "6e600769",
855+
"metadata": {},
856+
"outputs": [],
857+
"source": [
858+
"Path('move_test.txt').unlink()"
859+
]
860+
},
694861
{
695862
"cell_type": "code",
696863
"execution_count": null,
@@ -701,19 +868,55 @@
701868
"f.unlink()"
702869
]
703870
},
871+
{
872+
"cell_type": "code",
873+
"execution_count": null,
874+
"id": "fc53b116",
875+
"metadata": {},
876+
"outputs": [],
877+
"source": [
878+
"#| export\n",
879+
"def get_callable():\n",
880+
" \"Return callable objects defined in caller's module\"\n",
881+
" import inspect\n",
882+
" g = inspect.currentframe().f_back.f_globals\n",
883+
" return {\n",
884+
" f:o for f,o in g.items()\n",
885+
" if callable(o) and hasattr(o, '__module__') and o.__module__ == '__main__' and not f.startswith('_')\n",
886+
" }"
887+
]
888+
},
889+
{
890+
"cell_type": "code",
891+
"execution_count": null,
892+
"id": "6ef89850",
893+
"metadata": {},
894+
"outputs": [
895+
{
896+
"data": {
897+
"text/plain": [
898+
"'run_cmd; rg; sed; view; create; insert; str_replace; strs_replace; replace_lines; move_lines; get_callable'"
899+
]
900+
},
901+
"execution_count": null,
902+
"metadata": {},
903+
"output_type": "execute_result"
904+
}
905+
],
906+
"source": [
907+
"'; '.join(get_callable())"
908+
]
909+
},
704910
{
705911
"cell_type": "code",
706912
"execution_count": null,
707913
"id": "4092b227",
708914
"metadata": {},
709915
"outputs": [],
710916
"source": [
711-
"#| hide\n",
712917
"# Verify that all public functions defined in this module have valid schemas\n",
713918
"# (i.e., they have proper type annotations and docstrings required by get_schema)\n",
714-
"for f,_o in list(globals().items()):\n",
715-
" if callable(_o) and hasattr(_o, '__module__') and _o.__module__ == '__main__' and not f.startswith(('_', 'receive_')):\n",
716-
" test_eq(f, get_schema(globals()[f])['name'])"
919+
"for f,_o in get_callable().items(): test_eq(f, get_schema(globals()[f])['name'])"
717920
]
718921
},
719922
{

0 commit comments

Comments
 (0)