edq.util.dirent

Operations relating to directory entries (dirents).

These operations are designed for clarity and compatibility, not performance.

Only directories, files, and links will be handled. Other types of dirents may result in an error being raised.

In general, all recursive operations do not follow symlinks by default and instead treat the link as a file.

  1"""
  2Operations relating to directory entries (dirents).
  3
  4These operations are designed for clarity and compatibility, not performance.
  5
  6Only directories, files, and links will be handled.
  7Other types of dirents may result in an error being raised.
  8
  9In general, all recursive operations do not follow symlinks by default and instead treat the link as a file.
 10"""
 11
 12import atexit
 13import os
 14import shutil
 15import tempfile
 16import typing
 17import uuid
 18
 19import edq.util.constants
 20import edq.util.hash
 21
 22DEFAULT_ENCODING: str = edq.util.constants.DEFAULT_ENCODING
 23""" The default encoding that will be used when reading and writing. """
 24
 25DEPTH_LIMIT: int = 10000
 26
 27def exists(path: str) -> bool:
 28    """
 29    Check if a path exists.
 30    This will transparently call os.path.lexists(),
 31    which will include broken links.
 32    """
 33
 34    return os.path.lexists(path)
 35
 36def get_temp_path(prefix: str = '', suffix: str = '', rm: bool = True) -> str:
 37    """
 38    Get a path to a valid (but not currently existing) temp dirent.
 39    If rm is True, then the dirent will be attempted to be deleted on exit
 40    (no error will occur if the path is not there).
 41    """
 42
 43    path = None
 44    while ((path is None) or exists(path)):
 45        path = os.path.join(tempfile.gettempdir(), prefix + str(uuid.uuid4()) + suffix)
 46
 47    path = os.path.realpath(path)
 48
 49    if (rm):
 50        atexit.register(remove, path)
 51
 52    return path
 53
 54def get_temp_dir(prefix: str = '', suffix: str = '', rm: bool = True) -> str:
 55    """
 56    Get a temp directory.
 57    The directory will exist when returned.
 58    """
 59
 60    path = get_temp_path(prefix = prefix, suffix = suffix, rm = rm)
 61    mkdir(path)
 62    return path
 63
 64def mkdir(raw_path: str) -> None:
 65    """
 66    Make a directory (including any required parent directories).
 67    Does not complain if the directory (or parents) already exist
 68    (this includes if the directory or parents are links to directories).
 69    """
 70
 71    path = os.path.abspath(raw_path)
 72
 73    if (exists(path)):
 74        if (os.path.isdir(path)):
 75            return
 76
 77        raise ValueError(f"Target of mkdir already exists, and is not a dir: '{raw_path}'.")
 78
 79    _check_parent_dirs(raw_path)
 80
 81    os.makedirs(path, exist_ok = True)
 82
 83def _check_parent_dirs(raw_path: str) -> None:
 84    """
 85    Check all parents to ensure that they are all dirs (or don't exist).
 86    This is naturally handled by os.makedirs(),
 87    but the error messages are not consistent between POSIX and Windows.
 88    """
 89
 90    path = os.path.abspath(raw_path)
 91
 92    parent_path = path
 93    for _ in range(DEPTH_LIMIT):
 94        new_parent_path = os.path.dirname(parent_path)
 95        if (parent_path == new_parent_path):
 96            # We have reached root (are our own parent).
 97            return
 98
 99        parent_path = new_parent_path
