Skip to content

Commit a726dc1

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

File tree

3 files changed

+112
-95
lines changed

3 files changed

+112
-95
lines changed

Doc/library/pyclbr.rst

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,12 @@
1111
--------------
1212

1313
The :mod:`pyclbr` module can be used to determine some limited information
14-
about the classes, methods and top-level functions defined in a module. The
15-
information provided is sufficient to implement a traditional three-pane
16-
class browser. The information is extracted from the source code rather
17-
than by importing the module, so this module is safe to use with untrusted
18-
code. This restriction makes it impossible to use this module with modules
19-
not implemented in Python, including all standard and optional extension
20-
modules.
14+
about the classes, methods, and functions defined in a module. The
15+
information provided is sufficient to implement a module browser. The
16+
information is extracted from the source code rather than by importing the
17+
module, so this module is safe to use with untrusted code. This restriction
18+
makes it impossible to use this module with modules not implemented in Python,
19+
including all standard and optional extension modules.
2120

2221

2322
.. function:: readmodule(module, path=None)
@@ -32,8 +31,8 @@ modules.
3231
.. function:: readmodule_ex(module, path=None)
3332

3433
Like :func:`readmodule`, but the returned dictionary, in addition to
35-
mapping class names to class descriptor objects, also maps top-level
36-
function names to function descriptor objects. Moreover, if the module
34+
mapping class names to class descriptor objects, also maps function
35+
names to function descriptor objects. Moreover, if the module
3736
being read is a package, the key ``'__path__'`` in the returned
3837
dictionary has as its value a list which contains the package search
3938
path.
@@ -73,21 +72,33 @@ data members:
7372

7473
The parent of this object, if any.
7574

75+
.. versionadded:: 3.7
7676

77-
.. attribute:: Object.objects
77+
78+
.. attribute:: Object.children
7879

7980
A dictionary mapping object names to the objects that are defined inside the
8081
namespace created by the current object.
8182

83+
.. versionadded:: 3.7
84+
85+
86+
.. versionchanged:: 3.7
87+
:class:`Object` was added as a base class for :class:`Class` and
88+
:class:`Function` and, except as otherwise noted, the attributes
89+
were previously common to those two classes.
90+
91+
8292

8393
.. _pyclbr-class-objects:
8494

8595
Class Objects
8696
-------------
8797

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:
98+
:class:`Class` is a subclass of :class:`Object` whose objects are used as values
99+
in the dictionary returned by :func:`readmodule` and :func:`readmodule_ex`.
100+
In addition to the attributes from :class:`Object`, :class:`Class` objects
101+
also provide the following attributes:
91102

92103

93104
.. attribute:: Class.super
@@ -104,12 +115,18 @@ data members:
104115
A dictionary mapping method names to line numbers.
105116

106117

118+
.. versionchanged:: 3.7
119+
:class:`Class` became a subclass of :class:`Object`.
120+
121+
107122
.. _pyclbr-function-objects:
108123

109124
Function Objects
110125
----------------
111126

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.
127+
:class:`Function` is a subclass of :class:`Object` whose objects are used as
128+
values in the dictionary returned by :func:`readmodule_ex`. The only instance
129+
attributes are those from :class:`Object`.
115130

131+
.. versionchanged:: 3.7
132+
:class:`Function` became a subclass of :class:`Object`.

Lib/pyclbr.py

