From c891984df7808260df1c1ceabcafcc84c0bf628e Mon Sep 17 00:00:00 2001 From: Benjamin Bean Date: Fri, 24 Jan 2025 15:40:21 -0700 Subject: [PATCH] small fixes and improvements to geometry and tools files --- opencsp/common/lib/tool/file_tools.py | 12 +++- opencsp/common/lib/tool/image_tools.py | 22 +----- opencsp/common/lib/tool/log_tools.py | 95 +++++++++++++++++++++++--- 3 files changed, 95 insertions(+), 34 deletions(-) diff --git a/opencsp/common/lib/tool/file_tools.py b/opencsp/common/lib/tool/file_tools.py index 026316531..b146f2dc0 100755 --- a/opencsp/common/lib/tool/file_tools.py +++ b/opencsp/common/lib/tool/file_tools.py @@ -35,9 +35,11 @@ def path_components(input_dir_body_ext: str): See also: body_ext_given_file_dir_body_ext() """ + dir = os.path.dirname(input_dir_body_ext) body_ext = os.path.basename(input_dir_body_ext) body, ext = os.path.splitext(body_ext) + return dir, body, ext @@ -591,6 +593,13 @@ def create_directories_if_necessary(input_dir): except FileExistsError: # probably just created this directory in another thread pass + except Exception as ex: + lt.error_and_raise( + RuntimeError, + "Error in file_tools.create_directories_if_necessary(): " + + f"failed to create directory '{input_dir}' with error {ex}", + ex, + ) def create_subdir_path(base_dir: str, dir_name: str): @@ -841,8 +850,7 @@ def rename_directory(input_dir: str, output_dir: str): def copy_and_delete_file(input_dir_body_ext: str, output_dir_body_ext: str, copystat=True): """ - Like rename_file(), but with more surety that the file will still exist in - case the computer crashes while the file is in the process of being moved. + Like rename_file(), but it works across file systems. See also: copy_file(), rename_file() diff --git a/opencsp/common/lib/tool/image_tools.py b/opencsp/common/lib/tool/image_tools.py index 9697d951c..8288d7c35 100644 --- a/opencsp/common/lib/tool/image_tools.py +++ b/opencsp/common/lib/tool/image_tools.py @@ -8,6 +8,7 @@ import sys from typing import Callable, TypeVar +import exiftool import numpy as np from PIL import Image @@ -301,28 +302,11 @@ def getsizeof_approx(img: Image) -> int: return object_size + image_data_size -def _import_exiftool(): - # TODO should exiftool be added to requirements.txt? - # This also requires the installation of Phil Harvey's ExifTool. - # https://pypi.org/project/PyExifTool/ - # https://exiftool.org/ - try: - import exiftool - except ImportError: - lt.error_and_raise(ImportError, "Error in image_tools._import_exiftool(): " + - "exiftool is not currently installed as a standard part of OpenCSP." + - " To use exif information with OpenCSP, please follow the installation instructions at " + - "https://pypi.org/project/PyExifTool/#getting-pyexiftool.") - - def get_exif_value( data_dir: str, image_path_name_exts: str | list[str], exif_val: str = "EXIF:ISO", parser: Callable[[str], T] = int ) -> T | list[T]: """Returns the exif_val Exif information on the given images, if they have such information. If not, then None is returned for those images.""" - _import_exiftool() - import exiftool - # build the list of files if isinstance(image_path_name_exts, str): files = [ft.join(data_dir, image_path_name_exts)] @@ -365,10 +349,6 @@ def get_exif_value( def set_exif_value(data_dir: str, image_path_name_ext: str, exif_val: str, exif_name: str = "EXIF:ISO"): - # TODO should exiftool be added to requirements.txt? - _import_exiftool() - import exiftool - with exiftool.ExifToolHelper() as et: et.set_tags( ft.join(data_dir, image_path_name_ext), diff --git a/opencsp/common/lib/tool/log_tools.py b/opencsp/common/lib/tool/log_tools.py index 68824637f..df28e35e9 100644 --- a/opencsp/common/lib/tool/log_tools.py +++ b/opencsp/common/lib/tool/log_tools.py @@ -9,7 +9,9 @@ import re import socket import sys -from typing import Callable +from typing import Callable, Literal + +import numpy as np # Don't import any other opencsp libraries here. Log tools _must_ be able to be # imported before any other opencsp code. Instead, if there are other @@ -327,12 +329,18 @@ def critical(*vargs, **kwargs) -> int: return 0 -def error_and_raise(exception_class: Exception.__class__, msg: str) -> None: +def error_and_raise(exception_class: Exception.__class__, msg: str, base_exception: Exception = None) -> None: """Logs the given message at the "error" level and raises the given exception, also with this message. - Args: - exception_class (Exception.__class__): An exception class. See below for built-in exception types. - msg (str): The message to go along with the exception. + Parameters + ---------- + exception_class: Exception.__class__ + An exception class. See below for built-in exception types. + msg: str + The message to go along with the exception. + base_exception: Exception + The exception that caused this code to be called, if any. This will be + added onto the newly created exception_class' history. Example:: @@ -411,19 +419,30 @@ def error_and_raise(exception_class: Exception.__class__, msg: str) -> None: """ msg = str(msg) # Ensure that message is a string, to enable concatenation. error(msg) + try: e = exception_class(msg) except Exception as exc: raise RuntimeError(msg) from exc - raise e + + if base_exception is not None: + raise e from base_exception + else: + raise e -def critical_and_raise(exception_class: Exception.__class__, msg: str) -> None: +def critical_and_raise(exception_class: Exception.__class__, msg: str, base_exception: Exception = None) -> None: """Logs the given message at the "critical" level and raises the given exception, also with this message. - Args: - exception_class (Exception.__class__): An exception class. See error_and_raise() for a description of built-in exceptions. - msg (str): The message to go along with the exception. + Parameters + ---------- + exception_class: Exception.__class__ + An exception class. See error_and_raise() for a description of built-in exceptions. + msg: str + The message to go along with the exception. + base_exception: Exception + The exception that caused this code to be called, if any. This will be + added onto the newly created exception_class' history. Example:: @@ -433,11 +452,16 @@ def critical_and_raise(exception_class: Exception.__class__, msg: str) -> None: """ msg = str(msg) # Ensure that message is a string, to enable concatenation. critical(msg) + try: e = exception_class(msg) except Exception as exc: raise RuntimeError(msg) from exc - raise e + + if base_exception is not None: + raise e from base_exception + else: + raise e def log_and_raise_value_error(local_logger, msg) -> None: @@ -452,3 +476,52 @@ def log_and_raise_value_error(local_logger, msg) -> None: """ error(msg) raise ValueError(msg) + + +def log_progress( + percentage: int | float, carriage_return: bool | Literal['auto'] = 'auto', prev_percentage: int = None +): + """Prints the current progress as a progress bar and number. + + Parameters + ---------- + percentage : int | float + The current progress. If an integer, than the range is clipped to 0-100. + If a float, then the range is clipped to 0-1, unless >1 then it is cast + to an integer. + carriage_return : bool | 'auto', optional + If True, then a carriage return '\r' is printed instead of a newline, which will cause the next line printed to overwrite this line. + This can be used to "draw" the progress interactively in the terminal. + If 'auto', then this will be True when percentage != 100. + By default 'auto'. + prev_percentage: int, optional + If not None, then this is compared to the given percentage. If they are the same then nothing is printed. + + Returns + ------- + percentage: int + The value printed, in the range 0-100. Can be passed into the next call as prev_percentage. + """ + if isinstance(percentage, int): + percentage = int(np.clip(percentage, 0, 100)) + if prev_percentage is not None: + if prev_percentage == percentage: + # don't print again + return percentage + + if carriage_return == 'auto': + carriage_return = percentage != 100 + + sval = "|" + ("=" * percentage) + (" " * (100 - percentage)) + f"| {percentage}%" + if carriage_return: + info(sval, end='\r') + else: + info(sval) + + return percentage + + else: # isinstance(percentage, float) + if percentage > 1.0: + return log_progress(int(np.round(percentage)), carriage_return, prev_percentage) + else: + return log_progress(int(np.round(percentage * 100)), carriage_return, prev_percentage)