Created
October 12, 2023 05:17
-
-
Save TheMatt2/d9056b575174a046d350299f741ab5cf to your computer and use it in GitHub Desktop.
Python PathType helper type for input validation in argparse for paths.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
PathType | |
A helper type for input validation in argparse for paths. | |
This provides a convienent way to check the paths type, existance, and | |
potentially use "-" to reference stdin or stdout. | |
This class is provided as an alternative to argparse.FileType(), which | |
does not open the path, only validates it and supports directories. | |
The way argparse.FileType() operates is problematic, as it can cause the | |
file descriptor to be left open if an error occurs. To quote the argparse | |
docs page: | |
https://docs.python.org/3/library/argparse.html | |
- If one argument uses FileType and then a subsequent argument fails, | |
an error is reported but the file is not automatically closed. | |
In this case, it would be better to wait until after the parser has | |
run and then use the with-statement to manage the files. | |
Example Usage: | |
parser = argparse.ArgumentParser() | |
# Add an argument that must be an existing file, but can also be specified as a dash ('-') in the command. | |
parser.add_argument('existing_file', type = PathType(type='file', dash_ok=True, exists = True)) | |
# Add an argument for a folder that must NOT exist. Note: folders can not be "dash_ok". | |
parser.add_argument('non_existant_folder', type = PathType(type='dir', exists = False)) | |
# Add an argument for EITHER a folder or a file, but can be dash_ok, and don't check if it exists. | |
parser.add_argument('maybe_existant_file_or_folder',type=PathType(type=('dir','file').__contains__,dash_ok=True,exists=None)) | |
""" | |
# Code from https://stackoverflow.com/questions/11415570/directory-path-types-with-argparse | |
# | |
# This was also suggested to be an official change to argparse. | |
# https://mail.python.org/pipermail/stdlib-sig/2015-July/000990.html | |
# Changed to using callable() function, instead of checking for __call__() method | |
# and changed err to ArgumentTypeError. | |
# Also fixed os.path.sympath() reference to os.path.islink(). | |
import os | |
import stat | |
from argparse import ArgumentTypeError | |
class PathType: | |
_stat_name_to_values = { | |
"file" : stat.S_IFREG, | |
"dir" : stat.S_IFDIR, | |
"symlink" : stat.S_IFLNK, | |
"socket" : stat.S_IFSOCK, | |
"block device" : stat.S_IFBLK, | |
"character device" : stat.S_IFCHR, | |
"fifo device" : stat.S_IFIFO, | |
"door" : stat.S_IFDOOR, | |
"event port" : stat.S_IFPORT, | |
"whiteout" : stat.S_IFWHT | |
} | |
_stat_value_to_names = {v: n for n, v in _stat_name_to_values.items()} | |
_stat_value_to_names[stat.S_IFDIR] = "directory" | |
def __init__(self, type, exists = True): | |
""" | |
exists: | |
True : a path that must exist | |
False: a path that must not exist, in a valid parent directory | |
None : do not check if path exists | |
type: 'file', 'dir', 'symlink', None, a list of these | |
If None, the type of file/directory/symlink is not checked. | |
""" | |
self._type = [] | |
if exists not in [True, False, None]: | |
raise TypeError("exists argument is not a bool or None") | |
# Make sure type is file, dir, sym, None, list, or callable. | |
if isinstance(type, (list, tuple)): | |
# Type is a list, make sure that it includes only "file", "dir" | |
# or "sym" | |
for t in type: | |
if t in self._stat_name_to_values: | |
# Convert name to value | |
self._type.append(self._stat_name_to_values[t]) | |
elif t in self._stat_value_to_names: | |
# If t is an integer, assume it is a stat file type | |
self._type.append(t) | |
else: | |
raise ValueError(f"type value {t!r} is not a recognized file type") | |
elif type in self._stat_name_to_values: | |
# Single type value | |
self._type.append(self._stat_name_to_values[type]) | |
elif type is not None: | |
# If it's None, ignore | |
raise ValueError(f"type {type!r} is not a recognized file type") | |
self._exists = exists | |
def __call__(self, string): | |
# If the file must exist. | |
if len(self._type) > 1: | |
name = "path" | |
else: | |
name = self._stat_value_to_names[self._type[0]] | |
mode = None | |
if self._exists is not None: | |
try: | |
mode = stat.S_IFMT(os.stat(string).st_mode) | |
except FileNotFoundError: | |
if self._exists: | |
raise ArgumentTypeError(f"{string!r} {name} does not exist") | |
else: | |
if not self._exists: | |
raise ArgumentTypeError(f"{string!r} {name} already exists") | |
if self._exists and mode: | |
# Do type check | |
if mode not in self._type: | |
raise ArgumentTypeError(f"{string!r} is not a {name}") | |
if not mode: | |
# If the file did exist, not point in checking parent | |
p = os.path.dirname(string) or os.path.curdir | |
if not os.path.exists(p): | |
raise ArgumentTypeError(f"parent directory {p!r} does not exist") | |
elif not os.path.isdir(p): | |
raise ArgumentTypeError(f"parent path is {p!r} not a directory: %r") | |
return string |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment