argumentparser close file argument - python

argumentparser can take file type argument and leave the file open directly, for example:
parser.add_argument('infile', nargs='?', type=argparse.FileType('r'))
args = parser.parse_args().__dict__
input = args['infile'].readlines()
do I need to close args['infile'] in my program? Would argumentparser close it for me? I didn't find anywhere mention this in the documentations.

NO, it does not close the filetype object.. see this
The problem here is that FileType may return stdin or stdout, so it can’t just always close the file object.
A great number of unclosed file handles may cause problem to some OSes, but that’s it. On the plus side, the fact that argparse accepts for its type argument any callable that can check and convert a string input is simple, clean and works.
Also look at this

Some digging in the source reveals that it doesn't close it for you. That makes sense, as it also has to open files for writing, and you probably wouldn't want it to close those.

I seem to have stumbled onto a clear and concise way to close argparse files:
parser = argparse.ArgumentParser(description='Hello!')
parser.add_argument('src_file',
type=argparse.FileType('r', encoding='Windows-1252'), help='Input .txt file')
parser.add_argument('dst_file',
type=argparse.FileType('w', encoding='Windows-1252'), help='Output .foo file')
args = parser.parse_args()
# do your thing...
args.src_file.close()
args.dst_file.close()
#close(args.src_file) don't do this... gives error
This is with Python 3.10.4; googling the PEPs for this 'feature', nothing was found.
So either this:
Has existed all along but nobody knew,
Was added at some point but not mentioned,
Or it doesn't actually close the file (and doesn't give any error.)*
*The Python debugger in Code/Win10 reports the <_io.TextIOWrapper>'s closed boolean does change to True from this .close(), so it seems to be working as expected.

Related

python script that takes command line arguments needs to be called from another python script

I completely understand that I should have written the script right the first time, but the fact is I have a script that generates a data file based upon two values passed to it from the command line- like this:
[sinux1~]: ./sim_gen.py 100 .3
I need to call this script from within another script, iterating over a range of values. I searched around and after navigating through all of the "you shouldn't," I tried :
exec(open("./sim_gen.py 100 .3").read())
And this doesn't seem to work.
Help?
Let's break this down into pieces:
exec(open("./sim_gen.py 100 .3").read())
This is equivalent to:
f = open("./sim_gen.py 100 .3")
contents = f.read()
exec(contents)
That open is the same open you use for, say, reading a text file or a CSV. You're asking for a file named "sim_gen.py 100 .3" in the current directory. Do you have one? Of course not. So the open fails.
The best solution is, as you already know, to rewrite sim_gen.py so that you can import it and call a function and pass the arguments to it.
Failing that, the cleanest answer is probably to just run the Python script as a subprocess:
import subprocess
import sys
subprocess.run([sys.executable, "./sim_gen.py", "100", ".3"])
Notice that this is effectively the same thing you're doing when you run the script from your shell, so if it was OK there, it's almost surely OK here.
If you really need to exec for some reason, you will need to do something really hacky, and temporarily change argv for that script's code:
import sys
_argv = sys.argv
try:
sys.argv = ["./sim_gen.py", "100", ".3"]
with open("./sim_gen.py 100 .3"):
exec(f.read())
finally:
sys.argv = _argv
Although really, unless the point of running this is to silently modify your own module's globals or the like, you (a) almost certainly don't really need exec, and (b) want to pass an explicit globals argument even if you do really need it.

Python - Execute program with parameters with file output

I am trying to use Python to run an executable (Windows 7) with parameters. I have been able to make the program run, but the amount of parameters I can use that will prove the Python script worked with parameters is limited. The best one is formatted like so:
-debugoutput debug.txt
I have tested this using a windows shortcut with an edited target and it works, it creates a debug output in the program directory.
Here is the code I am using:
import subprocess
args = [r"C:\Users\MyName\LevelEditor\LevelEditor.exe", "-debugoutput debug.txt"]
subprocess.call(args)
This does run the program, but the debug output is not created. I have tried putting an "r" in front of the parameter but this made no difference. I assume it is a simple formatting error but I can't find any examples to learn from that are doing the same thing.
UPDATE:
Thanks for the answers everyone, all the same, simple formatting error indeed.
In-code definition results in invocation of shell command line:
C:\Users\MyName\LevelEditor\LevelEditor.exe "-debugoutput debug.txt"
As you can see, by merging -debugoutput debug.txt to single list element, you explicitly stated that space between them shouldn't be parsed as command line argument separator.
To achieve expected behavior put file name string as separate element to argument list.
[r"C:\Users\MyName\LevelEditor\LevelEditor.exe", "-debugoutput", "debug.txt"]
As far as I know you need to split the arguments by the space, so your args would look like:
args = [r"C:\Users\MyName\LevelEditor\LevelEditor.exe", "-debugoutput", "debug.txt"]
Does that work?
I do not know if it works, but
import subprocess
args = [r"C:\Users\MyName\LevelEditor\LevelEditor.exe", "-debugoutput", "debug.txt"]
subprocess.run(args)
Following the docs

Allow argument/option to override positional argument

