Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to implement deprecation in python with argument alias

We are developing a python library and would like to change the way some function arguments are named in some functions.

We would like to keep backward compatibility and thus we would like to find a way to create alias for function arguments.

Here is an example:

Old Version:

class MyClass(object):
  def __init__(self, object_id):
    self.id = object_id

New Version:

class MyClass(object):
  def __init__(self, id_object):
    self.id = id_object

How can we make the class to be compatible with both calling ways:

object1 = MyClass(object_id=1234)
object2 = MyClass(id_object=1234)

I could of course create something like this:

class MyClass(object):
  def __init__(self, object_id=None, id_object=None):
    if id_object is not None:
      self.id = id_object
    else:
      self.id = object_id

However, it would change the number of arguments and we strictly want to avoid this.

Is there any way to declare a method alias or an argument alias ?

like image 976
Jonathan DEKHTIAR Avatar asked Apr 12 '18 17:04

Jonathan DEKHTIAR


2 Answers

You could write a decorator:

import functools
import warnings

def deprecated_alias(**aliases):
    def deco(f):
        @functools.wraps(f)
        def wrapper(*args, **kwargs):
            rename_kwargs(f.__name__, kwargs, aliases)
            return f(*args, **kwargs)
        return wrapper
    return deco

def rename_kwargs(func_name, kwargs, aliases):
    for alias, new in aliases.items():
        if alias in kwargs:
            if new in kwargs:
                raise TypeError('{} received both {} and {}'.format(
                    func_name, alias, new))
            warnings.warn('{} is deprecated; use {}'.format(alias, new),
                          DeprecationWarning,
                          3)
            kwargs[new] = kwargs.pop(alias)

class MyClass(object):
    @deprecated_alias(object_id='id_object')
    def __init__(self, id_object):
        self.id = id_object

Alternatively, since you're on Python 3, you can make object_id a keyword-only argument:

import warnings

class MyClass(object):
    #                                  v Look here
    def __init__(self, id_object=None, *, object_id=None):
        if id_object is not None and object_id is not None:
            raise TypeError("MyClass received both object_id and id_object")
        elif id_object is not None:
            self.id = id_object
        elif object_id is not None:
            warnings.warn("object_id is deprecated; use id_object", DeprecationWarning, 2)
            self.id = object_id
        else:
            raise TypeError("MyClass missing id_object argument")
like image 156
user2357112 supports Monica Avatar answered Oct 01 '22 04:10

user2357112 supports Monica


Another way, less elegant than user2357112's answer, would be to use a metaclass factory:

import warnings

def Deprecation(deprec):

    class DeprecMeta(type):

        def __call__(cls, *args, **kwargs):
            new_kwargs = {k : v for k, v in kwargs.items() if k not in deprec}
            for k in deprec:
                if k in kwargs:
                    warnings.warn("{0}: Deprecated call with '{0}'. Use '{1}' instead".format(
                            cls.__name__, k, deprec[k]))
                    if deprec[k] not in kwargs:
                        new_kwargs[deprec[k]] = kwargs[k]
                    else:
                        raise TypeError('{} received both {} and {}'.format(
                                cls.__name__, k, deprec[k]))

            obj = cls.__new__(cls, *args, **new_kwargs)
            obj.__init__(*args, **new_kwargs)
            return obj

    return DeprecMeta

class MyClass(object, metaclass = Deprecation({'object_id': 'id_object'})):

    def __init__(self, id_object):
        self.id = id_object

object2 = MyClass(object_id=1234)
like image 45
Jacques Gaudin Avatar answered Oct 01 '22 02:10

Jacques Gaudin