#!/usr/bin/env python

####################################################################
# Doctest a single Python, Cython, Sage, TeX or ReST file, using
# Sage's custom doctesting tags and framework built on top of
# Python's doctest functionality.  In particular, this allows for
# sage: prompts, preparsing, optional doctests, random behavior, etc. 
#
# Return value in process exit code:
# 0: all tests passed
# 1: file not found
# 2: KeyboardInterrupt
# 3: doctest process was terminated by a signal
# 4: the doctesting framework raised an exception
# 100: failed doctests
####################################################################

# System imports
import os, re, sys, signal, time, shutil, tempfile, random

# if any of the following environment variables are set, use them for
# the timeout lengths.
TIMEOUT = os.getenv('SAGE_TIMEOUT')
TIMEOUT_LONG = os.getenv('SAGE_TIMEOUT_LONG')
TIMEOUT_VALGRIND = os.getenv('SAGE_TIMEOUT_VALGRIND')

if TIMEOUT is None:
    # the default timeout for doctests: 6 minutes (in seconds)    
    TIMEOUT = 6 * 60
else:
    # output from os.getenv is a string: convert to a number
    TIMEOUT = float(TIMEOUT)
    
if TIMEOUT_LONG is None:
    # the timeout value for long doctests: 30 minutes (in seconds)
    TIMEOUT_LONG = 30 * 60
else:
    TIMEOUT_LONG = float(TIMEOUT_LONG)
    
if TIMEOUT_VALGRIND is None:
    # the timeout for doctests running under valgrind tools: unreasonably long
    TIMEOUT_VALGRIND = 1024 * 1024
else:
    TIMEOUT_VALGRIND = float(TIMEOUT_VALGRIND)

long_time          = False
only_optional      = False
only_optional_tags = set([])
optional           = False
verbose            = False
random_order       = False         # default is that tests aren't run in random order

argv = sys.argv

import sage.misc.preparser

######################################################
# This code is copied from sage.misc.misc for speed:
######################################################
def is_64bit():
    return sys.maxint == 9223372036854775807

######################################################
# Temporary files
######################################################    
DOT_SAGE = os.environ['DOT_SAGE']
if 'SAGE_TESTDIR' not in os.environ or os.environ['SAGE_TESTDIR'] is "":
    SAGE_TESTDIR = os.path.join(DOT_SAGE, "tmp")
else:
    SAGE_TESTDIR = os.environ['SAGE_TESTDIR']

tmpfiles = [] # list of temporary files to be deleted if doctesting succeeds

def delete_tmpfiles():
    try:
        for f in tmpfiles:
            os.remove(f)
    except OSError:
        pass
    
######################################################
# Set environment variables
######################################################    
SAGE_ROOT = os.environ["SAGE_ROOT"]
if os.environ.has_key('SAGE_PATH'):
    os.environ["PYTHONPATH"] = os.environ["PYTHONPATH"] + ':' + os.environ['SAGE_PATH']


######################################################
# Custom flags for the valgrind modes
######################################################
logfile = ' --log-file=' + os.path.join('$HOME', '.sage', 'valgrind', 'sage-%s') + ' '
try:
    SAGE_MEMCHECK_FLAGS = os.environ['SAGE_MEMCHECK_FLAGS']
    print SAGE_MEMCHECK_FLAGS
except:
    suppfile = os.path.join('$SAGE_LOCAL', 'lib', 'valgrind', 'sage.supp')
    SAGE_MEMCHECK_FLAGS = " --leak-resolution=high %s --leak-check=full --num-callers=25 " % (logfile % 'memcheck.%p')

try:
    SAGE_MASSIF_FLAGS = os.environ['SAGE_MASSIF_FLAGS']
except:
    SAGE_MASSIF_FLAGS = " --depth=6 " + (logfile % 'massif.%p')

try:
    SAGE_CALLGRIND_FLAGS = os.environ['SAGE_CALLGRIND_FLAGS']
except:
    SAGE_CALLGRIND_FLAGS = logfile % 'callgrind.%p'

try:
    SAGE_CACHEGRIND_FLAGS = os.environ['SAGE_CACHEGRIND_FLAGS']
except:
    SAGE_CALLGRIND_FLAGS = logfile % 'cachegrind.%p'


######################################################
# The Python binary
######################################################
PYTHON = "python"


