
The positional wrapper is fixed so that the argspec of the wrapped function is preserved.
104 lines
3.9 KiB
Python
104 lines
3.9 KiB
Python
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import inspect
|
|
import warnings
|
|
|
|
import pbr.version
|
|
import wrapt
|
|
|
|
__version__ = pbr.version.VersionInfo('positional').version_string()
|
|
|
|
|
|
class positional(object):
|
|
"""A decorator to enforce passing arguments as keywords.
|
|
|
|
When you have a function that takes a lot of arguments you expect people to
|
|
pass those arguments as keyword arguments. Python however does not enforce
|
|
this. In future then if you decide that you want to insert a new argument
|
|
or rearrange the arguments or transition to using **kwargs you break
|
|
compatibility with users of your code who (wrongly) gave you 20 positional
|
|
arguments.
|
|
|
|
In python 3 there is syntax to prevent this however we are not all in the
|
|
position where we can write python 3 exclusive code. Positional solves the
|
|
problem in the mean time across both pythons by enforcing that certain
|
|
arguments must be past as keyword arguments.
|
|
|
|
:param max_positional_args: the maixmum number of arguments that can be
|
|
passed to this function without keyword parameters. Defaults to
|
|
enforcing that every parameter with a default value must be passed as a
|
|
keyword argument.
|
|
:type max_positional_args int
|
|
|
|
:param enforcement: defines the way incorrect usage is reported. Currenlty
|
|
accepts :py:attr:`positional.EXCEPT` to raise a TypeError or
|
|
:py:attr:`positional.WARN` to show a warning. A warning can be useful
|
|
for applying to functions that are already public as a deprecation
|
|
notice. Defaults to :py:attr:`positional.EXCEPT`.
|
|
"""
|
|
|
|
EXCEPT = 'except'
|
|
WARN = 'warn'
|
|
|
|
def __init__(self, max_positional_args=None, enforcement=EXCEPT):
|
|
self._max_positional_args = max_positional_args
|
|
self._enforcement = enforcement
|
|
|
|
@classmethod
|
|
def method(cls, max_positional_args=None, enforcement=EXCEPT):
|
|
if max_positional_args is not None:
|
|
max_positional_args += 1
|
|
|
|
def f(func):
|
|
return cls(max_positional_args, enforcement)(func)
|
|
return f
|
|
|
|
@classmethod
|
|
def classmethod(cls, *args, **kwargs):
|
|
def f(func):
|
|
return classmethod(cls.method(*args, **kwargs)(func))
|
|
return f
|
|
|
|
def __call__(self, func):
|
|
if self._max_positional_args is None:
|
|
spec = inspect.getargspec(func)
|
|
self._max_positional_args = len(spec.args) - len(spec.defaults)
|
|
|
|
plural = '' if self._max_positional_args == 1 else 's'
|
|
|
|
@wrapt.decorator
|
|
def inner(wrapped, instance, args, kwargs):
|
|
|
|
# If called on an instance, adjust args len for the 'self'
|
|
# parameter.
|
|
args_len = len(args)
|
|
if instance:
|
|
args_len += 1
|
|
|
|
if args_len > self._max_positional_args:
|
|
message = ('%(name)s takes at most %(max)d positional '
|
|
'argument%(plural)s (%(given)d given)' %
|
|
{'name': wrapped.__name__,
|
|
'max': self._max_positional_args,
|
|
'given': args_len,
|
|
'plural': plural})
|
|
|
|
if self._enforcement == self.EXCEPT:
|
|
raise TypeError(message)
|
|
elif self._enforcement == self.WARN:
|
|
warnings.warn(message, DeprecationWarning, stacklevel=2)
|
|
|
|
return wrapped(*args, **kwargs)
|
|
|
|
return inner(func)
|