aton.txt.edit
Description
Functions to manipulate the content of text files.
Index
insert_at()
insert_under()
replace()
replace_line()
replace_between()
delete_under()
correct_with_dict()
from_template()
1""" 2# Description 3 4Functions to manipulate the content of text files. 5 6 7# Index 8 9`insert_at()` 10`insert_under()` 11`replace()` 12`replace_line()` 13`replace_between()` 14`delete_under()` 15`correct_with_dict()` 16`from_template()` 17 18--- 19""" 20 21 22import mmap 23from . import find 24import aton.file as file 25import shutil 26 27 28def insert_at( 29 filepath, 30 text:str, 31 position:int 32 ) -> None: 33 """Inserts a `text` in the line with `position` index of a given `filepath`. 34 35 If `position` is negative, starts from the end of the file. 36 """ 37 file_path = file.get(filepath) 38 with open(file_path, 'r+') as f: 39 lines = f.read().splitlines() 40 if position < 0: 41 position = len(lines) + position + 1 42 if position < 0 or position > len(lines): 43 raise IndexError("Position out of range") 44 lines.insert(position, text) 45 f.seek(0) 46 f.write('\n'.join(lines)) 47 f.truncate() 48 return None 49 50 51def insert_under( 52 filepath, 53 key:str, 54 text:str, 55 insertions:int=0, 56 skips:int=0, 57 regex:bool=False 58 ) -> None: 59 """Inserts a `text` under the line(s) containing the `key` in `filepath`. 60 61 The keyword can be at any position within the line. 62 Regular expressions can be used by setting `regex=True`. 63 64 By default all matches are inserted with `insertions=0`, 65 but it can insert only a specific number of matches 66 with positive numbers (1, 2...), or starting from the bottom with negative numbers. 67 68 The text can be introduced after a specific number of lines after the match, 69 changing the value `skips`. Negative integers introduce the text in the previous lines. 70 """ 71 file_path = file.get(filepath) 72 if regex: 73 positions = find.pos_regex(file_path, key, insertions) 74 else: 75 positions = find.pos(file_path, key, insertions) 76 positions.reverse() # Must start replacing from the end, otherwise the atual positions may change! 77 # Open the file in read-write mode 78 with open(file_path, 'r+b') as f: 79 with mmap.mmap(f.fileno(), length=0, access=mmap.ACCESS_WRITE) as mm: 80 # Get the places to insert the text 81 for position in positions: 82 start, end = find.line_pos(mm, position, skips) 83 inserted_text = '\n' + text # Ensure we end in a different line 84 if end == 0: # If on the first line 85 inserted_text = text + '\n' 86 remaining_lines = mm[end:] 87 new_line = inserted_text.encode() 88 updated_content = new_line + remaining_lines 89 mm.resize(len(mm) + len(new_line)) 90 mm[end:] = updated_content 91 return None 92 93 94def replace( 95 filepath:str, 96 key:str, 97 text:str, 98 replacements:int=0, 99 regex:bool=False 100 ) -> None: 101 """Replaces the `key` with `text` in `filepath`. 102 103 It can also be used to delete the keyword with `text=''`. 104 To search with regular expressions, set `regex=True`. 105 106 The value `replacements` specifies the number of replacements to perform: 107 1 to replace only the first keyword found, 2, 3... 108 Use negative values to replace from the end of the file, 109 eg. to replace the last found key, use `replacements=-1`. 110 To replace all values, set `replacements = 0`, which is the value by default. 111 112 ``` 113 line... key ...line -> line... text ...line 114 ``` 115 """ 116 file_path = file.get(filepath) 117 if regex: 118 positions = find.pos_regex(file_path, key, replacements) 119 else: 120 positions = find.pos(file_path, key, replacements) 121 positions.reverse() # Must start replacing from the end, otherwise the atual positions may change! 122 with open(file_path, 'r+') as f: 123 content = f.read() 124 for start, end in positions: 125 content = "".join([content[:start], text, content[end:]]) 126 f.seek(0) 127 f.write(content) 128 f.truncate() 129 return None 130 131 132def replace_line( 133 filepath:str, 134 key:str, 135 text:str, 136 replacements:int=0, 137 skips:int=0, 138 additional:int=0, 139 regex:bool=False, 140 raise_errors:bool=False, 141 ) -> None: 142 """Replaces the entire line(s) containing the `key` with the `text` in `filepath`. 143 144 It can be used to delete line(s) by setting `text=''`. 145 Regular expressions can be used with `regex=True`. 146 147 The value `replacements` specifies the number of lines to replace: 148 1 to replace only the first line with the keyword, 2, 3... 149 Use negative values to replace from the end of the file, 150 e.g., to replace only the last line containing the keyword, use `replacements = -1`. 151 To replace all lines, set `replacements = 0`, which is the value by default. 152 153 The default line to replace is the matching line, 154 but it can be any other specific line after or before the matching line; 155 this is indicated with `skips` as a positive or negative integer. 156 157 More lines can be replaced with `additional` lines (int). 158 Note that the matched line plus the additional lines 159 will be replaced, this is, additional lines +1. 160 161 If the `key` is not found, an optional error can be raised with `raise_errors=True`. 162 """ 163 file_path = file.get(filepath) 164 if regex: 165 positions = find.pos_regex(file_path, key, replacements) 166 else: 167 positions = find.pos(file_path, key, replacements) 168 positions.reverse() # Must start replacing from the end, otherwise the atual positions may change! 169 if positions == [] and raise_errors: 170 raise ValueError(f'The line could not be replaced because the following key was not found:\n{key}') 171 # Open the file in read-write mode 172 with open(file_path, 'r+b') as f: 173 with mmap.mmap(f.fileno(), length=0, access=mmap.ACCESS_WRITE) as mm: 174 for position in positions: 175 # Get the positions of the full line containing the match 176 line_start, line_end = find.line_pos(mm, position, skips) 177 # Additional lines 178 if additional > 0: 179 for _ in range(abs(additional)): 180 line_end = mm.find(b'\n', line_end + 1, len(mm)-1) 181 if line_end == -1: 182 line_end = len(mm) - 1 183 break 184 elif additional < 0: 185 for _ in range(abs(additional)): 186 line_start = mm.rfind(b'\n', 0, line_start - 1) + 1 187 if line_start == -1: 188 line_start = 0 189 break 190 # Replace the line 191 old_line = mm[line_start:line_end] 192 new_line = text.encode() 193 if text == '': # Delete the line, and the extra \n 194 remaining_content = mm[line_end:] 195 mm.resize(len(mm) - len(old_line) - 1) 196 mm[line_start-1:] = remaining_content 197 # Directly modify the memory-mapped region 198 elif len(new_line) == len(old_line): 199 mm[line_start:line_end] = new_line 200 else: # Adjust content for differing line sizes 201 remaining_content = mm[line_end:] 202 updated_content = new_line + remaining_content 203 mm.resize(len(mm) + len(new_line) - len(old_line)) 204 mm[line_start:] = updated_content 205 return None 206 207 208def replace_between( 209 filepath:str, 210 key1:str, 211 key2:str, 212 text:str, 213 delete_keys:bool=False, 214 from_end:bool=False, 215 regex:bool=False 216 ) -> None: 217 """Replace with `text` between keywords `key1` and `key2` in `filepath`. 218 219 It can be used to delete the text between the keys by setting `text=''`. 220 Regular expressions can be used by setting `regex=True`. 221 222 Key lines are also deleted if `delete_keys=True`. 223 224 Only the first matches of the keywords are used by default; 225 you can use the last ones with `from_end = True`. 226 ``` 227 lines... 228 key1 229 text 230 key2 231 lines... 232 ``` 233 """ 234 file_path = file.get(filepath) 235 index = 1 236 if from_end: 237 index = -1 238 start, end = find.between_pos(file_path, key1, key2, delete_keys, index, regex) 239 with open(file_path, 'r+b') as f: 240 with mmap.mmap(f.fileno(), length=0, access=mmap.ACCESS_WRITE) as mm: 241 # Replace the line 242 old_content = mm[start:end] 243 new_content = text.encode() 244 if text == '': # Delete the line, and the extra \n 245 remaining_content = mm[end:] 246 mm.resize(len(mm) - len(old_content) - 1) 247 mm[start-1:] = remaining_content 248 # Directly modify the memory-mapped region 249 elif len(new_content) == len(old_content): 250 mm[start:end] = new_content 251 else: # Adjust the content for differing line sizes 252 remaining_content = mm[end:] 253 updated_content = new_content + remaining_content 254 mm.resize(len(mm) + len(new_content) - len(old_content)) 255 mm[start:] = updated_content 256 return None 257 258 259def delete_under( 260 filepath, 261 key:str, 262 match:int=1, 263 skips:int=0, 264 regex:bool=False 265 ) -> None: 266 """Deletes all the content under the line containing the `key` in `filepath`. 267 268 The keyword can be at any position within the line. 269 Regular expressions can be used by setting `regex=True`. 270 271 By default the first `match` is used; it can be any positive integer (0 is treated as 1!), 272 including negative integers to select a match starting from the end of the file. 273 274 The content can be deleted after a specific number of lines after the match, 275 changing the value `skips`, that skips the specified number of lines. 276 Negative integers start deleting the content from the previous lines. 277 """ 278 file_path = file.get(filepath) 279 if match == 0: 280 match = 1 281 if regex: 282 positions = find.pos_regex(file_path, key, match) 283 else: 284 positions = find.pos(file_path, key, match) 285 if match > 0: # We only want one match, and should be the last if matches > 0 286 positions.reverse() 287 position = positions[0] 288 # Open the file in read-write mode 289 with open(file_path, 'r+b') as f: 290 with mmap.mmap(f.fileno(), length=0, access=mmap.ACCESS_WRITE) as mm: 291 # Get the places to insert the text 292 start, end = find.line_pos(mm, position, skips) 293 mm.resize(len(mm) - len(mm[end:])) 294 mm[end:] = b'' 295 return None 296 297 298def correct_with_dict( 299 filepath:str, 300 correct:dict 301 ) -> None: 302 """Corrects the given text file `filepath` using a `correct` dictionary.""" 303 file_path = file.get(filepath) 304 with open(file_path, 'r+') as f: 305 content = f.read() 306 for key, value in correct.items(): 307 content = content.replace(key, value) 308 f.seek(0) 309 f.write(content) 310 f.truncate() 311 return None 312 313 314def from_template( 315 old:str, 316 new:str, 317 correct:dict=None, 318 comment:str=None, 319 ) -> None: 320 """Creates `new` file from `old`, replacing values from a `correct` dict, inserting a `comment` on top.""" 321 shutil.copy(old, new) 322 if comment: 323 insert_at(new, comment, 0) 324 if correct: 325 correct_with_dict(new, correct) 326 return None
29def insert_at( 30 filepath, 31 text:str, 32 position:int 33 ) -> None: 34 """Inserts a `text` in the line with `position` index of a given `filepath`. 35 36 If `position` is negative, starts from the end of the file. 37 """ 38 file_path = file.get(filepath) 39 with open(file_path, 'r+') as f: 40 lines = f.read().splitlines() 41 if position < 0: 42 position = len(lines) + position + 1 43 if position < 0 or position > len(lines): 44 raise IndexError("Position out of range") 45 lines.insert(position, text) 46 f.seek(0) 47 f.write('\n'.join(lines)) 48 f.truncate() 49 return None
Inserts a text in the line with position index of a given filepath.
If position is negative, starts from the end of the file.
52def insert_under( 53 filepath, 54 key:str, 55 text:str, 56 insertions:int=0, 57 skips:int=0, 58 regex:bool=False 59 ) -> None: 60 """Inserts a `text` under the line(s) containing the `key` in `filepath`. 61 62 The keyword can be at any position within the line. 63 Regular expressions can be used by setting `regex=True`. 64 65 By default all matches are inserted with `insertions=0`, 66 but it can insert only a specific number of matches 67 with positive numbers (1, 2...), or starting from the bottom with negative numbers. 68 69 The text can be introduced after a specific number of lines after the match, 70 changing the value `skips`. Negative integers introduce the text in the previous lines. 71 """ 72 file_path = file.get(filepath) 73 if regex: 74 positions = find.pos_regex(file_path, key, insertions) 75 else: 76 positions = find.pos(file_path, key, insertions) 77 positions.reverse() # Must start replacing from the end, otherwise the atual positions may change! 78 # Open the file in read-write mode 79 with open(file_path, 'r+b') as f: 80 with mmap.mmap(f.fileno(), length=0, access=mmap.ACCESS_WRITE) as mm: 81 # Get the places to insert the text 82 for position in positions: 83 start, end = find.line_pos(mm, position, skips) 84 inserted_text = '\n' + text # Ensure we end in a different line 85 if end == 0: # If on the first line 86 inserted_text = text + '\n' 87 remaining_lines = mm[end:] 88 new_line = inserted_text.encode() 89 updated_content = new_line + remaining_lines 90 mm.resize(len(mm) + len(new_line)) 91 mm[end:] = updated_content 92 return None
Inserts a text under the line(s) containing the key in filepath.
The keyword can be at any position within the line.
Regular expressions can be used by setting regex=True.
By default all matches are inserted with insertions=0,
but it can insert only a specific number of matches
with positive numbers (1, 2...), or starting from the bottom with negative numbers.
The text can be introduced after a specific number of lines after the match,
changing the value skips. Negative integers introduce the text in the previous lines.
95def replace( 96 filepath:str, 97 key:str, 98 text:str, 99 replacements:int=0, 100 regex:bool=False 101 ) -> None: 102 """Replaces the `key` with `text` in `filepath`. 103 104 It can also be used to delete the keyword with `text=''`. 105 To search with regular expressions, set `regex=True`. 106 107 The value `replacements` specifies the number of replacements to perform: 108 1 to replace only the first keyword found, 2, 3... 109 Use negative values to replace from the end of the file, 110 eg. to replace the last found key, use `replacements=-1`. 111 To replace all values, set `replacements = 0`, which is the value by default. 112 113 ``` 114 line... key ...line -> line... text ...line 115 ``` 116 """ 117 file_path = file.get(filepath) 118 if regex: 119 positions = find.pos_regex(file_path, key, replacements) 120 else: 121 positions = find.pos(file_path, key, replacements) 122 positions.reverse() # Must start replacing from the end, otherwise the atual positions may change! 123 with open(file_path, 'r+') as f: 124 content = f.read() 125 for start, end in positions: 126 content = "".join([content[:start], text, content[end:]]) 127 f.seek(0) 128 f.write(content) 129 f.truncate() 130 return None
Replaces the key with text in filepath.
It can also be used to delete the keyword with text=''.
To search with regular expressions, set regex=True.
The value replacements specifies the number of replacements to perform:
1 to replace only the first keyword found, 2, 3...
Use negative values to replace from the end of the file,
eg. to replace the last found key, use replacements=-1.
To replace all values, set replacements = 0, which is the value by default.
line... key ...line -> line... text ...line
133def replace_line( 134 filepath:str, 135 key:str, 136 text:str, 137 replacements:int=0, 138 skips:int=0, 139 additional:int=0, 140 regex:bool=False, 141 raise_errors:bool=False, 142 ) -> None: 143 """Replaces the entire line(s) containing the `key` with the `text` in `filepath`. 144 145 It can be used to delete line(s) by setting `text=''`. 146 Regular expressions can be used with `regex=True`. 147 148 The value `replacements` specifies the number of lines to replace: 149 1 to replace only the first line with the keyword, 2, 3... 150 Use negative values to replace from the end of the file, 151 e.g., to replace only the last line containing the keyword, use `replacements = -1`. 152 To replace all lines, set `replacements = 0`, which is the value by default. 153 154 The default line to replace is the matching line, 155 but it can be any other specific line after or before the matching line; 156 this is indicated with `skips` as a positive or negative integer. 157 158 More lines can be replaced with `additional` lines (int). 159 Note that the matched line plus the additional lines 160 will be replaced, this is, additional lines +1. 161 162 If the `key` is not found, an optional error can be raised with `raise_errors=True`. 163 """ 164 file_path = file.get(filepath) 165 if regex: 166 positions = find.pos_regex(file_path, key, replacements) 167 else: 168 positions = find.pos(file_path, key, replacements) 169 positions.reverse() # Must start replacing from the end, otherwise the atual positions may change! 170 if positions == [] and raise_errors: 171 raise ValueError(f'The line could not be replaced because the following key was not found:\n{key}') 172 # Open the file in read-write mode 173 with open(file_path, 'r+b') as f: 174 with mmap.mmap(f.fileno(), length=0, access=mmap.ACCESS_WRITE) as mm: 175 for position in positions: 176 # Get the positions of the full line containing the match 177 line_start, line_end = find.line_pos(mm, position, skips) 178 # Additional lines 179 if additional > 0: 180 for _ in range(abs(additional)): 181 line_end = mm.find(b'\n', line_end + 1, len(mm)-1) 182 if line_end == -1: 183 line_end = len(mm) - 1 184 break 185 elif additional < 0: 186 for _ in range(abs(additional)): 187 line_start = mm.rfind(b'\n', 0, line_start - 1) + 1 188 if line_start == -1: 189 line_start = 0 190 break 191 # Replace the line 192 old_line = mm[line_start:line_end] 193 new_line = text.encode() 194 if text == '': # Delete the line, and the extra \n 195 remaining_content = mm[line_end:] 196 mm.resize(len(mm) - len(old_line) - 1) 197 mm[line_start-1:] = remaining_content 198 # Directly modify the memory-mapped region 199 elif len(new_line) == len(old_line): 200 mm[line_start:line_end] = new_line 201 else: # Adjust content for differing line sizes 202 remaining_content = mm[line_end:] 203 updated_content = new_line + remaining_content 204 mm.resize(len(mm) + len(new_line) - len(old_line)) 205 mm[line_start:] = updated_content 206 return None
Replaces the entire line(s) containing the key with the text in filepath.
It can be used to delete line(s) by setting text=''.
Regular expressions can be used with regex=True.
The value replacements specifies the number of lines to replace:
1 to replace only the first line with the keyword, 2, 3...
Use negative values to replace from the end of the file,
e.g., to replace only the last line containing the keyword, use replacements = -1.
To replace all lines, set replacements = 0, which is the value by default.
The default line to replace is the matching line,
but it can be any other specific line after or before the matching line;
this is indicated with skips as a positive or negative integer.
More lines can be replaced with additional lines (int).
Note that the matched line plus the additional lines
will be replaced, this is, additional lines +1.
If the key is not found, an optional error can be raised with raise_errors=True.
209def replace_between( 210 filepath:str, 211 key1:str, 212 key2:str, 213 text:str, 214 delete_keys:bool=False, 215 from_end:bool=False, 216 regex:bool=False 217 ) -> None: 218 """Replace with `text` between keywords `key1` and `key2` in `filepath`. 219 220 It can be used to delete the text between the keys by setting `text=''`. 221 Regular expressions can be used by setting `regex=True`. 222 223 Key lines are also deleted if `delete_keys=True`. 224 225 Only the first matches of the keywords are used by default; 226 you can use the last ones with `from_end = True`. 227 ``` 228 lines... 229 key1 230 text 231 key2 232 lines... 233 ``` 234 """ 235 file_path = file.get(filepath) 236 index = 1 237 if from_end: 238 index = -1 239 start, end = find.between_pos(file_path, key1, key2, delete_keys, index, regex) 240 with open(file_path, 'r+b') as f: 241 with mmap.mmap(f.fileno(), length=0, access=mmap.ACCESS_WRITE) as mm: 242 # Replace the line 243 old_content = mm[start:end] 244 new_content = text.encode() 245 if text == '': # Delete the line, and the extra \n 246 remaining_content = mm[end:] 247 mm.resize(len(mm) - len(old_content) - 1) 248 mm[start-1:] = remaining_content 249 # Directly modify the memory-mapped region 250 elif len(new_content) == len(old_content): 251 mm[start:end] = new_content 252 else: # Adjust the content for differing line sizes 253 remaining_content = mm[end:] 254 updated_content = new_content + remaining_content 255 mm.resize(len(mm) + len(new_content) - len(old_content)) 256 mm[start:] = updated_content 257 return None
Replace with text between keywords key1 and key2 in filepath.
It can be used to delete the text between the keys by setting text=''.
Regular expressions can be used by setting regex=True.
Key lines are also deleted if delete_keys=True.
Only the first matches of the keywords are used by default;
you can use the last ones with from_end = True.
lines...
key1
text
key2
lines...
260def delete_under( 261 filepath, 262 key:str, 263 match:int=1, 264 skips:int=0, 265 regex:bool=False 266 ) -> None: 267 """Deletes all the content under the line containing the `key` in `filepath`. 268 269 The keyword can be at any position within the line. 270 Regular expressions can be used by setting `regex=True`. 271 272 By default the first `match` is used; it can be any positive integer (0 is treated as 1!), 273 including negative integers to select a match starting from the end of the file. 274 275 The content can be deleted after a specific number of lines after the match, 276 changing the value `skips`, that skips the specified number of lines. 277 Negative integers start deleting the content from the previous lines. 278 """ 279 file_path = file.get(filepath) 280 if match == 0: 281 match = 1 282 if regex: 283 positions = find.pos_regex(file_path, key, match) 284 else: 285 positions = find.pos(file_path, key, match) 286 if match > 0: # We only want one match, and should be the last if matches > 0 287 positions.reverse() 288 position = positions[0] 289 # Open the file in read-write mode 290 with open(file_path, 'r+b') as f: 291 with mmap.mmap(f.fileno(), length=0, access=mmap.ACCESS_WRITE) as mm: 292 # Get the places to insert the text 293 start, end = find.line_pos(mm, position, skips) 294 mm.resize(len(mm) - len(mm[end:])) 295 mm[end:] = b'' 296 return None
Deletes all the content under the line containing the key in filepath.
The keyword can be at any position within the line.
Regular expressions can be used by setting regex=True.
By default the first match is used; it can be any positive integer (0 is treated as 1!),
including negative integers to select a match starting from the end of the file.
The content can be deleted after a specific number of lines after the match,
changing the value skips, that skips the specified number of lines.
Negative integers start deleting the content from the previous lines.
299def correct_with_dict( 300 filepath:str, 301 correct:dict 302 ) -> None: 303 """Corrects the given text file `filepath` using a `correct` dictionary.""" 304 file_path = file.get(filepath) 305 with open(file_path, 'r+') as f: 306 content = f.read() 307 for key, value in correct.items(): 308 content = content.replace(key, value) 309 f.seek(0) 310 f.write(content) 311 f.truncate() 312 return None
Corrects the given text file filepath using a correct dictionary.
315def from_template( 316 old:str, 317 new:str, 318 correct:dict=None, 319 comment:str=None, 320 ) -> None: 321 """Creates `new` file from `old`, replacing values from a `correct` dict, inserting a `comment` on top.""" 322 shutil.copy(old, new) 323 if comment: 324 insert_at(new, comment, 0) 325 if correct: 326 correct_with_dict(new, correct) 327 return None
Creates new file from old, replacing values from a correct dict, inserting a comment on top.