def pad_zeros(s, size):
    """
    INPUT:
       - `s` -- something coercible to string
       - `size` -- integer

    Return s as a string with 0's padded on the left so the string has
    length exactly size.
    """
    s = str(s)
    return "0"*(size-len(s)) + s

index_cache = set([])
def new_index(n):
    """
    Return n-th example number.  If random_order is set, this is a randomly
    chosen number that we haven't returned before.  If random_order is not
    set, this just returns n.

    INPUT:
       - `n` -- integer
    """
    if random_order:
        while True:
            a = random.randint(0,2**32)
            if a not in index_cache:
                index_cache.add(a)
                return a
    else:
        return n

def test_code(filename):
    # Process exit codes for the generated doctest runner script:
    # 0: everything passed
    # 1-253: number of failed doctests
    # 254: >= 254 doctests failed
    # 255: exception raised by doctesting framework
    dict = { 'DIR'             : repr(os.path.join(SAGE_ROOT, 'local', 'bin')),
             'FILENAME'        : repr(filename),
             'OUTPUT_FILENAME' : repr(filename + '.timeit.sobj'),
             'TIMEIT'          : do_timeit, # global
             'VERBOSE'         : verbose }
    return """
if __name__ ==  '__main__':
    verbose = %(VERBOSE)s
    do_timeit = %(TIMEIT)s
    output_filename = %(OUTPUT_FILENAME)s

    import sys
    sys.path = sys.path + [%(DIR)s]
    import sagedoctest

    # execfile(%(FILENAME)s)
    m = sys.modules[__name__]
    m.__file__ = %(FILENAME)s

    try:

        # configure special sage doc test runner
        runner = sagedoctest.SageDocTestRunner(checker=None, verbose=verbose, optionflags=0)
        runner._collect_timeit_stats = do_timeit
        runner._reset_random_seed = True

        runner = sagedoctest.testmod_returning_runner(m,
                       # filename=%(FILENAME)s,
                       verbose=verbose,
                       globs=globals(),
                       runner=runner)
        runner.save_timeit_stats_to_file_named(output_filename)
    except:
        quit_sage(verbose=False)
        import traceback
        traceback.print_exc(file=sys.stdout)
        sys.exit(255)
    quit_sage(verbose=False)
    if runner.failures > 254:
        sys.exit(254)
    sys.exit(runner.failures)
""" % dict

NONE=0; LONG_TIME=1; RANDOM=2; OPTIONAL=3; NOT_IMPLEMENTED=4; NOT_TESTED=5

def comment_modifier(s):
    sind = s.find('#')
    if sind == -1 or s[sind:sind+3] == '###':
        return [], ''
    eind = s.find('###',sind+1)
    L = s[sind+1:eind].lower()
    v = []
    if ('optional' in L) or ('known bug' in L):
        v.append(OPTIONAL)
    if 'long time' in L:
        v.append(LONG_TIME)
    if 'not implemented' in L:
        v.append(NOT_IMPLEMENTED)
    if 'not tested' in L:
        v.append(NOT_TESTED)
    if 'random' in L:
        v.append(RANDOM)
    return v, L

def preparse_line_with_prompt(L):
    i = L.find(':')
    if i == -1:
        return sage.misc.preparser.preparse(L)
    else:
        return L[:i+1] + sage.misc.preparser.preparse(L[i+1:])

