"""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