18

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 ?

kale
  • 151
  • 1
  • 10
Jonathan DEKHTIAR
  • 3,456
  • 1
  • 21
  • 42
  • Your proposed solution allows for no id to be passed in, I would check to make sure a single id has been passed in. – user3483203 Apr 12 '18 at 17:16

2 Answers2

28

You could write a decorator:

from typing import Callable, Dict
import functools
import warnings


def deprecated_alias(**aliases: str) -> Callable:
    """Decorator for deprecated function and method arguments.

    Use as follows:

    @deprecated_alias(old_arg='new_arg')
    def myfunc(new_arg):
        ...

    """

    def deco(f: Callable):
        @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: str, kwargs: Dict[str, Any], aliases: Dict[str, str]):
    """Helper function for deprecating function arguments."""
    for alias, new in aliases.items():
        if alias in kwargs:
            if new in kwargs:
                raise TypeError(
                    f"{func_name} received both {alias} and {new} as arguments!"
                    f" {alias} is deprecated, use {new} instead."
                )
            warnings.warn(
                message=(
                    f"`{alias}` is deprecated as an argument to `{func_name}`; use"
                    f" `{new}` instead."
                ),
                category=DeprecationWarning,
                stacklevel=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")
user2357112
  • 260,549
  • 28
  • 431
  • 505
  • 1
    @JoranBeasley: Deprecation warning added. – user2357112 Apr 12 '18 at 17:18
  • Actually, according to the OP it's the other way around. id_object is the new one, object_id is old and deprecated :D – shmee Apr 12 '18 at 17:22
  • @shmee: Oh, huh. `id_object` just seemed weird enough compared to `object_id` that I thought `object_id` was the new one. Edited. – user2357112 Apr 12 '18 at 17:25
  • @user2357112 oh, I can think of some scenarios where this new notation would seem preferable. Anyway, nice solution. I've read about the kw-only arguments but have not seen them in action before. That looks like a neat use of it. – shmee Apr 12 '18 at 17:30
  • Absolutely perfect, I will go with the decorator solution. Thank you very much – Jonathan DEKHTIAR Apr 12 '18 at 17:38
  • What if after `id_object` there are additional mandatory arguments? We'll have to turn them all into optional and verify that they're instantiated. This complicates the solution and makes it less neat. – Yael Ben-Haim Jan 26 '23 at 10:16
  • @YaelBen-Haim: The `@deprecated_alias` decorator doesn't actually need you to make `id_object` optional, or any arguments after it. – user2357112 Jan 26 '23 at 10:30
2

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)
Jacques Gaudin
  • 15,779
  • 10
  • 54
  • 75