69

Without subclassing dict, what would a class need to be considered a mapping so that it can be passed to a method with **.

from abc import ABCMeta

class uobj:
    __metaclass__ = ABCMeta

uobj.register(dict)

def f(**k): return k

o = uobj()
f(**o)

# outputs: f() argument after ** must be a mapping, not uobj

At least to the point where it throws errors of missing functionality of mapping, so I can begin implementing.

I reviewed emulating container types but simply defining magic methods has no effect, and using ABCMeta to override and register it as a dict validates assertions as subclass, but fails isinstance(o, dict). Ideally, I dont even want to use ABCMeta.

martineau
  • 112,593
  • 23
  • 157
  • 280
dskinner
  • 9,871
  • 3
  • 30
  • 42

3 Answers3

94

The __getitem__() and keys() methods will suffice:

>>> class D:
        def keys(self):
            return ['a', 'b']
        def __getitem__(self, key):
            return key.upper()


>>> def f(**kwds):
        print kwds


>>> f(**D())
{'a': 'A', 'b': 'B'}
Ashwini Chaudhary
  • 232,417
  • 55
  • 437
  • 487
Raymond Hettinger
  • 199,887
  • 59
  • 344
  • 454
32

If you're trying to create a Mapping — not just satisfy the requirements for passing to a function — then you really should inherit from collections.abc.Mapping. As described in the documentation, you need to implement just:

__getitem__
__len__
__iter__

The Mixin will implement everything else for you: __contains__, keys, items, values, get, __eq__, and __ne__.

Neil G
  • 30,493
  • 34
  • 149
  • 247
1

The answer can be found by digging through the source.

When attempting to use a non-mapping object with **, the following error is given:

TypeError: 'Foo' object is not a mapping

If we search CPython's source for that error, we can find the code that causes that error to be raised:

case TARGET(DICT_UPDATE): {
    PyObject *update = POP();
    PyObject *dict = PEEK(oparg);
    if (PyDict_Update(dict, update) < 0) {
        if (_PyErr_ExceptionMatches(tstate, PyExc_AttributeError)) {
            _PyErr_Format(tstate, PyExc_TypeError,
                            "'%.200s' object is not a mapping",
                            Py_TYPE(update)->tp_name);

PyDict_Update is actually dict_merge, and the error is thrown when dict_merge returns a negative number. If we check the source for dict_merge, we can see what leads to -1 being returned:

/* We accept for the argument either a concrete dictionary object,
 * or an abstract "mapping" object.  For the former, we can do
 * things quite efficiently.  For the latter, we only require that
 * PyMapping_Keys() and PyObject_GetItem() be supported.
 */
if (a == NULL || !PyDict_Check(a) || b == NULL) {
    PyErr_BadInternalCall();
    return -1;

The key part being:

For the latter, we only require that PyMapping_Keys() and PyObject_GetItem() be supported.

Carcigenicate
  • 39,675
  • 9
  • 62
  • 99