Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Python Code Generator Written in Python

4.90/5 (15 votes)
11 Nov 2013MIT11 min read 85.9K   1.4K  
Code generator writes argument parsing code.

 Introduction

Program make_python_prog.py is a code generator that produces Python programs that parse command line arguments using Python 2.7's 'argparse' module.

The purpose of the program is to save typing. It is not a goal to handle all features of the 'argparse' module, but rather to produce code that runs, and, if desired, can be easily modified to take advantage of more advanced argparse features; and of course to modify the program to do whatever the programs is intended to do.

It is a goal to make the command-line input format for make_python_prog.py both simple and easy-to-remember. Thus additional argparse features, such a argument groups, and the 'choices' feature, and other argparse features are not added as these would complicate the command line input to the make_python_prog.py program. It is easier to modify the generated code to add these advanced argparse features.

Program make_python_prog.py runs under Python version 2.6 or 2.7 and produces programs that run under Python version 2.7. If the user copies the Python 2.7 distribution file argparse.py to the generated program folder, the generated program will run under Python version 2.6, and perhaps even earlier versions. 

Both program make_python_prog.py and the generated code conform to Python coding standard PEP8. Python is unusual, because there are no special keywords or brackets to define scope, it is completely defined by indentation. This makes it easy to write code rapidly, but whitespace becomes a detriment because program structure is not seen as well if the program is broken up with whitespace. 

Update: Nov. 10 2013

Program make_python_prog.py is still in the latest code drop. I added two other programs, make_python_2_prog.py and make_python_3_prog.py. These write Python code for before version 2.5 versions of Python, and Python version 3 respectively.

