From e32b4224d0e5a5a2faa7398211ad859e8a4cb0c8 Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Sat, 10 Mar 2007 22:13:27 +0000 Subject: Patch #1591665: implement the __dir__() special function lookup in PyObject_Dir. --- Doc/lib/libfuncs.tex | 59 +++++++---- Lib/test/test_builtin.py | 63 ++++++++++- Misc/NEWS | 10 +- Objects/object.c | 270 +++++++++++++++++++++++++---------------------- Python/bltinmodule.c | 19 ++-- 5 files changed, 259 insertions(+), 162 deletions(-) diff --git a/Doc/lib/libfuncs.tex b/Doc/lib/libfuncs.tex index 02cca83..b1d2983 100644 --- a/Doc/lib/libfuncs.tex +++ b/Doc/lib/libfuncs.tex @@ -274,21 +274,34 @@ class C: \end{funcdesc} \begin{funcdesc}{dir}{\optional{object}} - Without arguments, return the list of names in the current local - symbol table. With an argument, attempts to return a list of valid - attributes for that object. This information is gleaned from the - object's \member{__dict__} attribute, if defined, and from the class - or type object. The list is not necessarily complete. - If the object is a module object, the list contains the names of the - module's attributes. - If the object is a type or class object, - the list contains the names of its attributes, - and recursively of the attributes of its bases. - Otherwise, the list contains the object's attributes' names, - the names of its class's attributes, - and recursively of the attributes of its class's base classes. - The resulting list is sorted alphabetically. - For example: + Without arguments, return the list of names in the current local scope. With + an argument, attempt to return a list of valid attributes for that object. + + If the object has a method named \method{__dir__()}, this method will be + called and must return the list of attributes. This allows objects that + implement a custom \function{__getattr__()} or \function{__getattribute__()} + function to customize the way \function{dir()} reports their attributes. + + If the object does not provide \method{__dir__()}, the function tries its best + to gather information from the object's \member{__dict__} attribute, if + defined, and from its type object. The resulting list is not necessarily + complete, and may be inaccurate when the object has a custom + \function{__getattr__()}. + + The default \function{dir()} mechanism behaves differently with different + types of objects, as it attempts to produce the most relevant, rather than + complete, information: + \begin{itemize} + \item If the object is a module object, the list contains the names of the + module's attributes. + \item If the object is a type or class object, the list contains the names of + its attributes, and recursively of the attributes of its bases. + \item Otherwise, the list contains the object's attributes' names, the names + of its class's attributes, and recursively of the attributes of its class's + base classes. + \end{itemize} + + The resulting list is sorted alphabetically. For example: \begin{verbatim} >>> import struct @@ -296,13 +309,19 @@ class C: ['__builtins__', '__doc__', '__name__', 'struct'] >>> dir(struct) ['__doc__', '__name__', 'calcsize', 'error', 'pack', 'unpack'] +>>> class Foo(object): +... def __dir__(self): +... return ["kan", "ga", "roo"] +... +>>> f = Foo() +>>> dir(f) +['ga', 'kan', 'roo'] \end{verbatim} - \note{Because \function{dir()} is supplied primarily as a convenience - for use at an interactive prompt, - it tries to supply an interesting set of names more than it tries to - supply a rigorously or consistently defined set of names, - and its detailed behavior may change across releases.} + \note{Because \function{dir()} is supplied primarily as a convenience for use + at an interactive prompt, it tries to supply an interesting set of names + more than it tries to supply a rigorously or consistently defined set of + names, and its detailed behavior may change across releases.} \end{funcdesc} \begin{funcdesc}{divmod}{a, b} diff --git a/Lib/test/test_builtin.py b/Lib/test/test_builtin.py index d2f70ff..05e80e4 100644 --- a/Lib/test/test_builtin.py +++ b/Lib/test/test_builtin.py @@ -223,12 +223,67 @@ class BuiltinTest(unittest.TestCase): self.assertRaises(TypeError, delattr) def test_dir(self): - x = 1 - self.assert_('x' in dir()) - import sys - self.assert_('modules' in dir(sys)) + # dir(wrong number of arguments) self.assertRaises(TypeError, dir, 42, 42) + # dir() - local scope + local_var = 1 + self.assert_('local_var' in dir()) + + # dir(module) + import sys + self.assert_('exit' in dir(sys)) + + # dir(module_with_invalid__dict__) + import types + class Foo(types.ModuleType): + __dict__ = 8 + f = Foo("foo") + self.assertRaises(TypeError, dir, f) + + # dir(type) + self.assert_("strip" in dir(str)) + self.assert_("__mro__" not in dir(str)) + + # dir(obj) + class Foo(object): + def __init__(self): + self.x = 7 + self.y = 8 + self.z = 9 + f = Foo() + self.assert_("y" in dir(f)) + + # dir(obj_no__dict__) + class Foo(object): + __slots__ = [] + f = Foo() + self.assert_("__repr__" in dir(f)) + + # dir(obj_no__class__with__dict__) + # (an ugly trick to cause getattr(f, "__class__") to fail) + class Foo(object): + __slots__ = ["__class__", "__dict__"] + def __init__(self): + self.bar = "wow" + f = Foo() + self.assert_("__repr__" not in dir(f)) + self.assert_("bar" in dir(f)) + + # dir(obj_using __dir__) + class Foo(object): + def __dir__(self): + return ["kan", "ga", "roo"] + f = Foo() + self.assert_(dir(f) == ["ga", "kan", "roo"]) + + # dir(obj__dir__not_list) + class Foo(object): + def __dir__(self): + return 7 + f = Foo() + self.assertRaises(TypeError, dir, f) + def test_divmod(self): self.assertEqual(divmod(12, 7), (1, 5)) self.assertEqual(divmod(-12, 7), (-2, 2)) diff --git a/Misc/NEWS b/Misc/NEWS index e445d3e..7791a45 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -28,11 +28,15 @@ TO DO Core and Builtins ----------------- -- Removing indexing/slicing on BaseException. +- The dir() function has been extended to call the __dir__() method on + its argument, if it exists. If not, it will work like before. This allows + customizing the output of dir() in the presence of a __getattr__(). -- Remove the exceptions module, all the exceptions are already builtin. +- Removed indexing/slicing on BaseException. -- input() becomes raw_input(): the name input() now implements the +- Removed the exceptions module, all the exceptions are already builtin. + +- input() became raw_input(): the name input() now implements the functionality formerly known as raw_input(); the name raw_input() is no longer defined. diff --git a/Objects/object.c b/Objects/object.c index 065abdc..e2d1b13 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -1284,6 +1284,8 @@ PyCallable_Check(PyObject *x) return x->ob_type->tp_call != NULL; } +/* ------------------------- PyObject_Dir() helpers ------------------------- */ + /* Helper for PyObject_Dir. Merge the __dict__ of aclass into dict, and recursively also all the __dict__s of aclass's base classes. The order of merging isn't @@ -1343,158 +1345,174 @@ merge_class_dict(PyObject* dict, PyObject* aclass) return 0; } -/* Helper for PyObject_Dir. - If obj has an attr named attrname that's a list, merge its string - elements into keys of dict. - Return 0 on success, -1 on error. Errors due to not finding the attr, - or the attr not being a list, are suppressed. -*/ +/* Helper for PyObject_Dir without arguments: returns the local scope. */ +static PyObject * +_dir_locals() +{ + PyObject *locals = PyEval_GetLocals(); -static int -merge_list_attr(PyObject* dict, PyObject* obj, const char *attrname) + if (locals == NULL) { + PyErr_SetString(PyExc_SystemError, "frame does not exist"); + return NULL; + } + + /* the locals don't need to be DECREF'd */ + return PyMapping_Keys(locals); +} + +/* Helper for PyObject_Dir of type objects: returns __dict__ and __bases__. + We deliberately don't suck up its __class__, as methods belonging to the + metaclass would probably be more confusing than helpful. +*/ +static PyObject * +_specialized_dir_type(PyObject *obj) { - PyObject *list; - int result = 0; + PyObject *result = NULL; + PyObject *dict = PyDict_New(); - assert(PyDict_Check(dict)); - assert(obj); - assert(attrname); + if (dict != NULL && merge_class_dict(dict, obj) == 0) + result = PyDict_Keys(dict); - list = PyObject_GetAttrString(obj, attrname); - if (list == NULL) - PyErr_Clear(); + Py_XDECREF(dict); + return result; +} - else if (PyList_Check(list)) { - int i; - for (i = 0; i < PyList_GET_SIZE(list); ++i) { - PyObject *item = PyList_GET_ITEM(list, i); - if (PyString_Check(item)) { - result = PyDict_SetItem(dict, item, Py_None); - if (result < 0) - break; - } +/* Helper for PyObject_Dir of module objects: returns the module's __dict__. */ +static PyObject * +_specialized_dir_module(PyObject *obj) +{ + PyObject *result = NULL; + PyObject *dict = PyObject_GetAttrString(obj, "__dict__"); + + if (dict != NULL) { + if (PyDict_Check(dict)) + result = PyDict_Keys(dict); + else { + PyErr_Format(PyExc_TypeError, + "%.200s.__dict__ is not a dictionary", + PyModule_GetName(obj)); } } - Py_XDECREF(list); + Py_XDECREF(dict); return result; } -/* Like __builtin__.dir(arg). See bltinmodule.c's builtin_dir for the - docstring, which should be kept in synch with this implementation. */ - -PyObject * -PyObject_Dir(PyObject *arg) +/* Helper for PyObject_Dir of generic objects: returns __dict__, __class__, + and recursively up the __class__.__bases__ chain. +*/ +static PyObject * +_generic_dir(PyObject *obj) { - /* Set exactly one of these non-NULL before the end. */ - PyObject *result = NULL; /* result list */ - PyObject *masterdict = NULL; /* result is masterdict.keys() */ - - /* If NULL arg, return the locals. */ - if (arg == NULL) { - PyObject *locals = PyEval_GetLocals(); - if (locals == NULL) - goto error; - result = PyMapping_Keys(locals); - if (result == NULL) - goto error; + PyObject *result = NULL; + PyObject *dict = NULL; + PyObject *itsclass = NULL; + + /* Get __dict__ (which may or may not be a real dict...) */ + dict = PyObject_GetAttrString(obj, "__dict__"); + if (dict == NULL) { + PyErr_Clear(); + dict = PyDict_New(); } - - /* Elif this is some form of module, we only want its dict. */ - else if (PyModule_Check(arg)) { - masterdict = PyObject_GetAttrString(arg, "__dict__"); - if (masterdict == NULL) - goto error; - if (!PyDict_Check(masterdict)) { - PyErr_SetString(PyExc_TypeError, - "module.__dict__ is not a dictionary"); - goto error; - } + else if (!PyDict_Check(dict)) { + Py_DECREF(dict); + dict = PyDict_New(); } - - /* Elif some form of type or class, grab its dict and its bases. - We deliberately don't suck up its __class__, as methods belonging - to the metaclass would probably be more confusing than helpful. */ - else if (PyType_Check(arg)) { - masterdict = PyDict_New(); - if (masterdict == NULL) - goto error; - if (merge_class_dict(masterdict, arg) < 0) - goto error; + else { + /* Copy __dict__ to avoid mutating it. */ + PyObject *temp = PyDict_Copy(dict); + Py_DECREF(dict); + dict = temp; } - /* Else look at its dict, and the attrs reachable from its class. */ + if (dict == NULL) + goto error; + + /* Merge in attrs reachable from its class. */ + itsclass = PyObject_GetAttrString(obj, "__class__"); + if (itsclass == NULL) + /* XXX(tomer): Perhaps fall back to obj->ob_type if no + __class__ exists? */ + PyErr_Clear(); else { - PyObject *itsclass; - /* Create a dict to start with. CAUTION: Not everything - responding to __dict__ returns a dict! */ - masterdict = PyObject_GetAttrString(arg, "__dict__"); - if (masterdict == NULL) { - PyErr_Clear(); - masterdict = PyDict_New(); - } - else if (!PyDict_Check(masterdict)) { - Py_DECREF(masterdict); - masterdict = PyDict_New(); - } - else { - /* The object may have returned a reference to its - dict, so copy it to avoid mutating it. */ - PyObject *temp = PyDict_Copy(masterdict); - Py_DECREF(masterdict); - masterdict = temp; - } - if (masterdict == NULL) + if (merge_class_dict(dict, itsclass) != 0) goto error; + } - /* Merge in __members__ and __methods__ (if any). - XXX Would like this to go away someday; for now, it's - XXX needed to get at im_self etc of method objects. */ - if (merge_list_attr(masterdict, arg, "__members__") < 0) - goto error; - if (merge_list_attr(masterdict, arg, "__methods__") < 0) - goto error; + result = PyDict_Keys(dict); + /* fall through */ +error: + Py_XDECREF(itsclass); + Py_XDECREF(dict); + return result; +} - /* Merge in attrs reachable from its class. - CAUTION: Not all objects have a __class__ attr. */ - itsclass = PyObject_GetAttrString(arg, "__class__"); - if (itsclass == NULL) - PyErr_Clear(); - else { - int status = merge_class_dict(masterdict, itsclass); - Py_DECREF(itsclass); - if (status < 0) - goto error; - } - } +/* Helper for PyObject_Dir: object introspection. + This calls one of the above specialized versions if no __dir__ method + exists. */ +static PyObject * +_dir_object(PyObject *obj) +{ + PyObject * result = NULL; + PyObject * dirfunc = PyObject_GetAttrString((PyObject*)obj->ob_type, + "__dir__"); - assert((result == NULL) ^ (masterdict == NULL)); - if (masterdict != NULL) { - /* The result comes from its keys. */ - assert(result == NULL); - result = PyDict_Keys(masterdict); - if (result == NULL) - goto error; + assert(obj); + if (dirfunc == NULL) { + /* use default implementation */ + PyErr_Clear(); + if (PyModule_Check(obj)) + result = _specialized_dir_module(obj); + else if (PyType_Check(obj)) + result = _specialized_dir_type(obj); + else + result = _generic_dir(obj); } + else { + /* use __dir__ */ + result = PyObject_CallFunctionObjArgs(dirfunc, obj, NULL); + Py_DECREF(dirfunc); + if (result == NULL) + return NULL; - assert(result); - if (!PyList_Check(result)) { - PyErr_Format(PyExc_TypeError, - "Expected keys() to be a list, not '%.200s'", - result->ob_type->tp_name); - goto error; + /* result must be a list */ + /* XXX(gbrandl): could also check if all items are strings */ + if (!PyList_Check(result)) { + PyErr_Format(PyExc_TypeError, + "__dir__() must return a list, not %.200s", + result->ob_type->tp_name); + Py_DECREF(result); + result = NULL; + } } - if (PyList_Sort(result) != 0) - goto error; + + return result; +} + +/* Implementation of dir() -- if obj is NULL, returns the names in the current + (local) scope. Otherwise, performs introspection of the object: returns a + sorted list of attribute names (supposedly) accessible from the object +*/ +PyObject * +PyObject_Dir(PyObject *obj) +{ + PyObject * result; + + if (obj == NULL) + /* no object -- introspect the locals */ + result = _dir_locals(); else - goto normal_return; + /* object -- introspect the object */ + result = _dir_object(obj); - error: - Py_XDECREF(result); - result = NULL; - /* fall through */ - normal_return: - Py_XDECREF(masterdict); + assert(result == NULL || PyList_Check(result)); + + if (result != NULL && PyList_Sort(result) != 0) { + /* sorting the list failed */ + Py_DECREF(result); + result = NULL; + } + return result; } diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c index 3c149e4..5d87744 100644 --- a/Python/bltinmodule.c +++ b/Python/bltinmodule.c @@ -427,15 +427,16 @@ builtin_dir(PyObject *self, PyObject *args) PyDoc_STRVAR(dir_doc, "dir([object]) -> list of strings\n" "\n" -"Return an alphabetized list of names comprising (some of) the attributes\n" -"of the given object, and of attributes reachable from it:\n" -"\n" -"No argument: the names in the current scope.\n" -"Module object: the module attributes.\n" -"Type or class object: its attributes, and recursively the attributes of\n" -" its bases.\n" -"Otherwise: its attributes, its class's attributes, and recursively the\n" -" attributes of its class's base classes."); +"If called without an argument, return the names in the current scope.\n" +"Else, return an alphabetized list of names comprising (some of) the attributes\n" +"of the given object, and of attributes reachable from it.\n" +"If the object supplies a method named __dir__, it will be used; otherwise\n" +"the default dir() logic is used and returns:\n" +" for a module object: the module's attributes.\n" +" for a class object: its attributes, and recursively the attributes\n" +" of its bases.\n" +" for an other object: its attributes, its class's attributes, and\n" +" recursively the attributes of its class's base classes."); static PyObject * builtin_divmod(PyObject *self, PyObject *args) -- cgit v0.12