I am trying to make exclusion in my argparse parser. Basically what I want is to avoid --all option and filenames argument to be parsed (which I think succeeded).
But I want to create also another check where if I only pass python reader.py read --all, the filenames argument will get populated with all txt files in current directory.
So far I've come up with following code:
import argparse
import glob
parser = argparse.ArgumentParser()
subcommands = parser.add_subparsers(title='subcommands')
read_command = subcommands.add_parser('read')
read_command.add_argument('filenames', type=argparse.FileType(), nargs = '+')
read_command.add_argument('-a', '--all', action='store_true')
parsed = parser.parse_args()
if parsed.all and parsed.filenames:
raise SystemExit
if parsed.all:
parsed.filenames = glob.glob('*.txt')
print parsed
The problem is that if I try to run python reader.py read --all I get error error: too few arguments because of the filenames argument.
Is there a way to have this work like I want to without creating subcommand to read, for example python reader.py read all?
How can I access error messages in argparse? I'd like to have some default message that would say that filenames and --all can't be combined instead of SystemExit error.
Also I want to avoid using add_mutually_exclusive_group because this is just a snippet of my real parser where this approach wouldn't work (already checked in other SO topic).
I've heard about custom actions but it would really help to see example on it.
If filenames gets nargs="*", it should allow you to use --all alone. parsed.filenames will then be a [], which you can replace with the glob.
You could also test giving that argument a default derived from the glob - but see my caution regarding FileType.
Do you want the parser to open all the filenames you give it? Or would you rather open the files latter yourself (preferably in a with context). FileType opens the files (creating if necessary), and in the process checks their existence (which is nice), but leaves it up to you (or the program exit) to close them.
The documentation talks about issuing error messages yourself, and how to change them. parser.error('my message') with display the usage and message, and then exit.
if parsed.all and parsed.filenames:
parsed.error("Do you want to read ALL or just %s?"%parsed.filenames)
It is also possible trap SystemExit exceptions in a try/except clause.

How to open a file using argparse.FileType only if a condition is satisfied?

I am using python 3 argparse. I have multiple files passed as argparse.FileType which I use to write some data. I want to check some conditions and open those files only if they are met. However argparse opens them immediately, and they are created even if I exit with error code.
import argparse
from sys import exit
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--condition', action='store_true')
parser.add_argument('out1', type=argparse.FileType('w'))
parser.add_argument('out2', type=argparse.FileType('w'))
args = parser.parse_args()
if not args.condition:
print('ERROR: please use --condition')
exit(2)
args.out1.write('hello\n')
args.out2.write('world\n')
if __name__ == '__main__':
main()
If I run this example without passing the --condition argument, it will still create 2 new files. I don't want to create them in that case. Can I do that without passing filename and opening the files manually?
The simplest thing is to just accept the filenames as the default string type, and open the files later.
http://bugs.python.org/issue13824 (argparse.FileType opens a file and never closes it) implements a FileContext type, one that operates like FileType except that it returns a context that can be used later as:
with args.input() as f:
f.read()
etc
But doing filename checking while parsing, without actually opening or creating a file, is not trivial. And handling stdin/out which should not be closed adds a complication.
In that bug issue, Steven Bethard, the argparse developer, notes that FileType was intended for quick scripts, not for larger projects where proper opening and closing files matters.

Specifying default filenames with argparse, but not opening them on --help?

Let's say I have a script that does some work on a file. It takes this file's name on the command line, but if it's not provided, it defaults to a known filename (content.txt, say). With python's argparse, I use the following:
parser = argparse.ArgumentParser(description='my illustrative example')
parser.add_argument('--content', metavar='file',
default='content.txt', type=argparse.FileType('r'),
help='file to process (defaults to content.txt)')
args = parser.parse_args()
# do some work on args.content, which is a file-like object
This works great. The only problem is that if I run python myscript --help, I get an ArgumentError if the file isn't there (which I guess makes sense), and the help text is not shown. I'd rather it not try to open the file if the user just wants --help. Is there any way to do this? I know I could make the argument a string and take care of opening the file myself later (and I've been doing that), but it would be convenient to have argparse take care of it.
You could subclass argparse.FileType:
import argparse
import warnings
class ForgivingFileType(argparse.FileType):
def __call__(self, string):
try:
super(ForgivingFileType,self).__call__(string)
except IOError as err:
warnings.warn(err)
parser = argparse.ArgumentParser(description='my illustrative example')
parser.add_argument('--content', metavar='file',
default='content.txt', type=ForgivingFileType('r'),
help='file to process (defaults to content.txt)')
args = parser.parse_args()
This works without having to touch private methods like ArgumentParser._parse_known_args.
Looking at the argparse code, I see:
ArgumentParser.parse_args calls parse_known_args and makes sure that there isn't any pending argument to be parsed.
ArgumentParser.parse_known_args sets default values and calls ArgumentParser._parse_known_args
Hence, the workaround would be to use ArgumentParser._parse_known_args directly to detect -h and, after that, use ArgumentParser.parse_args as usual.
import sys, argparse
parser = argparse.ArgumentParser(description='my illustrative example', argument_default=argparse.SUPPRESS)
parser.add_argument('--content', metavar='file',
default='content.txt', type=argparse.FileType('r'),
help='file to process (defaults to content.txt)')
parser._parse_known_args(sys.argv[1:], argparse.Namespace())
args = parser.parse_args()
Note that ArgumentParser._parse_known_args needs a couple of parameters: the arguments from the command line and the namespace.
Of course, I wouldn't recommend this approach since it takes advantage of the internal argparse implementation and that might change in the future. However, I don't find it too messy, so you still might want to use it if you think maintenance risks pay off.
Use stdin as default:
parser.add_argument('file', default='-', nargs='?', type=argparse.FileType('r'))
Perhaps you could define your own type or action in the add_argument call that checks if the file exists, and returns a file handle if it does and None (or something else) otherwise.
This would require you to write some code of yourself as well though, but if the default value can not always be used you probably have to do some checking sooner or later. Like Manny D argues you might want to reconsider your default value.

Categories