Source code for rads.utility

"""Utility functions."""

import io
import os
from typing import IO, Any, List, Optional, Union, cast

from wrapt import ObjectProxy  # type: ignore

from .typing import PathLike, PathLikeOrFile

__all__ = [
    "ensure_open",
    "filestring",
    "isio",
    "xor",
    "contains_sublist",
    "merge_sublist",
    "delete_sublist",
    "fortran_float",
]


class _NoCloseIOWrapper(ObjectProxy):  # type: ignore
    def __exit__(self, *args: object, **kwargs: object) -> None:
        pass

    def close(self) -> None:
        pass


[docs]def ensure_open( file: PathLikeOrFile, mode: str = "r", buffering: int = -1, encoding: Optional[str] = None, errors: Optional[str] = None, newline: Optional[str] = None, closefd: bool = True, closeio: bool = False, ) -> IO[Any]: """Open file or leave file-like object open. This function behaves identically to :func:`open` but can also accept a file-like object in the `file` parameter. :param file: A path-like object giving the pathname (absolute or relative to the current working directory) of the file to be opened or an integer file descriptor of the file to be wrapped or a file-like object. .. note:: If a file descriptor is given, it is closed when the returned I/O object is closed, unless `closefd` is set to False. .. note:: If a file-like object is given closing the returned I/O object will not close the given file unless `closeio` is set to True. :param mode: See :func:`open` :param buffering: See :func:`open` :param encoding: See :func:`open` :param errors: See :func:`open` :param newline: See :func:`open` :param closefd: See :func:`open` :param closeio: If set to True then if `file` is a file like object it will be closed when either the __exit__ or close methods are called on the returned I/O object. By default these methods will be ignored when `file` is a file-like object. :return: An I/O object or the original file-like object if `file` is a file-like object. If this is the original file-like object and `closeio` is set to False (the default) then it's close and __exit__ methods will be no-ops. .. seealso:: :func:`open` """ if hasattr(file, "read"): if not closeio: return cast(IO[Any], _NoCloseIOWrapper(file)) return cast(IO[Any], file) return open( cast(Union[PathLike, int], file), mode, buffering, encoding, errors, newline, closefd, )
[docs]def filestring(file: PathLikeOrFile) -> Optional[str]: """Convert a PathLikeOrFile to a string. :param file: file or file-like object to get the string for. :return: The string representation of the filename or path. If it cannot get the name/path of the given file or file-like object or cannot convert it to a str, None will be returned. """ if isinstance(file, int): return None if hasattr(file, "read"): try: return cast(IO[Any], file).name except AttributeError: return None if not isinstance(file, (str, bytes)): file = os.fspath(cast(PathLike, file)) if isinstance(file, str): return file if isinstance(file, bytes): try: return cast(bytes, file).decode("utf-8") except UnicodeDecodeError: return None raise TypeError(f"'{type(file)}' is not a file like object")
[docs]def isio(obj: Any, *, read: bool = False, write: bool = False) -> bool: """Determine if object is IO like and is read and/or write. .. note:: Falls back to :code:`isinstnace(obj, io.IOBase)` if neither `read` nor `write` is True. :param obj: Object to check if it is an IO like object. :param read: Require `obj` to be readable if True. :param write: Require `obj` to be writable if True. :return: True if the given `obj` is readable and/or writeable as defined by the `read` and `write` arguments. """ if read or write: return (not read or hasattr(obj, "read")) and ( not write or hasattr(obj, "write") ) return isinstance(obj, io.IOBase)
[docs]def xor(a: bool, b: bool) -> bool: """Boolean XOR operator. This implements the XOR boolean operator and has the following truth table: ===== ===== ======= a b a XOR b ===== ===== ======= True True False True False True False True True False False False ===== ===== ======= :param a: First boolean value. :param b: Second boolean value. :return: The result of `a` XOR `b` from the truth table above. """ return (a and not b) or (not a and b)
[docs]def contains_sublist(list_: List[Any], sublist: List[Any]) -> bool: """Determine if a `list` contains a `sublist`. :param list_: list to search for the `sublist` in. :param sublist: Sub list to search for. :return: True if `list` contains `sublist`. """ # Adapted from: https://stackoverflow.com/a/12576755 if not sublist: return False for i in range(len(list_)): if list_[i] == sublist[0] and list_[i : i + len(sublist)] == sublist: return True return False
[docs]def merge_sublist(list_: List[Any], sublist: List[Any]) -> List[Any]: """Merge a `sublist` into a given `list_`. :param list_: List to merge `sublist` into. :param sublist: Sublist to merge into `list_` :return: A copy of `list_` with `sublist` at the end if `sublist` is not a sublist of `list_`. Otherwise, a copy of `list_` is returned unchanged. """ if contains_sublist(list_, sublist): return list_[:] return list_ + sublist
[docs]def delete_sublist(list_: List[Any], sublist: List[Any]) -> List[Any]: """Remove a `sublist` from the given `list_`. :param list_: List to remove the `sublist` from. :param sublist: Sublist to remove from `list_`. :return: A copy of `list_` with the `sublist` removed. """ if not sublist: return list_[:] for i in range(len(list_)): if list_[i] == sublist[0] and list_[i : i + len(sublist)] == sublist: return list_[:i] + list_[i + len(sublist) :] return list_[:]
[docs]def fortran_float(string: str) -> float: """Construct :class:`float` from Fortran style float strings. This function can convert strings to floats in all of the formats below: * ``3.14e10`` (also parsable with :class:`float`) * ``3.14E10`` (also parsable with :class:`float`) * ``3.14d10`` * ``3.14D10`` * ``3.14e+10`` (also parsable with :class:`float`) * ``3.14E+10`` (also parsable with :class:`float`) * ``3.14d+10`` * ``3.14D+10`` * ``3.14e-10`` (also parsable with :class:`float`) * ``3.14E-10`` (also parsable with :class:`float`) * ``3.14d-10`` * ``3.14D-10`` * ``3.14+100`` * ``3.14-100`` .. note:: Because RADS was written in Fortran, exponent characters in configuration and passindex files sometimes use 'D' or 'd' as the exponent separator instead of 'E' or 'e'. .. warning:: If you are Fortran developer stop using 'Ew.d' and 'Ew.dDe' formats and use 'Ew.dEe' instead. The first two are not commonly supported by other languages while the last version is the standard for nearly all languages. Ok, rant over. :param string: String to attempt to convert to a float. :return: The float parsed from the given `string`. :raises ValueError: If `string` does not represent a valid float. """ try: return float(string) except ValueError as err: try: return float(string.replace("d", "e").replace("D", "E")) except ValueError: try: return float(string.replace("+", "e+").replace("-", "e-")) except ValueError: raise err