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
def insert_at(filepath, text: str, position: int) -> 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.

def insert_under( filepath, key: str, text: str, insertions: int = 0, skips: int = 0, regex: bool = False) -> None:
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.

def replace( filepath: str, key: str, text: str, replacements: int = 0, regex: bool = False) -> None:
 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
def replace_line( filepath: str, key: str, text: str, replacements: int = 0, skips: int = 0, additional: int = 0, regex: bool = False, raise_errors: bool = False) -> None:
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.

def replace_between( filepath: str, key1: str, key2: str, text: str, delete_keys: bool = False, from_end: bool = False, regex: bool = False) -> None:
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...
def delete_under( filepath, key: str, match: int = 1, skips: int = 0, regex: bool = False) -> None:
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.

def correct_with_dict(filepath: str, correct: dict) -> None:
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.

def from_template(old: str, new: str, correct: dict = None, comment: str = None) -> None:
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.