6

I really like it when methods of objects, which modify the objects property, return self so that you can chain method calls. For example:

boundingBox.grow(0.05).shift(x=1.3)

instead of

boundingBox.grow(0.05)
boundingBox.shift(x=1.3)

I would like to search the code of my old projects to adjust this pattern. How can I find methods which don't have a return statement?

Ideally, I would like to let a program run over a folder. The program searches Python files, looks for classes, examines their methods and searches return statements. If no return statement is there, it outputs the filename, the name of the class and the name of the method.

Martin Thoma
  • 124,992
  • 159
  • 614
  • 958
  • The indentation-based block structure of Python makes it pretty easy to write such a program yourself. Just look for `def(...):` and check the last line of the following block to see if it has a `return`. – Mark Ransom May 12 '15 at 19:01
  • `return` does not have to be in the last line. I know I could probably write a program which would work ok for (most of) my code. But I would prefer a solution which is tested / used / maintained by others. – Martin Thoma May 12 '15 at 19:03
  • 1
    Assign the call of each method and test if it is `None`. – Malik Brahimi May 12 '15 at 19:05
  • 1
    You can have a `return` at any point in the function, but if there isn't one on the last line then there are conditions where it doesn't return anything, which I believe is what you want reported. I can't fault you for looking for an existing tool, just giving a backup suggestion. – Mark Ransom May 12 '15 at 19:07
  • 2
    someone is too used to javascript – Joran Beasley May 12 '15 at 19:12
  • It looks like you're only interested in `return self` specifically, which should be easy enough to search for! – jonrsharpe May 12 '15 at 19:25
  • @jonrsharpe No, I am not only interested in `return self`. Objects can have functions which return something completely different. And if there is already `return self`, I am also not interested in it. I am interested in an implicit `return None`. – Martin Thoma May 12 '15 at 19:28
  • @JoranBeasley Yes, I've seen that in JavaScript. And I like it. Do you think it is bad style in Python? (e.g. is there something in PEP8 or so about this?) – Martin Thoma May 12 '15 at 19:29
  • 1
    I dont think there is anything specific in pep8 ... but yeah I personally think it looks gross ... and returning `self` for something like `set_size` is a pretty non-standard convention in python in general... – Joran Beasley May 12 '15 at 19:32
  • @moose ah, I see, OK. Have you tried anything so far (`inspect` and `ast` come to mind)? – jonrsharpe May 12 '15 at 19:38
  • @moose This is definitely unpythonic; see the *last* paragraph of [More on Lists](https://docs.python.org/3/tutorial/datastructures.html#more-on-lists): "[Not doing t]his is a design principle for all mutable data structures in Python." – Veedrac May 13 '15 at 11:33

1 Answers1

6

You can get the names with ast, I will work on getting the line numbers:

import inspect
import importlib
import ast

class FindReturn(ast.NodeVisitor):
    def __init__(self):
        self.data = []

    def visit_ClassDef(self,node):
        self.data.append(node.name)
        self.generic_visit(node)

    def visit_FunctionDef(self, node):
        if not any(isinstance(n, ast.Return) for n in node.body):
            self.data.append(node.name)
        self.generic_visit(node)

mod = "test"
mod = importlib.import_module(mod)
p = ast.parse(inspect.getsource(mod))

f = FindReturn()
f.visit(p)

print(f.data)

Input:

class Foo(object):
    def __init__(self):
        self.foo = "foo"

    def meth1(self):
        self.bar = "bar"

    def meth2(self):
        self.foobar = "foobar"


    def meth3(self):
        self.returns = "foobar"
        return self.returns

class Bar(object):
    def __init__(self):
        self.foo = "foo"

    def meth1(self):
        self.bar = "bar"

    def meth2(self):
        self.foobar = "foobar"


    def meth3(self):
        self.returns = "foobar"
        return self.returns

Output:

['Foo', '__init__', 'meth1', 'meth2', 'Bar', '__init__', 'meth1', 'meth2']

The filename is obviously "test.py" here.

This is probably a nicer way to group the data:

import inspect
import importlib
import ast
from collections import defaultdict

mod = "test"
mod = importlib.import_module(mod)
p = ast.parse(inspect.getsource(mod))



data = defaultdict(defaultdict)
classes = [cls for cls in p.body if isinstance(cls, ast.ClassDef)]
for cls in classes:
    name = "class_{}".format(cls.name)
    data[mod][name] = {"methods": []}
    for node in cls.body:
        if not any(isinstance(n, ast.Return) for n in node.body):
            if node.name != "__init__":
                data[mod][name]["methods"].append(node.name)

Output:

{<module 'test' from '/home/padraic/test.pyc'>: defaultdict(None, {'class_Foo': {'methods': ['meth1', 'meth2']}, 'class_Bar': {'methods': ['meth1', 'meth2']}})}

To go through a directory:

data = defaultdict(defaultdict)
import os
path = "/home/padraic/tests"
for py in os.listdir(path):
    with open(os.path.join(path,py)) as f:
        p = ast.parse(f.read(), "", "exec")

    classes = [cls for cls in p.body if isinstance(cls, ast.ClassDef)]
    for cls in classes:
        name = "class_{}".format(cls.name)
        data[py][name] = {"methods": []}
        for node in cls.body:
            if not any(isinstance(n, ast.Return) for n in node.body):
                if node.name != "__init__":
                    data[py][name]["methods"].append(node.name)


from pprint import pprint as pp

pp(dict(data))

{'test.py': defaultdict(None, {'class_Foo': {'methods': ['meth1', 'meth2']}, 
'class_Bar': {'methods': ['meth1', 'meth2']}}),'test2.py': 
defaultdict(None, {'class_Test2': {'methods': ['test1', 'test2']}})}

Where test2 contains:

class Test2:
    def test1(self):
        pass

    def test2(self):
        self.f=4
        s = self.test_return()
        i = 3

    def test_return(self):
        return "Test2"

You can get the line before the method definition with node.lineno:

classes = [cls for cls in p.body if isinstance(cls, ast.ClassDef)]
    for cls in classes:
        name = "class_{}".format(cls.name)
        data[py][name] = {"methods": []}
        for node in cls.body:
            if not any(isinstance(n, ast.Return) for n in node.body):
                if node.name != "__init__":
                    data[py][name]["methods"].append({"meth":node.name,"line":node.lineno})

Output:

{'test.py': defaultdict(None, {'class_Foo': {'methods': [{'meth': 'meth1', 'line': 6}, {'meth': 'meth2', 'line': 9}]}, 'class_Bar': {'methods': [{'meth': 'meth1', 'line': 21}, {'meth': 'meth2', 'line': 24}]}}),
 'test2.py': defaultdict(None, {'class_Test2': {'methods': [{'meth': 'test1', 'line': 2}, {'meth': 'test2', 'line': 5}]}})}

Or we can guesstimate where the return is missing by getting the line number from the last arg in the body:

data[py][name]["methods"].append({"meth":node.name,"line": node.body[-1].lineno})

Output:

{'test.py': defaultdict(None, {'class_Foo': {'methods': [{'meth': 'meth1', 'line': 7},
 {'meth': 'meth2', 'line': 10}]}, 'class_Bar': {'methods': [{'meth': 'meth1', 'line': 22}, {'meth': 'meth2', 'line': 25}]}}),
 'test2.py': defaultdict(None, {'class_Test2': {'methods': [{'meth': 'test1', 'line': 3}, {'meth': 'test2', 'line': 8}]}})}

It might also be better to use iglob to ignore other files:

import glob
for py in glob.iglob(os.path.join(path,"*.py")):
    with open(os.path.join(path, py)) as f:
        p = ast.parse(f.read(), "", "exec")
Padraic Cunningham
  • 176,452
  • 29
  • 245
  • 321