def pre_execute(self):
     """Before the code is executed, we reset the list of variables/functions etc. 'used' (as we don't know them yet)."""
     # Reset the used list
     self.used = set()
     
     pb = ProvBuilder()
     self.pre_ticker = deepcopy(pb.get_ticker())
def replace(f, input_names, output_names, *args, **kwargs):
    """Provenance-enabled replacement for arbitrary functions"""
    
    # Inputs is a dictionary of argument names and values
    # Outputs is whatever the wrapped function returns
    # Source is the source code of the function, or its docstring.
    
    
    ## If we're dealing with a 'ufunc' (i.e. numpy universal function)
    if isinstance(f,np.ufunc):
        inputs = {'x{}'.format(n) : args[n-1] for n in range(1,f.nin+1)}
        source = f.__doc__
        
    ## If we're dealing with a 'wrapper_descriptor' (i.e. a wrapper around a C-function) we cannot retrieve the argument names
    elif isinstance(f,types.TypeType):
        inputs = {'x{}'.format(n) : args[n-1] for n in range(1,len(args)+1)}
        source = f.__doc__
        
    ## If we're dealing with a 'classobj' (i.e. an expression that instantiates a object of a class, or something... whatever.)
    elif inspect.isclass(f):
        inputs = inspect.getcallargs(f.__init__, f, *args, **kwargs)
        # Only use those inputs that have a value
        inputs = {k:v for k,v in inputs.items()}
        source = inspect.getsource(f)
        
    ## If we're dealing with a builtin function
    elif isinstance(f,types.BuiltinFunctionType):
        inputs = {}
        source = f.__name__
        
    # If we're dealing with the 'get_ipython' function, we need to take some extra care, otherwise we introduce a cycle in the provenance graph.
    elif hasattr(f,'__name__') and getattr(f,'__name__') == 'get_ipython':
        inputs = {}
        source = inspect.getsource(f)
        
    ## If we're dealing with any other function, we just get all args and kwargs as inputs as a dictionary.
    else :
        try :
            inputs = inspect.getcallargs(f, *args, **kwargs)
            # Only use those inputs that have a value
            inputs = {k:v for k,v in inputs.items()}
            
            for input,ivalue in inputs.items():
                log.debug(type(ivalue))
                try :
                    log.debug("{} {}".format(input,ivalue))
                    if ivalue is None or isinstance(ivalue,types.NoneType):
                        log.debug("Popping {}".format(input))
                        inputs.pop(input,ivalue)
                except Exception as e:
                    log.warning(e)
            
            source = inspect.getsource(f)
        except :
            log.warning('Function is not a Python function')
            inputs = {'x{}'.format(n) : args[n-1] for n in range(1,len(args)+1)}
            source = f.__doc__
    
    pb = ProvBuilder()
    
    pre_ticker = deepcopy(pb.get_ticker())
    
    outputs = f(*args, **kwargs)

    if hasattr(f,'__name__'):
        name = f.__name__
    elif hasattr(f,'__str__'):
        name = f.__str__
    elif hasattr(f,'__doc__'):
        name = f.__doc__
    else :
        name = 'unknown_function'
    
    pb.add_activity(name , source, inputs, outputs, input_names=input_names, output_names=output_names,pre_ticker=pre_ticker)
    
    replace.prov = pb.get_graph()
    
    # prov_wrapper.prov_ttl = pb.get_graph().serialize(format='turtle')
    
    return outputs