100
101        if (os.path.exists(parent_path) and (not os.path.isdir(parent_path))):
102            raise ValueError(f"Target of mkdir contains parent ('{os.path.basename(parent_path)}') that exists and is not a dir: '{raw_path}'.")
103
104    raise ValueError("Depth limit reached.")
105
106def remove(path: str) -> None:
107    """
108    Remove the given path.
109    The path can be of any type (dir, file, link),
110    and does not need to exist.
111    """
112
113    if (not exists(path)):
114        return
115
116    if (os.path.isfile(path) or os.path.islink(path)):
117        os.remove(path)
118    elif (os.path.isdir(path)):
119        shutil.rmtree(path)
120    else:
121        raise ValueError(f"Unknown type of dirent: '{path}'.")
122
123def same(a: str, b: str) -> bool:
124    """
125    Check if two paths represent the same dirent.
126    If either (or both) paths do not exist, false will be returned.
127    If either paths are links, they are resolved before checking
128    (so a link and the target file are considered the "same").
129    """
130
131    return (exists(a) and exists(b) and os.path.samefile(a, b))
132
133def move(raw_source: str, raw_dest: str, no_clobber: bool = False) -> None:
134    """
135    Move the source dirent to the given destination.
136    Any existing destination will be removed before moving.
137    """
138
139    source = os.path.abspath(raw_source)
140    dest = os.path.abspath(raw_dest)
141
142    if (not exists(source)):
143        raise ValueError(f"Source of move does not exist: '{raw_source}'.")
144
145    # If dest is a dir, then resolve the path.
146    if (os.path.isdir(dest)):
147        dest = os.path.abspath(os.path.join(dest, os.path.basename(source)))
148
149    # Skip if this is self.
150    if (same(source, dest)):
151        return
152
153    # Check for clobber.
154    if (exists(dest)):
155        if (no_clobber):
156            raise ValueError(f"Destination of move already exists: '{raw_dest}'.")
157
158        remove(dest)
159
160    # Create any required parents.
161    os.makedirs(os.path.dirname(dest), exist_ok = True)
162
163    shutil.move(source, dest)
164
165def copy(raw_source: str, raw_dest: str, no_clobber: bool = False) -> None:
166    """
167    Copy a dirent or directory to a destination.
168
169    The destination will be overwritten if it exists (and no_clobber is false).
170    For copying the contents of a directory INTO another directory, use copy_contents().
171
172    No copy is made if the source and dest refer to the same dirent.
173    """
174
175    source = os.path.abspath(raw_source)
176    dest = os.path.abspath(raw_dest)
177
178    if (same(source, dest)):
179        return
180
181    if (not exists(source)):
182        raise ValueError(f"Source of copy does not exist: '{raw_source}'.")
183
184    if (contains_path(source, dest)):
185        raise ValueError(f"Source of copy cannot contain the destination. Source: '{raw_source}', Destination: '{raw_dest}'.")
186
187    if (contains_path(dest, source)):
188        raise ValueError(f"Destination of copy cannot contain the source. Destination: '{raw_dest}', Source: '{raw_source}'.")
189
190    if (exists(dest)):
191        if (no_clobber):
192            raise ValueError(f"Destination of copy already exists: '{raw_dest}'.")
193
194        remove(dest)
195
196    mkdir(os.path.dirname(dest))
197
198    if (os.path.islink(source)):
199        # shutil.copy2() can generally handle (broken) links, but Windows is inconsistent (between 3.11 and 3.12) on link handling.
200        link_target = os.readlink(source)
201        os.symlink(link_target, dest)
202    elif (os.path.isfile(source)):
203        shutil.copy2(source, dest, follow_symlinks = False)
204    elif (os.path.isdir(source)):
205        mkdir(dest)
206
207        for child in sorted(os.listdir(source)):
208            copy(os.path.join(raw_source, child), os.path.join(raw_dest, child))
209    else:
210        raise ValueError(f"Source of copy is not a dir, fie, or link: '{raw_source}'.")
211
212def copy_contents(raw_source: str, raw_dest: str, no_clobber: bool = False) -> None:
213    """
214    Copy a file or the contents of a directory (excluding the top-level directory itself) into a destination.
215    If the destination exists, it must be a directory.
216
217    The source and destination should not be the same file.
218
219    For a file, this is equivalent to `mkdir -p dest && cp source dest`
220    For a dir, this is equivalent to `mkdir -p dest && cp -r source/* dest`
221    """
222
223    source = os.path.abspath(raw_source)
224    dest = os.path.abspath(raw_dest)
225
226    if (same(source, dest)):
227        raise ValueError(f"Source and destination of contents copy cannot be the same: '{raw_source}'.")
228
229    if (exists(dest) and (not os.path.isdir(dest))):
230        raise ValueError(f"Destination of contents copy exists and is not a dir: '{raw_dest}'.")
231
232    mkdir(dest)
233
234    if (os.path.isfile(source) or os.path.islink(source)):
235        copy(source, os.path.join(dest, os.path.basename(source)), no_clobber = no_clobber)
236    elif (os.path.isdir(source)):
237        for child in sorted(os.listdir(source)):
238            copy(os.path.join(raw_source, child), os.path.join(raw_dest, child), no_clobber = no_clobber)
239    else:
240        raise ValueError(f"Source of contents copy is not a dir, fie, or link: '{raw_source}'.")
241
242def read_file(
243        raw_path: str,
244        strip: bool = True,
245        encoding: typing.Union[str, None] = DEFAULT_ENCODING,
246        ) -> str:
247    """ Read the contents of a file. """
248
249    if (encoding is None):
250        encoding = DEFAULT_ENCODING
251
252    path = os.path.abspath(raw_path)
253
254    if (not exists(path)):
255        raise ValueError(f"Source of read does not exist: '{raw_path}'.")
256
257    with open(path, 'r', encoding = encoding) as file:
258        contents = file.read()
259
260    if (strip):
261        contents = contents.strip()
262
263    return contents
264
265def write_file(
266        raw_path: str,
267        contents: typing.Union[str, None],
268        strip: bool = True,
269        newline: bool = True,
270        encoding: typing.Union[str, None] = DEFAULT_ENCODING,
271        no_clobber: bool = False,
272        ) -> None:
273    """
274    Write the contents of a file.
275    If clobbering, any existing dirent will be removed before write.
276    """
277
278    if (encoding is None):
279        encoding = DEFAULT_ENCODING
280
281    path = os.path.abspath(raw_path)
282
283    if (exists(path)):
284        if (no_clobber):
285            raise ValueError(f"Destination of write already exists: '{raw_path}'.")
286
287        remove(path)
288
289    if (contents is None):
290        contents = ''
291
292    if (strip):
293        contents = contents.strip()
294
295    if (newline):
296        contents += "\n"
297
298    with open(path, 'w', encoding = encoding) as file:
299        file.write(contents)
300
301def read_file_bytes(raw_path: str) -> bytes:
302    """ Read the contents of a file as bytes. """
303
304    path = os.path.abspath(raw_path)
305
306    if (not exists(path)):
307        raise ValueError(f"Source of read bytes does not exist: '{raw_path}'.")
308
309    with open(path, 'rb') as file:
310        return file.read()
311
312def write_file_bytes(
313        raw_path: str, contents: typing.Union[bytes, str, None],
314        no_clobber: bool = False) -> None:
315    """
316    Write the contents of a file as bytes.
317    If clobbering, any existing dirent will be removed before write.
318    """
319
320    if (contents is None):
321        contents = b''
322
323    if (isinstance(contents, str)):
324        contents = contents.encode(DEFAULT_ENCODING)
325
326    path = os.path.abspath(raw_path)
327
328    if (exists(path)):
329        if (no_clobber):
330            raise ValueError(f"Destination of write bytes already exists: '{raw_path}'.")
331
332        remove(path)
333
334    with open(path, 'wb') as file:
335        file.write(contents)
336
337def contains_path(parent: str, child: str) -> bool:
338    """
339    Check if the parent path contains the child path.
340    This is pure lexical analysis, no dirent stats are checked.
341    Will return false if the (absolute) paths are the same
342    (this function does not allow a path to contain itself).
343    """
344
345    if ((parent == '') or (child == '')):
346        return False
347
348    parent = os.path.abspath(parent)
349    child = os.path.abspath(child)
350
351    child = os.path.dirname(child)
352    for _ in range(DEPTH_LIMIT):
353        if (parent == child):
354            return True
355
356        new_child = os.path.dirname(child)
357        if (child == new_child):
358            return False
359
360        child = new_child
361
362    raise ValueError("Depth limit reached.")
363
364def hash_file(raw_path: str) -> str:
365    """
366    Compute the SHA256 hash of the file (see edq.util.hash.sha256_hex()).
367    Links will has their path (according to os.readlink()).
368    Directories will raise an exception.
369    """
370
371    path = os.path.abspath(raw_path)
372
373    contents: typing.Any = None
374
375    if (not exists(path)):
376        raise ValueError(f"Target of hash file does not exist: '{raw_path}'.")
377
378    if (os.path.islink(path)):
379        contents = os.readlink(path)
380    elif (os.path.isfile(path)):
381        contents = read_file_bytes(raw_path)
382    else:
383        raise ValueError(f"Target of hash file is not a file: '{raw_path}'.")
384
385    return edq.util.hash.sha256_hex(contents)
386
387def tree(raw_path: str, hash_files: bool = False) -> typing.Dict[str, typing.Union[None, str, typing.Dict[str, typing.Any]]]:
388    """
389    Return a tree structure that includes all descendants of the given dirent (including the dirent itself).
390    If `hash_files` is true, then the value of non-dir keys will be the SHA256 hash of the file (see hash_file()),
391    otherwise the value will be None.
392    """
393
394    path = os.path.abspath(raw_path)
395
396    if (not exists(path)):
397        raise ValueError(f"Target of tree does not exist: '{raw_path}'.")
398
399    return {
400        os.path.basename(path): _tree(path, hash_files, 0),
401    }
402
403def _tree(path: str, hash_files: bool, level: int) -> typing.Union[str, None, typing.Dict[str, typing.Any]]:
404    """ Recursive helper for tree(). """
405
406    if (level > DEPTH_LIMIT):
407        raise ValueError("Depth limit reached.")
408
409    if (not os.path.isdir(path)):
410        if (hash_files):
411            return hash_file(path)
412
413        return None
414
415    result = {}
416    for child in sorted(os.listdir(path)):
417        result[child] = _tree(os.path.join(path, child), hash_files, level + 1)
418
419    return result
DEFAULT_ENCODING: str = 'utf-8'

