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(raw_path: str, strip: bool = True, encoding: str = DEFAULT_ENCODING) -> str:
243    """ Read the contents of a file. """
244
245    path = os.path.abspath(raw_path)
246
247    if (not exists(path)):
248        raise ValueError(f"Source of read does not exist: '{raw_path}'.")
249
250    with open(path, 'r', encoding = encoding) as file:
251        contents = file.read()
252
253    if (strip):
254        contents = contents.strip()
255
256    return contents
257
258def write_file(
259        raw_path: str, contents: typing.Union[str, None],
260        strip: bool = True, newline: bool = True,
261        encoding: str = DEFAULT_ENCODING,
262        no_clobber: bool = False) -> None:
263    """
264    Write the contents of a file.
265    If clobbering, any existing dirent will be removed before write.
266    """
267
268    path = os.path.abspath(raw_path)
269
270    if (exists(path)):
271        if (no_clobber):
272            raise ValueError(f"Destination of write already exists: '{raw_path}'.")
273
274        remove(path)
275
276    if (contents is None):
277        contents = ''
278
279    if (strip):
280        contents = contents.strip()
281
282    if (newline):
283        contents += "\n"
284
285    with open(path, 'w', encoding = encoding) as file:
286        file.write(contents)
287
288def read_file_bytes(raw_path: str) -> bytes:
289    """ Read the contents of a file as bytes. """
290
291    path = os.path.abspath(raw_path)
292
293    if (not exists(path)):
294        raise ValueError(f"Source of read bytes does not exist: '{raw_path}'.")
295
296    with open(path, 'rb') as file:
297        return file.read()
298
299def write_file_bytes(
300        raw_path: str, contents: typing.Union[bytes, str, None],
301        no_clobber: bool = False) -> None:
302    """
303    Write the contents of a file as bytes.
304    If clobbering, any existing dirent will be removed before write.
305    """
306
307    if (contents is None):
308        contents = b''
309
310    if (isinstance(contents, str)):
311        contents = contents.encode(DEFAULT_ENCODING)
312
313    path = os.path.abspath(raw_path)
314
315    if (exists(path)):
316        if (no_clobber):
317            raise ValueError(f"Destination of write bytes already exists: '{raw_path}'.")
318
319        remove(path)
320
321    with open(path, 'wb') as file:
322        file.write(contents)
323
324def contains_path(parent: str, child: str) -> bool:
325    """
326    Check if the parent path contains the child path.
327    This is pure lexical analysis, no dirent stats are checked.
328    Will return false if the (absolute) paths are the same
329    (this function does not allow a path to contain itself).
330    """
331
332    if ((parent == '') or (child == '')):
333        return False
334
335    parent = os.path.abspath(parent)
336    child = os.path.abspath(child)
337
338    child = os.path.dirname(child)
339    for _ in range(DEPTH_LIMIT):
340        if (parent == child):
341            return True
342
343        new_child = os.path.dirname(child)
344        if (child == new_child):
345            return False
346
347        child = new_child
348
349    raise ValueError("Depth limit reached.")
350
351def hash_file(raw_path: str) -> str:
352    """
353    Compute the SHA256 hash of the file (see edq.util.hash.sha256_hex()).
354    Links will has their path (according to os.readlink()).
355    Directories will raise an exception.
356    """
357
358    path = os.path.abspath(raw_path)
359
360    contents: typing.Any = None
361
362    if (not exists(path)):
363        raise ValueError(f"Target of hash file does not exist: '{raw_path}'.")
364
365    if (os.path.islink(path)):
366        contents = os.readlink(path)
367    elif (os.path.isfile(path)):
368        contents = read_file_bytes(raw_path)
369    else:
370        raise ValueError(f"Target of hash file is not a file: '{raw_path}'.")
371
372    return edq.util.hash.sha256_hex(contents)
373
374def tree(raw_path: str, hash_files: bool = False) -> typing.Dict[str, typing.Union[None, str, typing.Dict[str, typing.Any]]]:
375    """
376    Return a tree structure that includes all descendants of the given dirent (including the dirent itself).
377    If `hash_files` is true, then the value of non-dir keys will be the SHA256 hash of the file (see hash_file()),
378    otherwise the value will be None.
379    """
380
381    path = os.path.abspath(raw_path)
382
383    if (not exists(path)):
384        raise ValueError(f"Target of tree does not exist: '{raw_path}'.")
385
386    return {
387        os.path.basename(path): _tree(path, hash_files, 0),
388    }
389
390def _tree(path: str, hash_files: bool, level: int) -> typing.Union[str, None, typing.Dict[str, typing.Any]]:
391    """ Recursive helper for tree(). """
392
393    if (level > DEPTH_LIMIT):
394        raise ValueError("Depth limit reached.")
395
396    if (not os.path.isdir(path)):
397        if (hash_files):
398            return hash_file(path)
399
400        return None
401
402    result = {}
403    for child in sorted(os.listdir(path)):
404        result[child] = _tree(os.path.join(path, child), hash_files, level + 1)
405
406    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: str = 'utf-8') -> str:
243def read_file(raw_path: str, strip: bool = True, encoding: str = DEFAULT_ENCODING) -> str:
244    """ Read the contents of a file. """
245
246    path = os.path.abspath(raw_path)
247
248    if (not exists(path)):
249        raise ValueError(f"Source of read does not exist: '{raw_path}'.")
250
251    with open(path, 'r', encoding = encoding) as file:
252        contents = file.read()
253
254    if (strip):
255        contents = contents.strip()
256
257    return contents

