1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189
|
"""Generic re-useable self-contained helper functions."""
import functools
import inspect
import itertools
import logging
import os
import pathlib
import typing
import warnings
__all__ = ['attach',
'mkdirs',
'mapping_items',
'promote_pathlike',
'promote_pathlike_directory',
'deprecate_positional_args']
log = logging.getLogger(__name__)
def attach(object: typing.Any, /, name: str) -> typing.Callable:
"""Return a decorator doing ``setattr(object, name)`` with its argument.
>>> spam = type('Spam', (object,), {})() # doctest: +NO_EXE
>>> @attach(spam, 'eggs')
... def func():
... pass
>>> spam.eggs # doctest: +ELLIPSIS
<function func at 0x...>
"""
def decorator(func):
setattr(object, name, func)
return func
return decorator
def mkdirs(filename: typing.Union[os.PathLike, str], /, *, mode: int = 0o777) -> None:
"""Recursively create directories up to the path of ``filename``
as needed."""
dirname = os.path.dirname(filename)
if not dirname:
return
log.debug('os.makedirs(%r)', dirname)
os.makedirs(dirname, mode=mode, exist_ok=True)
def mapping_items(mapping, /):
"""Return an iterator over the ``mapping`` items,
sort if it's a plain dict.
>>> list(mapping_items({'spam': 0, 'ham': 1, 'eggs': 2})) # doctest: +NO_EXE
[('eggs', 2), ('ham', 1), ('spam', 0)]
>>> from collections import OrderedDict
>>> list(mapping_items(OrderedDict(enumerate(['spam', 'ham', 'eggs']))))
[(0, 'spam'), (1, 'ham'), (2, 'eggs')]
"""
result = iter(mapping.items())
if type(mapping) is dict:
result = iter(sorted(result))
return result
@typing.overload
def promote_pathlike(filepath: typing.Union[os.PathLike, str], /) -> pathlib.Path:
"""Return path object for path-like-object."""
@typing.overload
def promote_pathlike(filepath: None, /) -> None:
"""Return None for None."""
@typing.overload
def promote_pathlike(filepath: typing.Union[os.PathLike, str, None], /,
) -> typing.Optional[pathlib.Path]:
"""Return path object or ``None`` depending on ``filepath``."""
def promote_pathlike(filepath: typing.Union[os.PathLike, str, None]
) -> typing.Optional[pathlib.Path]:
"""Return path-like object ``filepath`` promoted into a path object.
See also:
https://docs.python.org/3/glossary.html#term-path-like-object
"""
return pathlib.Path(filepath) if filepath is not None else None
def promote_pathlike_directory(directory: typing.Union[os.PathLike, str, None], /, *,
default: typing.Union[os.PathLike, str, None] = None,
) -> pathlib.Path:
"""Return path-like object ``directory`` promoted into a path object (default to ``os.curdir``).
See also:
https://docs.python.org/3/glossary.html#term-path-like-object
"""
return pathlib.Path(directory if directory is not None
else default or os.curdir)
def deprecate_positional_args(*,
supported_number: int,
ignore_arg: typing.Optional[str] = None,
category: typing.Type[Warning] = PendingDeprecationWarning,
stacklevel: int = 1):
"""Mark supported_number of positional arguments as the maximum.
Args:
supported_number: Number of positional arguments
for which no warning is raised.
ignore_arg: Name of positional argument to ignore.
category: Type of Warning to raise
or None to return a nulldecorator
returning the undecorated function.
stacklevel: See :func:`warning.warn`.
Returns:
Return a decorator raising a category warning
on more than supported_number positional args.
See also:
https://docs.python.org/3/library/exceptions.html#FutureWarning
https://docs.python.org/3/library/exceptions.html#DeprecationWarning
https://docs.python.org/3/library/exceptions.html#PendingDeprecationWarning
"""
assert supported_number >= 0, f'supported_number => 0: {supported_number!r}'
if category is None:
def nulldecorator(func):
"""Return the undecorated function."""
return func
return nulldecorator
assert issubclass(category, Warning)
stacklevel += 1
def decorator(func):
signature = inspect.signature(func)
argnames = [name for name, param in signature.parameters.items()
if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD]
check_number = supported_number
if ignore_arg is not None:
ignored = [name for name in argnames if name == ignore_arg]
assert ignored, 'ignore_arg must be a positional arg'
check_number += len(ignored)
qualification = f' (ignoring {ignore_arg}))'
else:
qualification = ''
deprecated = argnames[supported_number:]
assert deprecated
log.debug('deprecate positional args: %s.%s(%r)',
func.__module__, func.__qualname__, deprecated)
# mangle function name in message for this package
func_name = func.__name__.lstrip('_')
func_name, sep, rest = func_name.partition('_legacy')
assert func_name and (not sep or not rest)
s_ = 's' if supported_number > 1 else ''
@functools.wraps(func)
def wrapper(*args, **kwargs):
if len(args) > check_number:
call_args = zip(argnames, args)
supported = dict(itertools.islice(call_args, check_number))
deprecated = dict(call_args)
assert deprecated
wanted = ', '.join(f'{name}={value!r}'
for name, value in deprecated.items())
warnings.warn(f'The signature of {func_name} will be reduced'
f' to {supported_number} positional arg{s_}{qualification}'
f' {list(supported)}: pass {wanted} as keyword arg{s_}',
stacklevel=stacklevel,
category=category)
return func(*args, **kwargs)
return wrapper
return decorator
|