Text and file editing primitives shared by the fastai editing tools
The text editors take a string and return the edited text. Invalid edits raise ValueError. File editors read and write a path and return a diff. Aidialog provides corresponding message editors.
Read fastcore.editskill for the editing toolkit’s naming, parameter and workflow conventions. It exposes these tools alongside the notebook editors from fastcore.nbio.
def insert_line( text:str, insert_line:int, # The 1-based line number after which to insert the text (0: before 1st line, 1: after 1st line, 2: after 2nd, etc.) new_str:str, # The text to insert):
def str_replace( text:str, old_str:str, # Text to find and replace new_str:str, # Text to replace with start_line:int=None, # Optional 1-based start line to limit search end_line:int=None, # Optional 1-based end line to limit search n_matches:int=None, # Max replacements (None=all) re_filter:str=None, # If provided, only process lines matching this regex (like g// in ex) invert_filter:bool=False, # Invert the filter (like g!// in ex) use_regex:bool=False, # Treat old_str as a regex, and new_str as an `re.sub` template?):
Replace occurrence(s) of old_str with new_str
Pass use_regex=True to use regex patterns and re.sub replacement templates. Word boundaries avoid renaming parts of longer names:
res = str_replace('use x; only_use x; used x', r'\buse\b', 'usage', use_regex=True)test_eq(res, 'usage x; only_use x; used x')res = str_replace('a=1\nb=2', r'(\w+)=(\d+)', r'\2=\1', use_regex=True)test_eq(res, '1=a\n2=b')with expect_fail(Exception, 'Failed to find'): str_replace('abc', r'\bxyz\b', 'q', use_regex=True)
The text, file, notebook-cell and message editors share their replacement options. doc() module overviews list these once under replace_params, rather than repeating them for every function.
Use re_filter to restrict replacement to matching lines. invert_filter=True selects the other lines:
start_line and end_line select an inclusive range. Their defaults are the first and last line. A negative end_line counts from the end, with -1 selecting the last line:
def strs_replace( text:str, old_strs:list[str], # List of strings to find and replace new_strs:list[str], # List of replacement strings (must match length of old_strs) start_line:int=None, # Optional 1-based start line to limit search end_line:int=None, # Optional 1-based end line to limit search n_matches:int=None, # Max replacements per string (None=all) re_filter:str=None, # If provided, only process lines matching this regex (like g// in ex) invert_filter:bool=False, # Invert the filter (like g!// in ex) use_regex:bool=False, # Treat old_strs as regexes, and new_strs as `re.sub` templates?):
Replace multiple strings simultaneously
res = strs_replace('f(a); g(a)', [r'\bf\b', r'\bg\b'], ['ff','gg'], use_regex=True)test_eq(res, 'ff(a); gg(a)')
def replace_lines( text:str, start_line:int=None, # Starting line number to replace (1-based); None means line 1 end_line:int=None, # Ending line number to replace (1-based, inclusive, negative counts from end); None means the last line new_content:str='', # New content to replace the specified lines):
Replace line range with new content; the defaults replace the entire contents
Omit line numbers to replace the entire text, including empty text. Pass only start_line to replace from that line to the end:
def del_lines( text:str, start_line:int, # Starting line number to delete (1-based); required end_line:int, # Ending line number to delete (1-based, inclusive, negative counts from end); required re_filter:str=None, # If provided, only delete lines matching this regex (like g// in ex) invert_filter:bool=False, # Invert the filter (like g!// in ex)):
Delete line range; deletion is destructive, so both line numbers must be given explicitly (1, -1 for all lines)
del_lines requires both line numbers and rejects None. Use 1, -1 for all lines or 5, 5 for line 5:
line_hash, lnhash and lnhash_at create exhash addresses without depending on the exhash package. Addresses use lineno|hash|. The hash is the low 12 bits of CRC32, encoded as two Base64url characters (A–Z, a–z, 0–9, -, _), high six bits first.
def line_hash( line:str, # Text to hash; when hashing a single line, omit its trailing newline)->str:
2-char Base64url hash of line
test_eq(line_hash('def foo(): pass'), 'Tw') # same value the exhash Rust impl producess ='a\ndef foo(): pass\nb'test_eq(lnhash_at(s, 2), '2|Tw|')test_eq(lnhash_at(s.splitlines(), 2), '2|Tw|')test_eq(lnhash_at(s, 2), lnhash(2, 'def foo(): pass'))lnhash_at(s, 2)
'2|Tw|'
File tools
File editors take the path first and return a unified diff. An unchanged result returns none: No changes. Invalid edits return error: ....
view_file('~/a/b.py', 3)
create_file('~/a/b/c.py', 'content here')
file_str_replace('myfile.py', 'old_name', 'new_name')
file_del_lines('myfile.py', 2, 4)
file_replace_lines('myfile.py', new_content=src) # no line numbers: replace the entire contents
file_str_replace, file_strs_replace and file_del_lines support line ranges, re_filter and invert_filter. These work like ex’s g// and g!// filters.
For Python syntax-tree replacements, use ast_replace or file_ast_replace. They accept ast-grep (pattern, replacement) rules with $VAR metavariables and require the optional remold package.
def view_file( path:str, # Path to view (expands `~` if needed) start_line:int=1, # Starting line to view end_line:int=None, # End line (defaults to last line if None; may be past EOF, which clamps to the last line - handy when the file size is unknown) nums:bool=True, # Show line numbers? lnhashs:bool=False, # Show exhash `lineno|hash|` addresses instead of line numbers?):
Read file contents, optionally limited to 1-based line range
def create_file( path:str, # Path to create (expands `~` if needed) contents:str, # Contents of file to create overwrite:bool=False, # Replace the file if it already exists?):
Create a new file with contents. Error if file exists, unless overwrite.
Wrap text editor f as a file editing function: path addressing, diff-or-error return
res = file_insert_line(test_path, 0, 'first')test_eq(test_txt().splitlines()[0], 'first')assertrepr(res) ==str(res)res
@@ -1 +1,2 @@
+first
alpha
res = file_str_replace(test_path, 'beta', 'BETA')assert'BETA'in test_txt(), f"Expected 'BETA' in file"res
@@ -2,3 +2,3 @@
alpha
-beta
+BETA
gamma
with expect_fail(ValueError, test_path): str_replace(test_path, 'beta', 'x')
res = file_strs_replace(test_path, ['gamma', 'delta'], ['GAMMA', 'DELTA'])test_eq(test_txt().splitlines()[-2:], ['GAMMA', 'DELTA'])res
@@ -3,3 +3,3 @@
BETA
-gamma
-delta
+GAMMA
+DELTA
Use file_replace_lines(path, new_content=src) to replace a whole file. Notebook cells have the equivalent cell_replace_lines(fname, id, new_content=src).
res = file_replace_lines(test_path, 2, 3, 'two\nthree\n')test_eq(test_txt().splitlines()[1:3], ['two', 'three'])res
@@ -1,4 +1,4 @@
first
-alpha
-BETA
+two
+three
GAMMA
def ast_replace( text:str, repls:list, # (pattern, replacement) ast-grep rules; replacement is a `$VAR` template or a callable(match)->str):
Apply ast-grep structural pattern replacements to python source
ast_replace and file_ast_replace use ast-grep patterns to match Python syntax. $VAR captures an expression. An unknown $VAR in the replacement raises an error.
Formatting and comments outside the matched span stay unchanged. Rules apply in order, each to the previous rule’s output.