Fixing pickle for nested classes

As of Python 2.7, names for nested classes are set by Python in a way which is incompatible with the pickling of such classes (pickling by name):

sage: class A:
....:     class B:
....:         pass
sage: A.B.__name__
'B'

instead of more natural "A.B". Furthermore upon pickling and unpickling a class with name "A.B" in a module mod, the standard cPickle module searches for "A.B" in mod.__dict__ instead of looking up "A" and then "B" in the result. See: https://groups.google.com/forum/#!topic/sage-devel/bHBV9KWAt64

This module provides two utilities to workaround this issue:

See also sage.misc.nested_class_test.

Note

In Python 3, nested classes, like any class for that matter, have __qualname__ and the standard pickle module uses it for pickling and unpickling. Thus the pickle module searches for "A.B" first by looking up "A" in mod, and then "B" in the result. So there is no pickling problem for nested classes in Python 3, and the two utilities are not really necessary. However, NestedClassMetaclass is used widely in Sage and affects behaviors of Sage objects in other respects than in pickling and unpickling. Hence we keep NestedClassMetaclass even with Python 3, for now. This module will be removed when we eventually drop support for Python 2.

EXAMPLES:

sage: from sage.misc.nested_class import A1, nested_pickle

sage: A1.A2.A3.__name__
'A3'
sage: A1.A2.A3  # py2
<class sage.misc.nested_class.A3 at ...>
sage: A1.A2.A3  # py3
<class 'sage.misc.nested_class.A1.A2.A3'>

sage: nested_pickle(A1)  # py2
<class sage.misc.nested_class.A1 at ...>
sage: nested_pickle(A1)  # py3
<class 'sage.misc.nested_class.A1'>

sage: A1.A2  # py2
<class sage.misc.nested_class.A1.A2 at ...>
sage: A1.A2  # py3
<class 'sage.misc.nested_class.A1.A2'>

sage: A1.A2.A3  # py2
<class sage.misc.nested_class.A1.A2.A3 at ...>
sage: A1.A2.A3  # py3
<class 'sage.misc.nested_class.A1.A2.A3'>
sage: A1.A2.A3.__name__
'A1.A2.A3'

sage: sage.misc.nested_class.__dict__['A1.A2'] is A1.A2
True
sage: sage.misc.nested_class.__dict__['A1.A2.A3'] is A1.A2.A3
True

All of this is not perfect. In the following scenario:

sage: class A1:
....:     class A2:
....:         pass
sage: class B1:
....:     A2 = A1.A2

sage: nested_pickle(A1)       # py2
<class __main__.A1 at ...>
sage: nested_pickle(B1)       # py2
<class __main__.B1 at ...>
sage: A1.A2                   # py2
<class __main__.A1.A2 at ...>
sage: B1.A2                   # py2
<class __main__.A1.A2 at ...>

sage: nested_pickle(A1)       # py3
<class '__main__.A1'>
sage: nested_pickle(B1)       # py3
<class '__main__.B1'>
sage: A1.A2                   # py3
<class '__main__.A1.A2'>
sage: B1.A2                   # py3
<class '__main__.A1.A2'>

The name for "A1.A2" could potentially be set to "B1.A2". But that will work anyway.

sage.misc.nested_class.modify_for_nested_pickle(cls, name_prefix, module, first_run=True)

Modify the subclasses of the given class to be picklable, by giving them a mangled name and putting the mangled name in the module namespace.

INPUT:

  • cls – the class to modify
  • name_prefix – the prefix to prepend to the class name
  • module – the module object to modify with the mangled name
  • first_run – optional bool (default True): whether or not this function is run for the first time on cls

NOTE:

This function would usually not be directly called. It is internally used in NestedClassMetaclass.

EXAMPLES:

sage: from sage.misc.nested_class import *
sage: class A(object):
....:     class B(object):
....:         pass
sage: module = sys.modules['__main__']
sage: A.B.__name__
'B'
sage: getattr(module, 'A.B', 'Not found')
'Not found'
sage: modify_for_nested_pickle(A, 'A', module)
sage: A.B.__name__
'A.B'
sage: getattr(module, 'A.B', 'Not found')
<class '__main__.A.B'>

