How can I execute a function that is inside a string from inside another function?
Question: I'm trying to make a Reddit Bot in Python but I have encountered an issue. The
objective of the bot is to read the comments of Reddit searching for
"get_tweets". When it finds this it will read the whole comment that will look
similar to this:

    
    
    get_tweets("TWITTER_USERNAME",NUMBER_OF_TWEETS,"INCLUDE_RE_TWEETS","INCLUDE_REPLIES")
    

The comment will serve as a function and the 4 parameters will be determined
by the user commenting. An example can be:

    
    
    get_tweets("BarackObama",5,"no","yes")  
    

I think I have everything covered except for the fact that I can't execute the
comment as a function because when I try it gives this error:

    
    
    SyntaxError: unqualified exec is not allowed in function 'run_bot' it contains a nested function with free variables 
    

Here is the whole code (except for the authentication for twitter and reddit):

    
    
    keywords = ['get_tweets']
    cache = []
    tweets_list = []
    
    def get_tweets(user,num,rt,rp):
        tline = api.user_timeline(screen_name=user, count=num)
        tweets = []
        for t in tline:
            reply = t.in_reply_to_screen_name
            tweet = t.text
            if rt.lower() == 'no' and rp.lower() == 'no':
                if tweet[0:2] != 'RT' and reply == None:
                    tweets.append(tweet + ' | Date Tweeted: ' + str(t.created_at))
            if rt.lower() == 'yes' and rp.lower() == 'no':
                if tweet[0:2] == 'RT' and reply == None:
                tweets.append(tweet + ' | Date Tweeted: ' + str(t.created_at))
            if rt.lower() == 'no' and rp.lower() == 'yes':
                if tweet[0:2] != 'RT' and reply != None:
                    tweets.append(tweet + ' | Date Tweeted: ' + str(t.created_at))
            if rt.lower() == 'yes' and rp.lower() == 'yes':
                if tweet[0:2] == 'RT' and reply != None:
                    tweets.append(tweet + ' | Date Tweeted: ' + str(t.created_at))
        tweets_list = tweets
    
    def run_bot():
        subreddit = r.get_subreddit('test')
        print('Searching...')
        comments = subreddit.get_comments(limit=100)
        for comment in comments:
            comment_text = comment.body.lower()
            isMatch = any(string in comment_text for string in keywords)
            if comment.id not in cache and isMatch:
                print('Found comment: ' + comment_text)
                exec comment_text
                cache.append(comment.id)
                start = []
                end = []
                open_p = comment.index('(')
                text = ''
                for a in re.finditer(',', comment):
                    start.append(a.start())
                    end.append(a.end())
                num = comment_text[end[0]:start[1]]
                user = comment_text[open_p:start[0]]
                for tweet in tweets_list:
                    text.append(' | ' + tweet + '\n\n')
                if num == 1:
                    reply = 'Here is the latest tweet from ' + user + ':\n\n' + text + '\n\n***\nI am a bot.'
                else:
                    reply = 'Here are the last ' + num + ' tweets from ' + user + ':\n\n' + text + '\n\n***\nI am a bot.'
                comment.reply(reply)         
    run_bot()
    


Answer: Using `exec` or `eval` is a huge security problem! The best you can try is to
extract the parameters from the string using regex or simply decompose it
using the `,` and then call your function with that. Luckily you only need
strings and numbers and therefore no dangerous parsing is necessary.  
One possible solution would be:

    
    
    import re
    
    def get_tweets(user, num, rt, rp):
        num = int(num)
        print user, num, rt, rp
    
    comment_text = 'get_tweets("BarackObama",5,"no","yes")'
    
    # capture 5 comma separated parts between '(' and ')'
    pattern = r'get_tweets\(([^,]*),([^,]*),([^,]*),([^,]*)\)'
    param_parts = re.match(pattern,comment_text).groups()
    
    # strip away surrounding ticks and spaces
    user,num,rt,rp = map(lambda x:x.strip('\'" '), param_parts)
    
    # parse the number
    num = int(num)
    
    # call get tweets
    get_tweets(user,num,rt,rp)
    

prints:

    
    
    BarackObama 5 no yes
    

Disadvantage:

  * Only works if the username does not contain a comma or begin/end with `'` or `"`, which I guess can be assumed (correct me if I'm wrong here).

Advantages:

  * Since the splitting is done at `,` you can also get rid of the ticks entirely which makes `get_tweets(BarackObama,5,no,yes)` valid as well.
  * We're using regex which means that the comment may contain additional text and we extract only what we need. 
  * If anyone wanted to inject code, you would just get a weird username, or a wrong number of arguments or an int which is not parsable or an invalid argument for `rt`/`rp`... which would all lead to an exception or no tweets at all. 
  * You can actually return a value from `get_tweets` and don't need to use a global variable.

