# Copyright (C) The Arvados Authors. All rights reserved. # # SPDX-License-Identifier: Apache-2.0 """Arvados internal utilities Everything in `arvados._internal` is support code for the Arvados Python SDK and tools. Nothing in this module is intended to be part of the public-facing SDK API. Classes and functions in this module may be changed or removed at any time. """ import functools import operator import re import time import warnings import typing as t HT = t.TypeVar('HT', bound=t.Hashable) class Timer: def __init__(self, verbose=False): self.verbose = verbose def __enter__(self): self.start = time.time() return self def __exit__(self, *args): self.end = time.time() self.secs = self.end - self.start self.msecs = self.secs * 1000 # millisecs if self.verbose: print('elapsed time: %f ms' % self.msecs) def deprecated(version=None, preferred=None): """Mark a callable as deprecated in the SDK This will wrap the callable to emit as a DeprecationWarning and add a deprecation notice to its docstring. If the following arguments are given, they'll be included in the notices: * preferred: str | None --- The name of an alternative that users should use instead. * version: str | None --- The version of Arvados when the callable is scheduled to be removed. """ if version is None: version = '' else: version = f' and scheduled to be removed in Arvados {version}' if preferred is None: preferred = '' else: preferred = f' Prefer {preferred} instead.' def deprecated_decorator(func): fullname = f'{func.__module__}.{func.__qualname__}' parent, _, name = fullname.rpartition('.') if name == '__init__': fullname = parent warning_msg = f'{fullname} is deprecated{version}.{preferred}' @functools.wraps(func) def deprecated_wrapper(*args, **kwargs): warnings.warn(warning_msg, DeprecationWarning, 2) return func(*args, **kwargs) # Get func's docstring without any trailing newline or empty lines. func_doc = re.sub(r'\n\s*$', '', func.__doc__ or '') match = re.search(r'\n([ \t]+)\S', func_doc) indent = '' if match is None else match.group(1) warning_doc = f'\n\n{indent}.. WARNING:: Deprecated\n{indent} {warning_msg}' # Make the deprecation notice the second "paragraph" of the # docstring if possible. Otherwise append it. docstring, count = re.subn( rf'\n[ \t]*\n{indent}', f'{warning_doc}\n\n{indent}', func_doc, count=1, ) if not count: docstring = f'{func_doc.lstrip()}{warning_doc}' deprecated_wrapper.__doc__ = docstring return deprecated_wrapper return deprecated_decorator def parse_seq( s: str, split: t.Callable[[str], t.Iterable[str]]=operator.methodcaller('split', ','), clean: t.Callable[[str], str]=operator.methodcaller('strip'), check: t.Callable[[str], bool]=bool, ) -> t.Iterator[str]: """Split, clean, and filter a string into multiple items The default arguments split on commas, strip substrings, and skip empty items. """ return (word for substr in split(s) if check(word := clean(substr))) def uniq(it: t.Iterable[HT]) -> t.Iterator[HT]: """Yield only unique items from an iterable The items must be hashable. """ seen = set() for item in it: if item not in seen: seen.add(item) yield item