PySide Bugzilla Closed for New Bugs

PySide is now a Qt Add-on and uses the Qt Project's JIRA Bug Tracker instead of this Bugzilla instance. This Bugzilla is left for reference purposes.

Bug 989 - Pickling QColor fails
: Pickling QColor fails
Status: CLOSED FIXED
Product: PySide
Classification: Unclassified
Component: QtGui
: 1.0.6
: All All
: P3 normal
Assigned To: renato filho
:
:
:
  Show dependency treegraph
 
Reported: 2011-08-28 00:24 EEST by bbreslauer
Modified: 2011-09-21 20:49 EEST (History)
9 users (show)

See Also:


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description bbreslauer 2011-08-28 00:24:08 EEST
When trying to pickle a QColor object, I get a PicklingError with the following
trace:

Traceback (most recent call last):
  File "test2.py", line 5, in <module>
    pickle.dumps(c)
  File "/usr/lib/python2.7/pickle.py", line 1374, in dumps
    Pickler(file, protocol).dump(obj)
  File "/usr/lib/python2.7/pickle.py", line 224, in dump
    self.save(obj)
  File "/usr/lib/python2.7/pickle.py", line 331, in save
    self.save_reduce(obj=obj, *rv)
  File "/usr/lib/python2.7/pickle.py", line 400, in save_reduce
    save(func)
  File "/usr/lib/python2.7/pickle.py", line 286, in save
    f(self, obj) # Call unbound method with explicit self
  File "/usr/lib/python2.7/pickle.py", line 748, in save_global
    (obj, module, name))
pickle.PicklingError: Can't pickle <built-in function fromRgbF>: it's not found
as __main__.fromRgbF


Here's a small script that demonstrates this:

from PySide.QtGui import QColor
import pickle

c = QColor('gray')
pickle.dumps(c)
Comment 1 renato filho 2011-08-30 00:36:58 EEST
I have studied the reduce function more deeper, and I discovery on problem
during QColor reduce, the problem is that you can only use constructor or
module function as reduce function, and QColor only supports constructor with
RGB this can cause errors during QColor reduce.

Then my idea is raise a error if a not RBG color call the __reduce__ function.
What do you think? Do you have any other suggestion?
Comment 2 bbreslauer 2011-08-30 05:51:54 EEST
Do you mean that you have to use a module method, and not a static function
(i.e. fromRgbF)?  If so, I think instead of having __reduce__ return something
of the form 

(<built-in function fromRgbF>, (0.43529411764705883, 0.8705882352941177,
0.4823529411764706, 0.19607843137254902))

I think you could have something of the form 

(<type 'PySide.QtGui.QColor'>, (111, 222, 123, 50))

. You specifically want the values to be integers, not floats, because the
QColor constructor only accepts integers (or a string or other Qt classes, but
the integer version should fully generic).  This does not really require the
QColor to be an rgb color, since I believe that any QColor is representable in
rgb, hsv, cmyk, and hsl, even though the spec will only be one of those.

This does not deal with the spec value, however. I'm not really sure what the
point of the spec value is, because once you've defined a QColor, it can be
represented in rgb, hsv, cmyk, and hsl. But, in order to reconstruct the exact
QColor object, __reduce__ can return additional values that are used by
__setstate__.  I know that you don't code the bindings in Python, but here is
what the Python 2 code could look like for __reduce__ and __setstate__.

def __reduce__(self):
    spec = self.spec()
    value = None  # either None or a list with 4 values
    if spec == QColor.Invalid:
    value = None
    elif spec == QColor.Rgb:
    value = self.getRgb()
    elif spec == QColor.Hsl:
    value = self.getHsl()
    elif spec == QColor.Hsv:
    value = self.getHsv()
    elif spec == QColor.Cmyk:
    value = self.getCmyk()
    return tuple([self.__class__, tuple(), tuple([spec, value]))

def __setstate__(self, state):
    spec = state[0]
    value = state[1]

    if spec == QColor.Invalid:
    return  # QColor is initialized to an invalid value
    elif spec == QColor.Rgb:
    self.setRgb(*value)  # unpack the value list
    elif spec == QColor.Hsl:
    self.setHsl(*value)
    elif spec == QColor.Hsv:
    self.setHsv(*value)
    elif spec == QColor.Cmyk:
    self.setCmyk(*value)

The following would happen when loading a pickled QColor class.
1) An invalid QColor is initialized (because the second returned value of
__reduce__ is a blank tuple)
2) The third returned value of __reduce__ is passed to __setstate__. Depending
on which kind of spec the original QColor had, the new QColor would have the
same spec and color.

Initially I thought about setting the RGB values and then using
QColor.convertTo to change the spec, but then I realized that just returns a
new QColor object; it does not convert the current object.

I think this should deal with all different QColor possibilities; i.e. a color
specified by any color scheme, as well as an invalid color. I don't think that
there is any other internal QColor state that is relevant. Maybe
setAllowX11ColorNames should be accounted for, but that is simple; just add
another argument onto the third return value in __reduce__, and account for it
in __setstate__.

I think this second solution (creating __setstate__) is much better, but I
don't know how easy it is to implement.  If you cannot, and have to use the
first method, then I think it would be sufficient to print a warning if the
spec is not RGB. Regardless of the type of color the user initially specified,
it can be reconstructed as a RGB color, so I don't think it really calls for an
error.
Comment 3 Farsmo 2011-08-30 12:17:35 EEST
The __setstate__ solution sounds good, but it should use getRgbF() instead of
getRgb().

We don't need to store 64-bit floats (32 byte per QColor), we can store the
16-bit state to save space (8 byte per QColor):

import struct

def __reduce__(self):
    spec = self.spec()
    value = None  # either None or a list with 4 or 5 values
    if spec == QColor.Invalid:
        value = None
    elif spec == QColor.Rgb:
        value = self.getRgbF()
    elif spec == QColor.Hsl:
        value = self.getHslF()
    elif spec == QColor.Hsv:
        value = self.getHsvF()
    elif spec == QColor.Cmyk:
        value = self.getCmykF()
    packed = struct.pack('<%dH'%len(value), *(int(round(x*65535)) for x in
value))
    return tuple([self.__class__, tuple(), (spec, packed))

def __setstate__(self, state):
    spec = state[0]
    packed = state[1]
    value = [x/65535.0 for x in struct.unpack('<%dH'%(len(packed)//2), packed)]

    if spec == QColor.Invalid:
        return  # QColor is initialized to an invalid value
    elif spec == QColor.Rgb:
        self.setRgb(*value)  # unpack the value list
    elif spec == QColor.Hsl:
        self.setHsl(*value)
    elif spec == QColor.Hsv:
        self.setHsv(*value)
    elif spec == QColor.Cmyk:
        self.setCmyk(*value)
Comment 4 Farsmo 2011-08-30 12:19:18 EEST
There's a typo,
    return tuple([self.__class__, tuple(), (spec, packed))
should be
    return (self.__class__, tuple(), (spec, packed))
Comment 5 bbreslauer 2011-08-30 17:29:59 EEST
That sounds good. But for the __setstate__ method, it should now use the float
versions of the functions, i.e. setRgb should be setRgbF, etc.
Comment 6 renato filho 2011-09-01 00:49:49 EEST
Thanks for the tips.

Fixed on pyside commit:

commit 5d34c3b14be3baa98a190f145b35cfd5de95043d
Author: Renato Filho <renato.filho@openbossa.org>
Date:   Wed Aug 31 16:57:28 2011 -0300
Comment 7 renato filho 2011-09-21 20:49:31 EEST
Release 1.0.7