Custom terminal command with argparse and .profile - python

I'm trying to create a command line interface for the dft.ba URL shortener using python's argparse and a function in .profile which calls my python script. This is the business end of the code in my python script:
parser = argparse.ArgumentParser(description='Shortens URLs with dft.ba')
parser.add_argument('LongURL',help='Custom string at the end of the shortened URL')
parser.add_argument('--source','-s',help='Custom string at the end of the shortened URL')
parser.add_argument('-c','--copy', action="store_true", default=False, help='Copy the shortened URL to the clipboard')
parser.add_argument('-q','--quiet', action="store_true", default=False, help="Execute without printing")
args = parser.parse_args()
target_url=args.LongURL
source=args.source
print source
query_params={'auth':'ip','target':target_url,'format':'json','src':source}
shorten='http://dft.ba/api/shorten'
response=requests.get(shorten,params=query_params)
data=json.loads(response.content)
shortened_url=data['response']['URL']
print data
print args.quiet
print args.copy
if data['error']:
print 'Error:',data['errorinfo']['extended']
elif not args.copy:
if args.quiet:
print "Error: Can't execute quietly without copy flag"
print 'Link:',shortened_url
else:
if not args.quiet:
print 'Link:',shortened_url
print 'Link copied to clipboard'
setClipboardData(shortened_url)
and then in .profile I have this:
dftba(){
cd ~/documents/scripts
python dftba.py "$1"
}
running dftba SomeURL will spit a shortened URL back at me, but none of the options work.When I try to use -s SomeSource before the LongURL, it gives error: argument --source/-s: expected one argument, when used afterwards it does nothing and when omitted gives error: too few arguments. -c and -q give error: too few arguments for some reason. The copy to clipboard function I'm using works perfectly fine if I force copying, however.
I'm very much feeling my way through this, so I'm sorry if I've made some glaringly obvious mistake. I get the feeling the problem's in my bash script, I just don't know where.
Any help would be greatly appreciated. Thank you.

Lets just focus on what the parser does
parser = argparse.ArgumentParser(description='Shortens URLs with dft.ba')
parser.add_argument('LongURL',help='Custom string at the end of the shortened URL')
parser.add_argument('--source','-s',help='Custom string at the end of the shortened URL')
parser.add_argument('-c','--copy', action="store_true", default=False, help='Copy the shortened URL to the clipboard')
parser.add_argument('-q','--quiet', action="store_true", default=False, help="Execute without printing")
args = parser.parse_args()
print args # add to debug the `argparse` behavior
LongURL is a positional argument that is always required. If missing you'll get the 'too few arguments' error message.
source is optional, but when provided must include an argument. If not given args.source is None. As written the source argument must be given in ADDITION to the LongURL one.
Both args.copy and args.quiet are booleans; default value is False; and True if given. (the default=False parameter is not needed.)
I haven't tried to work through the logic using copy and quiet. That won't come into play if there are problems earlier with LongURL and source.
Compare these samples:
In [38]: parser.parse_args('one'.split())
Out[38]: Namespace(LongURL='one', copy=False, quiet=False, source=None)
In [41]: parser.parse_args('-s one two -c -q'.split())
Out[41]: Namespace(LongURL='two', copy=True, quiet=True, source='one')
It may also help to look at what parse_args is parsing: sys.argv[1:] (if you have doubts about what you are getting from .profile).

Related

How do I suppress an argument when nothing is input on command line?

