[Tutor] Setting Command Line Arguments in IDLE

Martin A. Brown martin at linux-ip.net
Sun May 26 11:09:58 EDT 2019


Hello Richard,

In addition to the answers you have received from Alex and Alan, 
I'll add a bit longer of an answer mostly around how to separate the 
command-line invocation / argument handling part of your program 
from the pure-Python logic.

While I, also, cannot help terribly with the direct question about 
your IDLE environment, I hope this explanation is generally helpful 
and useful.

>I am working on a python script that will be provided arguments 
>when run from the system command line. Is there any place in IDLE 
>to provide equivalent arguments for testing while developing in 
>IDLE?

I can't really help so much with the IDLE part of the question, but 
I do have a solution that has worked very well for me for testing 
programs that are usually invoked by command-line.  My answer will 
focus on that angle rather than the IDLE angle.

>Is there any way to define the working directory for the program, 
>or will it always be the directory the script is in (it will be 
>typically run using the PATH, so not the same directory as the 
>script)?


Defining working directory.
---------------------------

You cannot control the initial working directory when your program 
has been executed.  It was executed by another process (or human, or 
alien from the Ophiucus Cluster) so your working directory is 
whatever was in the parent parent process just before 
forking/execution of your program.

However, you are free to chdir() once your program is running:

  >>> os.getcwd()
  '/home/mabrown'
  >>> os.chdir('/home/mabrown/tmp/')
  >>> os.getcwd()
  '/home/mabrown/tmp'

One bit of care that is warranted when you are calling os.chdir() 
(besides the usual error-checking, 'Can the user running this 
program actually chdir() to that directory?)' is to think about 
whether you want your program to use relative paths or absolute 
paths.  It's worth thinking about early because if you don't, you 
can end up with a mess of relative and absolute paths.  That way 
madness, bugs and possible data loss lie.


What is my install directory?
-----------------------------

If you need to know what directory your Python program is in, you 
can use os.path.dirname(os.path.abspath(__file__)).

Sometimes, there are reasons to find your data (or other code?) in 
the same directory as (or nearby) your Python program.  Think 
carefully about this and consider how that might work with 
your Python packaging solution, if you are going down that path.

In general, I have tried to use command-line options to find data 
directories or other files needed by a Python program, as that is 
more intelligible to users (and to me when I come back to the code a 
year later) than the automatic discovery of files that just happen 
to be in the same directory as my Python program.

Random note on $PATH and full pathnames
---------------------------------------

Have you ever been trying to diagnose a problem with a script and 
you realized about four months two late (because that 20 minute 
debugging session where you think you are going insane feels like 
four months by the time you are done), and you realized at the end 
that the problem was that you thought you were running a copy of 
script A in directory A, but you were actually running older copy B 
in directory B.  This meant that none of your changes were being 
reflected in the output and you couldn't figure it out.

Well, I'll tell you, I have never, ever had that experience!  Nope.  
Not even once.  Yeah.

Anyway, I have found that logging os.path.abspath(__file__) to the 
system log (or to STDERR or elsewhere) what the full path is to the 
file that is executing.  This is useful to avoid the "I think I'm 
running my dev copy, but am actually running something else." sort 
of problem.

>If not, is there an easy way to detect that I am running in IDLE so 
>I can fake the command line arguments when testing?

I'll give you an example (see below) of how I write the Python to 
call most of my programs that are invoked directly from the 
command-line.

The basic idea (which I think I picked up here) is to turn anything 
that you want to be able to test in your program into Python 
variables almost as soon as the program begins.

That way, things like STDIN, STDOUT and the arguments become simply 
file objects and lists of strings.  With file objects and lists of 
strings you can write testing functions to see how your program 
behaves when invoked a certain way.

So, this is fairly unexciting code:

  def cli(me, fin, fout, argv):
      config, args = collectconfiguration(me, argv)
      rc = process_something(fin, fout, config)
      if rc == 0:
          return os.EX_OK
      return 1
  
  
  if __name__ == '__main__':
      me = os.path.basename(sys.argv[0])
      sys.exit(cli(me, sys.stdin, sys.stdout, sys.argv[1:]))

You will see that I did not add os.environ to the argument list to 
the function cli(), but if my program(s) did anything with 
environment variables, I would add that here.

You will also see that I don't mess with sys.stderr.  In general, I 
like to stay away from redirecting that unless there's a strong 
need.

Benefits:

  * you can call cli() in your testing programs with different lists 
    of command-line invocations to see how your program behaves

  * you can see how 

-Martin

Note:  You will see in this example of collectconfiguration() 
(possibly not the best name for this function) that I make reference 
to sys.stdin and sys.stdout.  They are also arguments to cli().  I 
think that is OK because I use this sort of calling structure many 
places and I want to keep the signature for the cli() function the 
same everywhere.  In short, some programs may not actually have the 
'--input' / '--output' command-line options, in which case cli() has 
the variables fin and fout to hold those.

  def collectconfiguration(me, argv):
      ap = argparse.ArgumentParser(prog=me)
      ap.add_argument('--input',
                      default=sys.stdin, type=argparse.FileType('r'),
                      help='where to read input data [%(default)s]')
      ap.add_argument('--output',
                      default=sys.stdout, type=argparse.FileType('w'),
                      help='where to write output data [%(default)s]')
      ap.add_argument('--apiurl',
                      default='http://localhost:8080',
                      help='specify alternate API URL [%(default)s]')
      ap.add_argument('--delimiter',
                      default='\t', type=str,
                      help='set delimiter [%(default)s]')
      ap.add_argument('--loglevel',
                      default=logging.ERROR, type=arg_isloglevel,
                      help='set loglevel, e.g. INFO, ERROR [%(default)s]')
      ap.add_argument('--version', action='version', version=__version__)
  
      # -- and handle the CLI
      #
      config, args = ap.parse_known_args(argv)
  
      logger.setLevel(config.loglevel)
      logger.debug("Received the following configuration:")
      for param, value in sorted(vars(config).items()):
          logger.debug("  %s = %r", param, value)
      logger.debug("  args: %r", args)
  
      if args:
          ap.print_help()
          sys.exit("\nExtra arguments received: %r" % (args,))
  
      return config, args


  def arg_isloglevel(l, defaultlevel=logging.ERROR):
      try:
          level = int(l)
          return level
      except ValueError:
          pass
      level = getattr(logging, l.upper(), None)
      if not level:
          level = defaultlevel
      return level


-- 
Martin A. Brown
http://linux-ip.net/


More information about the Tutor mailing list