[GRASS-dev] [GRASS GIS] #2326: Command functions in grass.script.core miss a correct error reporting

GRASS GIS trac at osgeo.org
Sat Jun 14 23:07:06 PDT 2014


#2326: Command functions in grass.script.core miss a correct error reporting
--------------------------------+-------------------------------------------
 Reporter:  wenzeslaus          |       Owner:  grass-dev@…              
     Type:  enhancement         |      Status:  new                      
 Priority:  major               |   Milestone:  7.1.0                    
Component:  Python              |     Version:  svn-trunk                
 Keywords:  script, exceptions  |    Platform:  All                      
      Cpu:  Unspecified         |  
--------------------------------+-------------------------------------------
Changes (by wenzeslaus):

  * keywords:  script => script, exceptions


Comment:

 Here are my suggestions for changes in `grass.script.core`.

 The `read_command` function is similar to
 [https://docs.python.org/2/library/subprocess.html#subprocess.check_output
 subprocess.check_output] function, so I used the implementation from
 Python subprocess
 ([http://hg.python.org/cpython/file/2145593d108d/Lib/subprocess.py#l508
 cpython: Lib/subprocess.py]). It check the `stderr` parameter, uses
 `communicate()` and `poll()` and raises on non-zero return code. The main
 difference from `subprocess.check_output` (expect from GRASS parameter
 handing) is that stderr is stored to exception to provide a detailed
 message at the right place.

 {{{
 #!python
 def read_command(*args, **kwargs):
     """Run command and return its output."""
     # implemenation inspired by subprocess.check_output() function
     if 'stderr' in kwargs:
         raise ValueError('stderr argument not allowed, it would be
 overridden.')
     kwargs['stderr'] = PIPE
     process = pipe_command(*args, **kwargs)
     output, errors = process.communicate()
     returncode = process.poll()
     if returncode:
         cmd = kwargs.get("args")
         if cmd is None:
             cmd = args[0]
         raise CalledCommandError(returncode, cmd, errors=errors)
     return output
 }}}

 Note that `read_command()` is used by `parse_command()` function and that
 `pipe_command()` might be replaced by `start_command()` and `stdout=PIPE`.

 `write_command()` is similar to `read_command()` but without stdin and win
 stdout as a string:

 {{{
 #!python
 def write_command(*args, **kwargs):
     """!Passes all arguments to feed_command, with the string specified
     by the 'stdin' argument fed to the process' stdin.
     """
     # implemenation inspired by subprocess.check_output() function
     if 'stdin' not in kwargs:
         raise ValueError('stdin argument is required.')
     if 'stderr' in kwargs:
         raise ValueError('stderr argument not allowed, it would be
 overridden.')
     stdin = kwargs['stdin']  # get a string for stdin
     kwargs['stdin'] = PIPE  # to be able to send data to stdin
     kwargs['stderr'] = PIPE
     process = start_command(*args, **kwargs)
     unused, errors = process.communicate(stdin)
     returncode = process.poll()
     if returncode:
         cmd = kwargs.get("args")
         if cmd is None:
             cmd = args[0]
         raise CalledCommandError(returncode, cmd, erros=errors)
     # subprocess.check_call() function returns 0 for success
     # but this would create only confusion since we don't return for
 failure
 }}}

 `write_read_command()` is a new proposed function which is a combination
 of `write_command()` and `read_command()` functions.

 {{{
 #!python
 def write_read_command(*args, **kwargs):
     """Uses stdin string as stdin and returns stdout"""
     # implemenation inspired by subprocess.check_output() function
     if 'stdin' not in kwargs:
         raise ValueError('stdin argument is required.')
     if 'stdout' in kwargs:
         raise ValueError('stdout argument not allowed, it would be
 overridden.')
     if 'stderr' in kwargs:
         raise ValueError('stderr argument not allowed, it would be
 overridden.')
     stdin = kwargs['stdin']  # get a string for stdin
     kwargs['stdin'] = PIPE  # to be able to send data to stdin
     kwargs['stdout'] = PIPE
     kwargs['stderr'] = PIPE
     process = start_command(*args, **kwargs)
     output, errors = process.communicate(stdin)
     returncode = process.poll()
     if returncode:
         cmd = kwargs.get("args")
         if cmd is None:
             cmd = args[0]
         raise CalledCommandError(returncode, cmd, erros=errors)
     return output
 }}}

 I'm not sure with limitations of `write_command()`, `read_command()` and
 `write_read_command()` considering large inputs and outputs but they are
 using `out, err = p.communicate(stdin)` and `p.poll()` which should be the
 best way according to what Glynn is saying, library (subprocess) is using,
 and documentation is suggesting. Moreover, they are not worse that the
 existing functions and I expect that for special cases you need to use
 `start_command()` directly anyway.

 The missing function is now a function which would [comment:3 connect
 stdout and stderr] (`stdout=PIPE, stderr=STDOUT`). But this is again
 something special (e.g. used in GUI) which might not need a convenient
 function and user should use `start_command()` directly.

 Considering how `run_command()` function is used I would suggest to change
 it to raise an exception. This will break the code which was using it
 correctly and this code will have to be fixed manually and perhaps adding
 try-except will be necessary. However, most of the code will improve since
 it will at least fail with proper error message at proper place and not
 few lines further with hard-to-understand message. Adding `return 0` to
 the end of the function may provide a compatibility with previous behavior
 for cases when the return value was used. It will not create an error when
 everything is OK but it will not use the error handling at that place when
 the command fails. At the end, `return 0` should be removed anyway.

 {{{
 #!diff
 -    ps = start_command(*args, **kwargs)
 -    return ps.wait()
 +    if 'stdin' not in kwargs:
 +        raise ValueError('stdin argument is required.')
 +    else:
 +        stdin = kwargs['stdin']
 +        del kwargs['stdin']  # clean non-standard argument
 +    process = feed_command(*args, **kwargs)
 +    unused, errors = process.communicate(stdin)
 +    returncode = process.poll()
 +    if returncode:
 +        cmd = kwargs.get("args")
 +        if cmd is None:
 +            cmd = args[0]
 +        raise CalledCommandError(returncode, cmd, erros=errors)
 +    return 0
 }}}

 I don't see particular advantage in functions `pipe_command()` (sets
 `kwargs['stdout'] = PIPE`) and `feed_command()` (sets `kwargs['stdin'] =
 PIPE`). The the parameters can be set directly with the function
 `start_command()` and for me the functions does not contribute to
 readability because the ''feed'' does not tell me if the string is the
 input (as for `write_command()` function) or it justs sets `stdin=PIPE`.
 Similarly, ''pipe'' does not tell me if everything is piped (stdin, stdout
 and stderr), stdin and stdout are piped (as for  `write_read_command()`
 function) or just one of stdin and stdout is piped. It is even more
 confusing name when I consider piping in command line where I connect
 multiple processes (which is not done by PIPE constant in
 [https://docs.python.org/2/library/subprocess.html#replacing-shell-
 pipeline subprocess]).

 I wanted to change (instead of adding) the functions, especially
 `run_command()`, originally but then I was afraid of problems with
 releasing 7.0 and changing API. However, the usage statistics and the fact
 that Glynn [comment:1 suggested the same] convinced me that we should
 really do it. However, it is challenging, we have to change some code (at
 least 150 occurrences of `run_command()` and `write_command()` in trunk
 and addons but maybe also other functions) and I'm still not sure with
 some detail such as if stderr should be always captured to get the right
 error message (consider e.g. the result with `DEBUG=1`).

 Another consideration is raising exception versus calling `fatal()`
 function which can be set to raise exception but this exception would have
 to contain everything in a message while a custom exception (I used one
 derived from
 [https://docs.python.org/2/library/subprocess.html#subprocess.CalledProcessError
 subprocess.CalledProcessError]) can store return code, command (or
 command/module name) and stderr separately.

 For the 7.0 this actually should not be applied completely but the API
 should be changed to fit the improved implementation/new API, so
 `run_command()` function should have `return 0` but a waring in
 documentation. Then perhaps `pipe_command()` and `feed_command()`
 functions should be marked as depreciated.

 Adding (not tested) draft of proposed changes.

-- 
Ticket URL: <http://trac.osgeo.org/grass/ticket/2326#comment:6>
GRASS GIS <http://grass.osgeo.org>



More information about the grass-dev mailing list