0

When I write scripts, I have a common library of personal functions that I use by lazily importing the whole thing. So I wanted to clean that up so instead of importing thousands of lines of code, I could have a script automatically select the functions I need for that particular script and copy them into a file for publication. I got that working using a combination of ast and inspect, but now I want to copy the exact line of code where global variables are defined inside of my module.

For example if I use the shortcut eprint in the script it should scrape the line: eprint = Eprinter(verbose=1).eprint which creates a shortcut to a class method.

I'm trying to adopt the code I found here: https://stackoverflow.com/a/13230346/11343425

class GetAssignments(ast.NodeVisitor):
    def visit_Name(self, node):
        if isinstance(node.ctx, ast.Store):
            if node.id == 'eprint':
                print(node.id, node.lineno)
                return node.lineno

code = inspect.getsource(printing)
a = GetAssignments()
b = a.visit(ast.parse(code))
print('b =', b)

This will print the node.lineno just fine, but it returns None and I can't figure out why.

SurpriseDog
  • 462
  • 8
  • 18
  • I find the documentation on what a returned value from a `NodeVisitor` method means to be confusing - but I don't see any way in which it could be useful. What would you expect to happen if there was more than one reference to `eprint` in the function you're analyzing? The visitor method will be called multiple times, but you only get one return value from `.visit()`. – jasonharper Mar 26 '21 at 17:48
  • @jasonharper The code only picks up the line where it is defined as a global variable. It ignores the other lines which reference the class method. It actually prints the correct line number, it just won't return it. – SurpriseDog Mar 26 '21 at 17:52
  • 1
    I'm not talking about your specific code, I'm talking about the `ast` module in general. It *cannot* simply pass through the return value from a visitor method, because there can be any number of such methods called. You'll have to find some other way of getting that value to where you need it - perhaps `self.lineno = node.lineno` in the method, then look at `a.lineno` afterwards. – jasonharper Mar 26 '21 at 17:58
  • Thanks that worked. I'm still confused about why it won't return anything , but `a.lineno` at least gets the value for me. – SurpriseDog Mar 26 '21 at 18:01
  • Am I missing something? Why not simply use `from ... import ...` to import exactly those classes/functions you intend to use? – Paul M. Mar 26 '21 at 18:49
  • @PaulM. So I don't have to publish a script with hundreds of personal functions that aren't being used and then import only the correct ones. Instead I generate a new file with just the relevant functions. It's more elegant this way. – SurpriseDog Mar 26 '21 at 18:51
  • @PaulM. For example, if [empty.trash.py](https://github.com/SurpriseDog/empty.trash.py) needs the `rfs` function it pulls it from SurpriseDog.py instead of the hundreds of other functions that aren't relevant here so why would I include them? SurpriseDog.py was autogenerated by my script, but I needed the line: `eprint = Eprinter(verbose=1).eprint` which is what I'm working on getting. – SurpriseDog Mar 26 '21 at 19:04
  • @PaulM. Got the whole thing working: https://github.com/SurpriseDog/Private/blob/main/publish.py I'm sure there are edge cases where this will fail, but for now it's getting the job done. – SurpriseDog Mar 26 '21 at 21:38

1 Answers1

0

ast.NodeVisitor is meant to be subclassed which means visit_Name can't return anything. However, a custom function can be an intermediary like so:

class GetAssignments(ast.NodeVisitor):
    def visit_Name(self, node):
        if isinstance(node.ctx, ast.Store):
            if node.id == self.expr:
                print("Found line number", node.lineno)
                self.lineno = node.lineno

    def search(self, node, expr):
        self.expr = expr
        self.visit(node)
        return self.lineno
        
code = inspect.getsource(printing)

lineno = GetAssignments().search(ast.parse(code), 'eprint')
line = code.split('\n')[lineno-1]
print(lineno, line)
SurpriseDog
  • 462
  • 8
  • 18