0

Some python standard classes are slots, like datetime.datetime. This is not something I can change, and a lot of libraries expect datetime object.

I wanted to change the default __format__ method of an existing datetime object, but unfortunately, since this is a slot classes, it's forbidden:

In [10]: import datetime

In [11]: datetime.datetime.now().__format__ = lambda s, f: ''
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-11-c98141136d9d> in <module>()
----> 1 datetime.datetime.now().__format__ = lambda s, f: ''

AttributeError: 'datetime.datetime' object attribute '__format__' is read-only

Is it possible to abuse the dynamic nature of python to achieve this? I guess so.

Adrien Clerc
  • 2,636
  • 1
  • 21
  • 26

1 Answers1

0

Here is my solution:

def make_extendable(o):
    """
    Return an object that can be extended via its __dict__
    If it is a slot, the object type is copied and the object is pickled through
    this new type, before returning it.

    If there is already a __dict__, then the object is returned.
    """
    if getattr(o, "__dict__", None) is not None:
        return o

    # Now for fun
    # Don't take care of immutable types or constant for now
    import copy
    import copyreg

    cls = o.__class__
    new_cls = type(cls.__name__, (cls,), {"__module__": cls.__module__})
    # Support only Python >= 3.4
    pick = o.__reduce_ex__(4)
    if pick[0] == cls:
        # This is the case for datetime objects
        pick = (new_cls, *pick[1:])
    elif pick[0] in (copyreg.__newobj__, copyreg.__newobj_ex__):
        # Now the second item in pick is (cls, )
        # It should be rare though, it's only for slots
        pick = (pick[0], (new_cls,), *pick[2:])
    else:
        return ValueError(f"Unable to extend {o} of type {type(o)}")

    # Build new type
    return copy._reconstruct(o, None, *pick)

It basically do the following:

  1. Test if the object already has a __dict__. In this case, there is nothing to do.
  2. Create a new type, based on the provided object type. This new type is not a slot class, and try to mimics the base type as much as possible.
  3. Reduce the provided object as done in copy.copy, but only supports __reduce_ex__(4) for simplicity.
  4. Modify the reduced version to use the newly created type.
  5. Unpickle the new object using the modified reduced version.

The result for datetime:

In [13]: d = make_extendable(datetime.datetime.now())

In [14]: d
Out[14]: datetime(2019, 3, 29, 11, 24, 23, 285875)

In [15]: d.__class__.__mro__
Out[15]: (datetime.datetime, datetime.datetime, datetime.date, object)

In [16]: d.__str__ = lambda: 'Hello, world'

In [17]: d.__str__()
Out[17]: 'Hello, world'

Caveats

In random order:

  • Some types may not be reduced.
  • The returned object is a copy, not the initial one.
  • The class is not the same, but isinstance(d, datetime.datetime) will be True.
  • The class hierarchy will betray the hack.
  • It may be incredibly slow.
  • __format__ is a bit special, because you need to change the class instance, instead of the bound method because of how format works.
  • <Insert Your Negative Critic Here>.
Adrien Clerc
  • 2,636
  • 1
  • 21
  • 26