Here we demonstrate the effect of the first_run argument:

sage: modify_for_nested_pickle(A, 'X', module)
sage: A.B.__name__  # nothing changed
'A.B'
sage: modify_for_nested_pickle(A, 'X', module, first_run=False)
sage: A.B.__name__
'X.A.B'

Note that the class is now found in the module under both its old and its new name:

sage: getattr(module, 'A.B', 'Not found')   # py2
<class '__main__.X.A.B'>
sage: getattr(module, 'X.A.B', 'Not found') # py2
<class '__main__.X.A.B'>

sage: getattr(module, 'A.B', 'Not found')   # py3
<class '__main__.A.B'>
sage: getattr(module, 'X.A.B', 'Not found') # py3
<class '__main__.A.B'>
sage.misc.nested_class.nested_pickle(cls)

This decorator takes a class that potentially contains nested classes. For each such nested class, its name is modified to a new illegal identifier, and that name is set in the module. For example, if you have:

sage: from sage.misc.nested_class import nested_pickle
sage: module = sys.modules['__main__']
sage: class A(object):
....:     class B:
....:         pass
sage: nested_pickle(A)
<class '__main__.A'>

then the name of class "B" will be modified to "A.B", and the "A.B" attribute of the module will be set to class "B":

sage: A.B.__name__
'A.B'
sage: getattr(module, 'A.B', 'Not found')  # py2
<class __main__.A.B at ...>
sage: getattr(module, 'A.B', 'Not found')  # py3
<class '__main__.A.B'>

In Python 2.6, decorators work with classes; then @nested_pickle should work as a decorator:

sage: @nested_pickle    # todo: not implemented
....: class A2(object):
....:     class B:
....:         pass
sage: A2.B.__name__    # todo: not implemented
'A2.B'
sage: getattr(module, 'A2.B', 'Not found')    # todo: not implemented
<class __main__.A2.B at ...>

EXAMPLES:

sage: from sage.misc.nested_class import *
sage: loads(dumps(MainClass.NestedClass())) # indirect doctest
<sage.misc.nested_class.MainClass.NestedClass object at 0x...>
class sage.misc.nested_class.NestedClassMetaclass

Bases: type

A metaclass for nested pickling.

Check that one can use a metaclass to ensure nested_pickle is called on any derived subclass:

sage: from sage.misc.nested_class import NestedClassMetaclass
sage: class ASuperClass(object):                                  # py2
....:     __metaclass__ = NestedClassMetaclass                    # py2
sage: class ASuperClass(object, metaclass=NestedClassMetaclass):  # py3
....:     pass                                                    # py3
sage: class A3(ASuperClass):
....:     class B(object):
....:         pass
sage: A3.B.__name__
'A3.B'
sage: getattr(sys.modules['__main__'], 'A3.B', 'Not found')
<class '__main__.A3.B'>
class sage.misc.nested_class.MainClass

Bases: object

A simple class to test nested_pickle.

EXAMPLES:

sage: from sage.misc.nested_class import *
sage: loads(dumps(MainClass()))
<sage.misc.nested_class.MainClass object at 0x...>
class NestedClass

Bases: object

EXAMPLES:

sage: from sage.misc.nested_class import *
sage: loads(dumps(MainClass.NestedClass()))
<sage.misc.nested_class.MainClass.NestedClass object at 0x...>
class NestedSubClass

Bases: object

EXAMPLES:

sage: from sage.misc.nested_class import *
sage: loads(dumps(MainClass.NestedClass.NestedSubClass()))
<sage.misc.nested_class.MainClass.NestedClass.NestedSubClass object at 0x...>
sage: getattr(sage.misc.nested_class, 'MainClass.NestedClass.NestedSubClass')
<class 'sage.misc.nested_class.MainClass.NestedClass.NestedSubClass'>
sage: MainClass.NestedClass.NestedSubClass.__name__
'MainClass.NestedClass.NestedSubClass'
dummy(x, r=(1, 2, 3.4), *args, **kwds)

A dummy method to demonstrate the embedding of method signature for nested classes.