def doc_preparse(s):
    """
    Run the preparser on the documentation string s.
    This *only* preparses the input lines, i.e., those
    that begin with "sage:".or with "..."
    """
    sl = s.lower()

    # t: Deal with code whose output should be ignored. 
    t = []

    # following two: used only for parsing only_optional; list of comments
    comment_modifiers = []  
    last_prompt_comment = ''
    
    for L in s.splitlines():
        begin = L.lstrip()[:5]
        comment = ''
        if begin == 'sage:':
            c, comment = comment_modifier(L)
            last_prompt_comment = comment
            line = ''
            if LONG_TIME in c and not long_time:
                L = '\n'  # extra line so output ignored
            if RANDOM in c:
                L = L.replace('sage:', 'sage: print "ignore this"; ') 
            if NOT_TESTED in c:
                L = '\n'   # not tested
            if NOT_IMPLEMENTED in c:
                L = '\n'   # not tested
            if OPTIONAL in c and not (only_optional or optional):
                L = '\n'
            line = preparse_line_with_prompt(L)
            if RANDOM in c:
                # count spaces at the beginning of line to fix alignment later
                i = 0
                while i < len(line) and line[i].isspace():
                    i += 1
                # append a line saying 'ignore' followed by ellipsis (...)
                # and an empty line, to ignore the output given in the test
                line += '\n' + ' '*i + 'ignore ...\n'
            t.append(line)
            
        elif begin.startswith('...'):
            comment = last_prompt_comment
            i = L.find('.')
            t.append(L[:i+3] + sage.misc.preparser.preparse(L[i+3:]))
            
        else:
            comment = last_prompt_comment
            t.append(L)
        
        comment_modifiers.append(comment)

    # The very last line -- which is typically """ -- must never be marked as optional,
    # or it might not get included, which would be a syntax error. 
    comment_modifiers[-1] = ''

    # if only_optional is True, then we skip the entire block unless certain
    # conditions are met:
    #  1. If the tag list is empty, some line must contain # optional
    #  2. If the tag list is nonempty, some line must contain
    #                 # optional - nonempty,subset,of,tags
    #     In this case, we also strip out all #optional lines that don't
    #     contain a nonempty subset of tags, but keep all other lines.
    if only_optional:
        # check if any doctest is optional
        contains_optional_doctests = (len([x for x in comment_modifiers if 'optional' in x]) > 0)
        if len(only_optional_tags) == 0:
            # case 1
            if not contains_optional_doctests:
                t = []
        else:
            # case 2
            # first test that some line contains nonempty subset of tags, since
            # if not we won't bother at all.
            if contains_optional_doctests:
                # make a list of each optional line cut out by our tag choices
                v = [i for i in range(len(t)) if only_optional_include(comment_modifiers[i])]
                if len(v) == 0:
                    # if v is empty, don't test this block at all.
                    t = []
                else:
                    # otherwise, we test everything that is non-optional along with everything
                    # listed in v.
                    v = set(v)
                    t = [t[i] for i in range(len(t)) if  i in v  or  'optional' not in comment_modifiers[i]]
            else:
                t = []
    # Now put docstring together and return it.
    
    return '\n'.join(t)

def only_optional_include(s):
    """
    Return True if s is of the form
          # optional list,of,tags
    and the list of tags is a nonempty subset the global list only_optional_tags.
    If only_optional_tags is empty, then it is considered to be the
    set of all tags.

    INPUT:
        s -- a string
        only_optional_tags -- a global variable
    OUTPUT:
        bool 
    """
    i = s.find('optional')
    if i == -1:
        return False
    if len(only_optional_tags) == 0:
        # this doctest has # optional in it, but there are no tags, which
        # mean test everything that is optional.
        return True
    # delete all white spaces and -'s
    s = ''.join(s[i+len('optional')+1:].replace('-','').split())
    v = set(s.split(','))
    return (len(v) > 0 and v.issubset(only_optional_tags))


