Skip to content

Commit fe943b3

Browse files
author
georg.brandl
committed
#1826: allow dotted attribute paths in operator.attrgetter.
git-svn-id: http://svn.python.org/projects/python/trunk@61027 6015fed2-1504-0410-9fe1-9d1591cc4771
1 parent 4651e92 commit fe943b3

4 files changed

Lines changed: 78 additions & 5 deletions

File tree

Doc/library/operator.rst

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -499,15 +499,21 @@ expect a function argument.
499499

500500
Return a callable object that fetches *attr* from its operand. If more than one
501501
attribute is requested, returns a tuple of attributes. After,
502-
``f=attrgetter('name')``, the call ``f(b)`` returns ``b.name``. After,
503-
``f=attrgetter('name', 'date')``, the call ``f(b)`` returns ``(b.name,
502+
``f = attrgetter('name')``, the call ``f(b)`` returns ``b.name``. After,
503+
``f = attrgetter('name', 'date')``, the call ``f(b)`` returns ``(b.name,
504504
b.date)``.
505505

506+
The attribute names can also contain dots; after ``f = attrgetter('date.month')``,
507+
the call ``f(b)`` returns ``b.date.month``.
508+
506509
.. versionadded:: 2.4
507510

508511
.. versionchanged:: 2.5
509512
Added support for multiple attributes.
510513

514+
.. versionchanged:: 2.6
515+
Added support for dotted attributes.
516+
511517

512518
.. function:: itemgetter(item[, args...])
513519

Lib/test/test_operator.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,26 @@ def __getattr__(self, name):
386386
raise SyntaxError
387387
self.failUnlessRaises(SyntaxError, operator.attrgetter('foo'), C())
388388

389+
# recursive gets
390+
a = A()
391+
a.name = 'arthur'
392+
a.child = A()
393+
a.child.name = 'thomas'
394+
f = operator.attrgetter('child.name')
395+
self.assertEqual(f(a), 'thomas')
396+
self.assertRaises(AttributeError, f, a.child)
397+
f = operator.attrgetter('name', 'child.name')
398+
self.assertEqual(f(a), ('arthur', 'thomas'))
399+
f = operator.attrgetter('name', 'child.name', 'child.child.name')
400+
self.assertRaises(AttributeError, f, a)
401+
402+
a.child.child = A()
403+
a.child.child.name = 'johnson'
404+
f = operator.attrgetter('child.child.name')
405+
self.assertEqual(f(a), 'johnson')
406+
f = operator.attrgetter('name', 'child.name', 'child.child.name')
407+
self.assertEqual(f(a), ('arthur', 'thomas', 'johnson'))
408+
389409
def test_itemgetter(self):
390410
a = 'ABCDE'
391411
f = operator.itemgetter(2)

Misc/NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1196,6 +1196,8 @@ Library
11961196
Extension Modules
11971197
-----------------
11981198

1199+
- Patch #1826: operator.attrgetter() now supports dotted attribute paths.
1200+
11991201
- Patch #1957: syslogmodule: Release GIL when calling syslog(3)
12001202

12011203
- #2112: mmap.error is now a subclass of EnvironmentError and not a

Modules/operator.c

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,49 @@ attrgetter_traverse(attrgetterobject *ag, visitproc visit, void *arg)
495495
return 0;
496496
}
497497

498+
static PyObject *
499+
dotted_getattr(PyObject *obj, PyObject *attr)
500+
{
501+
char *s, *p;
502+
503+
#ifdef Py_USING_UNICODE
504+
if (PyUnicode_Check(attr)) {
505+
attr = _PyUnicode_AsDefaultEncodedString(attr, NULL);
506+
if (attr == NULL)
507+
return NULL;
508+
}
509+
#endif
510+
511+
if (!PyString_Check(attr)) {
512+
PyErr_SetString(PyExc_TypeError,
513+
"attribute name must be a string");
514+
return NULL;
515+
}
516+
517+
s = PyString_AS_STRING(attr);
518+
Py_INCREF(obj);
519+
for (;;) {
520+
PyObject *newobj, *str;
521+
p = strchr(s, '.');
522+
str = p ? PyString_FromStringAndSize(s, (p-s)) :
523+
PyString_FromString(s);
524+
if (str == NULL) {
525+
Py_DECREF(obj);
526+
return NULL;
527+
}
528+
newobj = PyObject_GetAttr(obj, str);
529+
Py_DECREF(str);
530+
Py_DECREF(obj);
531+
if (newobj == NULL)
532+
return NULL;
533+
obj = newobj;
534+
if (p == NULL) break;
535+
s = p+1;
536+
}
537+
538+
return obj;
539+
}
540+
498541
static PyObject *
499542
attrgetter_call(attrgetterobject *ag, PyObject *args, PyObject *kw)
500543
{
@@ -504,7 +547,7 @@ attrgetter_call(attrgetterobject *ag, PyObject *args, PyObject *kw)
504547
if (!PyArg_UnpackTuple(args, "attrgetter", 1, 1, &obj))
505548
return NULL;
506549
if (ag->nattrs == 1)
507-
return PyObject_GetAttr(obj, ag->attr);
550+
return dotted_getattr(obj, ag->attr);
508551

509552
assert(PyTuple_Check(ag->attr));
510553
assert(PyTuple_GET_SIZE(ag->attr) == nattrs);
@@ -516,7 +559,7 @@ attrgetter_call(attrgetterobject *ag, PyObject *args, PyObject *kw)
516559
for (i=0 ; i < nattrs ; i++) {
517560
PyObject *attr, *val;
518561
attr = PyTuple_GET_ITEM(ag->attr, i);
519-
val = PyObject_GetAttr(obj, attr);
562+
val = dotted_getattr(obj, attr);
520563
if (val == NULL) {
521564
Py_DECREF(result);
522565
return NULL;
@@ -531,7 +574,9 @@ PyDoc_STRVAR(attrgetter_doc,
531574
\n\
532575
Return a callable object that fetches the given attribute(s) from its operand.\n\
533576
After, f=attrgetter('name'), the call f(r) returns r.name.\n\
534-
After, g=attrgetter('name', 'date'), the call g(r) returns (r.name, r.date).");
577+
After, g=attrgetter('name', 'date'), the call g(r) returns (r.name, r.date).\n\
578+
After, h=attrgetter('name.first', 'name.last'), the call h(r) returns\n\
579+
(r.name.first, r.name.last).");
535580

536581
static PyTypeObject attrgetter_type = {
537582
PyVarObject_HEAD_INIT(NULL, 0)

0 commit comments

Comments
 (0)