The default encoding that will be used when reading and writing.

DEPTH_LIMIT: int = 10000
def exists(path: str) -> bool:
28def exists(path: str) -> bool:
29    """
30    Check if a path exists.
31    This will transparently call os.path.lexists(),
32    which will include broken links.
33    """
34
35    return os.path.lexists(path)

Check if a path exists. This will transparently call os.path.lexists(), which will include broken links.

def get_temp_path(prefix: str = '', suffix: str = '', rm: bool = True) -> str:
37def get_temp_path(prefix: str = '', suffix: str = '', rm: bool = True) -> str:
38    """
39    Get a path to a valid (but not currently existing) temp dirent.
40    If rm is True, then the dirent will be attempted to be deleted on exit
41    (no error will occur if the path is not there).
42    """
43
44    path = None
45    while ((path is None) or exists(path)):
46        path = os.path.join(tempfile.gettempdir(), prefix + str(uuid.uuid4()) + suffix)
47
48    path = os.path.realpath(path)
49
50    if (rm):
51        atexit.register(remove, path)
52
53    return path

Get a path to a valid (but not currently existing) temp dirent. If rm is True, then the dirent will be attempted to be deleted on exit (no error will occur if the path is not there).

def get_temp_dir(prefix: str = '', suffix: str = '', rm: bool = True) -> str:
55def get_temp_dir(prefix: str = '', suffix: str = '', rm: bool = True) -> str:
56    """
57    Get a temp directory.
58    The directory will exist when returned.
59    """
60
61    path = get_temp_path(prefix = prefix, suffix = suffix, rm = rm)
62    mkdir(path)
63    return path

