Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make a dict key deprecated?

Issue

Let's say I have a function in python that returns a dict with some objects.

class MyObj:
    pass

def my_func():
    o = MyObj()
    return {'some string' : o, 'additional info': 'some other text'}

At some point I notice that it would make sense to rename the key 'some string' as it is misleading and not well describing what is actually stored with this key. However, if I were to just change the key, people who use this bit of code, would be really annoyed because I didn't give them time via a deprecation period to adapt their code.

Current attempt

So the way I thought about implementing a deprecation warning is using a thin wrapper around dict:

from warnings import warn

class MyDict(dict):
    def __getitem__(self, key):
        if key == 'some string':
             warn('Please use the new key: `some object` instead of `some string`')
        return super().__getitem__(key)

This way I can create the dict with the old and the new key pointing towards the same object

class MyObj:
    pass

def my_func():
    o = MyObj()
    return MyDict({'some string' : o, 'some object' : o, 'additional info': 'some other text'})

Questions:

  • What are potential ways code might break if I add this change?
  • Is there an easier (as in, less amount of changes, using existing solutions, using common patterns) way to achieve this?
like image 229
NOhs Avatar asked Jan 08 '19 15:01

NOhs


1 Answers

To be honest I do not think there is something particularly wrong or an anti-pattern with your solution, except for the fact that my_func has to duplicate each deprecated key with its replacement (see below).

You could even generalize it a bit (in case you will decide to deprecate other keys):

class MyDict(dict):
    old_keys_to_new_keys = {'some string': 'some object'}
    def __getitem__(self, key):
        if key in self.old_keys_to_new_keys:
            msg = 'Please use the new key: `{}` instead of `{}`'.format(self.old_keys_to_new_keys[key], key)
            warn(msg)
        return super().__getitem__(key)

class MyObj:
    pass

def my_func():
    o = MyObj()
    return MyDict({'some string' : o, 'some object': o, 'additional info': 'some other text'})

Then

>> my_func()['some string'])
UserWarning: Please use the new key: `some object` instead of `some string`

All you have to do now in order to "deprecate" more keys is update old_keys_to_new_keys.

However,

note how my_func has to duplicate each deprecated key with its replacement. This violates the DRY principle and will clutter the code if and when you do need to deprecate more keys (and you will have to remember to update both MyDict.old_keys_to_new_keys and my_func). If I may quote Raymond Hettinger:

There must be a better way

This can be remedied with the following changes to __getitem__:

def __getitem__(self, old_key):
    if old_key in self.old_keys_to_new_keys:
        new_key = self.old_keys_to_new_keys[old_key]
        msg = 'Please use the new key: `{}` instead of `{}`'.format(new_key, old_key)
        warn(msg)
        self[old_key] = self[new_key]  # be warned - this will cause infinite recursion if
                                       # old_key == new_key but that should not really happen
                                       # (unless you mess up old_keys_to_new_keys)
    return super().__getitem__(old_key)

Then my_func can only use the new keys:

def my_func():
    o = MyObj()
    return MyDict({'some object': o, 'additional info': 'some other text'})

The behavior is the same, any code using the deprecated keys will get the warning (and, of course, accessing the new keys work):

print(my_func()['some string'])
# UserWarning: Please use the new key: `some object` instead of `some string`
# <__main__.MyObj object at 0x000002FBFF4D73C8>
print(my_func()['some object'])
# <__main__.MyObj object at 0x000002C36FCA2F28>
like image 74
DeepSpace Avatar answered Sep 30 '22 16:09

DeepSpace