bpo-38307: Add end_lineno attribute to pyclbr Objects (GH-24348)

For back-compatibility, make the new constructor parameter for public classes Function and Class
keyword-only with a default of None.

Co-authored-by: Aviral Srivastava <aviralsrivastava@Avirals-MacBook-Air.local
Co-authored-by: Terry Jan Reedy <tjreedy@udel.edu>
This commit is contained in:
Aviral Srivastava 2021-02-01 09:38:44 -08:00 committed by GitHub
parent b5931f1d9f
commit 000cde5984
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 56 additions and 35 deletions

View file

@ -435,6 +435,14 @@ py_compile
Added ``--quiet`` option to command-line interface of :mod:`py_compile`. Added ``--quiet`` option to command-line interface of :mod:`py_compile`.
(Contributed by Gregory Schevchenko in :issue:`38731`.) (Contributed by Gregory Schevchenko in :issue:`38731`.)
pyclbr
------
Added an ``end_lineno`` attribute to the ``Function`` and ``Class``
objects in the tree returned by :func:`pyclbr.readline` and
:func:`pyclbr.readline_ex`. It matches the existing (start) ``lineno``.
(Contributed by Aviral Srivastava in :issue:`38307`.)
shelve shelve
------ ------

View file

@ -61,15 +61,15 @@ class ModuleBrowserTest(unittest.TestCase):
# Nested tree same as in test_pyclbr.py except for supers on C0. C1. # Nested tree same as in test_pyclbr.py except for supers on C0. C1.
mb = pyclbr mb = pyclbr
module, fname = 'test', 'test.py' module, fname = 'test', 'test.py'
C0 = mb.Class(module, 'C0', ['base'], fname, 1) C0 = mb.Class(module, 'C0', ['base'], fname, 1, end_lineno=9)
F1 = mb._nest_function(C0, 'F1', 3) F1 = mb._nest_function(C0, 'F1', 3, 5)
C1 = mb._nest_class(C0, 'C1', 6, ['']) C1 = mb._nest_class(C0, 'C1', 6, 9, [''])
C2 = mb._nest_class(C1, 'C2', 7) C2 = mb._nest_class(C1, 'C2', 7, 9)
F3 = mb._nest_function(C2, 'F3', 9) F3 = mb._nest_function(C2, 'F3', 9, 9)
f0 = mb.Function(module, 'f0', fname, 11) f0 = mb.Function(module, 'f0', fname, 11, end_lineno=15)
f1 = mb._nest_function(f0, 'f1', 12) f1 = mb._nest_function(f0, 'f1', 12, 14)
f2 = mb._nest_function(f1, 'f2', 13) f2 = mb._nest_function(f1, 'f2', 13, 13)
c1 = mb._nest_class(f0, 'c1', 15) c1 = mb._nest_class(f0, 'c1', 15, 15)
mock_pyclbr_tree = {'C0': C0, 'f0': f0} mock_pyclbr_tree = {'C0': C0, 'f0': f0}
# Adjust C0.name, C1.name so tests do not depend on order. # Adjust C0.name, C1.name so tests do not depend on order.

View file