#!/usr/bin/env python3
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--selection', '-s')
parser.add_argument('--choice', '-c', default = argparse.SUPPRESS)
args = parser.parse_args()
def main(selection, choice):
print(selection)
print(choice)
if __name__=='__main__':
main(args.selection, args.choice)
The example provided is just to provide something simple and short that accurately articulates the actual problem I am facing in my project. My goal is to be able to ignore an argument within the code body when it is NOT typed into the terminal. I would like to be able to do this through passing the argument as a parameter for a function. I based my code off of searching 'suppress' in the following link: https://docs.python.org/3/library/argparse.html
When I run the code as is with the terminal input looking like so: python3 stackquestion.py -s cheese, I receive the following error on the line where the function is called:
AttributeError: 'Namespace' object has no attribute 'choice'
I've tried adding the following parameter into parser like so:
parser = argparse.ArgumentParser(argument_default=argparse.SUPPRESS)
I've also tried the above with
parser.add_argument('--choice', '-c')
But I get the same issue on the same line.
#Barmar answered this question in the comments. Using 'default = None' in parser.add_argument works fine; The code runs without any errors. I selected the anser from #BorrajaX because it's a simple solution to my problem.
According to the docs:
Providing default=argparse.SUPPRESS causes no attribute to be added if the command-line argument was not present:
But you're still assuming it will be there by using it in the call to main:
main(args.selection, args.choice)
A suppressed argument won't be there (i.e. there won't be an args.choice in the arguments) unless the caller specifically called your script adding --choice="something". If this doesn't happen, args.choice doesn't exist.
If you really want to use SUPPRESS, you're going to have to check whether the argument is in the args Namespace by doing if 'choice' in args: and operate accordingly.
Another option (probably more common) can be using a specific... thing (normally the value None, which is what argparse uses by default, anyway) to be used as a default, and if args.choice is None, then assume it hasn't been provided by the user.
Maybe you could look at this the other way around: You want to ensure selection is provided and leave choice as optional?
You can try to set up the arguments like this:
parser = argparse.ArgumentParser()
parser.add_argument('--selection', '-s', required=True)
parser.add_argument('--choice', '-c')
args = parser.parse_args()
if __name__ == '__main__':
if args.choice is None:
print("No choice provided")
else:
print(f"Oh, the user provided choice and it's: {args.choice}")
print(f"And selection HAS TO BE THERE, right? {args.selection}")

My argparse const=open() option always creates empty files?

I am a student and for practice am creating a little program that converts .fastq to .fasta files (so it deletes some lines, basically).
I am trying to implement the typical user input of an input file and an output file with the argparse library. For the output, I am trying to have three scenarios:
user puts -o outputfilename.fasta to create an outfile with custom name
user puts no argument, then it prints output in stdout
user puts -o with no followup, then it should create a file by itself with the name from input .fasta.
#!/usr/bin/python3
import argparse
import re
import sys
c=1
parser = argparse.ArgumentParser()
parser.add_argument("--input", "-i", required=True, dest="inputfile", type=argparse.FileType("r"))
parser.add_argument("--output", "-o", dest="outfilename", type=argparse.FileType("w"), nargs="?", default=sys.stdout, const=open('{}.fasta'.format(sys.argv[2]), "w" ))
args = parser.parse_args()
for line in args.inputfile:
if c==1:
line=re.sub ("[#]", ">", line)
args.outfilename.write (line)
c=c+1
elif c==2:
args.outfilename.write (line)
c=c+1
elif c==3:
c=c+1
else:
c=1
I am struggling with the third option, because the way my code is now, it always creates the extra file, but empty. So basically, it always runs my const= option, even though according to the manual, it shouldn't.
(just to be clear: I type -o outfilename.fasta and it produces the file plus an empty one from the input name. I type no argument and it prints it in my commandline and produces the empty inputname file. I type -o and it produces the inputfilename.fasta file with the correct lines in it)
nargs='?'. One argument will be consumed from the command line if possible, and produced as a single item. If no command-line argument is present, the value from default will be produced. Note that for optional arguments, there is an additional case - the option string is present but not followed by a command-line argument. In this case the value from const will be produced.
Because I thought the open command might be problematic, I tried it with
parser.add_argument("--output", "-o", dest="outfilename", type=argparse.FileType("w"), nargs="?", default=sys.stdout, const=argparse.FileType('{}.fasta'.format(sys.argv[2]), "w" ))
(I just wanted another way to write a file without using open)
and weirdly enough it only gave me this error message:
Traceback (most recent call last):
File "./fastqtofastaEXPANDED.py", line 19, in
args.outfilename.write (line)
AttributeError: 'FileType' object has no attribute 'write'
when I used -o argument. So that would tell me the opposite, that it does indeed only use the const option when I type -o, and not in the other cases (since the other ones worked fine, without extra files and without error messages).
I am confused as to why with the open parameter it seems to use const all the time....
I feel like a solution to my problem might be in the action classes, but I couldn't wrap my head around that yet. I would be no problem if the const just worked the way the manual says :D or is it error in the open, after all?
Thanks for your help!
EDIT: Since the const= probably won't work the way I wanted, I've created this work-around.
Basically just said that if the value is None, it will open a new file with name from the first input, minus suffix, plus new suffix.
If someone has a better solution, I am still open to change it :)
parser.add_argument("--output", "-o", dest="outfilename", type=argparse.FileType("w"), nargs="?", default=sys.stdout)
args = parser.parse_args()
if args.outfilename==None:
i=sys.argv[2][:sys.argv[2].rfind(".")]
args.outfilename=open("{}.fasta".format(i), "w")
#then all the line reading jazz...
if args.outfilename==None:
args.outfilename.close()
#to close the file, if it was used.
With this script:
import argparse, sys
# testing the use of `sys.argv` to create a filename (so-so idea)
if sys.argv[1:]:
constname = f'foobar{sys.argv[1]}.txt'
else:
constname = 'foobar1.txt'
parser = argparse.ArgumentParser()
a2 = parser.add_argument('number', type=int)
a1 = parser.add_argument('-o','--output', nargs='?', default='foobar0.txt',
const=constname, type=argparse.FileType('w'))
args = parser.parse_args()
print(args)
Sample runs:
1253:~/mypy$ rm foobar*
1253:~/mypy$ python3 stack63357111.py
usage: stack63357111.py [-h] [-o [OUTPUT]] number
stack63357111.py: error: the following arguments are required: number
1253:~/mypy$ ls foobar*
foobar0.txt
Even though argparse issues an error and quits, it creates the default file. That's because the output default is processed before the required check. Trying to create a const value based on some number in sys.argv is clumsy, and error prone.
1253:~/mypy$ python3 stack63357111.py 2
Namespace(number=2, output=<_io.TextIOWrapper name='foobar0.txt' mode='w' encoding='UTF-8'>)
1254:~/mypy$ ls foobar*
foobar0.txt
argparse creates the default and leaves it open for you to use.
1254:~/mypy$ python3 stack63357111.py 2 -o
Namespace(number=2, output=<_io.TextIOWrapper name='foobar2.txt' mode='w' encoding='UTF-8'>)
1254:~/mypy$ ls foobar*
foobar0.txt foobar2.txt
argparse creates the const based on that number positional
1254:~/mypy$ python3 stack63357111.py 2 -o foobar3.txt
Namespace(number=2, output=<_io.TextIOWrapper name='foobar3.txt' mode='w' encoding='UTF-8'>)
1254:~/mypy$ ls foobar*
foobar0.txt foobar2.txt foobar3.txt
Making a file with the user provided name.
Overall I think it's better to keep the use of FileType simple, and handle special cases after parsing. There's no virtue in doing everything in the parser. Its primary job is to determine what the user wants; your own code does the execution.

