Skip to content

Commit e0bf39a

Browse files
authored
Merge pull request #745 from PiotrCzapla/main
refactor tools: centralize error handling with explain_exc and ensure
2 parents 1ccb95c + 4ebf04f commit e0bf39a

File tree

3 files changed

+289
-86
lines changed

3 files changed

+289
-86
lines changed

fastcore/_modidx.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,8 @@
589589
'fastcore.test.test_warns': ('test.html#test_warns', 'fastcore/test.py')},
590590
'fastcore.tools': { 'fastcore.tools._fmt_path': ('tools.html#_fmt_path', 'fastcore/tools.py'),
591591
'fastcore.tools.create': ('tools.html#create', 'fastcore/tools.py'),
592+
'fastcore.tools.ensure': ('tools.html#ensure', 'fastcore/tools.py'),
593+
'fastcore.tools.explain_exc': ('tools.html#explain_exc', 'fastcore/tools.py'),
592594
'fastcore.tools.get_callable': ('tools.html#get_callable', 'fastcore/tools.py'),
593595
'fastcore.tools.insert': ('tools.html#insert', 'fastcore/tools.py'),
594596
'fastcore.tools.move_lines': ('tools.html#move_lines', 'fastcore/tools.py'),

fastcore/tools.py

Lines changed: 48 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,51 @@
22

33
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/12_tools.ipynb.
44

5-
# %% auto #0
6-
__all__ = ['run_cmd', 'rg', 'sed', 'valid_path', 'view', 'create', 'insert', 'str_replace', 'strs_replace', 'replace_lines',
7-
'move_lines', 'get_callable']
5+
# %% auto 0
6+
__all__ = ['explain_exc', 'ensure', 'valid_path', 'run_cmd', 'rg', 'sed', 'view', 'create', 'insert', 'str_replace',
7+
'strs_replace', 'replace_lines', 'move_lines', 'get_callable']
88

99
# %% ../nbs/12_tools.ipynb #578246d2
1010
from .imports import *
11+
from .xtras import truncstr
1112
from shlex import split
1213
from subprocess import run, DEVNULL
1314

14-
# %% ../nbs/12_tools.ipynb #1182336d
15+
# %% ../nbs/12_tools.ipynb
16+
def explain_exc(task=''):
17+
"""Convert an current exception to an LLM friendly error message."""
18+
try: raise sys.exc_info()[1]
19+
except (AssertionError, ZeroDivisionError, ValueError, FileNotFoundError) as e:
20+
return f"Error: {e}"
21+
except Exception as e: return f"Error {task}: {repr(e)}"
22+
23+
24+
# %% ../nbs/12_tools.ipynb
25+
def ensure(b: bool, msg:str=""):
26+
"Works like assert b, msg but raise ValueError and is not disabled when run with python -O"
27+
if not b: raise ValueError(msg)
28+
29+
30+
# %% ../nbs/12_tools.ipynb
31+
def valid_path(path:str, must_exist:bool=True) -> Path:
32+
'Return expanded/resolved Path, raising FileNotFoundError if must_exist and missing'
33+
p = Path(path).expanduser().resolve()
34+
if must_exist and not p.exists(): raise FileNotFoundError(f'File not found: {p}')
35+
return p
36+
37+
# %% ../nbs/12_tools.ipynb
1538
def run_cmd(
1639
cmd:str, # The command name to run
1740
argstr:str='', # All args to the command, will be split with shlex
1841
disallow_re:str=None, # optional regex which, if matched on argstr, will disallow the command
1942
allow_re:str=None # optional regex which, if not matched on argstr, will disallow the command
2043
):
2144
"Run `cmd` passing split `argstr`, optionally checking for allowed argstr"
22-
if disallow_re and re.search(disallow_re, argstr): return 'Error: args disallowed'
23-
if allow_re and re.search( allow_re, argstr): return 'Error: args not allowed'
24-
try: outp = run([cmd] + split(argstr), text=True, stdin=DEVNULL, capture_output=True)
25-
except Exception as e: return f'Error running cmd: {str(e)}'
45+
try:
46+
ensure(not (disallow_re and re.search(disallow_re, argstr)), 'args disallowed')
47+
ensure(not (allow_re and re.search( allow_re, argstr)), 'args not allowed')
48+
outp = run([cmd] + split(argstr), text=True, stdin=DEVNULL, capture_output=True)
49+
except: return explain_exc(f'running cmd')
2650
res = outp.stdout
2751
if res and outp.stderr: res += '\n'
2852
return res + outp.stderr
@@ -45,15 +69,8 @@ def sed(
4569
"Run the `sed` command with the args in `argstr` (e.g for reading a section of a file)"
4670
return run_cmd('sed', argstr, allow_re=allow_re, disallow_re=disallow_re)
4771

48-
# %% ../nbs/12_tools.ipynb #5dd1aaf3
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-
55-
# %% ../nbs/12_tools.ipynb #b718d65b
56-
def _fmt_path(f, p, skip_folders=()):
72+
# %% ../nbs/12_tools.ipynb
73+
def _fmt_path(f, p):
5774
'Format path with emoji for dirs/symlinks or size for files'
5875
parts = f.relative_to(p).parts
5976
if any(part.startswith('.') for part in parts): return None
@@ -80,13 +97,13 @@ def view(
8097
s, e = 1, len(lines)
8198
if view_range:
8299
s,e = view_range
83-
if not (1<=s<=len(lines)): return f'Error: Invalid start line {s}'
84-
if e!=-1 and not (s<=e<= len(lines)): return f'Error: Invalid end line {e}'
100+
ensure(1<=s<=len(lines), f'Invalid start line {s}')
101+
ensure(e==-1 or s<=e<=len(lines), f'Invalid end line {e}')
85102
lines = lines[s-1:None if e==-1 else e]
86103
if nums: lines = [f'{i+s:6d}{l}' for i, l in enumerate(lines)]
87104
content = '\n'.join(lines)
88105
return f'{header}\n{content}' if header else content
89-
except Exception as e: return f'Error viewing: {str(e)}'
106+
except: return explain_exc('viewing')
90107

91108
# %% ../nbs/12_tools.ipynb #36f58e38
92109
def create(
@@ -102,7 +119,7 @@ def create(
102119
p.parent.mkdir(parents=True, exist_ok=True)
103120
p.write_text(file_text)
104121
return f'Created file {p}.'
105-
except Exception as e: return f'Error creating file: {str(e)}'
122+
except: return explain_exc('creating file')
106123

107124
# %% ../nbs/12_tools.ipynb #434147ef
108125
def insert(
@@ -114,12 +131,12 @@ def insert(
114131
try:
115132
p = valid_path(path)
116133
content = p.read_text().splitlines()
117-
if not (0 <= insert_line <= len(content)): return f'Error: Invalid line number {insert_line}'
134+
ensure(0<=insert_line<=len(content), f'Invalid line number {insert_line}')
118135
content.insert(insert_line, new_str)
119136
new_content = '\n'.join(content)
120137
p.write_text(new_content)
121138
return f'Inserted text at line {insert_line} in {p}'
122-
except Exception as e: return f'Error inserting text: {str(e)}'
139+
except: return explain_exc('inserting text')
123140

124141
# %% ../nbs/12_tools.ipynb #9272ff7d
125142
def str_replace(
@@ -132,12 +149,12 @@ def str_replace(
132149
p = valid_path(path)
133150
content = p.read_text()
134151
count = content.count(old_str)
135-
if count == 0: return 'Error: Text not found in file'
136-
if count > 1: return f'Error: Multiple matches found ({count})'
152+
if count == 0: return f'Error: Text "{truncstr(old_str, 10)}" not found in file'
153+
if count > 1: return f'Error: Multiple matches found ({count}) of "{truncstr(old_str, 10)}"'
137154
new_content = content.replace(old_str, new_str, 1)
138155
p.write_text(new_content)
139156
return f'Replaced text in {p}'
140-
except Exception as e: return f'Error replacing text: {str(e)}'
157+
except: return explain_exc('replacing text')
141158

142159
# %% ../nbs/12_tools.ipynb #eb907119
143160
def strs_replace(
@@ -164,7 +181,7 @@ def replace_lines(
164181
content[start_line-1:end_line] = [new_content]
165182
p.write_text(''.join(content))
166183
return f"Replaced lines {start_line} to {end_line}."
167-
except Exception as e: return f'Error replacing lines: {str(e)}'
184+
except: return explain_exc('replacing lines')
168185

169186
# %% ../nbs/12_tools.ipynb #b136abf8
170187
def move_lines(
@@ -177,9 +194,9 @@ def move_lines(
177194
try:
178195
p = valid_path(path)
179196
lines = p.read_text().splitlines()
180-
assert 1 <= start_line <= end_line <= len(lines), f"Invalid range {start_line}-{end_line}"
181-
assert 1 <= dest_line <= len(lines) + 1, f"Invalid destination {dest_line}"
182-
assert not(start_line <= dest_line <= end_line + 1), "Destination within source range"
197+
ensure(1 <= start_line <= end_line <= len(lines), f"Invalid range {start_line}-{end_line}")
198+
ensure(1 <= dest_line <= len(lines) + 1, f"Invalid destination {dest_line}")
199+
ensure(not(start_line <= dest_line <= end_line + 1), "Destination within source range")
183200

184201
chunk = lines[start_line-1:end_line]
185202
del lines[start_line-1:end_line]
@@ -188,7 +205,7 @@ def move_lines(
188205
lines[dest_line-1:dest_line-1] = chunk
189206
p.write_text('\n'.join(lines) + '\n')
190207
return f"Moved lines {start_line}-{end_line} to line {dest_line}"
191-
except Exception as e: return f'Error: {str(e)}'
208+
except: return explain_exc()
192209

193210
# %% ../nbs/12_tools.ipynb #fc53b116
194211
def get_callable():

0 commit comments

Comments
 (0)