Commit 35ac5f82 authored by Serhiy Storchaka's avatar Serhiy Storchaka

Issue #22955: attrgetter, itemgetter and methodcaller objects in the operator

module now support pickling.  Added readable and evaluable repr for these
objects.  Based on patch by Josh Rosenberg.
parent 5418d0bf
...@@ -231,10 +231,13 @@ class attrgetter: ...@@ -231,10 +231,13 @@ class attrgetter:
After h = attrgetter('name.first', 'name.last'), the call h(r) returns After h = attrgetter('name.first', 'name.last'), the call h(r) returns
(r.name.first, r.name.last). (r.name.first, r.name.last).
""" """
__slots__ = ('_attrs', '_call')
def __init__(self, attr, *attrs): def __init__(self, attr, *attrs):
if not attrs: if not attrs:
if not isinstance(attr, str): if not isinstance(attr, str):
raise TypeError('attribute name must be a string') raise TypeError('attribute name must be a string')
self._attrs = (attr,)
names = attr.split('.') names = attr.split('.')
def func(obj): def func(obj):
for name in names: for name in names:
...@@ -242,7 +245,8 @@ class attrgetter: ...@@ -242,7 +245,8 @@ class attrgetter:
return obj return obj
self._call = func self._call = func
else: else:
getters = tuple(map(attrgetter, (attr,) + attrs)) self._attrs = (attr,) + attrs
getters = tuple(map(attrgetter, self._attrs))
def func(obj): def func(obj):
return tuple(getter(obj) for getter in getters) return tuple(getter(obj) for getter in getters)
self._call = func self._call = func
...@@ -250,19 +254,30 @@ class attrgetter: ...@@ -250,19 +254,30 @@ class attrgetter:
def __call__(self, obj): def __call__(self, obj):
return self._call(obj) return self._call(obj)
def __repr__(self):
return '%s.%s(%s)' % (self.__class__.__module__,
self.__class__.__qualname__,
', '.join(map(repr, self._attrs)))
def __reduce__(self):
return self.__class__, self._attrs
class itemgetter: class itemgetter:
""" """
Return a callable object that fetches the given item(s) from its operand. Return a callable object that fetches the given item(s) from its operand.
After f = itemgetter(2), the call f(r) returns r[2]. After f = itemgetter(2), the call f(r) returns r[2].
After g = itemgetter(2, 5, 3), the call g(r) returns (r[2], r[5], r[3]) After g = itemgetter(2, 5, 3), the call g(r) returns (r[2], r[5], r[3])
""" """
__slots__ = ('_items', '_call')
def __init__(self, item, *items): def __init__(self, item, *items):
if not items: if not items:
self._items = (item,)
def func(obj): def func(obj):
return obj[item] return obj[item]
self._call = func self._call = func
else: else:
items = (item,) + items self._items = items = (item,) + items
def func(obj): def func(obj):
return tuple(obj[i] for i in items) return tuple(obj[i] for i in items)
self._call = func self._call = func
...@@ -270,6 +285,14 @@ class itemgetter: ...@@ -270,6 +285,14 @@ class itemgetter:
def __call__(self, obj): def __call__(self, obj):
return self._call(obj) return self._call(obj)
def __repr__(self):
return '%s.%s(%s)' % (self.__class__.__module__,
self.__class__.__name__,
', '.join(map(repr, self._items)))
def __reduce__(self):
return self.__class__, self._items
class methodcaller: class methodcaller:
""" """
Return a callable object that calls the given method on its operand. Return a callable object that calls the given method on its operand.
...@@ -277,6 +300,7 @@ class methodcaller: ...@@ -277,6 +300,7 @@ class methodcaller:
After g = methodcaller('name', 'date', foo=1), the call g(r) returns After g = methodcaller('name', 'date', foo=1), the call g(r) returns
r.name('date', foo=1). r.name('date', foo=1).
""" """
__slots__ = ('_name', '_args', '_kwargs')
def __init__(*args, **kwargs): def __init__(*args, **kwargs):
if len(args) < 2: if len(args) < 2:
...@@ -284,12 +308,30 @@ class methodcaller: ...@@ -284,12 +308,30 @@ class methodcaller:
raise TypeError(msg) raise TypeError(msg)
self = args[0] self = args[0]
self._name = args[1] self._name = args[1]
if not isinstance(self._name, str):
raise TypeError('method name must be a string')
self._args = args[2:] self._args = args[2:]
self._kwargs = kwargs self._kwargs = kwargs
def __call__(self, obj): def __call__(self, obj):
return getattr(obj, self._name)(*self._args, **self._kwargs) return getattr(obj, self._name)(*self._args, **self._kwargs)
def __repr__(self):
args = [repr(self._name)]
args.extend(map(repr, self._args))
args.extend('%s=%r' % (k, v) for k, v in self._kwargs.items())
return '%s.%s(%s)' % (self.__class__.__module__,
self.__class__.__name__,
', '.join(args))
def __reduce__(self):
if not self._kwargs:
return self.__class__, (self._name,) + self._args
else:
from functools import partial
return partial(self.__class__, self._name, **self._kwargs), self._args
# In-place Operations *********************************************************# # In-place Operations *********************************************************#
def iadd(a, b): def iadd(a, b):
......
import unittest import unittest
import pickle
import sys
from test import support from test import support
...@@ -35,6 +37,9 @@ class Seq2(object): ...@@ -35,6 +37,9 @@ class Seq2(object):
class OperatorTestCase: class OperatorTestCase:
def setUp(self):
sys.modules['operator'] = self.module
def test_lt(self): def test_lt(self):
operator = self.module operator = self.module
self.assertRaises(TypeError, operator.lt) self.assertRaises(TypeError, operator.lt)
...@@ -396,6 +401,7 @@ class OperatorTestCase: ...@@ -396,6 +401,7 @@ class OperatorTestCase:
def test_methodcaller(self): def test_methodcaller(self):
operator = self.module operator = self.module
self.assertRaises(TypeError, operator.methodcaller) self.assertRaises(TypeError, operator.methodcaller)
self.assertRaises(TypeError, operator.methodcaller, 12)
class A: class A:
def foo(self, *args, **kwds): def foo(self, *args, **kwds):
return args[0] + args[1] return args[0] + args[1]
...@@ -491,5 +497,108 @@ class PyOperatorTestCase(OperatorTestCase, unittest.TestCase): ...@@ -491,5 +497,108 @@ class PyOperatorTestCase(OperatorTestCase, unittest.TestCase):
class COperatorTestCase(OperatorTestCase, unittest.TestCase): class COperatorTestCase(OperatorTestCase, unittest.TestCase):
module = c_operator module = c_operator
class OperatorPickleTestCase:
def copy(self, obj, proto):
with support.swap_item(sys.modules, 'operator', self.module):
pickled = pickle.dumps(obj, proto)
with support.swap_item(sys.modules, 'operator', self.module2):
return pickle.loads(pickled)
def test_attrgetter(self):
attrgetter = self.module.attrgetter
attrgetter = self.module.attrgetter
class A:
pass
a = A()
a.x = 'X'
a.y = 'Y'
a.z = 'Z'
a.t = A()
a.t.u = A()
a.t.u.v = 'V'
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
with self.subTest(proto=proto):
f = attrgetter('x')
f2 = self.copy(f, proto)
self.assertEqual(repr(f2), repr(f))
self.assertEqual(f2(a), f(a))
# multiple gets
f = attrgetter('x', 'y', 'z')
f2 = self.copy(f, proto)
self.assertEqual(repr(f2), repr(f))
self.assertEqual(f2(a), f(a))
# recursive gets
f = attrgetter('t.u.v')
f2 = self.copy(f, proto)
self.assertEqual(repr(f2), repr(f))
self.assertEqual(f2(a), f(a))
def test_itemgetter(self):
itemgetter = self.module.itemgetter
a = 'ABCDE'
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
with self.subTest(proto=proto):
f = itemgetter(2)
f2 = self.copy(f, proto)
self.assertEqual(repr(f2), repr(f))
self.assertEqual(f2(a), f(a))
# multiple gets
f = itemgetter(2, 0, 4)
f2 = self.copy(f, proto)
self.assertEqual(repr(f2), repr(f))
self.assertEqual(f2(a), f(a))
def test_methodcaller(self):
methodcaller = self.module.methodcaller
class A:
def foo(self, *args, **kwds):
return args[0] + args[1]
def bar(self, f=42):
return f
def baz(*args, **kwds):
return kwds['name'], kwds['self']
a = A()
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
with self.subTest(proto=proto):
f = methodcaller('bar')
f2 = self.copy(f, proto)
self.assertEqual(repr(f2), repr(f))
self.assertEqual(f2(a), f(a))
# positional args
f = methodcaller('foo', 1, 2)
f2 = self.copy(f, proto)
self.assertEqual(repr(f2), repr(f))
self.assertEqual(f2(a), f(a))
# keyword args
f = methodcaller('bar', f=5)
f2 = self.copy(f, proto)
self.assertEqual(repr(f2), repr(f))
self.assertEqual(f2(a), f(a))
f = methodcaller('baz', self='eggs', name='spam')
f2 = self.copy(f, proto)
# Can't test repr consistently with multiple keyword args
self.assertEqual(f2(a), f(a))
class PyPyOperatorPickleTestCase(OperatorPickleTestCase, unittest.TestCase):
module = py_operator
module2 = py_operator
@unittest.skipUnless(c_operator, 'requires _operator')
class PyCOperatorPickleTestCase(OperatorPickleTestCase, unittest.TestCase):
module = py_operator
module2 = c_operator
@unittest.skipUnless(c_operator, 'requires _operator')
class CPyOperatorPickleTestCase(OperatorPickleTestCase, unittest.TestCase):
module = c_operator
module2 = py_operator
@unittest.skipUnless(c_operator, 'requires _operator')
class CCOperatorPickleTestCase(OperatorPickleTestCase, unittest.TestCase):
module = c_operator
module2 = c_operator
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
...@@ -52,6 +52,10 @@ Core and Builtins ...@@ -52,6 +52,10 @@ Core and Builtins
Library Library
------- -------
- Issue #22955: attrgetter, itemgetter and methodcaller objects in the operator
module now support pickling. Added readable and evaluable repr for these
objects. Based on patch by Josh Rosenberg.
- Issue #22107: tempfile.gettempdir() and tempfile.mkdtemp() now try again - Issue #22107: tempfile.gettempdir() and tempfile.mkdtemp() now try again
when a directory with the chosen name already exists on Windows as well as when a directory with the chosen name already exists on Windows as well as
on Unix. tempfile.mkstemp() now fails early if parent directory is not on Unix. tempfile.mkstemp() now fails early if parent directory is not
......
...@@ -485,6 +485,41 @@ itemgetter_call(itemgetterobject *ig, PyObject *args, PyObject *kw) ...@@ -485,6 +485,41 @@ itemgetter_call(itemgetterobject *ig, PyObject *args, PyObject *kw)
return result; return result;
} }
static PyObject *
itemgetter_repr(itemgetterobject *ig)
{
PyObject *repr;
const char *reprfmt;
int status = Py_ReprEnter((PyObject *)ig);
if (status != 0) {
if (status < 0)
return NULL;
return PyUnicode_FromFormat("%s(...)", Py_TYPE(ig)->tp_name);
}
reprfmt = ig->nitems == 1 ? "%s(%R)" : "%s%R";
repr = PyUnicode_FromFormat(reprfmt, Py_TYPE(ig)->tp_name, ig->item);
Py_ReprLeave((PyObject *)ig);
return repr;
}
static PyObject *
itemgetter_reduce(itemgetterobject *ig)
{
if (ig->nitems == 1)
return Py_BuildValue("O(O)", Py_TYPE(ig), ig->item);
return PyTuple_Pack(2, Py_TYPE(ig), ig->item);
}
PyDoc_STRVAR(reduce_doc, "Return state information for pickling");
static PyMethodDef itemgetter_methods[] = {
{"__reduce__", (PyCFunction)itemgetter_reduce, METH_NOARGS,
reduce_doc},
{NULL}
};
PyDoc_STRVAR(itemgetter_doc, PyDoc_STRVAR(itemgetter_doc,
"itemgetter(item, ...) --> itemgetter object\n\ "itemgetter(item, ...) --> itemgetter object\n\
\n\ \n\
...@@ -503,7 +538,7 @@ static PyTypeObject itemgetter_type = { ...@@ -503,7 +538,7 @@ static PyTypeObject itemgetter_type = {
0, /* tp_getattr */ 0, /* tp_getattr */
0, /* tp_setattr */ 0, /* tp_setattr */
0, /* tp_reserved */ 0, /* tp_reserved */
0, /* tp_repr */ (reprfunc)itemgetter_repr, /* tp_repr */
0, /* tp_as_number */ 0, /* tp_as_number */
0, /* tp_as_sequence */ 0, /* tp_as_sequence */
0, /* tp_as_mapping */ 0, /* tp_as_mapping */
...@@ -521,7 +556,7 @@ static PyTypeObject itemgetter_type = { ...@@ -521,7 +556,7 @@ static PyTypeObject itemgetter_type = {
0, /* tp_weaklistoffset */ 0, /* tp_weaklistoffset */
0, /* tp_iter */ 0, /* tp_iter */
0, /* tp_iternext */ 0, /* tp_iternext */
0, /* tp_methods */ itemgetter_methods, /* tp_methods */
0, /* tp_members */ 0, /* tp_members */
0, /* tp_getset */ 0, /* tp_getset */
0, /* tp_base */ 0, /* tp_base */
...@@ -737,6 +772,91 @@ attrgetter_call(attrgetterobject *ag, PyObject *args, PyObject *kw) ...@@ -737,6 +772,91 @@ attrgetter_call(attrgetterobject *ag, PyObject *args, PyObject *kw)
return result; return result;
} }
static PyObject *
dotjoinattr(PyObject *attr, PyObject **attrsep)
{
if (PyTuple_CheckExact(attr)) {
if (*attrsep == NULL) {
*attrsep = PyUnicode_FromString(".");
if (*attrsep == NULL)
return NULL;
}
return PyUnicode_Join(*attrsep, attr);
} else {
Py_INCREF(attr);
return attr;
}
}
static PyObject *
attrgetter_args(attrgetterobject *ag)
{
Py_ssize_t i;
PyObject *attrsep = NULL;
PyObject *attrstrings = PyTuple_New(ag->nattrs);
if (attrstrings == NULL)
return NULL;
for (i = 0; i < ag->nattrs; ++i) {
PyObject *attr = PyTuple_GET_ITEM(ag->attr, i);
PyObject *attrstr = dotjoinattr(attr, &attrsep);
if (attrstr == NULL) {
Py_XDECREF(attrsep);
Py_DECREF(attrstrings);
return NULL;
}
PyTuple_SET_ITEM(attrstrings, i, attrstr);
}
Py_XDECREF(attrsep);
return attrstrings;
}
static PyObject *
attrgetter_repr(attrgetterobject *ag)
{
PyObject *repr = NULL;
int status = Py_ReprEnter((PyObject *)ag);
if (status != 0) {
if (status < 0)
return NULL;
return PyUnicode_FromFormat("%s(...)", Py_TYPE(ag)->tp_name);
}
if (ag->nattrs == 1) {
PyObject *attrsep = NULL;
PyObject *attr = dotjoinattr(PyTuple_GET_ITEM(ag->attr, 0), &attrsep);
if (attr != NULL)
repr = PyUnicode_FromFormat("%s(%R)", Py_TYPE(ag)->tp_name, attr);
Py_XDECREF(attrsep);
}
else {
PyObject *attrstrings = attrgetter_args(ag);
if (attrstrings != NULL) {
repr = PyUnicode_FromFormat("%s%R",
Py_TYPE(ag)->tp_name, attrstrings);
Py_DECREF(attrstrings);
}
}
Py_ReprLeave((PyObject *)ag);
return repr;
}
static PyObject *
attrgetter_reduce(attrgetterobject *ag)
{
PyObject *attrstrings = attrgetter_args(ag);
if (attrstrings == NULL)
return NULL;
return Py_BuildValue("ON", Py_TYPE(ag), attrstrings);
}
static PyMethodDef attrgetter_methods[] = {
{"__reduce__", (PyCFunction)attrgetter_reduce, METH_NOARGS,
reduce_doc},
{NULL}
};
PyDoc_STRVAR(attrgetter_doc, PyDoc_STRVAR(attrgetter_doc,
"attrgetter(attr, ...) --> attrgetter object\n\ "attrgetter(attr, ...) --> attrgetter object\n\
\n\ \n\
...@@ -757,7 +877,7 @@ static PyTypeObject attrgetter_type = { ...@@ -757,7 +877,7 @@ static PyTypeObject attrgetter_type = {
0, /* tp_getattr */ 0, /* tp_getattr */
0, /* tp_setattr */ 0, /* tp_setattr */
0, /* tp_reserved */ 0, /* tp_reserved */
0, /* tp_repr */ (reprfunc)attrgetter_repr, /* tp_repr */
0, /* tp_as_number */ 0, /* tp_as_number */
0, /* tp_as_sequence */ 0, /* tp_as_sequence */
0, /* tp_as_mapping */ 0, /* tp_as_mapping */
...@@ -775,7 +895,7 @@ static PyTypeObject attrgetter_type = { ...@@ -775,7 +895,7 @@ static PyTypeObject attrgetter_type = {
0, /* tp_weaklistoffset */ 0, /* tp_weaklistoffset */
0, /* tp_iter */ 0, /* tp_iter */
0, /* tp_iternext */ 0, /* tp_iternext */
0, /* tp_methods */ attrgetter_methods, /* tp_methods */
0, /* tp_members */ 0, /* tp_members */
0, /* tp_getset */ 0, /* tp_getset */
0, /* tp_base */ 0, /* tp_base */
...@@ -813,6 +933,13 @@ methodcaller_new(PyTypeObject *type, PyObject *args, PyObject *kwds) ...@@ -813,6 +933,13 @@ methodcaller_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
return NULL; return NULL;
} }
name = PyTuple_GET_ITEM(args, 0);
if (!PyUnicode_Check(name)) {
PyErr_SetString(PyExc_TypeError,
"method name must be a string");
return NULL;
}
/* create methodcallerobject structure */ /* create methodcallerobject structure */
mc = PyObject_GC_New(methodcallerobject, &methodcaller_type); mc = PyObject_GC_New(methodcallerobject, &methodcaller_type);
if (mc == NULL) if (mc == NULL)
...@@ -825,8 +952,8 @@ methodcaller_new(PyTypeObject *type, PyObject *args, PyObject *kwds) ...@@ -825,8 +952,8 @@ methodcaller_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
} }
mc->args = newargs; mc->args = newargs;
name = PyTuple_GET_ITEM(args, 0);
Py_INCREF(name); Py_INCREF(name);
PyUnicode_InternInPlace(&name);
mc->name = name; mc->name = name;
Py_XINCREF(kwds); Py_XINCREF(kwds);
...@@ -869,6 +996,142 @@ methodcaller_call(methodcallerobject *mc, PyObject *args, PyObject *kw) ...@@ -869,6 +996,142 @@ methodcaller_call(methodcallerobject *mc, PyObject *args, PyObject *kw)
return result; return result;
} }
static PyObject *
methodcaller_repr(methodcallerobject *mc)
{
PyObject *argreprs, *repr = NULL, *sep, *joinedargreprs;
Py_ssize_t numtotalargs, numposargs, numkwdargs, i;
int status = Py_ReprEnter((PyObject *)mc);
if (status != 0) {
if (status < 0)
return NULL;
return PyUnicode_FromFormat("%s(...)", Py_TYPE(mc)->tp_name);
}
if (mc->kwds != NULL) {
numkwdargs = PyDict_Size(mc->kwds);
if (numkwdargs < 0) {
Py_ReprLeave((PyObject *)mc);
return NULL;
}
} else {
numkwdargs = 0;
}
numposargs = PyTuple_GET_SIZE(mc->args);
numtotalargs = numposargs + numkwdargs;
if (numtotalargs == 0) {
repr = PyUnicode_FromFormat("%s(%R)", Py_TYPE(mc)->tp_name, mc->name);
Py_ReprLeave((PyObject *)mc);
return repr;
}
argreprs = PyTuple_New(numtotalargs);
if (argreprs == NULL) {
Py_ReprLeave((PyObject *)mc);
return NULL;
}
for (i = 0; i < numposargs; ++i) {
PyObject *onerepr = PyObject_Repr(PyTuple_GET_ITEM(mc->args, i));
if (onerepr == NULL)
goto done;
PyTuple_SET_ITEM(argreprs, i, onerepr);
}
if (numkwdargs != 0) {
PyObject *key, *value;
Py_ssize_t pos = 0;
while (PyDict_Next(mc->kwds, &pos, &key, &value)) {
PyObject *onerepr = PyUnicode_FromFormat("%U=%R", key, value);
if (onerepr == NULL)
goto done;
if (i >= numtotalargs) {
i = -1;
break;
}
PyTuple_SET_ITEM(argreprs, i, onerepr);
++i;
}
if (i != numtotalargs) {
PyErr_SetString(PyExc_RuntimeError,
"keywords dict changed size during iteration");
goto done;
}
}
sep = PyUnicode_FromString(", ");
if (sep == NULL)
goto done;
joinedargreprs = PyUnicode_Join(sep, argreprs);
Py_DECREF(sep);
if (joinedargreprs == NULL)
goto done;
repr = PyUnicode_FromFormat("%s(%R, %U)", Py_TYPE(mc)->tp_name,
mc->name, joinedargreprs);
Py_DECREF(joinedargreprs);
done:
Py_DECREF(argreprs);
Py_ReprLeave((PyObject *)mc);
return repr;
}
static PyObject *
methodcaller_reduce(methodcallerobject *mc)
{
PyObject *newargs;
if (!mc->kwds || PyDict_Size(mc->kwds) == 0) {
Py_ssize_t i;
Py_ssize_t callargcount = PyTuple_GET_SIZE(mc->args);
newargs = PyTuple_New(1 + callargcount);
if (newargs == NULL)
return NULL;
Py_INCREF(mc->name);
PyTuple_SET_ITEM(newargs, 0, mc->name);
for (i = 0; i < callargcount; ++i) {
PyObject *arg = PyTuple_GET_ITEM(mc->args, i);
Py_INCREF(arg);
PyTuple_SET_ITEM(newargs, i + 1, arg);
}
return Py_BuildValue("ON", Py_TYPE(mc), newargs);
}
else {
PyObject *functools;
PyObject *partial;
PyObject *constructor;
_Py_IDENTIFIER(partial);
functools = PyImport_ImportModule("functools");
if (!functools)
return NULL;
partial = _PyObject_GetAttrId(functools, &PyId_partial);
Py_DECREF(functools);
if (!partial)
return NULL;
newargs = PyTuple_New(2);
if (newargs == NULL) {
Py_DECREF(partial);
return NULL;
}
Py_INCREF(Py_TYPE(mc));
PyTuple_SET_ITEM(newargs, 0, (PyObject *)Py_TYPE(mc));
Py_INCREF(mc->name);
PyTuple_SET_ITEM(newargs, 1, mc->name);
constructor = PyObject_Call(partial, newargs, mc->kwds);
Py_DECREF(newargs);
Py_DECREF(partial);
return Py_BuildValue("NO", constructor, mc->args);
}
}
static PyMethodDef methodcaller_methods[] = {
{"__reduce__", (PyCFunction)methodcaller_reduce, METH_NOARGS,
reduce_doc},
{NULL}
};
PyDoc_STRVAR(methodcaller_doc, PyDoc_STRVAR(methodcaller_doc,
"methodcaller(name, ...) --> methodcaller object\n\ "methodcaller(name, ...) --> methodcaller object\n\
\n\ \n\
...@@ -888,7 +1151,7 @@ static PyTypeObject methodcaller_type = { ...@@ -888,7 +1151,7 @@ static PyTypeObject methodcaller_type = {
0, /* tp_getattr */ 0, /* tp_getattr */
0, /* tp_setattr */ 0, /* tp_setattr */
0, /* tp_reserved */ 0, /* tp_reserved */
0, /* tp_repr */ (reprfunc)methodcaller_repr, /* tp_repr */
0, /* tp_as_number */ 0, /* tp_as_number */
0, /* tp_as_sequence */ 0, /* tp_as_sequence */
0, /* tp_as_mapping */ 0, /* tp_as_mapping */
...@@ -906,7 +1169,7 @@ static PyTypeObject methodcaller_type = { ...@@ -906,7 +1169,7 @@ static PyTypeObject methodcaller_type = {
0, /* tp_weaklistoffset */ 0, /* tp_weaklistoffset */
0, /* tp_iter */ 0, /* tp_iter */
0, /* tp_iternext */ 0, /* tp_iternext */
0, /* tp_methods */ methodcaller_methods, /* tp_methods */
0, /* tp_members */ 0, /* tp_members */
0, /* tp_getset */ 0, /* tp_getset */
0, /* tp_base */ 0, /* tp_base */
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment