37

I would like to invoke the following code in-situ wherever I refer to MY_MACRO in my code below.

# MY_MACRO
frameinfo = getframeinfo(currentframe())
msg = 'We are on file ' + frameinfo.filename + ' and line ' +  str(frameinfo.lineno)
# Assumes access to namespace and the variables in which `MY_MACRO` is called. 
current_state = locals().items()

Here is some code that would use MY_MACRO:

def some_function:
    MY_MACRO

def some_other_function:
    some_function()
    MY_MACRO

class some_class:
  def some_method:
     MY_MACRO

In case it helps:

  1. One of the reasons why I would like to have this ability is because I would like to avoid repeating the code of MY_MACRO wherever I need it. Having something short and easy would be very helpful.
  2. Another reason is because I want to embed an IPython shell wihthin the macro and I would like to have access to all variables in locals().items() (see this other question)

Is this at all possible in Python? What would be the easiest way to get this to work?

Please NOTE that the macro assumes access to the entire namespace of the scope in which it's called (i.e. merely placing the code MY_MACRO in a function would not work). Note also that if I place MY_MACRO in a function, lineno would output the wrong line number.

Community
  • 1
  • 1
Amelio Vazquez-Reina
  • 91,494
  • 132
  • 359
  • 564
  • 10
    Um, why don't you just define a function? –  Mar 27 '13 at 21:32
  • 7
    @delnan Please read `NOTE` in the OP. – Amelio Vazquez-Reina Mar 27 '13 at 21:46
  • 3
    If you want to meddle with the frames, you can just go one (or any number really) frames up to get to a caller's scope. That's assuming you actually have a good reason to fiddle with the frames. There are also trace functions (refer to the documentation), which can do this less invasively. –  Mar 27 '13 at 21:56
  • Thanks @delnan, as a matter of fact I do. I need to start an IPython shell assuming that the **current frame** is the one in which I invoke `MY_MACRO`. – Amelio Vazquez-Reina Mar 27 '13 at 21:58
  • Are you opposed to using `exec(MY_MACRO)`? – Paul May 11 '13 at 04:31
  • 1
    If he used a function he would have to explicitly pass locals() to the function. I think exec also requires locals to be explicitly passed... It would be great for macros to have the same name and "CALL" mehcanics as functions, for optimization and error checking. For example, lets say we have a macro: – aoeu256 Jul 30 '19 at 18:30
  • 1
    @macro def myopen(filename): return [open, filename] if isinstance(filename, symbol) else open(filename). # <- This will open the filename at "compile" time rather than run-time so you can get errors of whether or not a file exists without having to run every single code branch. This is probably possible with macropy, let me check. – aoeu256 Jul 30 '19 at 18:39
  • There's PEP 638, but it's from 2020 and has no traction. – Adrian May 19 '22 at 07:26

7 Answers7

28

MacroPy is a project of mine which brings syntactic macros to Python. The project is only 3 weeks old, but if you look at the link, you'll see we have a pretty cool collection of demos, and the functionality you want can definitely be implemented using it.

On the other hand, python has some pretty amazing introspection capabilities, so I suspect you may be able to accomplish what you want purely using that functionality.

Li Haoyi
  • 15,330
  • 17
  • 80
  • 137
  • 9
    Is MacroPy still being supported? It looks great, and just what I need to implement dependency tracking - but it doesn't appear to work with Python 2.7.3, or install with Python 3.4... – Mike Sadler Sep 14 '16 at 12:44
19

How about a function you can call? This function accesses the caller's frame, and rather than using locals(), uses frame.f_locals to get the caller's namespace.

def my_function():
    frame = currentframe().f_back
    msg = 'We are on file {0.f_code.co_filename} and line {0.f_lineno}'.format(frame)
    current_state = frame.f_locals
    print current_state['some_variable']

Then just call it:

def some_function:
    my_function()

def some_other_function:
    some_function()
    my_function()

class some_class:
  def some_method:
     my_function()
