21935: Merge arvados.timer into arvados._internal
[arvados.git] / sdk / python / arvados / _internal / __init__.py
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: Apache-2.0
4 """Arvados internal utilities
5
6 Everything in `arvados._internal` is support code for the Arvados Python SDK
7 and tools. Nothing in this module is intended to be part of the public-facing
8 SDK API. Classes and functions in this module may be changed or removed at any
9 time.
10 """
11
12 import functools
13 import re
14 import time
15 import warnings
16
17 class Timer:
18     def __init__(self, verbose=False):
19         self.verbose = verbose
20
21     def __enter__(self):
22         self.start = time.time()
23         return self
24
25     def __exit__(self, *args):
26         self.end = time.time()
27         self.secs = self.end - self.start
28         self.msecs = self.secs * 1000  # millisecs
29         if self.verbose:
30             print('elapsed time: %f ms' % self.msecs)
31
32
33 def deprecated(version=None, preferred=None):
34     """Mark a callable as deprecated in the SDK
35
36     This will wrap the callable to emit as a DeprecationWarning
37     and add a deprecation notice to its docstring.
38
39     If the following arguments are given, they'll be included in the
40     notices:
41
42     * preferred: str | None --- The name of an alternative that users should
43       use instead.
44
45     * version: str | None --- The version of Arvados when the callable is
46       scheduled to be removed.
47     """
48     if version is None:
49         version = ''
50     else:
51         version = f' and scheduled to be removed in Arvados {version}'
52     if preferred is None:
53         preferred = ''
54     else:
55         preferred = f' Prefer {preferred} instead.'
56     def deprecated_decorator(func):
57         fullname = f'{func.__module__}.{func.__qualname__}'
58         parent, _, name = fullname.rpartition('.')
59         if name == '__init__':
60             fullname = parent
61         warning_msg = f'{fullname} is deprecated{version}.{preferred}'
62         @functools.wraps(func)
63         def deprecated_wrapper(*args, **kwargs):
64             warnings.warn(warning_msg, DeprecationWarning, 2)
65             return func(*args, **kwargs)
66         # Get func's docstring without any trailing newline or empty lines.
67         func_doc = re.sub(r'\n\s*$', '', func.__doc__ or '')
68         match = re.search(r'\n([ \t]+)\S', func_doc)
69         indent = '' if match is None else match.group(1)
70         warning_doc = f'\n\n{indent}.. WARNING:: Deprecated\n{indent}   {warning_msg}'
71         # Make the deprecation notice the second "paragraph" of the
72         # docstring if possible. Otherwise append it.
73         docstring, count = re.subn(
74             rf'\n[ \t]*\n{indent}',
75             f'{warning_doc}\n\n{indent}',
76             func_doc,
77             count=1,
78         )
79         if not count:
80             docstring = f'{func_doc.lstrip()}{warning_doc}'
81         deprecated_wrapper.__doc__ = docstring
82         return deprecated_wrapper
83     return deprecated_decorator