Argparse and mutually exclusive command line arguments

I have created a pastebin terminal client in python. It can take some command line arguments, like -o to open a file, -n to set a paste name etc. It also has option -l which lists the pastes and allows you to delete or view pastes. The problem is that I don't know how to do it in a nice way (using argparse) - it should not allow to use -l with any other options.
I added a simple logic:
if args.name:
if args.list:
print('The -l should be used alone. Check "pb -h" for help.')
sys.exit()
Can it be done using just argparse?
I know about mutually exclusive groups, I even have one (to set paste privacy) but I haven't figured this one yet.
Full code is available here: https://github.com/lkorba/pbstc/blob/master/pb
I don't think you can use argparse to achieve your goal in "a nice way" as you say.
I see 2 options here:
1) The simpler solution as I get it would be to just check your arguments after parsing them. Nothing fancy just:
args = parser.parse_args()
if args.list is not None:
if not (args.name is None and args.open is None and
args.public is None and args.format is None and args.exp is None):
parser.error('Cannot use list with name, open, public, format or exp argument')
2) On the other hand you could revise a bit your program and use
subparsers like:
subparsers = parser.add_subparsers(title="commands", dest="command")
parser_a = subparsers.add_parser('list', help='list help')
parser_b = subparsers.add_parser('action', help='Any action here')
parser_b.add_argument('-f', action="store", help='Choose paste format/syntax: text=None, '
'mysql=MYSQL, perl=Perl, python=Python etc...')
parser_b.add_argument('-n', '--name', action="store")
parser_b.add_argument('-o', '--open', action="store", help='Open file')
...
args = parser.parse_args()
if args.command == 'list':
...
elif args.command == 'action':
...
So, for example if you pass list -n='Name' as arguments in the latter case you will get an error:
usage: subparser_example.py [-h] {list,action} ...
subparser_example.py: error: unrecognized arguments: -n='Name'
Of course you also get (as overhead) one extra parameter action here...

error: unrecognized arguments (pycharm)