Get a temp directory. The directory will exist when returned.

def mkdir(raw_path: str) -> None:
65def mkdir(raw_path: str) -> None:
66    """
67    Make a directory (including any required parent directories).
68    Does not complain if the directory (or parents) already exist
69    (this includes if the directory or parents are links to directories).
70    """
71
72    path = os.path.abspath(raw_path)
73
74    if (exists(path)):
75        if (os.path.isdir(path)):
76            return
77
78        raise ValueError(f"Target of mkdir already exists, and is not a dir: '{raw_path}'.")
79
80    _check_parent_dirs(raw_path)
81
82    os.makedirs(path, exist_ok = True)

Make a directory (including any required parent directories). Does not complain if the directory (or parents) already exist (this includes if the directory or parents are links to directories).

def remove(path: str) -> None:
107def remove(path: str) -> None:
108    """
109    Remove the given path.
110    The path can be of any type (dir, file, link),
111    and does not need to exist.
112    """
113
114    if (not exists(path)):
115        return
116
117    if (os.path.isfile(path) or os.path.islink(path)):
118        os.remove(path)
119    elif (os.path.isdir(path)):
120        shutil.rmtree(path)
121    else:
122        raise ValueError(f"Unknown type of dirent: '{path}'.")

Remove the given path. The path can be of any type (dir, file, link), and does not need to exist.

