Index: Lib/pyclbr.py =================================================================== --- Lib/pyclbr.py (revision 74352) +++ Lib/pyclbr.py (working copy) @@ -15,14 +15,23 @@ is present for packages: the key '__path__' has a list as its value which contains the package search path. -A class is described by the class Class in this module. Instances -of this class have the following instance variables: - module -- the module name - name -- the name of the class - super -- a list of super classes (Class instances) +Classes and functions have a common superclass in this module, the Object +class. Every instance of this class have the following instance variables: + module -- the module name + name -- the name of the object + file -- the file in which the object was defined + lineno -- the line in the file on which the definition of the object + started + parent -- the parent of this object, if any + objects -- the other classes and function this object may contain +The 'objects' attribute is a dictionary where each key/value pair corresponds +to the name of the object and the object itself. + +A class is described by the class Class in this module. Instances +of this class have the following instance variables (plus the ones from +Object): + super -- a list of super classes (Class instances) methods -- a dictionary of methods - file -- the file in which the class was defined - lineno -- the line in the file on which the class statement occurred The dictionary of methods uses the method names as keys and the line numbers on which the method was defined as values. If the name of a super class is not recognized, the corresponding @@ -32,11 +41,6 @@ shouldn't happen often. A function is described by the class Function in this module. -Instances of this class have the following instance variables: - module -- the module name - name -- the name of the class - file -- the file in which the class was defined - lineno -- the line in the file on which the class statement occurred """ import sys @@ -45,34 +49,49 @@ from token import NAME, DEDENT, OP from operator import itemgetter -__all__ = ["readmodule", "readmodule_ex", "Class", "Function"] +__all__ = ["readmodule", "readmodule_ex", "Object", "Class", "Function"] _modules = {} # cache of modules we've seen +class Object: + def __init__(self, module, name, file, lineno, parent): + self.module = module + self.name = name + self.file = file + self.lineno = lineno + self.parent = parent + self.objects = {} + + def _addobject(self, name, obj): + self.objects[name] = obj + # each Python class is represented by an instance of this class -class Class: +class Class(Object): '''Class to represent a Python class.''' - def __init__(self, module, name, super, file, lineno): - self.module = module - self.name = name + def __init__(self, module, name, super, file, lineno, parent=None): + Object.__init__(self, module, name, file, lineno, parent) if super is None: super = [] self.super = super self.methods = {} - self.file = file - self.lineno = lineno def _addmethod(self, name, lineno): self.methods[name] = lineno -class Function: +class Function(Object): '''Class to represent a top-level Python function''' - def __init__(self, module, name, file, lineno): - self.module = module - self.name = name - self.file = file - self.lineno = lineno + def __init__(self, module, name, file, lineno, parent=None): + Object.__init__(self, module, name, file, lineno, parent) +def _newfunction(ob, name, lineno): + '''Helper function for creating a nested function or a method.''' + return Function(ob.module, name, ob.file, lineno, ob) + +def _newclass(ob, name, super, lineno): + '''Helper function for creating a nested class.''' + return Class(ob.module, name, super, ob.file, lineno, ob) + + def readmodule(module, path=None): '''Backwards compatible interface. @@ -164,17 +183,23 @@ tokentype, meth_name, start = g.next()[0:3] if tokentype != NAME: continue # Syntax error + cur_func = None if stack: - cur_class = stack[-1][0] - if isinstance(cur_class, Class): + cur_obj = stack[-1][0] + if isinstance(cur_obj, Object): + # it's a nested function or a method + cur_func = _newfunction(cur_obj, meth_name, lineno) + cur_obj._addobject(meth_name, cur_func) + + if isinstance(cur_obj, Class): # it's a method - cur_class._addmethod(meth_name, lineno) - # else it's a nested def + cur_func = _newfunction(cur_obj, meth_name, lineno) + cur_obj._addmethod(meth_name, lineno) else: # it's a function - dict[meth_name] = Function(fullmodule, meth_name, - fname, lineno) - stack.append((None, thisindent)) # Marker for nested fns + cur_func = Function(fullmodule, meth_name, fname, lineno) + dict[meth_name] = cur_func + stack.append((cur_func, thisindent)) # Marker for nested fns elif token == 'class': lineno, thisindent = start # close previous nested classes and defs @@ -225,9 +250,16 @@ super.append(token) # expressions in the base list are not supported inherit = names - cur_class = Class(fullmodule, class_name, inherit, - fname, lineno) - if not stack: + if stack: + cur_obj = stack[-1][0] + if isinstance(cur_obj, (Class, Function)): + # either a nested class or a class inside a function + cur_class = _newclass(cur_obj, class_name, inherit, + lineno) + cur_obj._addobject(class_name, cur_class) + else: + cur_class = Class(fullmodule, class_name, inherit, + fname, lineno) dict[class_name] = cur_class stack.append((cur_class, thisindent)) elif token == 'import' and start[1] == 0: @@ -325,18 +357,30 @@ else: path = [] dict = readmodule_ex(mod, path) + lineno_key = lambda a: getattr(a, 'lineno', 0) objs = dict.values() - objs.sort(lambda a, b: cmp(getattr(a, 'lineno', 0), - getattr(b, 'lineno', 0))) - for obj in objs: + objs.sort(key=lineno_key, reverse=True) + indent_level = 2 + while objs: + obj = objs.pop() + if isinstance(obj, list): + # Value of a __path__ key + continue + if not hasattr(obj, 'indent'): + obj.indent = 0 + + if isinstance(obj, Object): + new_objs = obj.objects.values() + new_objs.sort(key=lineno_key, reverse=True) + for ob in new_objs: + ob.indent = obj.indent + indent_level + objs.extend(new_objs) + if isinstance(obj, Class): - print "class", obj.name, obj.super, obj.lineno - methods = sorted(obj.methods.iteritems(), key=itemgetter(1)) - for name, lineno in methods: - if name != "__path__": - print " def", name, lineno + print "%sclass %s %s %s" % (' ' * obj.indent, obj.name, + obj.super, obj.lineno) elif isinstance(obj, Function): - print "def", obj.name, obj.lineno + print "%sdef %s %s" % (' ' * obj.indent, obj.name, obj.lineno) if __name__ == "__main__": _main() Index: Lib/test/test_pyclbr.py =================================================================== --- Lib/test/test_pyclbr.py (revision 74352) +++ Lib/test/test_pyclbr.py (working copy) @@ -2,11 +2,14 @@ Test cases for pyclbr.py Nick Mathewson ''' -from test.test_support import run_unittest +import os import sys +import pyclbr +import shutil from types import ClassType, FunctionType, MethodType, BuiltinFunctionType -import pyclbr from unittest import TestCase +from functools import partial +from test.test_support import TESTFN, run_unittest StaticMethodType = type(staticmethod(lambda: None)) ClassMethodType = type(classmethod(lambda c: None)) @@ -158,6 +161,124 @@ # self.checkModule('test.pyclbr_input') + def test_nested(self): + + def clbr_from_tuple(t, store, parent=None, lineno=1): + '''Create pyclbr objects from the given tuple t.''' + name = t[0] + obj = pickp(name) + if parent is not None: + store = store[parent].objects + ob_name = name.split()[1] + store[ob_name] = obj(name=ob_name, lineno=lineno, parent=parent) + parent = ob_name + + for item in t[1:]: + lineno += 1 + if isinstance(item, str): + obj = pickp(item) + ob_name = item.split()[1] + store[parent].objects[ob_name] = obj( + name=ob_name, lineno=lineno, parent=parent) + else: + lineno = clbr_from_tuple(item, store, parent, lineno) + + return lineno + + def tuple_to_py(t, output, indent=0): + '''Write python code to output according to the given tuple.''' + name = t[0] + output.write('%s%s():' % (' ' * indent, name)) + indent += 2 + + if not t[1:]: + output.write(' pass') + output.write('\n') + + for item in t[1:]: + if isinstance(item, str): + output.write('%s%s(): pass\n' % (' ' * indent, item)) + else: + tuple_to_py(item, output, indent) + + # Nested "thing" to test. + sample = ( + ("class A", + ("class B", + "def a"), + "def b"), + ("def c", + ("def d", + ("class C", + "def e") + ), + "def f") + ) + + pclass = partial(pyclbr.Class, module=None, file=None, super=None) + pfunc = partial(pyclbr.Function, module=None, file=None) + pickp = lambda name: pclass if name.startswith('class') else pfunc + + # Create a module for storing the Python code. + dirname = os.path.abspath(TESTFN) + modname = 'notsupposedtoexist' + fname = os.path.join(dirname, modname) + os.extsep + 'py' + os.mkdir(dirname) + orig_syspath = list(sys.path) + sys.path.insert(0, dirname) + + # Create pyclbr objects from the sample above, and also convert + # the same sample above to Python code and write it to fname. + d = {} + lineno = 1 + with open(fname, 'w') as output: + for t in sample: + newlineno = clbr_from_tuple(t, d, lineno=lineno) + lineno = newlineno + 1 + tuple_to_py(t, output) + + # Get the data returned by readmodule_ex to compare against + # our generated data. + try: + d_cmp = pyclbr.readmodule_ex(modname) + finally: + sys.path[:] = orig_syspath + try: + os.remove(fname) + shutil.rmtree(dirname) + except OSError: + pass + + # Finally perform the tests. + def check_objects(ob1, ob2): + self.assertEqual(ob1.lineno, ob2.lineno) + if ob1.parent is None: + self.assertIsNone(ob2.parent) + else: + # ob1 must come from our generated data since the parent + # attribute is always a string, while the ob2 must come + # from pyclbr which is always an Object instance. + self.assertEqual(ob1.parent, ob2.parent.name) + self.assertEqual( + ob1.__class__.__name__, + ob2.__class__.__name__) + self.assertEqual(ob1.objects.keys(), ob2.objects.keys()) + for name, obj in ob1.objects.items(): + obj_cmp = ob2.objects.pop(name) + del ob1.objects[name] + check_objects(obj, obj_cmp) + + objs = [] + for name, obj in d.items(): + self.assertIn(name, d_cmp) + obj_cmp = d_cmp.pop(name) + del d[name] + check_objects(obj, obj_cmp) + self.assertFalse(obj.objects) + self.assertFalse(obj_cmp.objects) + self.assertFalse(d) + self.assertFalse(d_cmp) + def test_others(self): cm = self.checkModule Index: Doc/library/pyclbr.rst =================================================================== --- Doc/library/pyclbr.rst (revision 74352) +++ Doc/library/pyclbr.rst (working copy) @@ -36,78 +36,74 @@ path. -.. _pyclbr-class-objects: +.. _pyclbr-object-objects: -Class Objects -------------- +Object Objects +-------------- -The :class:`Class` objects used as values in the dictionary returned by -:func:`readmodule` and :func:`readmodule_ex` provide the following data -members: +The class :class:`Object` is the base class for the classes :class:`Object` +and :class:`Function`. It provides the following data members: +.. attribute:: Object.module -.. attribute:: Class.module + The name of the module defining the object described. - The name of the module defining the class described by the class descriptor. +.. attribute:: Object.name -.. attribute:: Class.name + The name of the object. - The name of the class. +.. attribute:: Object.file -.. attribute:: Class.super + Name of the file in which the object was defined. - A list of :class:`Class` objects which describe the immediate base - classes of the class being described. Classes which are named as - superclasses but which are not discoverable by :func:`readmodule` are - listed as a string with the class name instead of as :class:`Class` - objects. +.. attribute:: Object.lineno -.. attribute:: Class.methods + The line number in the file named by :attr:`~Object.file` where + the definition of the object stated. - A dictionary mapping method names to line numbers. +.. attribute:: Object.parent -.. attribute:: Class.file + The parent of this object, if any. - Name of the file containing the ``class`` statement defining the class. +.. attribute:: Object.objects -.. attribute:: Class.lineno + A dictionary mapping object names to the objects that are defined inside the + namespace created by the current object. - The line number of the ``class`` statement within the file named by - :attr:`~Class.file`. +.. _pyclbr-class-objects: -.. _pyclbr-function-objects: +Class Objects +------------- -Function Objects ----------------- +The :class:`Class` objects used as values in the dictionary returned by +:func:`readmodule` and :func:`readmodule_ex` provide the following extra +data members: -The :class:`Function` objects used as values in the dictionary returned by -:func:`readmodule_ex` provide the following data members: +.. attribute:: Class.super + A list of :class:`Class` objects which describe the immediate base + classes of the class being described. Classes which are named as + superclasses but which are not discoverable by :func:`readmodule` are + listed as a string with the class name instead of as :class:`Class` + objects. -.. attribute:: Function.module - The name of the module defining the function described by the function - descriptor. +.. attribute:: Class.methods + A dictionary mapping method names to line numbers. -.. attribute:: Function.name - The name of the function. +.. _pyclbr-function-objects: +Function Objects +---------------- -.. attribute:: Function.file - - Name of the file containing the ``def`` statement defining the function. - - -.. attribute:: Function.lineno - - The line number of the ``def`` statement within the file named by - :attr:`~Function.file`. - +The :class:`Function` objects used as values in the dictionary returned by +:func:`readmodule_ex` provide only the members already defined by +:class:`Class` objects.