Ned Batchelder
  • 364,293
  • 75
  • 561
  • 662
  • Thanks, please see my comments to @Serdalis and my NOTE in the OP. Would this solution address them? – Amelio Vazquez-Reina Mar 27 '13 at 21:39
  • I amended my answer to explain a bit. – Ned Batchelder Mar 27 '13 at 21:40
  • Thanks Ned. This is great. What would happen if I open an embedded IPython shell e.g. `ipshell = InteractiveShellEmbed(config=cfg, banner1=banner_msg, exit_msg=exit_msg)'` in `MY_MACRO` (i.e. I invoke `ipshell()` in `MY_MACRO`). I believe IPython would not see the variables from the caller. (see my note in relation to [this question](http://stackoverflow.com/questions/15669186/using-ipython-as-an-effective-debugger)). Any thoughts on how to fix that? – Amelio Vazquez-Reina Mar 27 '13 at 21:45
  • It sounds like you want a debugger. – Ned Batchelder Mar 27 '13 at 21:48
  • Sort of. I want to use a full-fledged `IPython` shell to inspect the state of my program at locations of my choice. Embedding an IPython shell provides this functionality, but I would like it to show contextual information (that's what the other thread discusses) – Amelio Vazquez-Reina Mar 27 '13 at 21:52
  • Did you know that IPython has a debugger? http://ipython.org/ipython-doc/rel-0.10.2/html/api/generated/IPython.Debugger.html – Ned Batchelder Mar 27 '13 at 21:52
  • Yes, thank you, but the debugger is very limited in comparison to a full IPython shell. Embedding an IPython shell is supposed to serve this purpose, but I don't know how to have it run some code of my choice (e.g. displaying `lineno`, etc.) right before it loads. – Amelio Vazquez-Reina Mar 27 '13 at 21:55
  • I was hoping to wrap the call to `ipshell()` in a "Python macro", and that's what prompted this question. Your answer correctly addresses this question though... I guess I need keep looking to solve my problem. – Amelio Vazquez-Reina Mar 27 '13 at 21:56
  • This might not have been the case when the answer was originally posted, but `currentframe()` needs to be `inspect.currentframe()` (and inspect must be imported). – Ray May 10 '17 at 21:46
5

The use of exec is frowned upon, but it ought to do the trick here. For example, take the following macro:

MY_MACRO = """
print foo            
"""

and run it by using the following code:

foo = "breaking the rules"
exec MY_MACRO in globals(),locals() 

Always be careful with exec, because it can have strange side-effects and opens up opportunities for code injection.

Dan
  • 609
  • 5
  • 10
2

you could use function if you wanted to:

def MY_MACRO():
    frame = currentframe()
    try:
        macro_caller_locals = frame.f_back.f_locals
        print(macro_caller_locals['a'])

    finally:
        del frame

def some_function:
    a = 1
    MY_MACRO()
Serdalis
  • 10,296
  • 2
  • 38
  • 58
  • 1
    Hmm, Would `current_state` hold the variables of the namespace in which `MY_MACRO` is invoked? From what I understand, `locals()` would only return the `locals` of `macro_function()`. Am I wrong? – Amelio Vazquez-Reina Mar 27 '13 at 21:38
  • @user815423426 sorry, misunderstood the question, it's fixed now. – Serdalis Mar 27 '13 at 21:46
  • By editing the stack frames can you get continuations and gotos to work mwahahhahaha. – aoeu256 Jul 30 '19 at 18:43
2

I'm not sure if this is a good solution, but it's at least worth considering a macro preprocessor.

There are a few different extend-Python-with-macros projects, or wider projects that should make such a thing easier to do, but I only have expired links for all of them (Logix, MetaPython, Mython, Espy)… It might be worth looking for current links and/or newer/liver projects.

You can use something like m4 or cpp, or something more powerful, or even build one yourself. But really, you've just got a small, static set (so far, one) of purely textual macros. At worst you have to detect the indentation level of MY_MACRO and add that to the start of each line, which is trivial to do in a regex. Meaning sed, or a 3-liner Python script, can be your preprocessor.

However, there are two problems, or at least annoyances.

First, you need to preprocess your files. If you're already using C extension modules or generated code or any other code that needs you to setup.py (or make or scons or whatever) before you can run it, or you're using an IDE where you just hit cmd-R or ctrl-shift-B or whatever to test your code, this isn't a problem. But for the typical edit-test loop with a text editor in one window and an interactive interpreter in another… well, you've just turned it into an edit-compile-test loop. Ugh. The only solution I can think of is an import hook that preprocesses every file before importing it as a module, which seems like a lot of work for a small benefit.

Second, your line numbers and source (from MY_MACRO itself, as well as from tracebacks and inspect.getsource and so on) are going to be the line numbers of the preprocessed files, not the original source that you have open for editing. Since your preprocessed files are pretty readable, that isn't terrible (not as bad as coding CoffeeScript and debugging it as JavaScript, which most of the CoffeeScript community does every day…), but it's definitely an annoyance.

Of course one way to solve this is to build your own macro processor into the interpreter, at whichever stage in the parse/compile process you want. I'm guessing that's a whole lot more work than you want to do, but if you do, well, Guido always prefers to have an actual working design and implementation to reject instead of having to keep rejecting vague suggestions of "Hey, let's add macros to Python". :)

abarnert
  • 354,177
  • 51
  • 601
  • 671
2

If you need only line and function name of caller like I needed for debug, you can get caller function information by inspect.getouterframes link.

import inspect
def printDebugInfo():
  (frame,filename,line_number,function_name, lines, 
    index) = inspect.getouterframes(inspect.currentframe())[1]
  print(filename, line_number, function_name, lines, index)

def f1():
  printDebugInfo()

if __name__=='__main__':
  f1()
Community
  • 1
  • 1
Ahmad Yoosofan
  • 961
  • 12
  • 21
0

I'd say you should define a function to do this, since there are no macros in Python. It looks like you want to capture the current stack frame, which you could do simplify by passing in currentframe() from the call site to your shared function. Ditto with locals.

def print_frame_info(frameinfo, locals):
    msg = 'We are on file ' + frameinfo.filename + ' and line ' +  str(frameinfo.lineno)
    current_state = locals.items()

def some_other_function:
    some_function()
    print_frame_info(currentframe(), locals())
tom
  • 18,953
  • 4
  • 35
  • 35