Lines changed: 60 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,46 @@
1-
"""Parse a Python module and describe its classes and methods.
1+
"""Parse a Python module and describe its classes and functions.
22
33
Parse enough of a Python file to recognize imports and class and
4-
method definitions, and to find out the superclasses of a class.
4+
function definitions, and to find out the superclasses of a class.
55
66
The interface consists of a single function:
77
readmodule_ex(module [, path])
88
where module is the name of a Python module, and path is an optional
99
list of directories where the module is to be searched. If present,
1010
path is prepended to the system search path sys.path. The return
1111
value is a dictionary. The keys of the dictionary are the names of
12-
the classes defined in the module (including classes that are defined
13-
via the from XXX import YYY construct). The values are class
14-
instances of the class Class defined here. One special key/value pair
15-
is present for packages: the key '__path__' has a list as its value
16-
which contains the package search path.
12+
the classes and functions defined in the module (including classes that
13+
are defined via the from XXX import YYY construct). The values are class
14+
instances of the class Class and function instances of the class Function,
15+
respectively. One special key/value pair is present for packages: the
16+
key '__path__' has a list as its value which contains the package search
17+
path.
1718
1819
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+
class. Every instance of this class has the following instance variables:
2021
module -- the module name
2122
name -- the name of the object
2223
file -- the file in which the object was defined
2324
lineno -- the line in the file on which the definition of the object
2425
started
2526
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.
27+
children -- the nested objects (classes and functions) contained
28+
in this object
29+
The 'children' attribute is a dictionary mapping object names to objects.
2930
3031
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):
32+
of this class have the attributes from Object, plus the following:
3333
super -- a list of super classes (Class instances)
3434
methods -- a dictionary of methods
35-
The dictionary of methods uses the method names as keys and the line
36-
numbers on which the method was defined as values.
35+
'methods' maps method names to the line number where the definition begins.
3736
If the name of a super class is not recognized, the corresponding
3837
entry in the list of super classes is not a class instance but a
3938
string giving the name of the super class. Since import statements
4039
are recognized and imported modules are scanned as well, this
4140
shouldn't happen often.
4241
43-
A function is described by the class Function in this module.
42+
A function is described by the class Function in this module. The
43+
only instance attributes are those of Object.
4444
"""
4545

4646
import io
@@ -55,27 +55,25 @@
5555

5656

5757
class Object:
58-
"""Class to represent a Python object."""
58+
"""Class to represent a Python class or function."""
5959
def __init__(self, module, name, file, lineno, parent):
6060
self.module = module
6161
self.name = name
6262
self.file = file
6363
self.lineno = lineno
6464
self.parent = parent
65-
self.objects = {}
65+
self.children = {}
6666

67-
def _addobject(self, name, obj):
68-
self.objects[name] = obj
67+
def _addchild(self, name, obj):
68+
self.children[name] = obj
6969

7070

71-
# each Python class is represented by an instance of this class
71+
# Each Python class is represented by an instance of this class.
7272
class Class(Object):
7373
'''Class to represent a Python class.'''
7474
def __init__(self, module, name, super, file, lineno, parent=None):
7575
Object.__init__(self, module, name, file, lineno, parent)
76-
if super is None:
77-
super = []
78-
self.super = super
76+
self.super = [] if super is None else super
7977
self.methods = {}
8078

8179
def _addmethod(self, name, lineno):
@@ -127,7 +125,7 @@ def _readmodule(module, path, inpackage=None):
127125
package search path; otherwise, we are searching for a top-level
128126
module, and PATH is combined with sys.path.
129127
'''
130-
# Compute the full module name (prepending inpackage if set)
128+
# Compute the full module name (prepending inpackage if set).
131129
if inpackage is not None:
132130
fullmodule = "%s.%s" % (inpackage, module)
133131
else:
@@ -137,15 +135,15 @@ def _readmodule(module, path, inpackage=None):
137135
if fullmodule in _modules:
138136
return _modules[fullmodule]
139137

140-
# Initialize the dict for this module's contents
141-
dict = {}
138+
# Initialize the dict for this module's contents.
139+
tree = {}
142140

143-
# Check if it is a built-in module; we don't do much for these
141+
# Check if it is a built-in module; we don't do much for these.
144142
if module in sys.builtin_module_names and inpackage is None:
145-
_modules[module] = dict
146-
return dict
143+
_modules[module] = tree
144+
return tree
147145

148-
# Check for a dotted module name
146+
# Check for a dotted module name.
149147
i = module.rfind('.')
150148
if i >= 0:
151149
package = module[:i]
@@ -157,27 +155,26 @@ def _readmodule(module, path, inpackage=None):
157155
raise ImportError('No package named {}'.format(package))
158156
return _readmodule(submodule, parent['__path__'], package)
159157

160-
# Search the path for the module
158+
# Search the path for the module.
161159
f = None
162160
if inpackage is not None:
163161
search_path = path
164162
else:
165163
search_path = path + sys.path
166164
spec = importlib.util._find_spec_from_path(fullmodule, search_path)
167-
_modules[fullmodule] = dict
165+
_modules[fullmodule] = tree
168166
# is module a package?
169167
if spec.submodule_search_locations is not None:
170-
dict['__path__'] = spec.submodule_search_locations
168+
tree['__path__'] = spec.submodule_search_locations
171169
try:
172170
source = spec.loader.get_source(fullmodule)
173171
if source is None:
174-
return dict
172+
return tree
175173
except (AttributeError, ImportError):
176-
# not Python source, can't do anything with this module
177-
return dict
174+
# not Python source, can't do anything with this module.
175+
return tree
178176

179177
fname = spec.loader.get_filename(fullmodule)
180-
181178
f = io.StringIO(source)
182179

183180
stack = [] # stack of (class, indent) pairs
@@ -195,34 +192,34 @@ def _readmodule(module, path, inpackage=None):
195192
# close previous nested classes and defs
196193
while stack and stack[-1][1] >= thisindent:
197194
del stack[-1]
198-
tokentype, meth_name, start = next(g)[0:3]
195+
tokentype, func_name, start = next(g)[0:3]
199196
if tokentype != NAME:
200197
continue # Syntax error
201198
cur_func = None
202199
if stack:
203200
cur_obj = stack[-1][0]
204201
if isinstance(cur_obj, Object):
205202
# 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)
203+
cur_func = _newfunction(cur_obj, func_name, lineno)
204+
cur_obj._addchild(func_name, cur_func)
208205

209206
if isinstance(cur_obj, Class):
210207
# it's a method
211-
cur_obj._addmethod(meth_name, lineno)
208+
cur_obj._addmethod(func_name, lineno)
212209
else:
213210
# it's a function
214-
cur_func = Function(fullmodule, meth_name, fname, lineno)
215-
dict[meth_name] = cur_func
211+
cur_func = Function(fullmodule, func_name, fname, lineno)
212+
tree[func_name] = cur_func
216213
stack.append((cur_func, thisindent)) # Marker for nested fns.
217214
elif token == 'class':
218215
lineno, thisindent = start
219-
# close previous nested classes and defs
216+
# Close previous nested classes and defs.
220217
while stack and stack[-1][1] >= thisindent:
221218
del stack[-1]
222219
tokentype, class_name, start = next(g)[0:3]
223220
if tokentype != NAME:
224221
continue # Syntax error
225-
# parse what follows the class name
222+
# Parse what follows the class name.
226223
tokentype, token, start = next(g)[0:3]
227224
inherit = None
228225
if token == '(':
@@ -234,9 +231,9 @@ def _readmodule(module, path, inpackage=None):
234231
tokentype, token, start = next(g)[0:3]
235232
if token in (')', ',') and level == 1:
236233
n = "".join(super)
237-
if n in dict:
234+
if n in tree:
238235
# we know this super class
239-
n = dict[n]
236+
n = tree[n]
240237
else:
241238
c = n.split('.')
242239
if len(c) > 1:
@@ -270,11 +267,11 @@ def _readmodule(module, path, inpackage=None):
270267
# Either a nested class or a class inside a function.
271268
cur_class = _newclass(cur_obj, class_name, inherit,
272269
lineno)
273-
cur_obj._addobject(class_name, cur_class)
270+
cur_obj._addchild(class_name, cur_class)
274271
else:
275272
cur_class = Class(fullmodule, class_name, inherit,
276273
fname, lineno)
277-
dict[class_name] = cur_class
274+
tree[class_name] = cur_class
278275
stack.append((cur_class, thisindent))
279276
elif token == 'import' and start[1] == 0:
280277
modules = _getnamelist(g)
@@ -298,27 +295,27 @@ def _readmodule(module, path, inpackage=None):
298295
continue
299296
names = _getnamelist(g)
300297
try:
301-
# Recursively read the imported module
298+
# Recursively read the imported module.
302299
d = _readmodule(mod, path, inpackage)
303300
except:
304301
# If we can't find or parse the imported module,
305302
# too bad -- don't die here.
306303
continue
307-
# add any classes that were defined in the imported module
308-
# to our name space if they were mentioned in the list
304+
# Add any classes that were defined in the imported module
305+
# to our name space if they were mentioned in the list.
309306
for n, n2 in names:
310307
if n in d:
311-
dict[n2 or n] = d[n]
308+
tree[n2 or n] = d[n]
312309
elif n == '*':
313310
# don't add names that start with _
314311
for n in d:
315312
if n[0] != '_':
316-
dict[n] = d[n]
313+
tree[n] = d[n]
317314
except StopIteration:
318315
pass
319316

320317
f.close()
321-
return dict
318+
return tree
322319

323320

324321
def _getnamelist(g):
@@ -365,17 +362,20 @@ def _getname(g):
365362
def _main():
366363
# Main program for testing.
367364
import os
368-
mod = sys.argv[1]
365+
try:
366+
mod = sys.argv[1]
367+
except:
368+
mod = __file__
369369
if os.path.exists(mod):
370370
path = [os.path.dirname(mod)]
371371
mod = os.path.basename(mod)
372372
if mod.lower().endswith(".py"):
373373
mod = mod[:-3]
374374
else:
375375
path = []
376-
dict = readmodule_ex(mod, path)
376+
tree = readmodule_ex(mod, path)
377377
lineno_key = lambda a: getattr(a, 'lineno', 0)
378-
objs = sorted(dict.values(), key=lineno_key, reverse=True)
378+
objs = sorted(tree.values(), key=lineno_key, reverse=True)
379379
indent_level = 2
380380
while objs:
381381
obj = objs.pop()
@@ -386,7 +386,7 @@ def _main():
386386
obj.indent = 0
387387

388388
if isinstance(obj, Object):
389-
new_objs = sorted(obj.objects.values(),
389+
new_objs = sorted(obj.children.values(),
390390
key=lineno_key, reverse=True)
391391
for ob in new_objs:
392392
ob.indent = obj.indent + indent_level

0 commit comments

Comments
 (0)