@ -21,6 +21,7 @@ has the following attributes:
name -- name of the object; name -- name of the object;
file -- file in which the object is defined; file -- file in which the object is defined;
lineno -- line in the file where the object's definition starts; lineno -- line in the file where the object's definition starts;
end_lineno -- line in the file where the object's definition ends;
parent -- parent of this object, if any; parent -- parent of this object, if any;
children -- nested objects contained in this object. children -- nested objects contained in this object.
The 'children' attribute is a dictionary mapping names to objects. The 'children' attribute is a dictionary mapping names to objects.
@ -52,40 +53,50 @@ _modules = {} # Initialize cache of modules we've seen.
class _Object: class _Object:
"Information about Python class or function." "Information about Python class or function."
def __init__(self, module, name, file, lineno, parent): def __init__(self, module, name, file, lineno, end_lineno, parent):
self.module = module self.module = module
self.name = name self.name = name
self.file = file self.file = file
self.lineno = lineno self.lineno = lineno
self.end_lineno = end_lineno
self.parent = parent self.parent = parent
self.children = {} self.children = {}
if parent is not None: if parent is not None:
parent.children[name] = self parent.children[name] = self
# Odd Function and Class signatures are for back-compatibility.
class Function(_Object): class Function(_Object):
"Information about a Python function, including methods." "Information about a Python function, including methods."
def __init__(self, module, name, file, lineno, parent=None, is_async=False): def __init__(self, module, name, file, lineno,
super().__init__(module, name, file, lineno, parent) parent=None, is_async=False, *, end_lineno=None):
super().__init__(module, name, file, lineno, end_lineno, parent)
self.is_async = is_async self.is_async = is_async
if isinstance(parent, Class): if isinstance(parent, Class):
parent.methods[name] = lineno parent.methods[name] = lineno
class Class(_Object): class Class(_Object):
"Information about a Python class." "Information about a Python class."
def __init__(self, module, name, super_, file, lineno, parent=None): def __init__(self, module, name, super_, file, lineno,
super().__init__(module, name, file, lineno, parent) parent=None, *, end_lineno=None):
super().__init__(module, name, file, lineno, end_lineno, parent)
self.super = super_ or [] self.super = super_ or []
self.methods = {} self.methods = {}
# These 2 functions are used in these tests # These 2 functions are used in these tests
# Lib/test/test_pyclbr, Lib/idlelib/idle_test/test_browser.py # Lib/test/test_pyclbr, Lib/idlelib/idle_test/test_browser.py
def _nest_function(ob, func_name, lineno, is_async=False): def _nest_function(ob, func_name, lineno, end_lineno, is_async=False):
"Return a Function after nesting within ob." "Return a Function after nesting within ob."
return Function(ob.module, func_name, ob.file, lineno, ob, is_async) return Function(ob.module, func_name, ob.file, lineno,
parent=ob, is_async=is_async, end_lineno=end_lineno)
def _nest_class(ob, class_name, lineno, super=None): def _nest_class(ob, class_name, lineno, end_lineno, super=None):
"Return a Class after nesting within ob." "Return a Class after nesting within ob."
return Class(ob.module, class_name, super, ob.file, lineno, ob) return Class(ob.module, class_name, super, ob.file, lineno,
parent=ob, end_lineno=end_lineno)
def readmodule(module, path=None): def readmodule(module, path=None):
"""Return Class objects for the top-level classes in module. """Return Class objects for the top-level classes in module.
@ -108,6 +119,7 @@ def readmodule_ex(module, path=None):
""" """
return _readmodule(module, path or []) return _readmodule(module, path or [])
def _readmodule(module, path, inpackage=None): def _readmodule(module, path, inpackage=None):
"""Do the hard work for readmodule[_ex]. """Do the hard work for readmodule[_ex].
@ -198,9 +210,8 @@ class _ModuleBrowser(ast.NodeVisitor):
bases.append(name) bases.append(name)
parent = self.stack[-1] if self.stack else None parent = self.stack[-1] if self.stack else None
class_ = Class( class_ = Class(self.module, node.name, bases, self.file, node.lineno,
self.module, node.name, bases, self.file, node.lineno, parent parent=parent, end_lineno=node.end_lineno)
)
if parent is None: if parent is None:
self.tree[node.name] = class_ self.tree[node.name] = class_
self.stack.append(class_) self.stack.append(class_)
@ -209,9 +220,8 @@ class _ModuleBrowser(ast.NodeVisitor):
def visit_FunctionDef(self, node, *, is_async=False): def visit_FunctionDef(self, node, *, is_async=False):
parent = self.stack[-1] if self.stack else None parent = self.stack[-1] if self.stack else None
function = Function( function = Function(self.module, node.name, self.file, node.lineno,
self.module, node.name, self.file, node.lineno, parent, is_async parent, is_async, end_lineno=node.end_lineno)
)
if parent is None: if parent is None:
self.tree[node.name] = function self.tree[node.name] = function
self.stack.append(function) self.stack.append(function)

View file

@ -176,15 +176,15 @@ class PyclbrTest(TestCase):
actual = mb._create_tree(m, p, f, source, t, i) actual = mb._create_tree(m, p, f, source, t, i)
# Create descriptors, linked together, and expected dict. # Create descriptors, linked together, and expected dict.
f0 = mb.Function(m, 'f0', f, 1) f0 = mb.Function(m, 'f0', f, 1, end_lineno=5)
f1 = mb._nest_function(f0, 'f1', 2) f1 = mb._nest_function(f0, 'f1', 2, 4)
f2 = mb._nest_function(f1, 'f2', 3) f2 = mb._nest_function(f1, 'f2', 3, 3)
c1 = mb._nest_class(f0, 'c1', 5) c1 = mb._nest_class(f0, 'c1', 5, 5)
C0 = mb.Class(m, 'C0', None, f, 6) C0 = mb.Class(m, 'C0', None, f, 6, end_lineno=14)
F1 = mb._nest_function(C0, 'F1', 8) F1 = mb._nest_function(C0, 'F1', 8, 10)
C1 = mb._nest_class(C0, 'C1', 11) C1 = mb._nest_class(C0, 'C1', 11, 14)
C2 = mb._nest_class(C1, 'C2', 12) C2 = mb._nest_class(C1, 'C2', 12, 14)
F3 = mb._nest_function(C2, 'F3', 14) F3 = mb._nest_function(C2, 'F3', 14, 14)
expected = {'f0':f0, 'C0':C0} expected = {'f0':f0, 'C0':C0}
def compare(parent1, children1, parent2, children2): def compare(parent1, children1, parent2, children2):
@ -203,8 +203,8 @@ class PyclbrTest(TestCase):
self.assertIs(ob.parent, parent2) self.assertIs(ob.parent, parent2)
for key in children1.keys(): for key in children1.keys():
o1, o2 = children1[key], children2[key] o1, o2 = children1[key], children2[key]
t1 = type(o1), o1.name, o1.file, o1.module, o1.lineno t1 = type(o1), o1.name, o1.file, o1.module, o1.lineno, o1.end_lineno
t2 = type(o2), o2.name, o2.file, o2.module, o2.lineno t2 = type(o2), o2.name, o2.file, o2.module, o2.lineno, o2.end_lineno
self.assertEqual(t1, t2) self.assertEqual(t1, t2)
if type(o1) is mb.Class: if type(o1) is mb.Class:
self.assertEqual(o1.methods, o2.methods) self.assertEqual(o1.methods, o2.methods)

View file

@ -0,0 +1,3 @@
Add an 'end_lineno' attribute to the Class and Function objects that appear in the
tree returned by pyclbr functions. This and the existing 'lineno'
attribute define the extent of class and def statements. Patch by Aviral Srivastava.