0

I am trying to create a python CLI tool for monitoring a specific process at a given interval of time.

I am trying to customize the message that gets printed out when a ValueError is caught, while also trying to exit the program by using sys.exit(1), which can be found in the validate function within example.py. If I weren't to use sys.exit(1), the print command within the main function would've been executed.

Having that sorted out, I procceded with performing a unit test (using unittest), by using test_example.py, for the specific program, to check if a SystemExit is raised when a negative value is passed to the time argument.

As such, how could I make it so that the assertRaise for SystemExit will result as true?

I'm using python 3.10.4 and argparse 1.1 .

# example.py

import argparse, sys

def parse_args(args):
    parser = argparse.ArgumentParser() 
    parser.add_argument("-t", "--time", type=float, metavar=" ")
    return parser.parse_args(args)

def validate(data):
    try:
        if data.time < 0:            
            raise ValueError
    except ValueError:
        print(f"Time has a negative value: {data.time}. Please use a positive value")
        sys.exit(1)
    
def main():
    parsed_data = parse_args(sys.argv[1:])
    validate(parsed_data)    

    print(parsed_data.time)

if __name__ == '__main__':
    main()
# test_example.py

import unittest
from example import parse_args, validate

class TestExemplu(unittest.TestCase):    
    def test_negative_value(self):   
        with self.assertRaises(SystemExit) as cm:
            validate()

        the_exception = cm.exception
        self.assertEqual(the_exception.code, 1)


if __name__ == '__main__':
    unittest.main()

This is the error that I get:

test_negative_value (test_example.TestExemplu) ... ERROR

======================================================================
ERROR: test_negative_value (test_example.TestExemplu)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Users\tester\Downloads\cli_mon_tool_1\test_example.py", line 16, in test_negative_value
    validate()
TypeError: validate() missing 1 required positional argument: 'data'

----------------------------------------------------------------------
Ran 1 test in 0.002s

FAILED (errors=1)
jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
rd51
  • 252
  • 4
  • 11
  • Thank you for explaining it to me! How would I go about testing for SystemExit? Would it be with something like this: https://stackoverflow.com/a/13491726? – rd51 May 29 '22 at 16:40
  • I had tried this: https://pastebin.com/pTUPWUP3. But I get this error now in the console: https://pastebin.com/aUuKu4cX . I don't know how to make proper use of validate in that context, I am not even sure if I was supposed to do that. Sorry! – rd51 May 29 '22 at 16:54
  • Thanks! Does this look good enough for you? – rd51 May 29 '22 at 17:02
  • Sorry! I was just about to ask you about that. – rd51 May 29 '22 at 17:04
  • 1
    (Also FWIW I would use a custom type, e.g. `type=positive_float`, rather than validating the args after parsing - see e.g. https://stackoverflow.com/a/25470943/3001761.) – jonrsharpe May 29 '22 at 17:09
  • How does this look now to you? I'll give a read in a second to the link you just posted. – rd51 May 29 '22 at 17:17
  • 1
    That's a better example, but what is unclear from the message the failing test is giving you? The diagnostics seem pretty clear to me. – jonrsharpe May 29 '22 at 17:17
  • I don't know what to do next, I don't know how to deal with the data string. Should I use it like that validate('data')? – rd51 May 29 '22 at 17:21
  • 1
    _Read the error message._ Compare your current revision to earlier ones, which didn't give this error. – jonrsharpe May 29 '22 at 17:28
  • Thank you for editing! Also, I'm exhausted right now, so it's kind of hard to get what you're saying, especially since I'm new to all of this. – rd51 May 29 '22 at 17:32
  • 1
    It's a bit odd for `validate` to call `sys.exit` directly. Raise a `ValueError` instead, and let the caller decide if the appropriate response is to exit the program or not. – chepner May 29 '22 at 17:35
  • @chepner you mean something like this: https://pastebin.com/F3ERwtUn? I don't know how to go about the code that lets he caller decide if the appropriate response is to exit the program or no. I know the caller is validate(parsed_data). – rd51 May 29 '22 at 17:46
  • 1
    *`main`* is the caller; `validate` is the thing being called. `validate` doesn't know where `data` came from, so isn't in a position to decide if the program could try to get different, valid data. `main`, on the other hand, is the function that produced the value `parsed_data`; it *is* in a position to decide whether or not it's worth trying to call `parse_args` again to get valid data. (It helps that `main` is also written with it being the top-most called function in mind.) – chepner May 29 '22 at 18:05
  • 1
    As a general rule, code that makes decisions about exiting the program should be pushed upward as far as possible. (Yes, `main` *could* let the exception propagate and expect the top-level code to catch it instead of letting the interpreter exit with a traceback, but `main` is defined to act as the entry point to the program.) – chepner May 29 '22 at 18:07
  • @chepner Thank you for explaining, but I came up with a different solution, which was comfortable to me, since I don't know how to replicate yours! – rd51 May 31 '22 at 10:18

1 Answers1

0

I came up with this solution, according to these 2 links:

  1. Hide traceback unless a debug flag is set

  2. https://gist.github.com/maphew/e3a75c147cca98019cd8/7236687e4161b2c3c5eca0daec500a516cc21055


# example.py

import argparse, sys
debug = False

def parse_args(args):
    parser = argparse.ArgumentParser()
    parser.add_argument("-p", "--path", type=str, metavar=" ")
    parser.add_argument("-t", "--time", type=float, metavar=" ")
    return parser.parse_args(args)

def exceptionHandler(exception_type, exception, traceback, debug_hook=sys.excepthook):
    '''Print user friendly error messages normally, full traceback if DEBUG on.
       Adapted from http://stackoverflow.com/questions/27674602/hide-traceback-unless-a-debug-flag-is-set
    '''
    if debug:
        print('\n*** Error:')
        debug_hook(exception_type, exception, traceback)
    else:
        print("%s: %s" % (exception_type.__name__, exception))
sys.excepthook = exceptionHandler    

def validate(data):
    try:
        if data.time < 0:            
            raise ValueError
    except ValueError:        
        raise ValueError(f"Time has a negative value: {data.time}. Please use a positive value")
    try:
        if data.time == 0:            
            raise ValueError
    except ValueError as e:        
        raise ValueError(f"Time has a value of zero. Please use a positive value")    
    
def main():
    parsed_data = parse_args(sys.argv[1:])
    validate(parsed_data)

    print(parsed_data.path)
    print(parsed_data.time)

if __name__ == '__main__':
    main()

# test_example.py

import unittest
from example import parse_args, validate

class TestExemplu(unittest.TestCase):  
    
    def test_negative_value(self):
        parsed_data1 = parse_args(['-t', '-1'])       
        parsed_data2 = parse_args(['-t', '0'])       
        parsed_data3 = parse_args(['-t', '-1'])       
        self.assertRaises(ValueError, validate, parsed_data1)    
        self.assertRaises(ValueError, validate, parsed_data2)    
        try:    
            self.assertRaises(ValueError, validate, parsed_data2)
        except:
            pass     

if __name__ == '__main__':
    unittest.main()
rd51
  • 252
  • 4
  • 11