Skip to content Skip to sidebar Skip to footer

How To Register Implementation Of Abc.mutablemapping As A Dict Subclass?

I would like my SpreadSheet class below to be considered a dict subclass by the isinstance() built-in, but when I try to register it as such, an AttributeError Exception is thrown

Solution 1:

So, first things first, the "obvious way to do it", is to have a Json Encoder with a default method that would create a dict out of a CustomDict class while serializing:

Given

from collections.abc import MutableMapping
import json


classIdentaDict(MutableMapping):
    __getitem__ = lambda s, i: i
    __setitem__ = lambda s, i, v: None
    __delitem__ = lambda s, i: None
    __len__ = lambda s: 1
    __iter__ = lambda s: iter(['test_value'])

defdefault(obj):
    ifisinstance(obj, MutableMapping):
            returndict(obj)
    raise TypeError()

print(json.dumps(IdentaDict, default=default)

will just work.

second

If for some reason, this is not desirable (maybe creating a dict out of the CustomDict is not feasible, or would be too expensive), it is possible to monkeypatch the machinery of Python's json.encoder, so that it uses the appropriate call to isinstance:

from collections.abc import MutableMapping
from functools import partial
from unittest.mock import patch

import json

classIdentaDict(MutableMapping):
   ...

a = IdentaDict()

new_iterencoder = partial(
    json.encoder._make_iterencode,
    isinstance=lambda obj, cls: isinstance(obj, MutableMapping if cls == dictelse cls)
)

with patch("json.encoder.c_make_encoder", None), patch("json.encoder._make_iterencode", new_iterencoder):
    print(json.dumps(a))

(Note that while at it, I also disabled the native C encoder, so that the "pass indent to force Python encoder" hack is not needed. One never knows when an eager Python volunteer will implement indent in the C Json serializer and break that)

Also, the "mock.patch" thing is only needed if one plays mr. RightGuy and is worried about restoring the default behavior. Otherwise, just overriding both members of json.encoder in the application setup will make the changes proccess wide, and working for all json.dump[s] call, no changes needed to the calls - which might be more convenient.

third

Now, answering the actual question: what is possible is to have a mechanism that will create an actual subclass of "dict", but implementing all the methods needed by dict. Instead of re-doing the work done by collections.abc.MutableClass, it should be ok to just copy over both user methods and generated methods to the dict class:

import json
from abc import ABCMeta
from collections.abc import MutableMapping

classRealBase(ABCMeta):
    def__new__(mcls, name, bases, namespace, *, realbase=dict, **kwargs):
        abc_cls = super().__new__(mcls, name, bases, namespace, **kwargs)
        for attr_name indir(abc_cls):
            attr = getattr(abc_cls, attr_name)
            ifgetattr(attr, "__module__", None) == "collections.abc"and attr_name notin namespace:
                namespace[attr_name] = attr
        returntype.__new__(mcls, name, (realbase,), namespace)


classIdentaDict(MutableMapping, metaclass=RealBase):
    __getitem__ = lambda s, i: i
    __setitem__ = lambda s, i, v: None
    __delitem__ = lambda s, i: None
    __len__ = lambda s: 1
    __iter__ = lambda s: iter(['test_value'])

This will make the class work as expected, and return True to isinstance(IdentaClass(), dict). However the C Json Encoder will then try to use native dict API's to get its values: so json.dump(...) will not raise, but will fail unless the Python Json encoder is forced. Maybe this is why the instance check in json.encoder is for a strict "dict":

a = IdentaDict()


In [76]: a = IdentaDict()                                                                                                          

In [77]: a                                                                                                                         
Out[77]: {'test_value': 'test_value'}

In [78]: isinstance(a, dict)                                                                                                       
Out[78]: True

In [79]: len(a)                                                                                                                    
Out[79]: 1

In [80]: json.dumps(a)                                                                                                             
Out[80]: '{}'

In [81]: print(json.dumps(a, indent=4))                                                                                            
{
    "test_value": "test_value"
}

(Another side-effect of this metaclass is that as the value returned by __new__ is not an instance of ABCMeta, the metaclass __init__ won't be called. But people coding with multiple metaclass composition would have to be aware of such issues. This would be easily work-aroundable by explicitly calling mcls.__init__ at the end of __new__)

Solution 2:

I think I found a way to do it, based on a modified version of the suggestion in this answer to the question How to “perfectly” override a dict?.

Disclaimer: As the answer's author states, its a "monstrosity", so I probably would never actually use it in production code.

Here's the result:

from __future__ import print_function
try:
    from collections.abc import Mapping, MutableMapping  # Python 3except ImportError:
    from collections import Mapping, MutableMapping  # Python 2classSpreadSheet(MutableMapping):
    def__init__(self, tools=None, **kwargs):
        self.__class__ = dict# see https://stackoverflow.com/a/47361653/355230

        self._cells = {}
        self._tools = {'__builtins__': None}
        if tools isnotNone:
            self._tools.update(tools)  # Add caller supplied functions.    @classmethoddef__class__(cls):  # see https://stackoverflow.com/a/47361653/355230returndictdefclear(self):
        return self._cells.clear()

    def__contains__(self, key):
        return key in self._cells

    def__setitem__(self, key, formula):
        self._cells[key] = formula

    def__getitem__(self, key):
        returneval(self._cells[key], self._tools, self)

    def__len__(self):
        returnlen(self._cells)

    def__iter__(self):
        returniter(self._cells)

    def__delitem__(self, key):
        del self._cells[key]

    defgetformula(self, key):
        """ Return raw un-evaluated contents of cell. """return self._cells[key]

    defupdate(self, *args, **kwargs):
        for k, v indict(*args, **kwargs).iteritems():
            self[k] = v

#    # Doesn't work.#    type(dict).register(SpreadSheet)  # Register class as dict subclass.if __name__ == '__main__':

    import json
    from math import cos, sin, pi, tan

    # A small set of safe built-ins.
    tools = dict(len=len, sin=sin, cos=cos, pi=pi, tan=tan)

    ss = SpreadSheet(tools)
    ss['a1'] = '5'
    ss['a2'] = 'a1*6'
    ss['a3'] = 'a2*7'
    ss['b1'] = 'sin(pi/4)'print()
    print('isinstance(SpreadSheet(tools), dict) -> {}'.format(isinstance(ss, dict)))
    print()
    print('Static Contents via getformula():')
    print(json.dumps({k: ss.getformula(k) for k in ss.keys()}, indent=4))
    print()
    print('Dynamic Contents via __getitem__():')
    print("  ss['a1'] -> {!r}".format(ss['a1']))
    print("  ss['a2'] -> {!r}".format(ss['a2']))
    print("  ss['a3'] -> {!r}".format(ss['a3']))
    print("  ss['b1'] -> {!r}".format(ss['b1']))
    print()
    print("via json.dumps(ss, indent=4):")
    print(json.dumps(ss, indent=4))

Output:

isinstance(SpreadSheet(tools), dict) -> True

Static Contents via getformula():
{
    "a1": "5",
    "a2": "a1*6",
    "a3": "a2*7",
    "b1": "sin(pi/4)"
}

Dynamic Contents via __getitem__():
  ss['a1'] -> 5
  ss['a2'] -> 30
  ss['a3'] -> 210
  ss['b1'] -> 0.7071067811865475

via json.dumps(ss, indent=4):
{
    "a1": 5,
    "a2": 30,
    "a3": 210,
    "b1": 0.7071067811865475
}

Note: I got the idea for this class from an old ActiveState recipe by Raymond Hettinger.

Solution 3:

You can do something like:

import json

defjson_default(obj):
    ifisinstance(obj, SpreadSheet):
        return obj._cells
    raise TypeError

cheet = SpreadSheet()    
cheet['a'] = 5
cheet['b'] = 23
cheet['c'] = -4print(json.dumps(cheet, default=json_default))

Output:

{"a": 5, "b": 23, "c": -4}

The key is the function json_default that tells the json decoder how to serialize your class!

Post a Comment for "How To Register Implementation Of Abc.mutablemapping As A Dict Subclass?"