Package: pathtype
pathtype: Validate paths in command line argumentsThepathtypePython package makes it simple to validate paths in command line (CLI)
arguments. It's made to be used with theargparseargument parser. It can validate
the existence of the file, its permissions, its file name, file extension, etc. Withpathtype, you keep path arguments validation inside the command line parsing logic,
away from your core application code.Use it as thetypeargument inparser.add_argument()to automatically have a CLI
path argument validated and returned as apathlib.Pathinstance.It works with Python 3.7+, both with Posix and Windows paths.Exampleimportargparseimportpathtypeif__name__=="__main__":parser=argparse.ArgumentParser()# We use `pathtype.Path` as the `type` argument to validate that the --image# argument is a readable image file (checks the file extension).parser.add_argument("--image",required=True,help="Image file to open (PNG, GIF or JPEG supported)",type=pathtype.Path(readable=True,name_matches_re=r"\.(png|jpe?g|gif)$"))# Path validations are done automatically by calling the next line, no need to# add code to validate that the path can be read and that it has the correct# extension.args=parser.parse_args()# args.image is an instance of pathlib.Path. And since using `readable` implies# `exists`, we know the file already exists and is readable by the current user.print(args.image.exists())# TrueInstallationpathtyperequires Python 3.7+.Install with pip:pipinstallpathtypeUsageUsingpathtype.Pathwithout any arguments simply converts the CLI argument to apathlib.Pathinstance:parser.add_argument("my_arg",type=pathtype.Path())args=parser.parse_args()print(type(args.my_arg))# >>> <class 'pathlib.PosixPath'>But you will generally also want to run some validations on the properties of the path.Predefined validations (basic usage)Multiple validations are available to have the path validated during CLI arguments
parsing. If a validation fails, argument parsing will fail in the usual manner. If
it succeeds, the argument will be converted to apathlib.Pathinstance.To validate that...use ...the path points to an existing file or directorypathtype.Path(exists=True)the path does NOT point to an existing file or directorypathtype.Path(not_exists=True)the path's parent directory existspathtype.Path(parent_exists=True)the file can be created (*)pathtype.Path(creatable=True)the file can be created or, if it already exists, it's writable (*)pathtype.Path(writable_or_creatable=True)the current user has some permissions on the file or directory (*)pathtype.Path(readable=True, writable=True, executable=True)thefile name(the last part of the path) matches a regular expressionpathtype.Path(name_matches_re=r"\.jpe?g$")thefile namematches a glob patternpathtype.Path(name_matches_glob="*.pkl")thefull(absolute and normalized) path matches a regular expressionpathtype.Path(path_matches_re="/home/.+/logs/?$")thefullpath matches a glob patternpathtype.Path(path_matches_glob="/home/*/*.pkl")(*) All permission related validations use the current user's permission. For example,
thecreatablevalidation validates that the user running your code has permissions to
create the file. Ignored on Windows.Combining validationsYou can combine multiple validations together.ExampleValidate that the path is a text file (*.txt) that doesn't exist yet, but that the
current user has permissions to create the file (implies that the parent directory
exists):parser.add_argument("--file",type=pathtype.Path(not_exists=True,creatable=True,name_matches_glob="*.txt"))args=parser.parse_args(["--file","path/to/my_file.txt"])Custom validation (advanced usage)You can also create your own custom validations (or "validators") and use them alone,
or in combination with the predefined validations.Making a custom validatorA custom validator is a callable object (generally a function) that has the
following signature:defvalidator(path:pathlib.Path,arg:str)->NoneThe validator must accept two arguments,pathandarg, that are two views of the
original CLI argument. If the original CLI argument was"../path/to/file", thenpath = pathlib.Path("../path/to/file")andarg = "../path/to/file".If the validator considers that its validation failed, it must raise one of the
following exception:argparse.ArgumentTypeErrorTypeErrorValueErrorRaising any other type of error won't be nicely handled byargparse.If its validation passes, it must end without returning anything.Using a custom validatorYou use the validator by passing it to thevalidatorparameter ofpathtype.Path().You can also pass an iterable (ex: a list) of validators, and they will be executed
sequentially.ExampleThe next example creates two (strange) custom validators: one that validates that the
file name contains the letter "a", the other validates that the file name doesn't
contain the letter "b". The command line argument--path-1uses only the first
validator, the command line argument--path-2uses both.defmust_have_a(path:pathlib.Path,arg:str):"""Custom validator that fails if the file name doesn't contain the letter 'a'."""if"a"notinpath.name:raiseargparse.ArgumentTypeError('The file name must have the letter "a"')defmust_not_have_b(path:pathlib.Path,arg:str):"""Custom validator that fails if the file name contains the letter 'b'."""if"b"inpath.name:raiseargparse.ArgumentTypeError('The file name must NOT have the letter "b"')parser=argparse.ArgumentParser()parser.add_argument("--path-1",type=pathtype.Path(validator=must_have_a))parser.add_argument("--path-2",type=pathtype.Path(validator=[must_have_a,must_not_have_b]))Using predefined validations with a custom validatorYou can still use any of the predefined validations (as presented in the "basic
usage" section) when using a custom validator.ExampleThe following would validate the existence of the file and run a custom validator.parser.add_argument(...type=pathtype.Path(validator=must_not_have_b,exists=True))Warning:Validators invalidatorare always runafterany of the predefined
validations. So in the previous example, the existence of the file is validatedfirstand only then the custom validator is executed.If you need to change the order, you would have to removeexists=Trueand instead
add an "existence" validator to your list of custom validators, in the order you wish.But you don't need to recreate validators for any of the predefined validations. They
are all available in thepathtype.validationmodule. Just instantiate a class and
use it like a custom validator.ExampleThe following changes the order of validation of the previous example: first the
custom validator is executed before validating the existence. The latter validator is
simply an instance ofpathtype.validation.Exists.frompathtype.validationimportExistsexist_validator=Exists()parser.add_argument(...type=pathtype.Path(validator=[must_not_have_b,exist_validator]))Logical combination of validatorsThe classespathtype.validation.Anyandpathtype.validation.Allallow you to create
validators that are logical combinations of other validators (i.e.ORorANDexpressions).Any: an instance of this class, initialized with a sequence of validators, is a
validator that will pass ifanyof its validators passes, and fail if they all
fail. Equivalent to anORexpression.All: Similarly, an instance of this class is a validator that will pass ifallof its validators pass, and fails ifanyfails. Equivalent to anANDexpression.Those two classes can be used to create complex validation trees.ExampleWe create a validator that validates that the file name contains "a"ORthat it
doesn't contain "b":frompathtype.validationimportAnyor_validator=Any(must_have_a,must_not_have_b)parser.add_argument(...type=pathtype.Path(validator=or_validator))Complete custom validator exampleWe want a custom validator that validates that the path is inside the current user's
home directory:importos.pathimportpathlibimportargparseimportpathtypedefis_inside_home_dir(path:pathlib.Path,arg:str):"""Validate that the path is inside the current user's home directory."""expanded_path=os.path.expanduser(path)resolved_path=pathlib.Path(os.path.abspath(expanded_path))user_dir=pathlib.Path.home()# We check that `resolved_path` starts with the same directories as `user_dir`is_child=resolved_path.parts[:len(user_dir.parts)]==user_dir.partsifnotis_child:raiseargparse.ArgumentTypeError(f"path ({resolved_path}) is not in the user's home directory ({user_dir})")if__name__=="__main__":parser=argparse.ArgumentParser()parser.add_argument("--path",type=pathtype.Path(validator=is_inside_home_dir))# The following line won't raise any errorargs=parser.parse_args(["--path","~/valid/path"])print(repr(args.path))# PosixPath('~/valid/path')# The following line will fail since the path is not inside the user's directoryargs=parser.parse_args(["--path","/at-root"])# Fails with this message:#   usage: example.py [-h] [--path PATH]#   example.py: error: argument --path: path (/at-root) is not in the user's home directory (/home/user)NotesAll paths instances are actuallyconcrete paths(i.e. created withpathlib.Path()),
and not pure paths (i.e.pathlib.PurePath()). This means that if ran on Windows,
the path argument will be converted to an instance ofpathlib.WindowsPath, and on
other systems it'll be converted to an instance ofpathlib.PosixPath. Behavior
may change on different OS's, so it's best not to parse argument across OS's.Validations are run once, during argument parsing. Always remember that, by the
time you actually use the path, some properties of the file may have changed. For
example, let's say you usepathtype.Path(exists=True). Although the file may
exist at the time of argument parsing, another process may delete the file by the
time you actually want to access it. So only use this package as a user-friendly
"first check".