def same(a: str, b: str) -> bool:
124def same(a: str, b: str) -> bool:
125    """
126    Check if two paths represent the same dirent.
127    If either (or both) paths do not exist, false will be returned.
128    If either paths are links, they are resolved before checking
129    (so a link and the target file are considered the "same").
130    """
131
132    return (exists(a) and exists(b) and os.path.samefile(a, b))

Check if two paths represent the same dirent. If either (or both) paths do not exist, false will be returned. If either paths are links, they are resolved before checking (so a link and the target file are considered the "same").

def move(raw_source: str, raw_dest: str, no_clobber: bool = False) -> None:
134def move(raw_source: str, raw_dest: str, no_clobber: bool = False) -> None:
135    """
136    Move the source dirent to the given destination.
137    Any existing destination will be removed before moving.
138    """
139
140    source = os.path.abspath(raw_source)
141    dest = os.path.abspath(raw_dest)
142
143    if (not exists(source)):
144        raise ValueError(f"Source of move does not exist: '{raw_source}'.")
145
146    # If dest is a dir, then resolve the path.
147    if (os.path.isdir(dest)):
148        dest = os.path.abspath(os.path.join(dest, os.path.basename(source)))
149
150    # Skip if this is self.
151    if (same(source, dest)):
152        return
153
154    # Check for clobber.
155    if (exists(dest)):
156        if (no_clobber):
157            raise ValueError(f"Destination of move already exists: '{raw_dest}'.")
158
159        remove(dest)
160
161    # Create any required parents.
162    os.makedirs(os.path.dirname(dest), exist_ok = True)
163
164    shutil.move(source, dest)

Move the source dirent to the given destination. Any existing destination will be removed before moving.

def copy(raw_source: str, raw_dest: str, no_clobber: bool = False) -> None:
166def copy(raw_source: str, raw_dest: str, no_clobber: bool = False) -> None:
167    """
168    Copy a dirent or directory to a destination.
169
170    The destination will be overwritten if it exists (and no_clobber is false).
171    For copying the contents of a directory INTO another directory, use copy_contents().
172
173    No copy is made if the source and dest refer to the same dirent.
174    """
175
176    source = os.path.abspath(raw_source)
177    dest = os.path.abspath(raw_dest)
178
179    if (same(source, dest)):
180        return
181
182    if (not exists(source)):
183        raise ValueError(f"Source of copy does not exist: '{raw_source}'.")
184
185    if (contains_path(source, dest)):
186        raise ValueError(f"Source of copy cannot contain the destination. Source: '{raw_source}', Destination: '{raw_dest}'.")
187
188    if (contains_path(dest, source)):
189        raise ValueError(f"Destination of copy cannot contain the source. Destination: '{raw_dest}', Source: '{raw_source}'.")
190
191    if (exists(dest)):
192        if (no_clobber):
193            raise ValueError(f"Destination of copy already exists: '{raw_dest}'.")
194
195        remove(dest)
196
197    mkdir(os.path.dirname(dest))
198
199    if (os.path.islink(source)):
200        # shutil.copy2() can generally handle (broken) links, but Windows is inconsistent (between 3.11 and 3.12) on link handling.
201        link_target = os.readlink(source)
202        os.symlink(link_target, dest)
203    elif (os.path.isfile(source)):
204        shutil.copy2(source, dest, follow_symlinks = False)
205    elif (os.path.isdir(source)):
206        mkdir(dest)
207
208        for child in sorted(os.listdir(source)):
209            copy(os.path.join(raw_source, child), os.path.join(raw_dest, child))
210    else:
211        raise ValueError(f"Source of copy is not a dir, fie, or link: '{raw_source}'.")