def extract_doc(file_name, library_code=False):
    """
    INPUT:
        file_name -- string; name of the file to extract the
                     docstrings from.
        library_code -- bool (default: False); if True, this file is
                    Sage core library code, so we do not import it,
                    and do doctest in a temporary directory.
    """
    F = open(file_name).readlines()
    # Put line numbers on every input line
    v = []
    i = 1
    j = 0
    while j < len(F):
        L = F[j].rstrip()
        if L.lstrip()[:5] == 'sage:':
            while j < len(F) and L.endswith('\\') and not L.endswith('\\\\'):
                j += 1
                i += 1
                L = L[:-1] + F[j].lstrip().lstrip('...').rstrip()
            L += '###_sage"line %s:_sage_    %s_sage"'%(i, L.strip())
        j += 1
        i += 1
        v.append(L)

    # 32/64-bit.  If we're on a 32-bit computer, remove all lines that
    # contains "# 64-bit", and if we're on a 64-bit machine remove all
    # lines that contain "# 32-bit".  This makes it possible to have
    # different output in the doctests for different bit machines.
    
    if is_64bit():
        exclude_string = "# 32-bit"
    else:
        exclude_string = "# 64-bit"
        
    F = '\n'.join([L for L in v if not exclude_string in L])

    if is_64bit():
        F = F.replace('# 64-bit','')
    else:
        F = F.replace('# 32-bit','')
    
    F = F.replace('\'"""\'','')

    base, ext = os.path.splitext(file_name)
    name = os.path.basename(base)
    if ext == ".tex":
        F = pythonify_tex(F)
    elif ext == ".rst":
        F = pythonify_rst(F)
  
    s = "# -*- coding: utf-8 -*-\n"
    s += "from sage.all_cmdline import *; \n"
    s += "import sage.plot.plot; sage.plot.plot.DOCTEST_MODE=True\n"  # turn off image popup
    s += """
def warning_function(f):
    import warnings

    def doctest_showwarning(message, category, filename, lineno, file=f, line=None):
        try:
            file.write(warnings.formatwarning(message, category, 'doctest', lineno, line))
        except IOError:
            pass # the file (probably stdout) is invalid
    return doctest_showwarning

def change_warning_output(file):
    import warnings
    warnings.showwarning = warning_function(file)
"""

    if not library_code:
        if ext in ['.py', '.pyx','.spyx']:
            os.system('cp -f %s %s' % (file_name, SAGE_TESTDIR))
            tmpfiles.append(os.path.join(SAGE_TESTDIR, '%s%s' % (name, ext)))
            if ext == '.py':
                s += "from %s import *\n\n" % name
            else:
                s += "cython(open('%s').read())\n\n" % file_name
        elif ext == '.sage':
            os.system('sage -preparse %s' % file_name)
            os.system('mv -f %s.py %s' % (base, SAGE_TESTDIR))
            tmpfiles.append(os.path.join(SAGE_TESTDIR, name + '.py'))
            s += "from %s import *\n\n" % (name)
        if ext in ['.py', '.sage']:
            tmpfiles.append(os.path.join(SAGE_TESTDIR, name + '.pyc'))

    n = 0
    while True:
        i = F.find('"""')
        if i == -1: break
        name = "example"        
        k = F[i+3:].find('"""')
        if k == -1: break
        j = i+3 + k
        try:
            doc = doc_preparse(F[i:j+3])
        except SyntaxError:
            doc = F[i:j+3]
        if len(doc):
            doc = '""">>> set_random_seed(0L)\n\n' + '>>> change_warning_output(sys.stdout)\n\n' + doc[3:]
            if random_order:
                n_str = pad_zeros(new_index(n),10)
            else:
                n_str = str(n)
            s += "def %s_%s():"%(name,n_str)
            n += 1
            s += "\tr"+ doc + "\n\n"
        F = F[j+3:]

    if n == 0:
        return  ''
    s += test_code(os.path.abspath(file_name))

    # Allow for "sage:" instead of the traditional Python ">>>".
    s = s.replace("sage:",">>>").replace('_sage"','')
    
    return s

def pythonify_tex(F):
    """
    INPUT:
        F -- string; read in latex file
    OUTPUT:
        string -- python program that has functions with docstrings made from the
                  verbatim examples in the latex file.
    """
    # Close links:
    F = F.replace('\\end{verbatim}%link','')
    F = F.replace('%link\n\\begin{verbatim}','')
    
    # Get rid of skipped code
    s = ''
    while True:
        i = F.find('%skip')
        if i == -1:
            s += F
            break
        s += F[:i]
        F = F[i:]
        j = F.find('\\end{verbatim}')
        if j == -1:
            break
        F = F[j + len('\\end{verbatim}')+1:]
    F = s
    
    # Make the verbatim environ's get extracted via the usual parser above
    F = F.replace("\\begin{verbatim}",'"""')
    F = F.replace("\\end{verbatim}",'"""')
    return F

