Skip to content

Commit b94e281

Browse files
committed
bpo-6691: Support for nested classes and functions in pyclbr
1 parent 8a8d285 commit b94e281

File tree

3 files changed

+255
-88
lines changed

3 files changed

+255
-88
lines changed

Doc/library/pyclbr.rst

Lines changed: 40 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -39,78 +39,77 @@ modules.
3939
path.
4040

4141

42-
.. _pyclbr-class-objects:
42+
.. _pyclbr-object-objects:
4343

44-
Class Objects
45-
-------------
46-
47-
The :class:`Class` objects used as values in the dictionary returned by
48-
:func:`readmodule` and :func:`readmodule_ex` provide the following data
49-
attributes:
50-
51-
52-
.. attribute:: Class.module
44+
Object Objects
45+
--------------
46+
The class :class:`Object` is the base class for the classes
47+
:class:`Class` and :class:`Function`. It provides the following
48+
data members:
5349

54-
The name of the module defining the class described by the class descriptor.
5550

51+
.. attribute:: Object.module
5652

57-
.. attribute:: Class.name
53+
The name of the module defining the object described.
5854

59-
The name of the class.
6055

56+
.. attribute:: Object.name
6157

62-
.. attribute:: Class.super
58+
The name of the object.
6359

64-
A list of :class:`Class` objects which describe the immediate base
65-
classes of the class being described. Classes which are named as
66-
superclasses but which are not discoverable by :func:`readmodule` are
67-
listed as a string with the class name instead of as :class:`Class`
68-
objects.
6960

61+
.. attribute:: Object.file
7062

71-
.. attribute:: Class.methods
63+
Name of the file in which the object was defined.
7264

73-
A dictionary mapping method names to line numbers.
7465

66+
.. attribute:: Object.lineno
7567

76-
.. attribute:: Class.file
68+
The line number in the file named by :attr:`~Object.file` where
69+
the definition of the object started.
7770

78-
Name of the file containing the ``class`` statement defining the class.
7971

72+
.. attribute:: Object.parent
8073

81-
.. attribute:: Class.lineno
74+
The parent of this object, if any.
8275

83-
The line number of the ``class`` statement within the file named by
84-
:attr:`~Class.file`.
8576

77+
.. attribute:: Object.objects
8678

87-
.. _pyclbr-function-objects:
79+
A dictionary mapping object names to the objects that are defined inside the
80+
namespace created by the current object.
8881

89-
Function Objects
90-
----------------
9182

92-
The :class:`Function` objects used as values in the dictionary returned by
93-
:func:`readmodule_ex` provide the following attributes:
83+
.. _pyclbr-class-objects:
9484

85+
Class Objects
86+
-------------
9587

96-
.. attribute:: Function.module
88+
The :class:`Class` objects used as values in the dictionary returned by
89+
:func:`readmodule` and :func:`readmodule_ex` provide the following extra
90+
data members:
9791

98-
The name of the module defining the function described by the function
99-
descriptor.
10092

93+
.. attribute:: Class.super
10194

102-
.. attribute:: Function.name
95+
A list of :class:`Class` objects which describe the immediate base
96+
classes of the class being described. Classes which are named as
97+
superclasses but which are not discoverable by :func:`readmodule` are
98+
listed as a string with the class name instead of as :class:`Class`
99+
objects.
103100

104-
The name of the function.
105101

102+
.. attribute:: Class.methods
106103

107-
.. attribute:: Function.file
104+
A dictionary mapping method names to line numbers.
108105

109-
Name of the file containing the ``def`` statement defining the function.
110106

107+
.. _pyclbr-function-objects:
111108

112-
.. attribute:: Function.lineno
109+
Function Objects
110+
----------------
113111

114-
The line number of the ``def`` statement within the file named by
115-
:attr:`~Function.file`.
112+
The :class:`Function` objects used as values in the dictionary returned by
113+
:func:`readmodule_ex` provide only the members already defined by
114+
:class:`Class` objects.
116115

Lib/pyclbr.py

