Skip to content

Instantly share code, notes, and snippets.

@TheMatt2
Created October 12, 2023 05:17
Show Gist options
  • Save TheMatt2/d9056b575174a046d350299f741ab5cf to your computer and use it in GitHub Desktop.
Save TheMatt2/d9056b575174a046d350299f741ab5cf to your computer and use it in GitHub Desktop.
Python PathType helper type for input validation in argparse for paths.
"""
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