python argparse limit arg values action API? - python

The following snipped calls parse_args() to identify a parameter --num and that tests that the value is acceptable in separate code after parsing.
import argparse
def cmd_line_opts(cmdline):
parser = argparse.ArgumentParser(description="blah blah",)
parser.add_argument('--num', default=None, type=int,)
return parser.parse_args(cmdline)
p = cmd_line_opts(['--num', '2'])
if p.num < 1:
raise ValueError('--num must be > 0')
p = cmd_line_opts(['--num', '0'])
if p.num < 1:
raise ValueError('--num must be > 0')
I am wondering if it would be possible to include the test and a suitable error message directly in the parsing. I suspect that this involves using the Action API but I can't figure out how to signal a failure when returning from the action.

You just need a custom type for the argument.
def positive_int(s):
try:
s = int(s)
if s <= 0:
raise ValueError()
except ValueError:
raise ArgumentTypeError("Not a valid positive integer: {}".format(s))
# ...
parser.add_argument("--num", type=positive_int, default=0)
The type argument is any callable that accepts a string and returns a value to use for the argument's value. It doesn't have to be an actual type (built-in or otherwise).

Full credit to #chepner for his answer, above. At least on my system you need qualify ArgumentTypeError. I put it in a comment but thought it would be helpful to someone who just wanted to copy and paste the code.
import argparse
def positive_int(s):
try:
s = int(s)
if s <= 0:
raise ValueError()
except ValueError:
raise argparse.ArgumentTypeError("Not a valid positive integer: {}".format(s))
def cmd_line_opts(cmdline):
parser = argparse.ArgumentParser(description="blah blah",)
parser.add_argument("--num", type=positive_int, default=0)
return parser.parse_args(cmdline)
p = cmd_line_opts(['--num', 'x'])

Related

How to check input arguments in a python script with CLI?