Lines changed: 96 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,23 @@
1515
is present for packages: the key '__path__' has a list as its value
1616
which contains the package search path.
1717
18-
A class is described by the class Class in this module. Instances
19-
of this class have the following instance variables:
20-
module -- the module name
21-
name -- the name of the class
22-
super -- a list of super classes (Class instances)
18+
Classes and functions have a common superclass in this module, the Object
19+
class. Every instance of this class have the following instance variables:
20+
module -- the module name
21+
name -- the name of the object
22+
file -- the file in which the object was defined
23+
lineno -- the line in the file on which the definition of the object
24+
started
25+
parent -- the parent of this object, if any
26+
objects -- the other classes and function this object may contain
27+
The 'objects' attribute is a dictionary where each key/value pair corresponds
28+
to the name of the object and the object itself.
29+
30+
A class is described by the class Class in this module. Instances
31+
of this class have the following instance variables (plus the ones from
32+
Object):
33+
super -- a list of super classes (Class instances)
2334
methods -- a dictionary of methods
24-
file -- the file in which the class was defined
25-
lineno -- the line in the file on which the class statement occurred
2635
The dictionary of methods uses the method names as keys and the line
2736
numbers on which the method was defined as values.
2837
If the name of a super class is not recognized, the corresponding
@@ -32,11 +41,6 @@
3241
shouldn't happen often.
3342
3443
A function is described by the class Function in this module.
35-
Instances of this class have the following instance variables:
36-
module -- the module name
37-
name -- the name of the class
38-
file -- the file in which the class was defined
39-
lineno -- the line in the file on which the class statement occurred
4044
"""
4145

4246
import io
@@ -45,33 +49,54 @@
4549
import tokenize
4650
from token import NAME, DEDENT, OP
4751

48-
__all__ = ["readmodule", "readmodule_ex", "Class", "Function"]
52+
__all__ = ["readmodule", "readmodule_ex", "Object", "Class", "Function"]
4953

5054
_modules = {} # cache of modules we've seen
5155

52-
# each Python class is represented by an instance of this class
53-
class Class:
54-
'''Class to represent a Python class.'''
55-
def __init__(self, module, name, super, file, lineno):
56+
57+
class Object:
58+
"""Class to represent a Python object."""
59+
def __init__(self, module, name, file, lineno, parent):
5660
self.module = module
5761
self.name = name
62+
self.file = file
63+
self.lineno = lineno
64+
self.parent = parent
65+
self.objects = {}
66+
67+
def _addobject(self, name, obj):
68+
self.objects[name] = obj
69+
70+
71+
# each Python class is represented by an instance of this class
72+
class Class(Object):
73+
'''Class to represent a Python class.'''
74+
def __init__(self, module, name, super, file, lineno, parent=None):
75+
Object.__init__(self, module, name, file, lineno, parent)
5876
if super is None:
5977
super = []
6078
self.super = super
6179
self.methods = {}
62-
self.file = file
63-
self.lineno = lineno
6480

6581
def _addmethod(self, name, lineno):
6682
self.methods[name] = lineno
6783

68-
class Function:
84+
85+
class Function(Object):
6986
'''Class to represent a top-level Python function'''
70-
def __init__(self, module, name, file, lineno):
71-
self.module = module
72-
self.name = name
73-
self.file = file
74-
self.lineno = lineno
87+
def __init__(self, module, name, file, lineno, parent=None):
88+
Object.__init__(self, module, name, file, lineno, parent)
89+
90+
91+
def _newfunction(ob, name, lineno):
92+
'''Helper function for creating a nested function or a method.'''
93+
return Function(ob.module, name, ob.file, lineno, ob)
94+
95+
96+
def _newclass(ob, name, super, lineno):
97+
'''Helper function for creating a nested class.'''
98+
return Class(ob.module, name, super, ob.file, lineno, ob)
99+
75100

76101
def readmodule(module, path=None):
77102
'''Backwards compatible interface.
@@ -138,7 +163,6 @@ def _readmodule(module, path, inpackage=None):
138163
search_path = path
139164
else:
140165
search_path = path + sys.path
141-
# XXX This will change once issue19944 lands.
142166
spec = importlib.util._find_spec_from_path(fullmodule, search_path)
143167
_modules[fullmodule] = dict
144168
# is module a package?
@@ -174,17 +198,22 @@ def _readmodule(module, path, inpackage=None):
174198
tokentype, meth_name, start = next(g)[0:3]
175199
if tokentype != NAME:
176200
continue # Syntax error
201+
cur_func = None
177202
if stack:
178-
cur_class = stack[-1][0]
179-
if isinstance(cur_class, Class):
180-
# it's a method
181-
cur_class._addmethod(meth_name, lineno)
182-
# else it's a nested def
203+
cur_obj = stack[-1][0]
204+
if isinstance(cur_obj, Object):
205+
# It's a nested function or a method.
206+
cur_func = _newfunction(cur_obj, meth_name, lineno)
207+
cur_obj._addobject(meth_name, cur_func)
208+
209+
if isinstance(cur_obj, Class):
210+
# it's a method
211+
cur_obj._addmethod(meth_name, lineno)
183212
else:
184213
# it's a function
185-
dict[meth_name] = Function(fullmodule, meth_name,
186-
fname, lineno)
187-
stack.append((None, thisindent)) # Marker for nested fns
214+
cur_func = Function(fullmodule, meth_name, fname, lineno)
215+
dict[meth_name] = cur_func
216+
stack.append((cur_func, thisindent)) # Marker for nested fns.
188217
elif token == 'class':
189218
lineno, thisindent = start
190219
# close previous nested classes and defs
@@ -235,9 +264,16 @@ def _readmodule(module, path, inpackage=None):
235264
super.append(token)
236265
# expressions in the base list are not supported
237266
inherit = names
238-
cur_class = Class(fullmodule, class_name, inherit,
239-
fname, lineno)
240-
if not stack:
267+
if stack:
268+
cur_obj = stack[-1][0]
269+
if isinstance(cur_obj, Object):
270+
# Either a nested class or a class inside a function.
271+
cur_class = _newclass(cur_obj, class_name, inherit,
272+
lineno)
273+
cur_obj._addobject(class_name, cur_class)
274+
else:
275+
cur_class = Class(fullmodule, class_name, inherit,
276+
fname, lineno)
241277
dict[class_name] = cur_class
242278
stack.append((cur_class, thisindent))
243279
elif token == 'import' and start[1] == 0:
@@ -284,6 +320,7 @@ def _readmodule(module, path, inpackage=None):
284320
f.close()
285321
return dict
286322

323+
287324
def _getnamelist(g):
288325
# Helper to get a comma-separated list of dotted names plus 'as'
289326
# clauses. Return a list of pairs (name, name2) where name2 is
@@ -304,6 +341,7 @@ def _getnamelist(g):
304341
break
305342
return names
306343

344+
307345
def _getname(g):
308346
# Helper to get a dotted name, return a pair (name, token) where
309347
# name is the dotted name, or None if there was no dotted name,
@@ -323,10 +361,10 @@ def _getname(g):
323361
parts.append(token)
324362
return (".".join(parts), token)
325363

364+
326365
def _main():
327366
# Main program for testing.
328367
import os
329-
from operator import itemgetter
330368
mod = sys.argv[1]
331369
if os.path.exists(mod):
332370
path = [os.path.dirname(mod)]
@@ -336,17 +374,28 @@ def _main():
336374
else:
337375
path = []
338376
dict = readmodule_ex(mod, path)
339-
objs = list(dict.values())
340-
objs.sort(key=lambda a: getattr(a, 'lineno', 0))
341-
for obj in objs:
377+
lineno_key = lambda a: getattr(a, 'lineno', 0)
378+
objs = sorted(dict.values(), key=lineno_key, reverse=True)
379+
indent_level = 2
380+
while objs:
381+
obj = objs.pop()
382+
if isinstance(obj, list):
383+
# Value of a __path__ key
384+
continue
385+
if not hasattr(obj, 'indent'):
386+
obj.indent = 0
387+
388+
if isinstance(obj, Object):
389+
new_objs = sorted(obj.objects.values(),
390+
key=lineno_key, reverse=True)
391+
for ob in new_objs:
392+
ob.indent = obj.indent + indent_level
393+
objs.extend(new_objs)
342394
if isinstance(obj, Class):
343-
print("class", obj.name, obj.super, obj.lineno)
344-
methods = sorted(obj.methods.items(), key=itemgetter(1))
345-
for name, lineno in methods:
346-
if name != "__path__":
347-
print(" def", name, lineno)
395+
print("{}class {} {} {}"
396+
.format(' ' * obj.indent, obj.name, obj.super, obj.lineno))
348397
elif isinstance(obj, Function):
349-
print("def", obj.name, obj.lineno)
398+
print("{}def {} {}".format(' ' * obj.indent, obj.name, obj.lineno))
350399

351400
if __name__ == "__main__":
352401
_main()

0 commit comments

Comments
 (0)