Copy a dirent or directory to a destination.

The destination will be overwritten if it exists (and no_clobber is false). For copying the contents of a directory INTO another directory, use copy_contents().

No copy is made if the source and dest refer to the same dirent.

def copy_contents(raw_source: str, raw_dest: str, no_clobber: bool = False) -> None:
213def copy_contents(raw_source: str, raw_dest: str, no_clobber: bool = False) -> None:
214    """
215    Copy a file or the contents of a directory (excluding the top-level directory itself) into a destination.
216    If the destination exists, it must be a directory.
217
218    The source and destination should not be the same file.
219
220    For a file, this is equivalent to `mkdir -p dest && cp source dest`
221    For a dir, this is equivalent to `mkdir -p dest && cp -r source/* dest`
222    """
223
224    source = os.path.abspath(raw_source)
225    dest = os.path.abspath(raw_dest)
226
227    if (same(source, dest)):
228        raise ValueError(f"Source and destination of contents copy cannot be the same: '{raw_source}'.")
229
230    if (exists(dest) and (not os.path.isdir(dest))):
231        raise ValueError(f"Destination of contents copy exists and is not a dir: '{raw_dest}'.")
232
233    mkdir(dest)
234
235    if (os.path.isfile(source) or os.path.islink(source)):
236        copy(source, os.path.join(dest, os.path.basename(source)), no_clobber = no_clobber)
237    elif (os.path.isdir(source)):
238        for child in sorted(os.listdir(source)):
239            copy(os.path.join(raw_source, child), os.path.join(raw_dest, child), no_clobber = no_clobber)
240    else:
241        raise ValueError(f"Source of contents copy is not a dir, fie, or link: '{raw_source}'.")

Copy a file or the contents of a directory (excluding the top-level directory itself) into a destination. If the destination exists, it must be a directory.

The source and destination should not be the same file.

