"""Miscellaneous utilities."""
import collections.abc
import io
import keyword
import platform
import types
import uuid
from .typing import Dict, Iterable, Iterator, List, Optional, Set, T, TextIO, Type, Union, overload
# backport contextlib.nullcontext for Python < 3.7
try:
from contextlib import nullcontext # pylint: disable=unused-import # novermin
except ImportError: # pragma: no cover
class nullcontext: # type: ignore[no-redef]
def __init__(self, enter_result: T = None) -> None: # type: ignore[assignment]
self.enter_result = enter_result # type: T
def __enter__(self) -> T:
return self.enter_result
def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException],
traceback: Optional[types.TracebackType]) -> None:
pass
is_windows = platform.system() == 'Windows'
@overload
def first_truthy(*args: T) -> Optional[T]:
... # pragma: no cover
@overload
def first_truthy(args: Iterable[T]) -> Optional[T]: # noqa: F811
... # pragma: no cover
[docs]def first_truthy(*args): # type: ignore[no-untyped-def] # noqa: F811
"""Return the first *truthy* value from a list of values.
Args:
*args: variable length argument list
* If one positional argument is provided, it should be an iterable of the values.
* If two or more positional arguments are provided, then the value list is the positional argument list.
Returns:
the first *truthy* value, if no *truthy* values found or sequence is empty, return :data:`None`
Raises:
TypeError: if no arguments provided
"""
if not args:
raise TypeError('no arguments provided')
if len(args) == 1:
args = args[0]
return next(filter(bool, args), None) # pylint: disable=filter-builtin-not-iterating
@overload
def first_non_none(*args: T) -> Optional[T]:
... # pragma: no cover
@overload
def first_non_none(args: Iterable[T]) -> Optional[T]: # noqa: F811
... # pragma: no cover
[docs]def first_non_none(*args): # type: ignore[no-untyped-def] # noqa: F811
"""Return the first non-:data:`None` value from a list of values.
Args:
*args: variable length argument list
* If one positional argument is provided, it should be an iterable of the values.
* If two or more positional arguments are provided, then the value list is the positional argument list.
Returns:
the first non-:data:`None` value, if all values are :data:`None` or sequence is empty, return :data:`None`
Raises:
TypeError: if no arguments provided
"""
if not args:
raise TypeError('no arguments provided')
if len(args) == 1:
args = args[0]
return next(filter(lambda x: x is not None, args), None) # pylint: disable=filter-builtin-not-iterating
[docs]class UUID4Generator:
"""UUID 4 generator wrapper to prevent UUID collisions."""
def __init__(self, dash: bool = True) -> None:
"""Constructor of UUID 4 generator wrapper.
Args:
dash: whether the generated UUID string has dashes or not
"""
self.used_uuids = set() # type: Set[str]
self.dash = dash
[docs] def gen(self) -> str:
"""Generate a new UUID 4 string that is guaranteed not to collide with used UUIDs.
Returns:
a new UUID 4 string
"""
while True:
new_uuid = uuid.uuid4()
nuid = str(new_uuid) if self.dash else new_uuid.hex
if nuid not in self.used_uuids: # pragma: no cover
break
self.used_uuids.add(nuid)
return nuid
[docs]class MakeTextIO:
"""Context wrapper class to handle :obj:`str` and *file* objects together.
Attributes:
obj (Union[str, TextIO]): the object to manage in the context
sio (Optional[StringIO]): the I/O object to manage in the context
only if :attr:`self.obj <MakeTextIO.obj>` is :obj:`str`
pos (Optional[int]): the original offset of :attr:`self.obj <MakeTextIO.obj>`,
only if :attr:`self.obj <MakeTextIO.obj>` is a seekable *file* object
"""
def __init__(self, obj: Union[str, TextIO]) -> None:
"""Initialize context.
Args:
obj: the object to manage in the context
"""
self.obj = obj
[docs] def __enter__(self) -> TextIO:
"""Enter context.
* If :attr:`self.obj <MakeTextIO.obj>` is :obj:`str`, a
:class:`~io.StringIO` will be created and returned.
* If :attr:`self.obj <MakeTextIO.obj>` is a seekable *file* object,
it will be seeked to the beginning and returned.
* If :attr:`self.obj <MakeTextIO.obj>` is an unseekable *file* object,
it will be returned directly.
"""
if isinstance(self.obj, str):
#: StringIO: the I/O object to manage in the context
#: only if :attr:`self.obj <MakeTextIO.obj>` is :obj:`str`
self.sio = io.StringIO(self.obj, newline='') # turn off newline translation # pylint: disable=W0201
return self.sio
if self.obj.seekable():
#: int: the original offset of :attr:`self.obj <MakeTextIO.obj>`,
#: only if :attr:`self.obj <MakeTextIO.obj>` is a seekable
#: :class:`TextIO <io.TextIOWrapper>`
self.pos = self.obj.tell() # pylint: disable=W0201
#: Union[str, TextIO]: the object to manage in the context
self.obj.seek(0)
return self.obj
[docs] def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException],
traceback: Optional[types.TracebackType]) -> None:
"""Exit context.
* If :attr:`self.obj <MakeTextIO.obj>` is :obj:`str`, the
:class:`~io.StringIO` (:attr:`self.sio <MakeTextIO.sio>`) will be closed.
* If :attr:`self.obj <MakeTextIO.obj>` is a seekable *file* object,
its stream position (:attr:`self.pos <MakeTextIO.pos>`) will be recovered.
"""
if isinstance(self.obj, str):
self.sio.close()
elif self.obj.seekable():
self.obj.seek(self.pos)
[docs]class Config(collections.abc.MutableMapping):
"""Configuration namespace.
This class is inspired from :class:`argparse.Namespace` for storing
internal attributes and/or configuration variables.
"""
def __init__(self, **kwargs: object) -> None:
for name, value in kwargs.items():
setattr(self, name, value)
def __contains__(self, key: object) -> bool:
return key in self.__dict__
def __iter__(self) -> Iterator[str]:
return iter(self.__dict__)
def __len__(self) -> int:
return len(self.__dict__)
def __getitem__(self, key: str) -> object:
return self.__dict__[key]
def __setitem__(self, key: str, value: object) -> None:
self.__dict__[key] = value
def __delitem__(self, key: str) -> None:
del self.__dict__[key]
def __eq__(self, other: object) -> bool:
return isinstance(other, Config) and self.__dict__ == other.__dict__
def __repr__(self) -> str:
type_name = type(self).__name__
arg_strings = [] # type: List[str]
star_args = {} # type: Dict[str, object]
for name, value in sorted(self.__dict__.items()):
if name.isidentifier() and not keyword.iskeyword(name) and name != '__debug__':
arg_strings.append('%s=%r' % (name, value))
else: # wrap invalid names into a dict to make __repr__ round-trip
star_args[name] = value
if star_args:
arg_strings.append('**%s' % repr(star_args))
return '%s(%s)' % (type_name, ', '.join(arg_strings))
__all__ = ['first_truthy', 'first_non_none', 'UUID4Generator', 'Config']