def pythonify_rst(F):
    """
    INPUT:
        F -- string; read in ReST file
    OUTPUT:
        string -- python program that has functions with docstrings made from the
                  verbatim examples in the ReST file.
    """
    import re
    
    def get_next_verbatim_block(s, pos):
        while True:
            # regular expression search in string s[pos:] for:
            # whitespace followed by "::" at the start of a line
            srch = re.search('^(\s*).*::\s*$', s[pos:], re.M)

            #Return -1 if we don't find anything.
            if srch is None:
               return "", -1, False 
            
            pos += srch.start()

            prev_line = F.rfind("\n", 0, pos)
            start_of_options = F.rfind("\n", 0, prev_line if prev_line != -1 else 0)
            options = F[start_of_options:pos]

            pos += (srch.end() - srch.start())

            #Don't return skipped blocks
            if '.. skip' in options:
                continue

            #Check to see if we need to link up with the previous
            #block
            link_previous = True if '.. link' in options else False

            #Find the first line that isn't indented as much as
            #whatever followed the double colon.  The whitespace before
            #the double colon is stored in srch.groups()[0]
            lw = len(srch.groups()[0])  # length of whitespace before ::
            #So match anything of the form
            # beg. of line + whitespace of length at most lw + nonwhitespace
            no_indent = re.search("^\s{,%s}\S"%lw, s[pos:], re.M)

            if no_indent is None:
                return s[pos:], None, link_previous

            block, pos = s[pos:pos+no_indent.start()], pos+no_indent.start()
            if re.compile('^\s*sage: ', re.M).search(block) is not None:
                return block, pos, link_previous

    s = ''
    pos = 0
    while pos is not None and pos != -1:
        block, pos, link_previous = get_next_verbatim_block(F, pos)

        #Break if we can't find any more verbatim blocks
        if pos == -1:
            break

        #Check to see if we have to link this block
        #up with the previous block
        if link_previous:
            #Get rid of the trailing quotes and newlines from the
            #previous block
            s = s[:-5]

            s += '%s\n"""\n\n'%block
        else:
            s += '\n"""\n%s\n"""\n\n'%block

    #print s
    #print "--"*100
    return s

    

def post_process(s, file, tmpname):
    s = s.replace('.doctest_','')
    if file[-3:] == 'pyx':
        s = s.replace('"%s", line'%file[:-1], '"%s", line'%file)
    i = s.find("Failed example:")
    cnt = 0
    while i != -1:
        k = s[:i].rfind('File')
        k += s[k:].find(',')
        j = s[i:].find('###line')
        s = s[:k] + ', ' + s[i+j+3:]
        i = s.find("Failed example:")
        cnt += 1
        if cnt > 1000:
            break
    s = s.replace(':_sage_',':\n').replace('>>>','sage:')
    c = '###line [0-9]*\n'
    r = re.compile(c)
    s = r.sub('\n',s)
    if cnt > 0:
        s += "For whitespace errors, see the file %s"%tmpname
    return (s, cnt)

def test_file(file, library_code):
    if os.path.exists(file):
        name = os.path.basename(file)
        name = name[:name.find(".")]
        s = extract_doc(file, library_code=library_code)
        if len(s) == 0:
            sys.exit(0)

        f = os.path.join(SAGE_TESTDIR, ".doctest_%s.py" % name)

        open(f,"w").write(s)
        tmpfiles.append(f)
        cmd = "%s %s"%(PYTHON, f)
        if gdb:
            print "*"*80
            print "Type r at the (gdb) prompt to run the doctests."
            print "Type bt if there is a crash to see a traceback."
            print "*"*80
            cmd = "gdb --args " + cmd

        if memcheck:
            cmd = "valgrind --tool=memcheck " + SAGE_MEMCHECK_FLAGS + cmd
        if massif:
            cmd = "valgrind --tool=massif " + SAGE_MASSIF_FLAGS + cmd
        if cachegrind:
            cmd = "valgrind --tool=cachegrind " +  SAGE_CACHEGRIND_FLAGS + cmd

        VALGRIND = os.path.join(DOT_SAGE, 'valgrind')
        if not os.path.exists(VALGRIND):
          os.makedirs(VALGRIND)

        tm = time.time()
        try:
            out = ''; err = ''
            if verbose or gdb or memcheck or massif or cachegrind:
                import subprocess
                proc = subprocess.Popen(cmd,shell=True)
                while time.time()-tm <= TIMEOUT and proc.poll() == None:
                    time.sleep(0.1)
                if time.time()-tm >=TIMEOUT:
                    os.kill(proc.pid, 9)
                    print "*** *** Error: TIMED OUT! PROCESS KILLED! *** ***"
                e = proc.poll()
            else:
                outf = tempfile.NamedTemporaryFile()
                import subprocess
                proc = subprocess.Popen(cmd,shell=True, stdout=outf.file.fileno(), stderr = outf.file.fileno())
                while time.time()-tm <= TIMEOUT and proc.poll() == None:
                    time.sleep(0.1)
                if time.time()-tm >=TIMEOUT:
                    os.kill(proc.pid, 9)
                    print "*** *** Error: TIMED OUT! PROCESS KILLED! *** ***"
                outf.file.seek(0)
                out = outf.read()
                e = proc.poll()
        except KeyboardInterrupt:
            print "KeyboardInterrupt -- interrupted after %s seconds!" % (time.time()-tm)
            sys.exit(2)
        if not verbose and 'raise KeyboardInterrupt' in err:
            print "*"*80 + "Control-C pressed -- interrupting doctests." + "*"*80
            sys.exit(2)

        if time.time() - tm >= TIMEOUT:
            err += "*** *** Error: TIMED OUT! *** ***"
            print err

        s, numfail = post_process(out, file, f)
        s += err

        if e == 255:
            # The doctesting code raised an exception
            if not verbose:
                print "Exception raised by doctesting framework. Use -verbose for details."
            sys.exit(4)

        if numfail == 0 and e > 0:
            numfail = e
        if numfail > 0:
            if not (verbose or gdb or memcheck or massif or cachegrind):
                print s
            sys.exit(100)
        elif e < 0:
            if not verbose:
                print "The doctested process was killed by signal %s" % (-e)
            sys.exit(3)
        else:
            delete_tmpfiles()
            sys.exit(0)
    else:
        print "Error running %s, since file %s does not exist."%(
            argv[0], argv[1])
        sys.exit(1)


