3

I have to save various properties of custom GTK elements to a file for future use and decided to use JSON because of the simple format and nesting of dicts.

Many properties are GTK enums, like gtk.PAGE_ORIENTATION_PORTRAIT, gtk.ANCHOR_CENTER and pango.ALIGN_LEFT. They have a unique name which can be retrieved with obj.value_name to get a valid JSON type.

Currently I have 2 methods for each of my elements: to_str() to get the value_name and from_str() which maps the str to the enum again. I would like to automate this so I don't forget to call these and clean up the code a bit. The JSONEncoder and JSONDecodr are doing exactly this, or so I thought...

This is the example given in the Python docs and it works as expected.

import json

class ComplexEncoder(json.JSONEncoder):
    def default(self, obj):
        print "default method called for:", obj
        if isinstance(obj, complex):
            return [obj.real, obj.imag]
        return json.JSONEncoder.default(self, obj)

print json.dumps(2 + 1j, cls=ComplexEncoder)

Based on this example I've added the GTK enums:

import json
import gtk

ENUMS = [gtk.PAGE_ORIENTATION_PORTRAIT, gtk.PAGE_ORIENTATION_LANDSCAPE]

class GtkEncoder(json.JSONEncoder):
    def default(self, obj):
        print "default method called for:", obj
        if obj in ENUMS:
            return obj.value_name
        return json.JSONEncoder.default(self, obj)

print json.dumps(gtk.PAGE_ORIENTATION_LANDSCAPE, cls=GtkEncoder)

Notice the added print statement in the default method. In the original example, this method is called without problems, but not in the GTK example. The default method is never called and it returns <enum GTK_PAGE_ORIENTATION_LANDSCAPE of type GtkPageOrientation> which isn't valid JSON ofcourse.

So, is there a way to automatically encode/decode these enums or am I stuck with the current manual approach? Note that my data structure to dump is not a single value, but a dict or dicts.

Timo
  • 164
  • 2
  • 12

2 Answers2

2

The observed behavior occurs because the value gtk.PAGE_ORIENTATION_LANDSCAPE is an instance of class 'gtk._gtk.PageOrientation' which inherits type 'gobject.GEnum' which in turn inherits type 'int'.

So your GTK-enums are ints, and the json code assumes it can handle ints and thus does not call the default method of your encoder.

Unfortunately the current json implementation isn't that helpful in encoding subclassed types like this :-/ No inheriting and overriding makes this possible (at least I cannot find any solution for this). Just too many hardcoded places where the value is checked for isinstance(value, (int, long)).

But you can of course patch the source of the json encoder to achieve your goal without having to implement the whole json functionality anew. For this copy the file encoder.py from the json library (for me this is /usr/lib/python2.7/json/encoder.py) to your working directory and patch it.

In the functions _iterencode_list() and _iterencode_dict() (they are local to function _make_iterencode()) you can find checks for being of type int or long; if so, the current implementation just calls str(value). Change that to encodeInt(value) (in three places!) and implement your own encodeInt() function in encoder.py:

def encodeInt(value):
  try:
    return value.value_name
  except:
    return str(value)

Then, in your original code, you will have to import directly that patched file:

import encoder

and you will have to make sure the C implementation isn't used anymore but instead the patched code. (You see, normally, the (faster) C implementation is used, and that Python code in which we patched something is not.) To achieve this, just add after the import:

encoder.c_make_encoder = None

Now your patched encoder can be used:

print encoder.JSONEncoder().encode({
  gtk.PAGE_ORIENTATION_PORTRAIT: [
    gtk.PAGE_ORIENTATION_LANDSCAPE
  ],
  gtk.PAGE_ORIENTATION_LANDSCAPE: gtk.PAGE_ORIENTATION_PORTRAIT })

prints:

{"GTK_PAGE_ORIENTATION_PORTRAIT": [GTK_PAGE_ORIENTATION_LANDSCAPE], "GTK_PAGE_ORIENTATION_LANDSCAPE": GTK_PAGE_ORIENTATION_PORTRAIT}

Note that Json dict keys always must be strings. That's why your values get double quotes when used as keys. But that's a fate even normal ints — when used as keys — share. They also get stringified.

You can have a look at http://pastebin.com/2HAtN9E8 to see all the sources.

Alfe
  • 56,346
  • 20
  • 107
  • 159
  • Thanks for the inheritance hint, I actually thought it would be something like that. Unfortunatly, this is only working if only the enum is being encoded. When putting the enum into a list or dict, the `value` arg for `encode` is that entire object, so it fails. – Timo Feb 06 '14 at 15:47
  • Urgs. Okay, I thought that `encode` routine was recursive. I'll have a look and see if I can mend my solution. – Alfe Feb 06 '14 at 15:49
  • Bummer. That json internal code doesn't make it easy to achieve that. I'm onto it but I won't come up with anything in the next 20 hours. – Alfe Feb 06 '14 at 16:00
  • Thanks for looking into it. I spent many hours on this, but can't find a builtin solution. – Timo Feb 06 '14 at 19:15
  • I've updated my answer. Unfortunately the news aren't good. The only solution I see is patching the json library. See answer for details. – Alfe Feb 07 '14 at 13:32
  • I am unimpressed by the json implementation. There needs to be a way to override the defaults for basestring, float, int, long, null, True, and False. Patching the code works, but its not the best. The new Enum library which is backported to 2.x makes this much more important because Enum is hashable and makes a nice set of keys for dictionaries. – srock Jun 10 '14 at 17:21
0

I found a solution, although this may not work for gtk, it does however work for enums in general.

If you can use IntEnum instead of Enum, then you can override str in your Enum class to return a string of whatever you want, name or value most likely.

import json
from enum import IntEnum

class TrekCaptains(IntEnum):
    Kirk  = 0
    Picard = 1

    def __str__(self):
        return '{0}'.format(self.value)


s = {TrekCaptains.Kirk:'Original Series',
     TrekCaptains.Picard:'Next Generation'}
j = json.dumps(s)
print j

#result
#{"0": "Original Series", "1": "Next Generation"}
srock
  • 533
  • 1
  • 7
  • 15