Use voussoirkit.progressbars in spinal.

This commit is contained in:
voussoir 2022-11-11 15:22:01 -08:00
parent 30a0b49097
commit ea7f7d5843
No known key found for this signature in database
GPG key ID: 5F7554F8C26DACCB

View file

@ -6,14 +6,13 @@ import collections
import hashlib import hashlib
import os import os
import shutil import shutil
import sys
import time import time
from voussoirkit import bytestring from voussoirkit import bytestring
from voussoirkit import dotdict from voussoirkit import dotdict
from voussoirkit import pathclass from voussoirkit import pathclass
from voussoirkit import progressbars
from voussoirkit import ratelimiter from voussoirkit import ratelimiter
from voussoirkit import safeprint
from voussoirkit import sentinel from voussoirkit import sentinel
from voussoirkit import vlogging from voussoirkit import vlogging
from voussoirkit import winglob from voussoirkit import winglob
@ -60,42 +59,22 @@ class SourceNotFile(SpinalException):
class SpinalError(SpinalException): class SpinalError(SpinalException):
pass pass
def callback_progress_v1(path, written_bytes, total_bytes):
'''
Example of a copy callback function.
Prints "filename written/total (percent%)"
'''
if written_bytes >= total_bytes:
ends = '\r\n'
else:
ends = ''
percent = (100 * written_bytes) / max(total_bytes, 1)
percent = f'{percent:07.3f}'
written = '{:,}'.format(written_bytes)
total = '{:,}'.format(total_bytes)
written = written.rjust(len(total), ' ')
status = f'{path.absolute_path} {written}/{total} ({percent}%)\r'
safeprint.safeprint(status, end=ends)
sys.stdout.flush()
def copy_directory( def copy_directory(
source, source,
destination=None, destination=None,
*, *,
bytes_per_second=None, bytes_per_second=None,
callback_directory_progress=None,
callback_file_progress=None,
callback_permission_denied=None, callback_permission_denied=None,
callback_post_file=None, callback_post_file=None,
callback_pre_directory=None, callback_pre_directory=None,
callback_pre_file=None, callback_pre_file=None,
callback_verify_hash_progress=None,
chunk_size='dynamic', chunk_size='dynamic',
destination_new_root=None, destination_new_root=None,
directory_progressbar=None,
dry_run=False, dry_run=False,
exclude_directories=None, exclude_directories=None,
exclude_filenames=None, exclude_filenames=None,
file_progressbar=None,
files_per_second=None, files_per_second=None,
hash_class=None, hash_class=None,
overwrite=OVERWRITE_OLD, overwrite=OVERWRITE_OLD,
@ -103,6 +82,7 @@ def copy_directory(
skip_symlinks=True, skip_symlinks=True,
stop_event=None, stop_event=None,
verify_hash=False, verify_hash=False,
verify_hash_progressbar=None,
): ):
''' '''
Copy all of the contents from source to destination, Copy all of the contents from source to destination,
@ -118,16 +98,6 @@ def copy_directory(
bytes_per_second: bytes_per_second:
Passed into each `copy_file` as `bytes_per_second`. Passed into each `copy_file` as `bytes_per_second`.
callback_directory_progress:
This function will be called after each file copy with three arguments:
name of file copied, number of bytes written to destination directory
so far, total bytes needed (based on precalcsize).
If `precalcsize` is False, this function will receive written bytes
for both written and total, showing 100% always.
callback_file_progress:
Passed into each `copy_file` as `callback_progress`.
callback_permission_denied: callback_permission_denied:
Passed into `walk` and each `copy_file` as `callback_permission_denied`. Passed into `walk` and each `copy_file` as `callback_permission_denied`.
@ -150,9 +120,6 @@ def copy_directory(
If you think copy_dir should be rewritten as a generator instead, If you think copy_dir should be rewritten as a generator instead,
I agree! I agree!
callback_verify_hash_progress:
Passed into each `copy_file` as `callback_verify_hash_progress`.
chunk_size: chunk_size:
Passed into each `copy_file` as `chunk_size`. Passed into each `copy_file` as `chunk_size`.
@ -163,6 +130,13 @@ def copy_directory(
`destination` and `destination_new_root` are mutually exclusive. `destination` and `destination_new_root` are mutually exclusive.
directory_progressbar:
An instance of voussoirkit.progressbars.ProgressBar.
This progressbar will be updated after each file copy with the number of
bytes written into the destination directory so far. If `precalcsize` is
True, the progressbar will have its total set to that value. Otherwise
it will be indeterminate.
dry_run: dry_run:
Do everything except the actual file copying. Do everything except the actual file copying.
@ -176,6 +150,9 @@ def copy_directory(
Maximum number of files to be processed per second. Helps to keep CPU Maximum number of files to be processed per second. Helps to keep CPU
usage low. usage low.
file_progressbar:
Passed into each `copy_file` as `progressbar`.
hash_class: hash_class:
Passed into each `copy_file` as `hash_class`. Passed into each `copy_file` as `hash_class`.
@ -184,9 +161,7 @@ def copy_directory(
precalcsize: precalcsize:
If True, calculate the size of source before beginning the copy. If True, calculate the size of source before beginning the copy.
This number can be used in the callback_directory_progress function. This number can be used in the directory_progressbar.
Else, callback_directory_progress will receive written bytes as total
bytes (showing 100% always).
This may take a while if the source directory is large. This may take a while if the source directory is large.
skip_symlinks: skip_symlinks:
@ -201,6 +176,9 @@ def copy_directory(
verify_hash: verify_hash:
Passed into each `copy_file` as `verify_hash`. Passed into each `copy_file` as `verify_hash`.
verify_hash_progressbar:
Passed into each `copy_file` as `verify_hash_progressbar`.
Returns a dotdict containing at least these values: Returns a dotdict containing at least these values:
`source` pathclass.Path `source` pathclass.Path
`destination` pathclass.Path `destination` pathclass.Path
@ -236,7 +214,11 @@ def copy_directory(
else: else:
total_bytes = 0 total_bytes = 0
callback_directory_progress = callback_directory_progress or do_nothing directory_progressbar = progressbars.normalize(
directory_progressbar,
topic=destination.absolute_path,
total=total_bytes if precalcsize else None,
)
callback_pre_directory = callback_pre_directory or do_nothing callback_pre_directory = callback_pre_directory or do_nothing
callback_pre_file = callback_pre_file or do_nothing callback_pre_file = callback_pre_file or do_nothing
callback_post_file = callback_post_file or do_nothing callback_post_file = callback_post_file or do_nothing
@ -311,29 +293,27 @@ def copy_directory(
bytes_per_second=bytes_per_second, bytes_per_second=bytes_per_second,
callback_permission_denied=callback_permission_denied, callback_permission_denied=callback_permission_denied,
callback_pre_copy=callback_pre_file, callback_pre_copy=callback_pre_file,
callback_progress=callback_file_progress,
callback_verify_hash_progress=callback_verify_hash_progress,
chunk_size=chunk_size, chunk_size=chunk_size,
dry_run=dry_run, dry_run=dry_run,
hash_class=hash_class, hash_class=hash_class,
overwrite=overwrite, overwrite=overwrite,
progressbar=file_progressbar,
verify_hash=verify_hash, verify_hash=verify_hash,
verify_hash_progressbar=verify_hash_progressbar,
) )
if copied.written: if copied.written:
written_files += 1 written_files += 1
written_bytes += copied.written_bytes written_bytes += copied.written_bytes
if precalcsize is False: directory_progressbar.step(written_bytes)
callback_directory_progress(copied.destination, written_bytes, written_bytes)
else:
callback_directory_progress(copied.destination, written_bytes, total_bytes)
callback_post_file(copied) callback_post_file(copied)
if files_per_second is not None: if files_per_second is not None:
files_per_second.limit(1) files_per_second.limit(1)
directory_progressbar.done()
results = dotdict.DotDict( results = dotdict.DotDict(
source=source, source=source,
destination=destination, destination=destination,
@ -350,17 +330,17 @@ def copy_file(
source, source,
destination=None, destination=None,
*, *,
destination_new_root=None,
bytes_per_second=None, bytes_per_second=None,
callback_verify_hash_progress=None,
callback_progress=None,
callback_permission_denied=None, callback_permission_denied=None,
callback_pre_copy=None, callback_pre_copy=None,
chunk_size='dynamic', chunk_size='dynamic',
destination_new_root=None,
dry_run=False, dry_run=False,
hash_class=None, hash_class=None,
overwrite=OVERWRITE_OLD, overwrite=OVERWRITE_OLD,
progressbar=None,
verify_hash=False, verify_hash=False,
verify_hash_progressbar=None,
): ):
''' '''
Copy a file from one place to another. Copy a file from one place to another.
@ -396,15 +376,6 @@ def copy_file(
This function may return the BAIL sentinel (return spinal.BAIL) and This function may return the BAIL sentinel (return spinal.BAIL) and
that file will not be copied. that file will not be copied.
callback_progress:
If provided, this function will be called after writing
each chunk_size bytes to destination with three parameters:
the Path object being copied, number of bytes written so far,
total number of bytes needed.
callback_verify_hash_progress:
Passed into `hash_file` as callback_progress when verifying the hash.
chunk_size: chunk_size:
An integer number of bytes to read and write at a time. An integer number of bytes to read and write at a time.
Or, the string 'dynamic' to enable dynamic chunk sizing that aims to Or, the string 'dynamic' to enable dynamic chunk sizing that aims to
@ -429,11 +400,17 @@ def copy_file(
If any other value, the file will not be overwritten. False or None If any other value, the file will not be overwritten. False or None
would be good values to pass. would be good values to pass.
progressbar:
An instance of voussoirkit.progressbars.ProgressBar.
verify_hash: verify_hash:
If True, the copied file will be read back after the copy is complete, If True, the copied file will be read back after the copy is complete,
and its hash will be compared against the hash of the source file. and its hash will be compared against the hash of the source file.
If hash_class is None, then the global HASH_CLASS is used. If hash_class is None, then the global HASH_CLASS is used.
verify_hash_progressbar:
Passed into `hash_file` as `progressbar` when verifying the hash.
Returns a dotdict containing at least these values: Returns a dotdict containing at least these values:
`source` pathclass.Path `source` pathclass.Path
`destination` pathclass.Path `destination` pathclass.Path
@ -448,6 +425,7 @@ def copy_file(
source = pathclass.Path(source) source = pathclass.Path(source)
source.correct_case() source.correct_case()
source_bytes = source.size
if not source.is_file: if not source.is_file:
raise SourceNotFile(source) raise SourceNotFile(source)
@ -456,7 +434,11 @@ def copy_file(
destination = new_root(source, destination_new_root) destination = new_root(source, destination_new_root)
destination = pathclass.Path(destination) destination = pathclass.Path(destination)
callback_progress = callback_progress or do_nothing progressbar = progressbars.normalize(
progressbar,
topic=destination.absolute_path,
total=0 if dry_run else source_bytes,
)
callback_pre_copy = callback_pre_copy or do_nothing callback_pre_copy = callback_pre_copy or do_nothing
if destination.is_dir: if destination.is_dir:
@ -484,12 +466,9 @@ def copy_file(
return results return results
if dry_run: if dry_run:
if callback_progress is not None: progressbar.done()
callback_progress(destination, 0, 0)
return results return results
source_bytes = source.size
if callback_pre_copy(source, destination, dry_run=dry_run) is BAIL: if callback_pre_copy(source, destination, dry_run=dry_run) is BAIL:
return results return results
@ -551,7 +530,7 @@ def copy_file(
destination_handle.write(data_chunk) destination_handle.write(data_chunk)
results.written_bytes += data_bytes results.written_bytes += data_bytes
callback_progress(destination, results.written_bytes, source_bytes) progressbar.step(results.written_bytes)
if bytes_per_second is not None: if bytes_per_second is not None:
bytes_per_second.limit(data_bytes) bytes_per_second.limit(data_bytes)
@ -560,9 +539,7 @@ def copy_file(
chunk_time = time.perf_counter() - chunk_start chunk_time = time.perf_counter() - chunk_start
chunk_size = dynamic_chunk_sizer(chunk_size, chunk_time, IDEAL_CHUNK_TIME) chunk_size = dynamic_chunk_sizer(chunk_size, chunk_time, IDEAL_CHUNK_TIME)
if results.written_bytes == 0: progressbar.done()
# For zero-length files, we want to get at least one call in there.
callback_progress(destination, results.written_bytes, source_bytes)
# Fin # Fin
log.loud('Closing source handle.') log.loud('Closing source handle.')
@ -576,7 +553,7 @@ def copy_file(
if verify_hash: if verify_hash:
file_hash = _verify_hash( file_hash = _verify_hash(
destination, destination,
callback_progress=callback_verify_hash_progress, progressbar=verify_hash_progressbar,
hash_class=hash_class, hash_class=hash_class,
known_hash=results.hash.hexdigest(), known_hash=results.hash.hexdigest(),
known_size=source_bytes, known_size=source_bytes,
@ -631,8 +608,8 @@ def hash_file(
hash_class, hash_class,
*, *,
bytes_per_second=None, bytes_per_second=None,
callback_progress=None,
chunk_size='dynamic', chunk_size='dynamic',
progressbar=None,
): ):
''' '''
hash_class: hash_class:
@ -643,24 +620,26 @@ def hash_file(
an existing Ratelimiter object, or a string parseable by bytestring. an existing Ratelimiter object, or a string parseable by bytestring.
The bytestring BYTE, KIBIBYTE, etc constants may help. The bytestring BYTE, KIBIBYTE, etc constants may help.
callback_progress:
A function that takes three parameters:
path object, bytes ingested so far, bytes total
chunk_size: chunk_size:
An integer number of bytes to read at a time. An integer number of bytes to read at a time.
Or, the string 'dynamic' to enable dynamic chunk sizing that aims to Or, the string 'dynamic' to enable dynamic chunk sizing that aims to
keep a consistent pace of progress bar updates. keep a consistent pace of progress bar updates.
progressbar:
An instance from voussoirkit.progressbars.
''' '''
path = pathclass.Path(path) path = pathclass.Path(path)
path.assert_is_file() path.assert_is_file()
hasher = hash_class() hasher = hash_class()
bytes_per_second = limiter_or_none(bytes_per_second) bytes_per_second = limiter_or_none(bytes_per_second)
callback_progress = callback_progress or do_nothing progressbar = progressbars.normalize(
progressbar,
topic=path.absolute_path,
total=path.size,
)
checked_bytes = 0 checked_bytes = 0
file_size = path.size
handle = path.open('rb') handle = path.open('rb')
@ -680,7 +659,7 @@ def hash_file(
hasher.update(chunk) hasher.update(chunk)
checked_bytes += this_size checked_bytes += this_size
callback_progress(path, checked_bytes, file_size) progressbar.step(checked_bytes)
if bytes_per_second is not None: if bytes_per_second is not None:
bytes_per_second.limit(this_size) bytes_per_second.limit(this_size)
@ -689,6 +668,7 @@ def hash_file(
chunk_time = time.perf_counter() - chunk_start chunk_time = time.perf_counter() - chunk_start
chunk_size = dynamic_chunk_sizer(chunk_size, chunk_time, IDEAL_CHUNK_TIME) chunk_size = dynamic_chunk_sizer(chunk_size, chunk_time, IDEAL_CHUNK_TIME)
progressbar.done()
return hasher return hasher
def is_xor(*args): def is_xor(*args):