def has_opt(opt):
    if '-' + opt in argv:
        i = argv.index('-' + opt)
        del argv[i]
        return True
    elif '--' + opt in argv:
        i = argv.index('--' + opt)
        del argv[i]
        return True
    return False

def parse_only_opt():
    """
    Search through argv for the -only-optional=list,of,tags option.
    If it is there, return True and a set of tags as a list of strings.
    If not, return False and an empty set.

    NOTE: Because it's very natural/easy to type only_optional instead
    of only-optional as an option (given that variable names have
    underscores in Python), using only_optional is accepted in
    addition to only-optional.

    OUTPUT:
        bool, set
    """
    for j, X in enumerate(argv):
        Z = X.lstrip('-')
        if Z.startswith('only-optional=') or Z.startswith('only_optional='):
            i = Z.find('=')
            del argv[j]
            return True, set(Z[i+1:].split(','))
        elif Z.startswith('only-optional') or Z.startswith('only_optional'):  # no equals
            del argv[j]
            return True, set([])
    return False, []

def parse_rand():
    """
    Randomize seed and return True if we randomize doctest order.
    Otherwise we leave doctests in the "traditional order" and return False.
    """
    for j, X in enumerate(argv):
        Z = X.lstrip('-')
        if Z.startswith('randorder'):
            del argv[j]
            i = Z.find('=')
            if i != -1:
                random.seed(Z[i+1:])
            return True
    return False

def usage():
    print "\n\nUsage: sage-doctest [same options as sage -t] filenames"
    sys.exit(1)
        
if __name__ ==  '__main__':
    import os, sys
    if len(argv) == 1:
        usage()
    else:
        if has_opt('help') or has_opt('h') or has_opt('?'):
            usage()
        optional   = has_opt('optional')
        long_time  = has_opt('long')
        verbose    = has_opt('verbose')
        do_timeit  = has_opt('timeit')
        gdb        = has_opt('gdb')
        memcheck   = has_opt('memcheck') or has_opt('valgrind')
        massif     = has_opt('massif')
        cachegrind = has_opt('cachegrind')
        force_lib  = has_opt('force_lib')
        random_order = parse_rand()
        only_optional, only_optional_tags = parse_only_opt()
        if long_time:
            TIMEOUT = TIMEOUT_LONG
        if gdb or memcheck or massif or cachegrind:
            TIMEOUT = TIMEOUT_VALGRIND
        if argv[1][0] == '-':
            usage()
            
        ext = os.path.splitext(argv[1])[1]

        library_code = True
        dev_path = os.path.realpath(os.environ['SAGE_DEVEL'] + '/sage')
        our_path = os.path.realpath(argv[1])

        if not force_lib and (ext in ['.spyx', '.sage'] or
                              not dev_path in our_path):
            library_code = False

        try:
            test_file(argv[1], library_code = library_code)
        except KeyboardInterrupt:
            sys.exit(1)
