Index: Doc/library/pyclbr.rst =================================================================== --- Doc/library/pyclbr.rst (revision 85150) +++ Doc/library/pyclbr.rst (working copy) @@ -35,78 +35,78 @@ 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 started. - A dictionary mapping method names to line numbers. +.. attribute:: Object.parent -.. attribute:: Class.file + For objects nested inside another, the enclosing object, or ``None``. - Name of the file containing the ``class`` statement defining the class. + ..versionadded:: 3.2 -.. attribute:: Class.lineno +.. attribute:: Object.objects - The line number of the ``class`` statement within the file named by - :attr:`~Class.file`. + A dictionary mapping object names to the objects that are defined inside the + namespace created by the current object. + ..versionadded:: 3.2 -.. _pyclbr-function-objects: -Function Objects ----------------- +.. _pyclbr-class-objects: -The :class:`Function` objects used as values in the dictionary returned by -:func:`readmodule_ex` provide the following data members: +Class 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: -.. attribute:: Function.module +.. attribute:: Class.super - The name of the module defining the function described by the function - descriptor. + 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.name +.. attribute:: Class.methods - The name of the function. + A dictionary mapping method names to line numbers. -.. attribute:: Function.file +.. _pyclbr-function-objects: - Name of the file containing the ``def`` statement defining the function. +Function Objects +---------------- - -.. 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. Index: Lib/pyclbr.py =================================================================== --- Lib/pyclbr.py (revision 85150) +++ Lib/pyclbr.py (working copy) @@ -15,14 +15,24 @@ 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 has 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 -- for objects nested inside another object, the enclosing + object, or None. + 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 +42,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 +50,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 +184,23 @@ tokentype, meth_name, start = next(g)[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 +251,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,17 +358,29 @@ else: path = [] dict = readmodule_ex(mod, path) - objs = dict.values() - objs.sort(key=lambda a: getattr(a, 'lineno', 0)) - for obj in objs: + lineno_key = lambda a: getattr(a, 'lineno', 0) + objs = sorted(dict.values(), + 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 = sorted(obj.objects.values(), + 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.items(), key=itemgetter(1)) - for name, lineno in methods: - if name != "__path__": - print(" def", name, lineno) + print(' ' * obj.indent + "class", obj.name, obj.super, obj.lineno) elif isinstance(obj, Function): - print("def", obj.name, obj.lineno) + print(' ' * obj.indent + "def", obj.name, obj.lineno) if __name__ == "__main__": _main() Index: Lib/test/test_pyclbr.py =================================================================== --- Lib/test/test_pyclbr.py (revision 85150) +++ Lib/test/test_pyclbr.py (working copy) @@ -3,10 +3,13 @@ Nick Mathewson ''' from test.support import run_unittest +from test import support +import os import sys from types import FunctionType, MethodType, BuiltinFunctionType import pyclbr from unittest import TestCase +from functools import partial StaticMethodType = type(staticmethod(lambda: None)) ClassMethodType = type(classmethod(lambda c: None)) @@ -151,6 +154,119 @@ # self.checkModule('test.pyclbr_input', ignore=['om']) + 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(support.TESTFN) + modname = 'notsupposedtoexist' + fname = os.path.join(dirname, modname) + os.extsep + 'py' + os.mkdir(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: + with support.DirsOnSysPath(dirname): + d_cmp = pyclbr.readmodule_ex(modname) + finally: + support.unlink(fname) + support.rmtree(dirname) + + # 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 list(ob1.objects.items()): + obj_cmp = ob2.objects.pop(name) + del ob1.objects[name] + check_objects(obj, obj_cmp) + + objs = [] + for name, obj in list(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