I'm writing a small script to learn Python. The script prints a chess tournament table for N players. It has a simple CLI with a single argument N. Now I'm trying the following approach:
import argparse
def parse_args(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Tournament tables")
parser.add_argument('N', help="number of players (2 at least)", type=int)
args = parser.parse_args(argv)
if args.N < 2:
parser.error("N must be 2 at least")
return args.N
def main(n: int) -> None:
print(F"Here will be the table for {n} players")
if __name__ == '__main__':
main(parse_args())
But this seems to have a flaw. The function main doesn't check n for invalid input (as it's the job of CLI parser). So if somebody calls main directly from another module (a tester for example), he may call it with lets say 0, and the program most likely crashes.
How should I properly handle this issue?
I'm considering several possible ways, but not sure what is the best.
Add a proper value checking and error handling to main. This option looks ugly to me, as it violates the DRY principle and forces main to double the job of CLI.
Just document that main must take only n >= 2, and its behaviour is unpredicted otherwise. Possibly to combine with adding an assertion check to main, like this:
assert n >= 2, "n must be 2 or more"
Perhaps such a function should not be external at all? So the whole chosen idiom is wrong and the script's entry point should be rewritten another way.
???
You could have main do all the checking aind raise ArgumentError if something is amiss. Then catch that exception and forward it to the parser for display. Something along these lines:
import argparse
def run_with_args(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Tournament tables")
parser.add_argument('N', help="number of players (2 at least)", type=int)
args = parser.parse_args(argv)
try:
main(args.N)
except argparse.ArgumentError as ex:
parser.error(str(ex))
def main(n: int) -> None:
if N < 2:
raise argparse.ArgumentError("N must be 2 at least")
print(F"Here will be the table for {n} players")
if __name__ == '__main__':
run_with_args()
If you don't want to expose argparse.ArgumentError to library users of main, you can also create a custom exception type instead of it.
A common way of running argparse when wanting to test functions/CLI is to have the main function take a the sys.argv list and then call parse_args from within main like so:
arg.py
import argparse
def parse_args(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Tournament tables", prog="prog")
parser.add_argument("N", help="number of players (2 at least)", type=int)
args = parser.parse_args(argv)
if args.N < 2:
parser.error("N must be 2 at least")
return args
def main(argv: list[str] | None = None) -> None:
args = parse_args(argv)
print(f"Here will be the table for {args.N} players")
if __name__ == "__main__":
main()
This way a test can call main with a hypothetical CLI:
test_main.py
import pytest
from arg import main
def test_main(capsys):
with pytest.raises(SystemExit):
main(["0"])
out, err = capsys.readouterr()
assert err.splitlines()[-1] == "prog: error: N must be 2 at least"
I’ve been using Pydantic liberally for enforcing data typing at runtime, within my code itself. Your N>=2 is easily enforced with a validator.
It’s a very robust, extremely widely used, library and very fast as it’s more of a data ingestion validator than a type checker.
And you could write the call as follows. How you call main is entirely up to you: direct call, argparse, click…
class ParamChecker(BaseModel):
n : int
#validator('n')
def n2(cls, v):
if v <2:
raise ValueError('must be 2+')
return v
def main(n: int) -> None:
params = ParamChecker(**locals())
Pydantic also gets you informative, if not really end user friendly, error messages.

Pass a keyword variable to a function as an argument

I'm doing a project that is requiring a lot of input validation. I currently have a function defined as follows:
def get_valid_float(inputMessage,errorMessage):
while True:
variableInput = input(inputMessage)
try:
variableInput = float(variableInput)
return variableInput
except ValueError:
print(errorMessage)
This function allows me to choose a custom message to prompt the user. It will then validate that the user input is indeed a float, and will print a custom error message in the event that it is not. It will loop until the user gives a valid input.
However, I would rather not create a function to validate each and every data type. It seems like it would be best to combine these into one get_valid_input() function, and pass a third argument allowing me to choose what data type I am attempting to verify. For example, get_valid_input(complex,inputMessage,errorMessage).
I am obviously unable to pass a keyword as an argument. This makes me think the only way to do this would to be to do something like this:
def get_valid_float(dataType,inputMessage,errorMessage):
if dataType == "float"
while True:
variableInput = input(inputMessage)
try:
variableInput = float(variableInput)
return variableInput
except ValueError:
print(errorMessage)
elif dataType == "integer"
while True:
variableInput = input(inputMessage)
try:
variableInput = int(variableInput)
return variableInput
except ValueError:
print(errorMessage)
And so on, with an elif for every data type. Surely there is an easier way to do this, that somehow allows me to execute the line variableInput = {dataType}(variableInput) to confirm that they input a value of data type "dataType". Any ideas?
Just pass as an argument the actual data type, rather than the name of the data type. E.g:
def get_valid_input(dataType, inputMessage, errorMessage):
while True:
value = input(inputMessage)
try:
value = dataType(value)
break
except ValueError:
print(errorMessage)
You would call it like this:
floatvalue = get_valid_input(float, "enter a float value: ", "that is an invalid float")
intvalue = get_valid_input(int, "enter an integer value: ", "that is an invalid integer")
I am obviously unable to pass a keyword as an argument.
Not sure why you're saying that, but you can! :)
Also no need for error message, just catch all Exceptions (Not recommended but since you are just printing out the error it seems fine here)
The message strings aren't really needed, try using the name of the dataType and the exception's message like this:
def get_valid_data(dataType):
while True:
variableInput = input(f"Put in data of type {dataType.__name__}: ")
try:
variableInput = dataType(variableInput)
return variableInput
except Exception as e:
print(e)
get_valid_data(int)
>>> Put in data of type int: "hi"
>>> invalid literal for int() with base 10: '"hi"'

Python, type checking and exceptions

i have to check if the content of some variable are of some specific type (it is a sanity check i want to implement). If each one of these is not of the specified type then the program has to abort.
I'm not experienced in python, i've a glance to the official guide on the web where it is stated that it is possible to define own exceptions by class implementation. I don't know if that would be hard or not, but since i don't have to do something particularly complicated is it possible to do that in a simple way?
Update
I've implemented the exception int exception in the following way:
class TypeCheckingException(Exception):
def __init__(self,type_in,type_exp):
self.type_in = type_in
self.type_exp = type_exp
The specific line of the implementation its use are:
try:
if isinstance(var1,str) == 0:
raise TypeCheckingException(type(var1),str)
if isinstance(var2,str) == 0:
raise TypeCheckingException(type(var2),str)
if isinstance(var3,int) == 0:
raise TypeCheckingException(type(var3),int)
if isinstance(var4,int) == 0:
raise TypeCheckingException(type(var4),int)
if isinstance(var5,str) == 0:
raise TypeCheckingException(type(var5),str)
if isinstance(var6,float) == 0:
raise TypeCheckingException(type(var6),float)
except TypeCheckingException, tce:
print "Type expected input " + str(tce.type_in) + " while type expected is " + str(tce.type_exp) + "."
In this case works, but the error output is quite verbose... i'd like to make it shorter...
Any clue?
You want to use isinstance(variable, type).
>>> a = 'str'
>>> isinstance(a, str)
True
You won't need to use exception handling with this route. You can check for instance type, then exit, if necessary.

Python Decorator recreating Locals()

Some of my functions use a "fail_silently" flag. It is used in the following way:
def test(a, b, c=1, fail_silently = False)
try:
return int(a) + c
except:
if fail_silently:
return None
raise
Therefore, if there is an error, we catch it and fail gracefully. The key here is that it can be toggled dynamically by whoever is calling it.
I am using this for a many different functions and class methods and thought to make it a decorator.
There are a few problems:
I want to be able name the flag "raise_exception" or "fail_silently" or "whatever"...
The flag may or may not have a default value
The flag may or may not be passed in (usually not)
It needs to work with class methods too
Thus my decorator needs to look for (assuming the flag is called "fail_silently")
the flag in the following locations in this order
**kwargs (the passed in function arguments), simple dictionary get on flag
*args - get the positional argument of the flag, then scan for index in args (which might not be there)
Get the default value of the flag from the function
The problem is the code is now getting really messy and has many points of failure.
def try_except_response(parameter = 'fail_silently'):
def real_decorator(func):
#functools.wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs) # the function itself,
except: # if it raises an Exception!
# check to see if the Flag is in passed in kwargs!
if kwargs.get(parameter)== True:
return None
elif kwargs.get(parameter) == False:
raise
else:
# Flag is not in kwargs, check to see if it is in args
function_args, vargs, kewords, defaults = inspect.getargspec(func) # get the index of argument of interest
try:
argument_index = function_args.index(parameter) # get the index of argument of interest
except ValueError:
raise ValueError('The inputted decorator for "fail_silently" was not found! The behavior will not work.')
if len(args) > argument_index: # check to see if it is inputted
keyword = args[argument_index] # get the value!!
if keyword == True:
return None
elif kwargs == False:
raise
else:
raise ValueError('The "fail_silently" flag did not return a value that we understand. Must be True or False')
else:
# not in args either! let's go get the default value.
# TODO MORE STUFF!!!
raise
raise
raise
return wrapper
What is the best way this can be cleaned up? Or alternatives? I am thinking about implementing a version of locals() that will create a dictionary of all parameters passed into the function and then check from there...

Putting custom error messages in Python

I want to create custom error messages for a function.
def tr( launch_speed , launch_angle_deg , num_samples ):
#Error displays
try:
launch_speed>0
except:
raise Exception("Launch speed has to be positive!")
try:
0<launch_angle_deg<90
except:
raise Exception("Launch angle has to be 0 to 90 degrees!")
try:
um_samples = int(input())
except:
raise Exception("Integer amount of samples!")
try:
num_samples >=2
except:
raise Exception("At least 2 samples!")
Essentially, what I want is to get an error message every time a wrong value has been written in the function variables, and I've tried creating these messages based on what I've gathered on the Internet, but it doesn't seem to work.
You can't use try: except: for everything; for example, launch_speed>0 will not raise an error for negative values. Instead, I think you want e.g.
if launch_speed < 0: # note spacing, and if not try
raise ValueError("Launch speed must be positive.") # note specific error
You should also test for and raise more specific errors (see "the evils of except"), e.g.:
try:
num_samples = int(raw_input()) # don't use input in 2.x
except ValueError: # note specific error
raise TypeError("Integer amount of samples!")
You can see the list of built-in errors in the documentation.
Why not go one step further and build your own exception types? There's a quick tutorial in the docs which could be used something like:
class Error(Exception):
"""Base class for exceptions defined in this module"""
pass
class LaunchError(Error):
"""Errors related to the launch"""
pass
class LaunchSpeedError(LaunchError):
"""Launch speed is wrong"""
pass
class LaunchAngleError(LaunchError):
"""Launch angle is wrong"""
pass
class SamplesError(Error):
"""Error relating to samples"""
pass
In this case the default functionality of Exception is fine, but you may be able to get finer granularity in what you catch by defining extra exceptions.
if launch_speed < 0:
raise LaunchSpeedError("Launch speed must be positive")
if 0 <= launch_angle < 90:
raise LaunchAngleError("Launch angle must be between 0 and 90")
um_samples = input()
try:
um_samples = int(um_samples)
except ValueError:
raise SampleError("Samples must be an integer, not {}".format(um_samples))
if um_samples < 2:
raise SampleError("Must include more than one sample, not {}".format(str(um_samples)))

Categories