I am trying to run code using user's args as:
parser = argparse.ArgumentParser()
args = parser.parse_args()
parser = argparse.ArgumentParser(description='Script for running daily batch jobs for T3000 Project')
parser.add_argument("from_date", help='date in string yyyy-mm-dd', default='2017-10-1')
parser.add_argument("to_date", help='date in string yyyy-mm-dd', default='2017-12-31')
args = parser.parse_args()
main(
from_date=args.from_date,
to_date=args.to_date
)
While passing the arguments, I am following the path in Pycharm as: Run->Edit Configurations->Script Parameters: "2017-10-31" "2017-11-1"
I am getting error:
driver.py: error: unrecognized arguments: 2017-10-31 2017-11-1
Process finished with exit code 2
I have seen the link, which seems similar to my problem, but given solution didn't work for me. I am missing something I guess. Help will be appreciated.
Your first argument parser:
parser = argparse.ArgumentParser()
args = parser.parse_args()
is expecting no arguments, but you have passed in two. That is where the complaint is coming from. The solution is simply to remove those two lines - I don't know why you have them there in the first place.
I dont understand the comment by #lxop, I had the same error but I require both these lines. My code is the same apart from variables as the code used in the original question.
The solution for me is to set up my parameter in Pycharm as follows, quotations around the parameter are optional. I get the error when -o is missing.
-o filename.csv
My argparse setup line is
parser.add_argument("-o", "--outfile",
help="Enter the name of a .csv file to contain output or default of radarOutTemp.csv will be used",
default="radarOutTemp.csv")

Get version string from argparse

I'm trying to get back the version string I defined in argparse for use in logging.
I'm using a typical setup along the lines of:
__version__ = "0.1"
parser = argparse.ArgumentParser()
parser.add_argument('--version', '-V', action='version', version="%(prog)s " + __version__)
args = parser.parse_args()
When I print parser.version() or parser.print_version() or parser.format_version() I get None. One solution is to call parser.parse_args(['-V']) but that also terminates the execution of the program. I know I can just re-create the string and pass it to the logger, but I thought there must be a way to get this from argparse. I'm using python v2.7.5 and argparse v1.1.
There is no public API to get that information. The parser.version attribute is deprecated. You'd have to find the argparse._VersionAction object and get it from there:
version_action = next((action for action in parser._get_optional_actions()
if isinstance(action, argparse._VersionAction)), None)
print version_action.version if version_action else 'unknown'
This uses private methods that are subject to change, and all you get is the exact same string you gave to argparse:
>>> import argparse
>>> parser = argparse.ArgumentParser()
>>> parser.add_argument('--version', '-V', action='version', version="%(prog)s " + __version__)
_VersionAction(option_strings=['--version', '-V'], dest='version', nargs=0, const=None, default='==SUPPRESS==', type=None, choices=None, help="show program's version number and exit", metavar=None)
>>> version_action = next((action for action in parser._get_optional_actions() if isinstance(action, argparse._VersionAction)), None)
>>> print version_action.version if version_action else 'unknown'
%(prog)s 0.1
Much easier just to store that value somewhere else too, then pass it to argparse and use the value directly.
format_version (and print_version which uses format_version) displays the parser.version attribute, which you can set with the version parameter. But as Martijn wrote, that approach to showing a version is depricated. You'll get a warning message.
That action='version' argument takes a different route. It has its own version parameter (but using parser.version as a backup). When triggered by the -v argument string, it displays that version info and calls system exit.
You could still call -v, and avoid the system exit with a try block:
try:
p.parse_args(['-V'])
except SystemExit:
pass
That's handy for testing, but not something you want in production. Plus it traps other exits like help and errors.
As with any argument, you can save a link to the Action, and display, use, or even modify its attributes.
version_action = parser.add_argument('--version', '-V', action='version',
version="%(prog)s " + __version__)
print version_action
print version_action.version
assert isinstance(version_action, argparse._VersionAction)
Functionally this is the same as Martijn's search of the parser._get_optional_actions(), but simpler.
You could replicate the action of the _VersionAction.__call__, without the system exit, with:
def foo(action, parser):
formatter = parser._get_formatter()
formatter.add_text(action.version)
return formatter.format_help()
foo(version_action, parser)
Though in this case all it does is fill in the %(prog)s string.
http://bugs.python.org/issue9399 'Provide a 'print' action for argparse', discusses adding a 'print' or 'write' action type. It would behave like the 'version' one, but without the system exit, and possibly more control over formatting and print destination.
No, there must not be a way to get it from argparse. Why would there be? You tell argparse what the version number is, not the other way around.
And you don't need to "recreate" the string. Just create it once, and then pass it into argparse.
I'm sure it's available somehow, from some attribute on argparse, but getting it from there really makes no sense, and requires you to use argparse internals, which may change in the future.

Categories

Resources