7

The argparse package does a great job when dealing with command line arguments. I'm wondering however if there is any way to ask argparse to check for file extension (e.g ".txt"). The idea would be to derived one the class related to argparse.FileType. I would be interested in any suggestion.

Keep in mind that I have more than 50 subcommands in my program all having there own CLI. Thus, I would be interest in deriving a class that could be imported in each of them more than adding some uggly tests in all my commands.

Thanks a lot.

# As an example one would be interested in turning this...
parser_grp.add_argument('-o', '--outputfile',
                        help="Output file.",
                        default=sys.stdout,
                        metavar="TXT",
                        type=argparse.FileType('w'))


# Into that...
from somewhere import FileTypeWithExtensionCheck 
parser_grp.add_argument('-o', '--outputfile',
                        help="Output file.",
                        default=sys.stdout,
                        metavar="TXT",
                        type=FileTypeWithExtensionCheck('w', '.[Tt][Xx][Tt]$'))
5
  • stackoverflow.com/questions/15203829/… This should help Commented Jan 19, 2018 at 17:53
  • Apparently the original argparse developer included FileType to help with small scripts that routinely take input/output files. But I think it's most valuable as a model for writing your own type class. The key thing is that type has to be a callable, whether that's a function or the __call__ of a class. Commented Jan 19, 2018 at 17:53
  • 1
    I would be angry at a program if it rejected my filename because it has the wrong extension. It should care about the file's contents instead. Commented Jan 19, 2018 at 17:54
  • One downside to following FileType too closely is that it opens the file or even creates a new one. In bigger scripts it is better to operate on a file using the with context. So consider a type that checks filename format, but doesn't actually the file. Commented Jan 19, 2018 at 17:58
  • In bugs.python.org/issue13824 I explore a FileContext class that can check for a file's existence or form, but returns an unopened file, one that can be used directly in a with context. Handling stdin/out was tricky, since you don't normally want to close those. Commented Jan 19, 2018 at 18:18

2 Answers 2

6

You could subclass the argparse.FileType() class, and override the __call__ method to do filename validation:

class FileTypeWithExtensionCheck(argparse.FileType):
    def __init__(self, mode='r', valid_extensions=None, **kwargs):
        super().__init__(mode, **kwargs)
        self.valid_extensions = valid_extensions

    def __call__(self, string):
        if self.valid_extensions:
            if not string.endswith(self.valid_extensions):
                raise argparse.ArgumentTypeError(
                    'Not a valid filename extension')
        return super().__call__(string)

You could also support a regex if you really want to, but using str.endswith() is a more common and simpler test.

This takes either a single string, or a tuple of strings specifying valid extensions:

parser_grp.add_argument(
    '-o', '--outputfile', help="Output file.",
    default=sys.stdout, metavar="TXT",
    type=argparse.FileTypeWithExtensionCheck('w', valid_extensions=('.txt', '.TXT', '.text'))
)

You need to handle this in the __call__ method because the FileType() instance is essentially treated like any other type= argument; as a callable, and you can indicate that the specific argument isn't suitable by raising the ArgumentTypeError exception.

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you @Matijn. This is exactly what I was looking for. Best.
5

My solution is to create an closure that does the extension checking:

import argparse

def ext_check(expected_extension, openner):
    def extension(filename):
        if not filename.lower().endswith(expected_extension):
            raise ValueError()
        return openner(filename)
    return extension

parser = argparse.ArgumentParser()
parser.add_argument('outfile', type=ext_check('.txt', argparse.FileType('w')))

# test out
args = parser.parse_args()
args.outfile.write('Hello, world\n')

Notes

  • ext_check basically is a wrapper for argparse.FileType
  • It takes an expected extension to check and an openner
  • For simplicity, the expected extension is in lower case, the filename will be converted to lower case prior to validation
  • openner in this case is an argparse.FileType('w') callable (most likely a function, but I don't care, as long as it is a callable).
  • ext_check returns a callable, which is a function called extension. I name it this way, so that the error will come out as followed (note the word extension bellow, which is the name of the function):

    error: argument outfile: invalid extension value: 'foo.txt2'
    
  • Within the extension function, we check the file extension, if passed, we pass the file name to the openner.

What I like about this solution

  • Concise
  • Require almost no knowledge of how argparse.FileType works since it just act as a wrapper around it

What I don't like about it

  • Caller has to know about closure to understand how it works
  • I have no control over the error message. That is why I have to name my inner function extension to get a somewhat meaningful error message as seen above.

Other possible solutions

  • Create a custom action, see the documentation for argparse
  • Subclass argparse.FileType as Martijn Pieters has done

Each of these solutions has its own strong points and weaknesses

4 Comments

If the type callable returns a ValueError or TypeError, the error message is standardized. If it returns a ArgumentTypeError (as FileType does), its message is used. See the parser._get_value method for details.
I love it. Thank you @hpaulj
This is a nice workaround. However Matijn solution is closer to what I was looking for. Thank a lot for helping Hai vu.
To further elaborate, the Python docs have a nice example snippet about the type argument explaining what @hpaulj pointed. docs.python.org/3.7/library/argparse.html#type

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.