Skip to content

Commit 2c84c09

Browse files
authored
Merge pull request #741 from PiotrCzapla/main
Make handling of `~` consistent, along with error handling.
2 parents 361c998 + 88cff63 commit 2c84c09

File tree

3 files changed

+109
-71
lines changed

3 files changed

+109
-71
lines changed

fastcore/_modidx.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,7 @@
596596
'fastcore.tools.sed': ('tools.html#sed', 'fastcore/tools.py'),
597597
'fastcore.tools.str_replace': ('tools.html#str_replace', 'fastcore/tools.py'),
598598
'fastcore.tools.strs_replace': ('tools.html#strs_replace', 'fastcore/tools.py'),
599+
'fastcore.tools.valid_path': ('tools.html#valid_path', 'fastcore/tools.py'),
599600
'fastcore.tools.view': ('tools.html#view', 'fastcore/tools.py')},
600601
'fastcore.transform': {},
601602
'fastcore.utils': {},

fastcore/tools.py

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
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', 'move_lines',
7-
'get_callable']
6+
__all__ = ['run_cmd', 'rg', 'sed', 'valid_path', 'view', 'create', 'insert', 'str_replace', 'strs_replace', 'replace_lines',
7+
'move_lines', 'get_callable']
88

99
# %% ../nbs/12_tools.ipynb
1010
from .imports import *
@@ -46,15 +46,20 @@ def sed(
4646
return run_cmd('sed', argstr, allow_re=allow_re, disallow_re=disallow_re)
4747

4848
# %% ../nbs/12_tools.ipynb
49+
def valid_path(path:str, must_exist:bool=True) -> Path:
50+
'Return expanded/resolved Path, raising FileNotFoundError if must_exist and missing'
51+
p = Path(path).expanduser().resolve()
52+
if must_exist and not p.exists(): raise FileNotFoundError(f'File not found: {p}')
53+
return p
54+
4955
def view(
5056
path:str, # Path to directory or file to view
5157
view_range:tuple[int,int]=None, # Optional 1-indexed (start, end) line range for files, end=-1 for EOF
5258
nums:bool=False # Whether to show line numbers
5359
):
5460
'View directory or file contents with optional line range and numbers'
5561
try:
56-
p = Path(path).expanduser().resolve()
57-
if not p.exists(): return f'Error: File not found: {p}'
62+
p = valid_path(path)
5863
header = None
5964
if p.is_dir():
6065
files = [str(f) for f in p.glob('**/*')
@@ -81,7 +86,7 @@ def create(
8186
) -> str:
8287
'Creates a new file with the given content at the specified path'
8388
try:
84-
p = Path(path)
89+
p = valid_path(path, must_exist=False)
8590
if p.exists():
8691
if not overwrite: return f'Error: File already exists: {p}'
8792
p.parent.mkdir(parents=True, exist_ok=True)
@@ -97,8 +102,7 @@ def insert(
97102
) -> str:
98103
'Insert new_str at specified line number'
99104
try:
100-
p = Path(path)
101-
if not p.exists(): return f'Error: File not found: {p}'
105+
p = valid_path(path)
102106
content = p.read_text().splitlines()
103107
if not (0 <= insert_line <= len(content)): return f'Error: Invalid line number {insert_line}'
104108
content.insert(insert_line, new_str)
@@ -115,8 +119,7 @@ def str_replace(
115119
) -> str:
116120
'Replace first occurrence of old_str with new_str in file'
117121
try:
118-
p = Path(path)
119-
if not p.exists(): return f'Error: File not found: {p}'
122+
p = valid_path(path)
120123
content = p.read_text()
121124
count = content.count(old_str)
122125
if count == 0: return 'Error: Text not found in file'
@@ -144,12 +147,14 @@ def replace_lines(
144147
new_content:str, # New content to replace the specified lines
145148
):
146149
"Replace lines in file using start and end line-numbers (index starting at 1)"
147-
if not (p := Path(path)).exists(): return f"Error: File not found: {p}"
148-
content = p.readlines()
149-
if not new_content.endswith('\n'): new_content+='\n'
150-
content[start_line-1:end_line] = [new_content]
151-
p.write_text(''.join(content))
152-
return f"Replaced lines {start_line} to {end_line}."
150+
try:
151+
p = valid_path(path)
152+
content = p.readlines()
153+
if not new_content.endswith('\n'): new_content+='\n'
154+
content[start_line-1:end_line] = [new_content]
155+
p.write_text(''.join(content))
156+
return f"Replaced lines {start_line} to {end_line}."
157+
except Exception as e: return f'Error replacing lines: {str(e)}'
153158

154159
# %% ../nbs/12_tools.ipynb
155160
def move_lines(
@@ -159,19 +164,21 @@ def move_lines(
159164
dest_line: int, # Destination line number (1-based, where lines will be inserted before)
160165
) -> str:
161166
"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}"
167+
try:
168+
p = valid_path(path)
169+
lines = p.read_text().splitlines()
170+
assert 1 <= start_line <= end_line <= len(lines), f"Invalid range {start_line}-{end_line}"
171+
assert 1 <= dest_line <= len(lines) + 1, f"Invalid destination {dest_line}"
172+
assert not(start_line <= dest_line <= end_line + 1), "Destination within source range"
173+
174+
chunk = lines[start_line-1:end_line]
175+
del lines[start_line-1:end_line]
176+
# Adjust dest if it was after the removed chunk
177+
if dest_line > end_line: dest_line -= len(chunk)
178+
lines[dest_line-1:dest_line-1] = chunk
179+
p.write_text('\n'.join(lines) + '\n')
180+
return f"Moved lines {start_line}-{end_line} to line {dest_line}"
181+
except Exception as e: return f'Error: {str(e)}'
175182

176183
# %% ../nbs/12_tools.ipynb
177184
def get_callable():

nbs/12_tools.ipynb

Lines changed: 73 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -95,14 +95,14 @@
9595
"name": "stdout",
9696
"output_type": "stream",
9797
"text": [
98-
"\u001b[34m__pycache__\u001b[m\u001b[m\n",
99-
"_parallel_win.ipynb\n",
100-
"_quarto.yml\n",
101-
"00_test.ipynb\n",
10298
"000_tour.ipynb\n",
99+
"00_test.ipynb\n",
103100
"01_basics.ipynb\n",
104101
"02_foundation.ipynb\n",
105-
"03_xtras\n"
102+
"03_xtras.ipynb\n",
103+
"03a_parallel.ipynb\n",
104+
"03b_net.ipynb\n",
105+
"04_docments.ipy\n"
106106
]
107107
}
108108
],
@@ -128,7 +128,7 @@
128128
"name": "stdout",
129129
"output_type": "stream",
130130
"text": [
131-
"ls: f*: No such file or directory\n",
131+
"ls: cannot access 'f*': No such file or directory\n",
132132
"\n"
133133
]
134134
}
@@ -375,15 +375,20 @@
375375
"outputs": [],
376376
"source": [
377377
"#| export\n",
378+
"def valid_path(path:str, must_exist:bool=True) -> Path:\n",
379+
" 'Return expanded/resolved Path, raising FileNotFoundError if must_exist and missing'\n",
380+
" p = Path(path).expanduser().resolve()\n",
381+
" if must_exist and not p.exists(): raise FileNotFoundError(f'File not found: {p}')\n",
382+
" return p\n",
383+
"\n",
378384
"def view(\n",
379385
" path:str, # Path to directory or file to view\n",
380386
" view_range:tuple[int,int]=None, # Optional 1-indexed (start, end) line range for files, end=-1 for EOF\n",
381387
" nums:bool=False # Whether to show line numbers\n",
382388
"):\n",
383389
" 'View directory or file contents with optional line range and numbers'\n",
384390
" try:\n",
385-
" p = Path(path).expanduser().resolve()\n",
386-
" if not p.exists(): return f'Error: File not found: {p}'\n",
391+
" p = valid_path(path)\n",
387392
" header = None\n",
388393
" if p.is_dir():\n",
389394
" files = [str(f) for f in p.glob('**/*') \n",
@@ -456,17 +461,17 @@
456461
"name": "stdout",
457462
"output_type": "stream",
458463
"text": [
459-
"Directory contents of /Users/jhoward/aai-ws/fastcore/nbs:\n",
460-
"/Users/jhoward/aai-ws/fastcore/nbs/llms.txt\n",
461-
"/Users/jhoward/aai-ws/fastcore/nbs/000_tour.ipynb\n",
462-
"/Users/jhoward/aai-ws/fastcore/nbs/parallel_test.py\n",
463-
"/Users/jhoward/aai-ws/fastcore/nbs/_quarto.yml\n",
464-
"/Users/jhoward/aai-ws/fastcore/nbs/08_style.ipynb\n"
464+
"Directory contents of /path:\n",
465+
"/path/000_tour.ipynb\n",
466+
"/path/00_test.ipynb\n",
467+
"/path/01_basics.ipynb\n",
468+
"/path/02_foundation.ipynb\n",
469+
"/path/03_xtras.ipynb\n"
465470
]
466471
}
467472
],
468473
"source": [
469-
"print(view('.', (1,5)))"
474+
"print(view('.', (1,5)).replace(os.getcwd(), '/path'))"
470475
]
471476
},
472477
{
@@ -484,7 +489,7 @@
484489
") -> str:\n",
485490
" 'Creates a new file with the given content at the specified path'\n",
486491
" try:\n",
487-
" p = Path(path)\n",
492+
" p = valid_path(path, must_exist=False)\n",
488493
" if p.exists():\n",
489494
" if not overwrite: return f'Error: File already exists: {p}'\n",
490495
" p.parent.mkdir(parents=True, exist_ok=True)\n",
@@ -503,14 +508,14 @@
503508
"name": "stdout",
504509
"output_type": "stream",
505510
"text": [
506-
"Created file test.txt.\n",
511+
"Created file /path/test.txt.\n",
507512
"Contents:\n",
508513
" 1 │ Hello, world!\n"
509514
]
510515
}
511516
],
512517
"source": [
513-
"print(create('test.txt', 'Hello, world!'))\n",
518+
"print(create('test.txt', 'Hello, world!').replace(os.getcwd(), '/path'))\n",
514519
"f = Path('test.txt')\n",
515520
"test_eq(f.exists(), True)\n",
516521
"print('Contents:\\n', view(f, nums=True))"
@@ -531,8 +536,7 @@
531536
") -> str:\n",
532537
" 'Insert new_str at specified line number'\n",
533538
" try:\n",
534-
" p = Path(path)\n",
535-
" if not p.exists(): return f'Error: File not found: {p}'\n",
539+
" p = valid_path(path)\n",
536540
" content = p.read_text().splitlines()\n",
537541
" if not (0 <= insert_line <= len(content)): return f'Error: Invalid line number {insert_line}'\n",
538542
" content.insert(insert_line, new_str)\n",
@@ -577,8 +581,7 @@
577581
") -> str:\n",
578582
" 'Replace first occurrence of old_str with new_str in file'\n",
579583
" try:\n",
580-
" p = Path(path)\n",
581-
" if not p.exists(): return f'Error: File not found: {p}'\n",
584+
" p = valid_path(path)\n",
582585
" content = p.read_text()\n",
583586
" count = content.count(old_str)\n",
584587
" if count == 0: return 'Error: Text not found in file'\n",
@@ -663,12 +666,14 @@
663666
" new_content:str, # New content to replace the specified lines\n",
664667
"):\n",
665668
" \"Replace lines in file using start and end line-numbers (index starting at 1)\"\n",
666-
" if not (p := Path(path)).exists(): return f\"Error: File not found: {p}\"\n",
667-
" content = p.readlines()\n",
668-
" if not new_content.endswith('\\n'): new_content+='\\n'\n",
669-
" content[start_line-1:end_line] = [new_content]\n",
670-
" p.write_text(''.join(content))\n",
671-
" return f\"Replaced lines {start_line} to {end_line}.\""
669+
" try:\n",
670+
" p = valid_path(path)\n",
671+
" content = p.readlines()\n",
672+
" if not new_content.endswith('\\n'): new_content+='\\n'\n",
673+
" content[start_line-1:end_line] = [new_content]\n",
674+
" p.write_text(''.join(content))\n",
675+
" return f\"Replaced lines {start_line} to {end_line}.\"\n",
676+
" except Exception as e: return f'Error replacing lines: {str(e)}'"
672677
]
673678
},
674679
{
@@ -691,6 +696,27 @@
691696
"print(view('test.txt', nums=True))"
692697
]
693698
},
699+
{
700+
"cell_type": "code",
701+
"execution_count": null,
702+
"id": "0a6b79f4",
703+
"metadata": {},
704+
"outputs": [
705+
{
706+
"data": {
707+
"text/plain": [
708+
"'Error replacing lines: File not found: /path/missing.txt'"
709+
]
710+
},
711+
"execution_count": null,
712+
"metadata": {},
713+
"output_type": "execute_result"
714+
}
715+
],
716+
"source": [
717+
"replace_lines('missing.txt', 1, 2, 'Replaced first two lines').replace(os.getcwd(), '/path')"
718+
]
719+
},
694720
{
695721
"cell_type": "code",
696722
"execution_count": null,
@@ -706,19 +732,21 @@
706732
" dest_line: int, # Destination line number (1-based, where lines will be inserted before)\n",
707733
") -> str:\n",
708734
" \"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}\""
735+
" try:\n",
736+
" p = valid_path(path)\n",
737+
" lines = p.read_text().splitlines()\n",
738+
" assert 1 <= start_line <= end_line <= len(lines), f\"Invalid range {start_line}-{end_line}\"\n",
739+
" assert 1 <= dest_line <= len(lines) + 1, f\"Invalid destination {dest_line}\"\n",
740+
" assert not(start_line <= dest_line <= end_line + 1), \"Destination within source range\"\n",
741+
" \n",
742+
" chunk = lines[start_line-1:end_line]\n",
743+
" del lines[start_line-1:end_line]\n",
744+
" # Adjust dest if it was after the removed chunk\n",
745+
" if dest_line > end_line: dest_line -= len(chunk)\n",
746+
" lines[dest_line-1:dest_line-1] = chunk\n",
747+
" p.write_text('\\n'.join(lines) + '\\n')\n",
748+
" return f\"Moved lines {start_line}-{end_line} to line {dest_line}\"\n",
749+
" except Exception as e: return f'Error: {str(e)}'"
722750
]
723751
},
724752
{
@@ -838,14 +866,16 @@
838866
"text": [
839867
"Error: Destination within source range\n",
840868
"Error: Invalid range 10-12\n",
841-
"Error: Invalid destination 99\n"
869+
"Error: Invalid destination 99\n",
870+
"Error: File not found: /path/mising.txt\n"
842871
]
843872
}
844873
],
845874
"source": [
846875
"print(move_lines('move_test.txt', 2, 3, 3)) # dest within source range\n",
847876
"print(move_lines('move_test.txt', 10, 12, 1)) # invalid range\n",
848-
"print(move_lines('move_test.txt', 1, 2, 99)) # invalid destination"
877+
"print(move_lines('move_test.txt', 1, 2, 99)) # invalid destination\n",
878+
"print(move_lines('mising.txt', 1, 2, 99).replace(os.getcwd(), '/path')) # missing file"
849879
]
850880
},
851881
{

0 commit comments

Comments
 (0)