# Copyright (c) 2018 Hubert Kario # # See the LICENSE file for legal information regarding use of this file. """Methods for deprecating old names for arguments or attributes.""" import warnings from functools import wraps def deprecated_params(names, warn="Param name '{old_name}' is deprecated, " "please use '{new_name}'"): """Decorator to translate obsolete names and warn about their use. :param dict names: dictionary with pairs of new_name: old_name that will be used for translating obsolete param names to new names :param str warn: DeprecationWarning format string for informing the user what is the current parameter name, uses 'old_name' for the deprecated keyword name and 'new_name' for the current one. Example: "Old name: {old_name}, use {new_name} instead". """ def decorator(func): @wraps(func) def wrapper(*args, **kwargs): for new_name, old_name in names.items(): if old_name in kwargs: if new_name in kwargs: raise TypeError("got multiple values for keyword " "argument '{0}'".format(new_name)) warnings.warn(warn.format(old_name=old_name, new_name=new_name), DeprecationWarning, stacklevel=2) kwargs[new_name] = kwargs.pop(old_name) return func(*args, **kwargs) return wrapper return decorator def deprecated_instance_attrs(names, warn="Attribute '{old_name}' is deprecated, " "please use '{new_name}'"): """Decorator to deprecate class instance attributes. Translates all names in `names` to use new names and emits warnings if the translation was necessary. Does apply only to instance variables and attributes (won't modify behaviour of class variables, static methods, etc. :param dict names: dictionary with paris of new_name: old_name that will be used to translate the calls :param str warn: DeprecationWarning format string for informing the user what is the current parameter name, uses 'old_name' for the deprecated keyword name and 'new_name' for the current one. Example: "Old name: {old_name}, use {new_name} instead". """ # reverse the dict as we're looking for old attributes, not new ones names = dict((j, i) for i, j in names.items()) def decorator(clazz): def getx(self, name, __old_getx=getattr(clazz, "__getattr__", None)): if name in names: warnings.warn(warn.format(old_name=name, new_name=names[name]), DeprecationWarning, stacklevel=2) return getattr(self, names[name]) if __old_getx: if hasattr(__old_getx, "__func__"): return __old_getx.__func__(self, name) return __old_getx(self, name) raise AttributeError("'{0}' object has no attribute '{1}'" .format(clazz.__name__, name)) getx.__name__ = "__getattr__" clazz.__getattr__ = getx def setx(self, name, value, __old_setx=getattr(clazz, "__setattr__")): if name in names: warnings.warn(warn.format(old_name=name, new_name=names[name]), DeprecationWarning, stacklevel=2) setattr(self, names[name], value) else: __old_setx(self, name, value) setx.__name__ = "__setattr__" clazz.__setattr__ = setx def delx(self, name, __old_delx=getattr(clazz, "__delattr__")): if name in names: warnings.warn(warn.format(old_name=name, new_name=names[name]), DeprecationWarning, stacklevel=2) delattr(self, names[name]) else: __old_delx(self, name) delx.__name__ = "__delattr__" clazz.__delattr__ = delx return clazz return decorator def deprecated_attrs(names, warn="Attribute '{old_name}' is deprecated, " "please use '{new_name}'"): """Decorator to deprecate all specified attributes in class. Translates all names in `names` to use new names and emits warnings if the translation was necessary. Note: uses metaclass magic so is incompatible with other metaclass uses :param dict names: dictionary with paris of new_name: old_name that will be used to translate the calls :param str warn: DeprecationWarning format string for informing the user what is the current parameter name, uses 'old_name' for the deprecated keyword name and 'new_name' for the current one. Example: "Old name: {old_name}, use {new_name} instead". """ # prepare metaclass for handling all the class methods, class variables # and static methods (as they don't go through instance's __getattr__) class DeprecatedProps(type): pass metaclass = deprecated_instance_attrs(names, warn)(DeprecatedProps) def wrapper(cls): cls = deprecated_instance_attrs(names, warn)(cls) # apply metaclass orig_vars = cls.__dict__.copy() slots = orig_vars.get('__slots__') if slots is not None: if isinstance(slots, str): slots = [slots] for slots_var in slots: orig_vars.pop(slots_var) orig_vars.pop('__dict__', None) orig_vars.pop('__weakref__', None) return metaclass(cls.__name__, cls.__bases__, orig_vars) return wrapper