Read the contents of a file.

def write_file( raw_path: str, contents: Optional[str], strip: bool = True, newline: bool = True, encoding: str = 'utf-8', no_clobber: bool = False) -> None:
259def write_file(
260        raw_path: str, contents: typing.Union[str, None],
261        strip: bool = True, newline: bool = True,
262        encoding: str = DEFAULT_ENCODING,
263        no_clobber: bool = False) -> None:
264    """
265    Write the contents of a file.
266    If clobbering, any existing dirent will be removed before write.
267    """
268
269    path = os.path.abspath(raw_path)
270
271    if (exists(path)):
272        if (no_clobber):
273            raise ValueError(f"Destination of write already exists: '{raw_path}'.")
274
275        remove(path)
276
277    if (contents is None):
278        contents = ''
279
280    if (strip):
281        contents = contents.strip()
282
283    if (newline):
284        contents += "\n"
285
286    with open(path, 'w', encoding = encoding) as file:
287        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:
289def read_file_bytes(raw_path: str) -> bytes:
290    """ Read the contents of a file as bytes. """
291
292    path = os.path.abspath(raw_path)
293
294    if (not exists(path)):
295        raise ValueError(f"Source of read bytes does not exist: '{raw_path}'.")
296
297    with open(path, 'rb') as file:
298        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:
300def write_file_bytes(
301        raw_path: str, contents: typing.Union[bytes, str, None],
302        no_clobber: bool = False) -> None:
303    """
304    Write the contents of a file as bytes.
305    If clobbering, any existing dirent will be removed before write.
306    """
307
308    if (contents is None):
309        contents = b''
310
311    if (isinstance(contents, str)):
312        contents = contents.encode(DEFAULT_ENCODING)
313
314    path = os.path.abspath(raw_path)
315
316    if (exists(path)):
317        if (no_clobber):
318            raise ValueError(f"Destination of write bytes already exists: '{raw_path}'.")
319
320        remove(path)
321
322    with open(path, 'wb') as file:
323        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:
325def contains_path(parent: str, child: str) -> bool:
326    """
327    Check if the parent path contains the child path.
328    This is pure lexical analysis, no dirent stats are checked.
329    Will return false if the (absolute) paths are the same
330    (this function does not allow a path to contain itself).
331    """
332
333    if ((parent == '') or (child == '')):
334        return False
335
336    parent = os.path.abspath(parent)
337    child = os.path.abspath(child)
338
339    child = os.path.dirname(child)
340    for _ in range(DEPTH_LIMIT):
341        if (parent == child):
342            return True
343
344        new_child = os.path.dirname(child)
345        if (child == new_child):
346            return False
347
348        child = new_child
349
350    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:
352def hash_file(raw_path: str) -> str:
353    """
354    Compute the SHA256 hash of the file (see edq.util.hash.sha256_hex()).
355    Links will has their path (according to os.readlink()).
356    Directories will raise an exception.
357    """
358
359    path = os.path.abspath(raw_path)
360
361    contents: typing.Any = None
362
363    if (not exists(path)):
364        raise ValueError(f"Target of hash file does not exist: '{raw_path}'.")
365
366    if (os.path.islink(path)):
367        contents = os.readlink(path)
368    elif (os.path.isfile(path)):
369        contents = read_file_bytes(raw_path)
370    else:
371        raise ValueError(f"Target of hash file is not a file: '{raw_path}'.")
372
373    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]]]:
375def tree(raw_path: str, hash_files: bool = False) -> typing.Dict[str, typing.Union[None, str, typing.Dict[str, typing.Any]]]:
376    """
377    Return a tree structure that includes all descendants of the given dirent (including the dirent itself).
378    If `hash_files` is true, then the value of non-dir keys will be the SHA256 hash of the file (see hash_file()),
379    otherwise the value will be None.
380    """
381
382    path = os.path.abspath(raw_path)
383
384    if (not exists(path)):
385        raise ValueError(f"Target of tree does not exist: '{raw_path}'.")
386
387    return {
388        os.path.basename(path): _tree(path, hash_files, 0),
389    }

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.