For a file, this is equivalent to mkdir -p dest && cp source dest For a dir, this is equivalent to mkdir -p dest && cp -r source/* dest

def read_file( raw_path: str, strip: bool = True, encoding: Optional[str] = 'utf-8') -> str:
243def read_file(
244        raw_path: str,
245        strip: bool = True,
246        encoding: typing.Union[str, None] = DEFAULT_ENCODING,
247        ) -> str:
248    """ Read the contents of a file. """
249
250    if (encoding is None):
251        encoding = DEFAULT_ENCODING
252
253    path = os.path.abspath(raw_path)
254
255    if (not exists(path)):
256        raise ValueError(f"Source of read does not exist: '{raw_path}'.")
257
258    with open(path, 'r', encoding = encoding) as file:
259        contents = file.read()
260
261    if (strip):
262        contents = contents.strip()
263
264    return contents

Read the contents of a file.

def write_file( raw_path: str, contents: Optional[str], strip: bool = True, newline: bool = True, encoding: Optional[str] = 'utf-8', no_clobber: bool = False) -> None:
266def write_file(
267        raw_path: str,
268        contents: typing.Union[str, None],
269        strip: bool = True,
270        newline: bool = True,
271        encoding: typing.Union[str, None] = DEFAULT_ENCODING,
272        no_clobber: bool = False,
273        ) -> None:
274    """
275    Write the contents of a file.
276    If clobbering, any existing dirent will be removed before write.
277    """
278
279    if (encoding is None):
280        encoding = DEFAULT_ENCODING
281
282    path = os.path.abspath(raw_path)
283
284    if (exists(path)):
285        if (no_clobber):
286            raise ValueError(f"Destination of write already exists: '{raw_path}'.")
287
288        remove(path)
289
290    if (contents is None):
291        contents = ''
292
293    if (strip):
294        contents = contents.strip()
295
296    if (newline):
297        contents += "\n"
298
299    with open(path, 'w', encoding = encoding) as file:
300        file.write(contents)

Write the contents of a file. If clobbering, any existing dirent will be removed before write.

def read_file_bytes(raw_path: str) -> bytes:
302def read_file_bytes(raw_path: str) -> bytes:
303    """ Read the contents of a file as bytes. """
304
305    path = os.path.abspath(raw_path)
306
307    if (not exists(path)):
308        raise ValueError(f"Source of read bytes does not exist: '{raw_path}'.")
309
310    with open(path, 'rb') as file:
311        return file.read()

Read the contents of a file as bytes.

def write_file_bytes( raw_path: str, contents: Union[bytes, str, NoneType], no_clobber: bool = False) -> None:
313def write_file_bytes(
314        raw_path: str, contents: typing.Union[bytes, str, None],
315        no_clobber: bool = False) -> None:
316    """
317    Write the contents of a file as bytes.
318    If clobbering, any existing dirent will be removed before write.
319    """
320
321    if (contents is None):
322        contents = b''
323
324    if (isinstance(contents, str)):
325        contents = contents.encode(DEFAULT_ENCODING)
326
327    path = os.path.abspath(raw_path)
328
329    if (exists(path)):
330        if (no_clobber):
331            raise ValueError(f"Destination of write bytes already exists: '{raw_path}'.")
332
333        remove(path)
334
335    with open(path, 'wb') as file:
336        file.write(contents)

Write the contents of a file as bytes. If clobbering, any existing dirent will be removed before write.

def contains_path(parent: str, child: str) -> bool:
338def contains_path(parent: str, child: str) -> bool:
339    """
340    Check if the parent path contains the child path.
341    This is pure lexical analysis, no dirent stats are checked.
342    Will return false if the (absolute) paths are the same
343    (this function does not allow a path to contain itself).
344    """
345
346    if ((parent == '') or (child == '')):
347        return False
348
349    parent = os.path.abspath(parent)
350    child = os.path.abspath(child)
351
352    child = os.path.dirname(child)
353    for _ in range(DEPTH_LIMIT):
354        if (parent == child):
355            return True
356
357        new_child = os.path.dirname(child)
358        if (child == new_child):
359            return False
360
361        child = new_child
362
363    raise ValueError("Depth limit reached.")

Check if the parent path contains the child path. This is pure lexical analysis, no dirent stats are checked. Will return false if the (absolute) paths are the same (this function does not allow a path to contain itself).

def hash_file(raw_path: str) -> str:
365def hash_file(raw_path: str) -> str:
366    """
367    Compute the SHA256 hash of the file (see edq.util.hash.sha256_hex()).
368    Links will has their path (according to os.readlink()).
369    Directories will raise an exception.
370    """
371
372    path = os.path.abspath(raw_path)
373
374    contents: typing.Any = None
375
376    if (not exists(path)):
377        raise ValueError(f"Target of hash file does not exist: '{raw_path}'.")
378
379    if (os.path.islink(path)):
380        contents = os.readlink(path)
381    elif (os.path.isfile(path)):
382        contents = read_file_bytes(raw_path)
383    else:
384        raise ValueError(f"Target of hash file is not a file: '{raw_path}'.")
385
386    return edq.util.hash.sha256_hex(contents)

Compute the SHA256 hash of the file (see edq.util.hash.sha256_hex()). Links will has their path (according to os.readlink()). Directories will raise an exception.

def tree( raw_path: str, hash_files: bool = False) -> Dict[str, Union[NoneType, str, Dict[str, Any]]]:
388def tree(raw_path: str, hash_files: bool = False) -> typing.Dict[str, typing.Union[None, str, typing.Dict[str, typing.Any]]]:
389    """
390    Return a tree structure that includes all descendants of the given dirent (including the dirent itself).
391    If `hash_files` is true, then the value of non-dir keys will be the SHA256 hash of the file (see hash_file()),
392    otherwise the value will be None.
393    """
394
395    path = os.path.abspath(raw_path)
396
397    if (not exists(path)):
398        raise ValueError(f"Target of tree does not exist: '{raw_path}'.")
399
400    return {
401        os.path.basename(path): _tree(path, hash_files, 0),
402    }

Return a tree structure that includes all descendants of the given dirent (including the dirent itself). If hash_files is true, then the value of non-dir keys will be the SHA256 hash of the file (see hash_file()), otherwise the value will be None.