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
|
"""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_argnames: typing.Sequence[str] = ('cls', 'self'),
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_argnames: Name(s) of arguments to ignore
('cls' and 'self' by default).
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 at least one: {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
and name not in ignore_argnames]
log.debug('deprecate positional args: %s.%s(%r)',
func.__module__, func.__qualname__,
argnames[supported_number:])
@functools.wraps(func)
def wrapper(*args, **kwargs):
if len(args) > supported_number:
call_args = zip(argnames, args)
supported = itertools.islice(call_args, supported_number)
supported = dict(supported)
deprecated = dict(call_args)
assert deprecated
func_name = func.__name__.lstrip('_')
func_name, sep, rest = func_name.partition('_legacy')
assert not set or not rest
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'
f"{'s' if supported_number > 1 else ''}"
f' {list(supported)}: pass {wanted}'
' as keyword arg(s)',
stacklevel=stacklevel,
category=category)
return func(*args, **kwargs)
return wrapper
return decorator
|