Before Python 2.5, the string.format method did not exist, so program make_python_2_prog.py uses the old way to insert strings in strings. As long as a modified file argparse2.py is provided for versions of Python before Python 2.7, which has argparse.py built-in, then make_python_2_prog.py will write code that runs on Python 2.x, at least as far back as Python 2.2 (I think - I didn't test that far back). The old way to insert strings in strings works on all later versions of Python (I think even version 3.x?). 

Unfortunately, the code generated by the make_python_2_prog.py program for earlier versions of Python cannot run the code in module argparse.py.  There are instructions below for how to modify argparse.py to be used wiht that program.

Program make_python_3_prog.py is for Python version 3.0 and later versions of Python. Version 3 continues to support the string.format method and has a new way to print. The old version 2 way to print no longer works. So where: 

print "Hello" 

worked for Python 2.x, 

Python 3.x's print requires parenthesis because print is now a built-in function.

print("Hello")

Program make_python_3_prog.py only runs on post 3.0 versions of Python and the generated code will only run on version 3.0 and later versions of Python. I did not test this at all (yet)! I just inspected the code. If anyone has issues, please report this in the message section below.

I am still running Python 2.7, and I use program make_python_prog.py, which supports the string.format method. Until I go to the most recent version of Python, that will be my program of choice. I'm supplying the other code generators for those who are still running older versions and those who are up to the latest version.

By the way, most people I know who write Python are still using Python 2.6 or Python 2.7. Python 3.x is better syntactically and has better features, particularly built-in Unicode support, and it has been gaining ground for some time. However, there are lots of libraries, particularly third-party libraries, to the best of my knowledge, still don't work with Python 3.x.

Using program make_python_prog.py

This program generates python programs that parse command-line arguments and displays the parsed values. The purpose of this program is to save typing and allowing the rapid creation of python programs that can be modified for some intended purpose.

Arguments passed to this program specify the program name and the creation of generated program arguments, including the parameter names, also known as the variable names, parameter types, number of times a parameter may be entered, and whether a parameter is optional, and whether the parameter is entered or controlled by specifying a command-line switch.

A folder is created with the same name as the base name of the program name passed on the command line and the generated code is created in this folder.

Usage:

See further below for example command lines.

Command line format:

python make_python_prog.py <program name> <parameter text>

The formal specification for the program command line is in the form of:

text/
Below <x> indicates x is required and [y] indicates that y is optional
python make_python_prog.py <program_name> [parameter 1] [parameter 2} ... [parameter N]

Each parameter is a comma delimited list of the form:

<variable_name[="initial_value"]>[,parameter_type][,parameter_count_token][,parameter_switch][,parameter_switch]

The parameter_type, parameter_count_token, and any parameter_switch specifier can be in any order.

About variable_name

The variable name must always be the first item specified for each parameter. The variable name may only contain letters of the English alphabet or the underscore character. The initial_value for a variable name should only be surrounded by double quote characters if the parameter type is a string, and even then the double quotes are only necessary if the inital_value string contains whitespace.

The only valid default values for a Boolean parameter are False and True.

About parameter_type

The parameter_type specifier can be one of the following characters. If no initial_value is specified for each of these types, the indicated initial value default is used.

  • s - A string parameter, defaults to the empty string, or ''.
  • i - An integer parameter, defaults to 0.
  • f - A floating point parameter, defaults to 0.0.
  • b - A Boolean parameter, defaults to False.

If the parameter_type is not specified, then the parameter_type defaults to 's', which indicates a string argument.

About parameter_count_token 

The optional count token controls the number of arguments that are accepted for specified argument type. If the number is more than one, then the variable specified by the given name will be a python list. This final optional count parameter is used as 'nargs' in the argument parser code. The nargs parameter would typically be one of the following:

  • * - Accept 0 or more of the argument type.
  • + - Accept 1 or more of the argument type.
  • ? - The argument is optional.
  • [A positive integer] - Accept the specified number of arguments, e.g. 2

If the parameter_count_token is not specified, then at run-time, only one value is entered on the command line for that parameter. if the parameter_count_token indicates multiple values, then variable_name will identify a Python list instance, and each value entered will be added to the list by the parsing code.

About parameter_switch 

An initial dash character indicates a parameter_switch. A single dash character indicates a short, or single character, switch name. Two initial dash characters specify a long-name switch.

Both a short-name switch and a long-name switch can be specified.

The '-h' and '--help' switches are implemented automatically and should not be specified as switch parameters. Using either one of these help switches results in the __doc__ string at the start of the generated program being printed.

Additional information regarding Boolean parameters.

A Boolean parameter, with parameter_type 'b', is typically used as an optional switch parameter. Using the switch for a Boolean parameter in the generated program results in the variable name for the Boolean argument being set to the opposite of the initial_value, which for the default for a Boolean parameter, changes the variable from False to True.

Example command lines:

python make_python_prog.py foo alpha,i beta,f,+ file_name gamma,-g,--gm,b

This command line generates a program named 'foo.py' that takes an integer parameter named 'alpha', and one or more floating point parameters that are in a list named 'beta', then a string parameter named file_name, and finally an optional parameter named 'gamma' that is a Boolean value that is only 'True' if either the '-g' switch or the '--gm' switch are specified.

python make_python_prog.py foo file_name='foo.txt',?

The ? in this command line makes the file_name parameter an optional parameter. If no argument is specified, then variable 'file_name' is set to 'foo.txt'.

 

Program Structure

The 'main' function near the end of the program starts argument processing. Main is called at the very end of the program. 

For each command line parameter, the main functions uses an ArgInfo class instance to store the variable name and attributes of the variable, including the type, default (initial) value, and other data relative to the variable. After the comma-delimited parameter data is parsed and stored in an ArgInfo class instance, the instance is stored in a list, designated by variable name 'param_info_list'.

The ArgInfo class and methods can be seen here. The self.name parameter stores the variable name. The rest should be obvious.

Python
class ArgInfo:
    """ Instances of the ArgInfo class store argument properties. """

    name_regex = re.compile('[A-Za-z_]*[0-9A-Za-z_]')

    def __init__(self):
        self.name = ''
        self.default_value = ''
        self.data_type = ''
        self.count_token = ''
        self.switch_name_list = []

    def get_switch_name_list(self):
        return self.switch_name_list

    def set_switch_name_list(self, switch_name_list):
        # Validate each switch name in the switch name list.
        for switch_name in switch_name_list:
            # Strip off up to two leading hyphen characters.
            if switch_name.startswith('-'):
                switch_name = switch_name[1:]
            if switch_name.startswith('-'):
                switch_name = switch_name[1:]
            self.validate_name(switch_name)
        self.switch_name_list = switch_name_list

    def get_name(self):
        return self.name

    def set_name(self, name):
        self.validate_name(name)
        self.name = name

    def get_default_value(self):
        return self.default_value

    def set_default_value(self, default_value):
        self.default_value = default_value

    def get_type(self):
        return self.data_type

    def set_type(self, type):
        self.data_type = type

    def get_count_token(self):
        return self.count_token

    def set_count_token(self, count):
        self.count_token = count

    def validate_name(self, name):
        result = ArgInfo.name_regex.match(name)
        if not result:
            raise ValueError('Illegal name: {0}'.format(name)) 

 

After creating a folder to store the program files, the main function opens the new generated program file and calls the write_program function, passing the file specifier, the base_program_name, which for "foobar.py" would be "foobar", and the list of ArgInfo instances. 

Python
# Open the program file and write the program.
    with open(program_name, 'w') as outfile:
        write_program(outfile, base_program_name, param_info_list)

The write_program function is very simple. First, the write_program_header function is called. That function writes the boilerplate that starts every Python program and the program imports, including importing the ArgumentParser class from module argparse.

Next, the 'write_primary_main_function' function writes a function that takes a comma-delimited list of arguments and writes code to print each argument name and value.

Finally, the write_program_end function writes the 'main' function for the generated program, which contains the argument parsing code. The write_program_end function is the most complicated function. It produces the lines of code necessary for the python argparse.ArgumentParser instance to parse the command lines arguments. For each ArgInfo instance in param_info_list, separate lines are written for the argparse.ArgumentParser instance.

def write_program(outfile, base_program_name, param_info_list):
    """ Main function to write the python program. """
    # Extract just the argument names to a list.
    param_list = []
    for param_info in param_info_list:
        param_list.append(param_info.get_name())
    write_program_header(outfile, base_program_name, param_list)
    write_primary_main_function(outfile, base_program_name, param_list)
    write_program_end(outfile, base_program_name, param_list, param_info_list)
 

If the program command line is:

python make_python_prog.py Enroll name age,i company="The Company",? height,f,-h,--height is_member,b,-m,--member

Then the variables 'name', 'age', 'company', 'height', and 'is_member' are created by the write_program_end function. The resulting 'main' function in the generated code is:

def main(argv=None):
    # Initialize the command line parser.
    parser = ArgumentParser(description='TODO: Text to display before the argument help.',
                            epilog='Copyright (c) 2013 TODO: your-name-here.',
                            add_help=True,
                            argument_default=None, # Global argument default
                            usage=__doc__)
    parser.add_argument(action='store', dest='name', help='TODO:')
    parser.add_argument(action='store', dest='age', type=int, help='TODO:')
    parser.add_argument(action='store', dest='company', default=The Company, nargs='?', help='TODO:')
    parser.add_argument('-h', '--height', action='store', dest='height', type=float, default=0.0, help='TODO:')
    parser.add_argument('-m', '--member', action='store_true', dest='is_member', default=False, help='TODO:')
    # Parse the command line.
    arguments = parser.parse_args(args=argv)
    name = arguments.name
    age = arguments.age
    company = arguments.company
    height = arguments.height
    is_member = arguments.is_member
    status = 0
    try:
        Enroll_main(name, age, company, height, is_member)
    except ValueError as value_error:
        print value_error
        status = -1
    except EnvironmentError as environment_error:
        print environment_error
        status = -1
    return status

 

This code will run as-is, but the user will want to search for the string 'TODO:' and replace that with meaningful text. For example, the help text for each line of code to add a variable to the parser should have help text added.

parser.add_argument(action='store', dest='name', help='TODO:')

Program make_python_prog.py source code

Python
#!/usr/bin/env python
#=======================================================================
# Copyright (C) 2013 William Hallahan
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
#=======================================================================
"""
    This program generates python programs that parse command-line
    arguments and displays the parsed values.  The purpose of this
    program is to save typing and allowing the rapid creation of
    python programs that can be modified for some intended purpose.
    
    Arguments passed to this program specify the program name and the
    creation of generated program arguments, including the parameter
    names, also known as the variable names, parameter types, number
    of times a parameter may be entered, and whether a parameter is
    optional, and whether the parameter is entered or controlled by
    specifying a command-line switch.

Usage:

    See further below for example command lines.

    Command line format:

        python make_python_prog.py <program name> <parameter text>

    The formal specification for the program command line is in the form of:

      Below <x> indicates x is required and [y] indicates that y is optional.

        python make_python_prog.py <program_name> [parameter 1] [parameter 2} ... [parameter N] 

    Each parameter is a comma delimited list of the form:

        <variable_name[="initial_value"]>[,parameter_type][,parameter_count_token][,parameter_switch][,parameter_switch]

    The parameter_type, parameter_count_token, and any parameter_switch specifier
    can be in any order.

            About variable_name

    The variable name must always be the first item specified for each
    parameter.  The variable name may only contain letters of the
    English alphabet or the underscore character.  The initial_value for
    a variable name should only be surrounded by double quote characters
    if the parameter type is a string, and even then the double quotes
    are only necessary if the inital_value string contains whitespace.
    
    The only valid default values for a Boolean parameter are False and True.

            About parameter_type

    The parameter_type specifier can be one of the following characters.
    If no initial_value is specified for each of these types, the
    indicated initital value default is used.

        s - A string parameter, defaults to the empty string, or ''.
        i - An integer parameter, defaults to 0.
        f - A floating point parameter, defaults to 0.0.
        b - A Boolean parameter, defaults to False.

    If the parameter_type is not specified, then the parameter_type defaults
    to 's', which indicates a string argument.

            About parameter_count_token

    The optional count token controls the number of arguments that
    are accepted for specified argument type.  If the number is more
    than one, then the variable specified by the given name will be a
    python list.  This final optional count parameter is used as 'nargs'
    in the argument parser code.  The nargs parameter would typically
    be one of the following:

        * - Accept 0 or more of the argument type.
        + - Accept 1 or more of the argument type.
        ? - The argument is optional.
        [A positive integer] - Accept the specified number
                               of arguments, e.g. 2

    If the parameter_count_token is not specified, then at runtime, only
    one value is entered on the command line for that parameter.  if the
    parameter_count_token indicates multiple values, then variable_name
    will identify a Python list instance, and each value entered will
    be added to the list by the parsing code.

            About parameter_switch

    An initial dash character indicate a parameter_switch.  A single
    dash character indicates a short, or single character, switch name.
    Two initial dash characters specify a long-name switch.

    Both a short-name switch and a long-name switch can be specified.

    The '-h' and '--help' switches are implemented automatically and
    should not be specified as switch parameters.  Using either one of
    these help switches results in the __doc__ string at the start of
    the generated program being printed.

            Additional information regarding Boolean parameters.

    A Boolean parameter, with parameter_type 'b', is typically used as
    an optional switch parameter.  Using the switch for a Boolean
    parameter in the generated program results in the variable name for
    the Boolean argument being set to the opposite of the initial_value,
    which for the default for a Boolean parameter, changes the variable
    from False to True.

    Example command lines:

        python make_python_prog.py foo alpha,i beta,f,+ file_name gamma,-g,--gm,b

    This command line generates a program named 'foo.py' that takes
    an integer parameter named 'alpha', and one or more floating point
    parameters that are in a list named 'beta', then a string parameter
    named file_name, and finally an optional parameter named 'gamma'
    that is a Boolean value that is only 'True' if either the '-g'
    switch or the '--gm' switch are specified.

        python make_python_prog.py foo file_name='foo.txt',?

    The ? in this command line makes the file_name parameter an optional
    parameter.  If no argument is specified, then variable 'file_name'
    is set to 'foo.txt'.
"""
import sys
import os
import re
import time
import keyword

class ArgInfo:
    """ Instances of the ArgInfo class store argument properties. """

    name_regex = re.compile('[A-Za-z_]*[0-9A-Za-z_]')

    def __init__(self):
        self.name = ''
        self.default_value = ''
        self.data_type = ''
        self.count_token = ''
        self.switch_name_list = []

    def get_switch_name_list(self):
        return self.switch_name_list

    def set_switch_name_list(self, switch_name_list):
        # Validate each switch name in the switch name list.
        for switch_name in switch_name_list:
            # Strip off up to two leading hyphen characters.
            if switch_name.startswith('-'):
                switch_name = switch_name[1:]
            if switch_name.startswith('-'):
                switch_name = switch_name[1:]
            self.validate_name(switch_name)
        self.switch_name_list = switch_name_list

    def get_name(self):
        return self.name

    def set_name(self, name):
        self.validate_name(name)
        self.name = name

    def get_default_value(self):
        return self.default_value

    def set_default_value(self, default_value):
        self.default_value = default_value

    def get_type(self):
        return self.data_type

    def set_type(self, type):
        self.data_type = type

    def get_count_token(self):
        return self.count_token

    def set_count_token(self, count):
        self.count_token = count

    def validate_name(self, name):
        result = ArgInfo.name_regex.match(name)
        if not result:
            raise ValueError('Illegal name: {0}'.format(name))

def validate_no_duplicate_switches(arg_info_list):
    """ Throw an exception if there are any duplicate switch names. """
    switch_list = []
    for arg_info in arg_info_list:
        for switch_name in arg_info.get_switch_name_list():
            if switch_name in switch_list:
                raise ValueError('Duplicate switch name {0}.'.format(switch_name))
            switch_list.append(switch_name)

def is_integer(s):
    """ Returns True if and only if the passed string specifies
        an integer value.
    """
    try:
        x = int(s)
        is_int = True
    except ValueError:
        is_int = False
    return is_int

def is_floating_point(s):
    """ Returns True if and only if the passed string specifies
        a floating point value.
    """
    try:
        x = float(s)
        is_float = True
    except ValueError:
        is_float = False
    return is_float

def write_program_header(outfile, base_program_name, param_list):
    """ Writes a program header in the following form:

        #!/usr/bin/env python
        <three double quotes>
            python template.py --switch1 <value1> --switch2 <value2>
        <three double quotes>

        import sys
        from argparse import ArgumentParser
    """
    outfile.write('#!/usr/bin/env python\n')
    outfile.write('"""\n')
    outfile.write('    python {0}.py\n\n'.format(base_program_name))
    outfile.write('    TODO: Add usage information here.\n')
    outfile.write('"""\n')
    outfile.write('import sys\n')
    outfile.write('# TODO: Uncomment or add imports here.\n')
    outfile.write('#import os\n')
    outfile.write('#import re\n')
    outfile.write('#import time\n')
    outfile.write('#import subprocess\n')
    if param_list != None and len(param_list) > 0:
        outfile.write('from argparse import ArgumentParser\n')
    outfile.write('\n')
    return

def get_function_call_string(function_name, param_list):
    """ Writes a function call, such as:
        'somename_main(input_file_name)'
    """
    function_call = '{0}('.format(function_name)
    number_of_params = len(param_list)
    for i in xrange(0, number_of_params):
        function_call = '{0}{1}'.format(function_call, param_list[i])
        if i != (number_of_params - 1):
            function_call = '{0}, '.format(function_call)
    function_call = '{0})'.format(function_call)
    return function_call

def write_function_start(outfile, function_name, param_list):
    """ Writes a function call, such as:
        'def somename_main(input_file_name):'
        <three double quotes> TODO: <three double quotes>
    """
    if function_name == 'main':
        outfile.write('# Start of main program.\n')
    # write the function declaration.
    function_call = get_function_call_string(function_name, param_list)
    function_declaration = 'def {0}:\n'.format(function_call)
    outfile.write(function_declaration)
    if function_name != 'main':
        outfile.write('    """ TODO: Add docstring here. """\n')
    return

def write_primary_main_function(outfile, base_program_name, param_list):
    """ Writes a function with the following form:

            <three double quotes> <myprogram>_main
            <three double quotes>
            def <myprogram>_main(<argument_list>):
                # TODO: Add code here.
                return 0
    """
    function_name = '{0}_main'.format(base_program_name)
    write_function_start(outfile, function_name, param_list)
    outfile.write('    # TODO: Add or delete code here.\n')
    outfile.write('    # Dump all passed argument values.\n')
    for param in param_list:
        outfile.write("    print '{0} = {1}0{2}'.format(repr({3}))\n".format(param, '{', '}', param))
    # End of test code.
    outfile.write('    return 0\n\n')
    return

def get_year():
    """ Returns a string that contains the year. """
    now = time.ctime()
    now_list = now.split()
    year = now_list[4]
    return year

def get_default_value_for_type(arg_type):
    """ Get the argument default value based on the argument type. """
    # An empty argument type defaults to type 'string'.
    arg_default_value_dict = {'string' : "''", 'boolean' : 'False', 'int' : '0', 'float' : '0.0'}
    return arg_default_value_dict[arg_type]

def write_argument_parsing_code(outfile, param_info_list):
    """ Write argument parsing code.  The form of the code
        varies depending on the passed param_info_list, but
        will be similar to:

        # Initialize the command line parser.
        parser = ArgumentParser(description='TODO: Text to display before the argument help.',
                                epilog='TODO: Text to display after the argument help.',
                                add_help=True,
                                argument_default=None, # Global argument default
                                usage=__doc__)
        parser.add_argument('-s', '--switch1', action='store', dest='value1', required=True,
                            help='The something1')
        parser.add_argument('-t', '--switch2', action='store', dest='value2',
                            help='The something2')
        parser.add_argument('-b', '--optswitchbool', action='store_true', dest='optboolean1', default=False,
                            help='Do something')
        # Parse the command line.
        args = parser.parse_args()
        value1 = args.value1
        value2 = args.value2
        optboolean1 = args.optboolean1
"""
    if len(param_info_list) > 0:
        outfile.write('    # Initialize the command line parser.\n')
        outfile.write("    parser = ArgumentParser(description='TODO: Text to display before the argument help.',\n")
        outfile.write("                            epilog='Copyright (c) {0} TODO: your-name-here - All Rights Reserved.',\n".format(get_year()))
        outfile.write("                            add_help=True,\n")
        outfile.write("                            argument_default=None, # Global argument default\n")
        outfile.write("                            usage=__doc__)\n")
        # For each parameter, write code to add an argument variable to the argument parser.
        for param_info in param_info_list:
            # Get the argument data.
            switch_name_list = param_info.get_switch_name_list()
            arg_name = param_info.get_name()
            arg_default_value = param_info.get_default_value()
            arg_type = param_info.get_type()
            arg_count = param_info.get_count_token()
            # Create a string to add the argument to the parser.
            argument_string = '    parser.add_argument('
            if len(switch_name_list) > 0:
                switches_string = repr(switch_name_list)
                switches_string = switches_string.replace('[', '')
                switches_string = switches_string.replace(']', '')
                argument_string = "{0}{1},".format(argument_string, switches_string)
            if not argument_string.endswith('('):
                argument_string = "{0} ".format(argument_string)
            if arg_type == 'boolean':
                if not arg_default_value or arg_default_value == 'False':
                    argument_string = "{0}action='store_true',".format(argument_string)
                elif arg_default_value == 'True':
                    argument_string = "{0}action='store_false',".format(argument_string)
            else:
                argument_string = "{0}action='store',".format(argument_string)
            # Add text to set the argument name.
            argument_string = "{0} dest='{1}',".format(argument_string, arg_name)
            if arg_type == 'int':
                argument_string = "{0} type=int,".format(argument_string)
            elif arg_type == 'float':
                argument_string = "{0} type=float,".format(argument_string)
            elif arg_type != 'boolean':
                # The default type is 'string'.
                arg_type = 'string'
            # If the parameter is not a required parameter, then set a default value.
            if len(switch_name_list) > 1 or arg_count == '?':
                # If there is no default value then specify a default value based on the type
                # of the argument.
                if not arg_default_value:
                    arg_default_value = get_default_value_for_type(arg_type)
                argument_string = '{0} default={1},'.format(argument_string, arg_default_value)
            elif arg_default_value:
                print "'{0}' is a required parameter.  Default argument value '{1}' ignored".format(arg_name, arg_default_value)
                print "Use parameter_count_token '?' to change to a non-required parameter."
            # Set the optional argument count.
            if arg_count and arg_count in '*+?':
                argument_string = "{0} nargs='{1}',".format(argument_string, arg_count)
            elif is_integer(arg_count):
                argument_string = "{0} nargs={1},".format(argument_string, int(arg_count))
            # Add text to set the argument help string.
            argument_string = "{0} help='TODO:')\n".format(argument_string)
            # Write the line of code that adds the argument to the parser.
            outfile.write(argument_string)
        # Write code the parse the arguments
        outfile.write('    # Parse the command line.\n')
        outfile.write('    arguments = parser.parse_args(args=argv)\n')
        # Write code to extract each parameter value from the argument parser.
        for param_info in param_info_list:
            arg_name = param_info.get_name()
            outfile.write("    {0} = arguments.{1}\n".format(arg_name, arg_name))
    return

def write_program_end(outfile, base_program_name, param_list, param_info_list):
    """ Writes code with the following form:

        <three double quotes> main
        <three double quotes>
        def main():
            <Argument parsing code here>
            [primary_main-function call here]
            return 0

        if __name__ == "__main__":
            sys.exit(main())
    """  
    write_function_start(outfile, 'main', ['argv=None'])
    if param_list != None and len(param_list) > 0:
        write_argument_parsing_code(outfile, param_info_list)
    function_name = '{0}_main'.format(base_program_name)
    function_call = get_function_call_string(function_name, param_list)
    outfile.write('    status = 0\n')
    outfile.write('    try:\n')
    function_call = '        {0}\n'.format(function_call)
    outfile.write(function_call)
    outfile.write('    except ValueError as value_error:\n')
    outfile.write('        print value_error\n')
    outfile.write('        status = -1\n')
    outfile.write('    except EnvironmentError as environment_error:\n')
    outfile.write('        print environment_error\n')
    outfile.write('        status = -1\n')
    outfile.write('    return status\n\n')
    outfile.write('if __name__ == "__main__":\n')
    outfile.write('    sys.exit(main())\n')
    return

def write_program(outfile, base_program_name, param_info_list):
    """ Main function to write the python program. """
    # Extract just the argument names to a list.
    param_list = []
    for param_info in param_info_list:
        param_list.append(param_info.get_name())
    write_program_header(outfile, base_program_name, param_list)
    write_execute_function(outfile, base_program_name, param_list)
    write_program_end(outfile, base_program_name, param_list, param_info_list)

def file_exists(file_name):
    """ Return True if and only if the file exists. """
    exists = True
    try:
        with open(file_name) as f:
            pass
    except IOError as io_error:
        exists = False
    return exists

def validate_arg_default_value(arg_type, arg_default_value, argument):
    """ Validate the argument default value based on the argument type. """
    # An empty argument type defaults to type 'string'.
    if not arg_type or arg_type == 'string':
        if arg_default_value:
            # String default values must begin and end with single quote characters.
            if not arg_default_value.startswith("'") or not arg_default_value.endswith("'"):
                arg_default_value = repr(arg_default_value)
    # If the parameter is a boolean parameter, then the default
    # value must be either the string value 'False' or 'True'.
    elif arg_type == 'boolean':
        if arg_default_value:
            if arg_default_value != 'False' and arg_default_value != 'True':
                raise ValueError("Boolean default value {0} in {1} must be either 'False' or 'True'".format(arg_default_value, argument))
    elif arg_type == 'int':
        if arg_default_value and not is_integer(arg_default_value):
            raise ValueError('Integer default value {0} in {1} is not a valid number.'.format(arg_default_value, argument))
    else: # arg_type == 'float':
        if arg_default_value and not is_floating_point(arg_default_value):
            raise ValueError('Floating point default value {0} in {1} is not a valid floating point number.'.format(arg_default_value, argument))
    return

def validate_variable_name(arg_name, unique_name_list):
    """ Validate the variable name. """
    # Ensure the variable name is not a python keyword.
    if keyword.iskeyword(arg_name):
        raise ValueError('Variable name "{0}" is a python keyword.'.format(arg_name))
    # Ensure the variable name is unique.
    if arg_name in unique_name_list:
        raise ValueError('Variable name "{0}" was already specified.'.format(arg_name))
    unique_name_list.append(arg_name)

def create_folder_under_current_directory(folder_name):
    current_path = os.getcwd()
    new_path = os.path.join(current_path, folder_name)
    if not os.path.exists(folder_name):
        os.makedirs(folder_name)
    return new_path

# Start of main program.
def main(argv=None):
    if argv == None:
	    argv = sys.argv
    # Set the default return value to indicate success.
    status = 0
    # There must be at least one argument that is the program name.
    if len(argv) < 2:
        print 'Program: make_python_prog.py\nUse -h or --help for more information.'
        return status
    # Get the program name or  "-h" or "--help".
    base_program_name = argv[1]
    if base_program_name == '-h' or base_program_name == '--help':
        print __doc__
        return status
    program_name = base_program_name
    # Make sure the base program name does not have the '.py' extension.
    if base_program_name.endswith('.py'):
        base_program_name = base_program_name[:-3]
    # Make sure the base program name is a valid program name.
    param_info = ArgInfo()
    try:
        param_info.validate_name(base_program_name)
        # Add the file extension '.py'.
        program_name = '{0}.py'.format(base_program_name)
        # Don't allow programs to be created with the same name as this program.
        lower_case_program_name = program_name.lower()
        if lower_case_program_name == 'make_python_prog.py':
            raise ValueError('The generated program name cannot be the same name as this program.')
        # The argument list to this program can start with an optional argument
        # switch, followed by the argument name followed by the type of the argument,
        # each separated by a comma character.  The argument type must be one of the
        # following: 's' for string, 'b' for boolean, 'i' for int, or 'f' for float.
        arg_type_dict = {'s' : 'string', 'b' : 'boolean', 'i' : 'int', 'f' : 'float'}
        unique_name_list = []
        param_info_list = []
        for i in xrange(2, len(argv)):
            # Get the current argument string.
            argument = argv[i].strip()
            arg_item_list = argument.split(',')
            # Create a new ArgInfo instance to store the argument settings.
            param_info = ArgInfo()
            # Get the argument name and remove it from the list.
            arg_name = arg_item_list.pop(0)
            # Check for a default value for the argument name.
            arg_default_value = ''
            arg_name_list = arg_name.split('=')
            if len(arg_name_list) > 1:
                arg_name = arg_name_list[0]
                arg_default_value = arg_name_list[1]
            # Declare optional argument setting variables.
            arg_switch_list = []
            arg_count_token = ''
            arg_type = ''
            # Loop and parse any optional comma-delimited parameters.
            for arg_item in arg_item_list:
                # Does the parameter specify an argument switch?
                if arg_item.startswith('-'):
                    arg_switch_list.append(arg_item)
                    continue
                # Does the parameter specify an argument count token?
                elif (len(arg_item) == 1 and arg_item in '*+?') or is_integer(arg_item):
                    # Was an argument count token already found?
                    if arg_count_token:
                        raise ValueError(
                          'Argument count token {1} in {0} is a duplicate count token.'.format(arg_item, argument))
                    arg_count_token = arg_item
                    continue
                # Does the parameter specify an argument type?
                elif (len(arg_item) == 1 and arg_item in 'sbif'):
                    # Was an argument type already found?
                    if arg_type:
                        raise ValueError('Argument type {1} in {0} is a duplicate type.'.format(arg_item, argument))
                    # Look up the argument type token.
                    arg_type = arg_type_dict.get(arg_item)
                    continue
                # The input is invalid.
                raise ValueError('Parameter {0} contains invalid setting {1}.'.format(argument, arg_item))
            # Validate the argument default value and the variable name.
            validate_arg_default_value(arg_type, arg_default_value, argument)
            validate_variable_name(arg_name, unique_name_list)
            # Save the argument parameters.
            param_info.set_name(arg_name)
            param_info.set_default_value(arg_default_value)
            param_info.set_switch_name_list(arg_switch_list)
            param_info.set_count_token(arg_count_token)
            param_info.set_type(arg_type)
            # Add the argument info to the list of arguments.
            param_info_list.append(param_info)
        validate_no_duplicate_switches(param_info_list)
        # If the output file already exists, then prompt the user to overwrite the file.
        if file_exists(program_name):
            print "File '{0}' already exists. Enter 'y' or 'Y' to overwrite the file. >".format(program_name),
            c = raw_input()
            if c != 'y' and c != 'Y':
                return status
        # Create a 'base_program_name' folder.
        new_path = create_folder_under_current_directory(base_program_name)
        # Change the current working directory to the new folder.
        os.chdir(new_path)
        # Open the program file and write the program.
        with open(program_name, 'w') as outfile:
            write_program(outfile, base_program_name, param_info_list)
            print 'Created program {0}'.format(program_name)
    except EnvironmentError as environment_error:
        print environment_error
        status = -1
    except ValueError as value_error:
        print value_error
        status = -1
    return status

if __name__ == "__main__":
    sys.exit(main())

 argparse for program make_python_2_prog.py

Programs generated by the make_python_2_prog.py program, which is for very early versions of Python prior to version 2.4 (2.5?) require a modified argparse.py file.   Here are the steps to modify the file so that it will work on earlier versions of Python.

  1. Download file argparse.py.  Rename it to be named argparse2.py.
  2. Copy argparse2.py to the generated program folder.
  3. When you try to run the program, you will get two errors in file argparse2.py that will be easy to fix.

Specifically, change line 1131 in file argparse.py

except IOError as e:

to:

except IOError, e:

and change line 1595 from:

default_prefix = '-' if '-' in prefix_chars else prefix_chars[0]

to:

default_prefix = '-'
if '-' not in prefix_chars: 
    default_prefix = prefix_chars[0]

Copy the new file argparse2.py to the folder that contains the generated program, and then the program will run.

Points of Interest

Even if you don't want to use the argparse module to parse arguments, perhaps because your running an early version of Python that does not include the argparse module, and you don't want to locate and download file argparse.py, this program still saves typing as it writes most of the boilerplate necessary for a good Python program.

Both program make_python_prog.py and the generated code conform to Python PEP8. Python is unusual, because there are no special keywords or brackets to define scope, it is completely defined by indentation. This makes it easy to write code rapidly, but white-space becomes a detriment because program structure is not seen as well if the program text is broken up with white-space.

History 

  • First posting.
  • Fixed the final paragraph under "Points of Interest" about whitespace. The text ended with "if the program is broken" instead of "if the program text is broken up with white-space".
  • Removed the words "All Rights Reserved" in the copyright notice in the generated code. Updated the zip file and the code text in the article.
  • In addition to program make_python_prog.py, now two other versions, make_python_2_prog.py and make_python_3_prog.py are supplied.
  • Renamed the "write_execute_function" function to be named "write_primary_main_function".
    Renamed the generated function name from "execute_<somename>" to "<somename>_main".

License

This article, along with any associated source code and files, is licensed under The MIT License