Search code examples
pythonclassooppyinstallerargparse

argparse validation in a python class


I'm trying an OOP approach to my Python code which eventually will be converted to an .EXE file created with PyInstaller. The idea is to pass a series of arguments from the user input (n to a program that eventually will go something like (myprogram.exe -secureFolder C:/Users -thisisacsvfile.csv -countyCode 01069 -utmZone 15).

I can initialize a class definition and pass the arguments like:

import argparse
import sys

class myprogram():
    def __init__(self, secureFolder, inputCsvFile, countyCode, utmZone):
        self.secureFolder = secureFolder
        self.inputCsvFile = inputCsvFile
        self.countyCode = countyCode
        self.utmZone = utmZone
        
        
if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument("secureFolder", help = "A directory where the files are located", type=str)
    parser.add_argument("inputCsvFile",  help="A CSV file containing the results for a particular county (e.g. 36107.csv)", type=str)
    parser.add_argument("countyCode", help = "The FIPS county code", type = str)
    parser.add_argument("utmZone", help = "The UTM zone code for that specific county (e.g. 18)", type = int)

However, I need to validate every user argument and that's the part where I'm getting confused. In other words, I need to check if the secureFolder exists, if the inputCsvFile is indeed a CSV and contains some specific columns and other operations for the rest of the arguments. What I don't know exactly, where do I perform these operations? After the class definition? Before the OOP approach, I was doing something like:

# Check if all the arguments were passed
undefined_arguments = [attr for attr in vars(args) if getattr(args, attr) is None]
if undefined_arguments:
    print("The following arguments were not defined:", undefined_arguments)
else:
    print("All arguments were defined.")

# 1a. Check inputCsvFile
if args.inputCsvFile is None:
    sys.exit("Please select an input CSV file to process (e.g. inputCsvFile.../myfile.csv) ")
else:
    if not os.path.isfile(args.inputCsvFile):
        sys.exit (f"File {args.inputCsvFile} doesn't appear to exists...please check if the file exists or if you have privileges to access it")
    else:
        grid_file_csv = args.inputCsvFile 
        print (f"{args.inputCsvFile} found...")

# 1b. Check if inputCsvFile is a CSV:
if not args.inputCsvFile.endswith('.csv'):
    raise ValueError("Invalid input file. Expected a CSV file.")
    sys.exit('No propper CSV file has been passed...')

# 2. Check if the FIPS code
if args.countyCode is None:
   sys.exit("Please specify a valid county code (e.g. -countyCode3607)")
             
# Check the UTM area code
if args.utmzone is None:
   sys.exit("Please specify a valid UTM zone area (e.g. -utmZone 16): ")

if args.utmZone is not None:
    val = args.utmZone 
    if val < 1 and val > 20:
        raise Exception('UTM zone area should be between 1 and 20')
        sys.exit()

Solution

  • This is largely a question of style and preference. In my view, wherever possible, values needed to construct any class should be validated before the class is constructed - especially when it relies on user input.

    So, get the command line arguments, validate them, then construct your class. Something like this:

    import argparse
    from sys import stderr
    import os
    
    
    class myprogram():
        def __init__(self, secureFolder, inputCsvFile, countyCode, utmZone):
            self.secureFolder = secureFolder
            self.inputCsvFile = inputCsvFile
            self.countyCode = countyCode
            self.utmZone = utmZone
    
        def __str__(self):
            return f'{self.secureFolder=}, {self.inputCsvFile=}, {self.countyCode=}, {self.utmZone=}'
    
        @staticmethod
        def validate(ns):
            if not os.path.isdir(ns.secureFolder):
                print(f'{ns.secureFolder} is not a valid folder', file=stderr)
                return None
            try:
                with open(ns.inputCsvFile) as _:
                    ...
            except Exception as e:
                print(f'Unable to open {ns.inputCsvFile} due to {e}', file=stderr)
                return None
            if ns.countyCode is None:
                print(f'{ns.countyCode} is an invalid county code', file=stderr)
                return None
            if (z := ns.utmZone) is None or z < 1 or z > 60:
                print(f'{z} is not a valid utmZone', file=stderr)
                return None
            return myprogram(ns.secureFolder, ns.inputCsvFile, ns.countyCode, ns.utmZone)
    
    
    if __name__ == '__main__':
        parser = argparse.ArgumentParser()
        cli = [
            ('-secureFolder', 'A directory where the files are located', str),
            ('-inputCsvFile', 'A CSV file containing the results for a particular county (e.g., 36107.csv)', str),
            ('-countyCode', 'The FIPS county code', str),
            ('-utmZone', 'The UTM zone code for that specific county (e.g., 18)', int)
        ]
        for c, h, t in cli:
            parser.add_argument(c, help=h, type=t)
    
        args = parser.parse_args()
    
        if (mp := myprogram.validate(args)) is None:
            print('Unable to construct class instance')
        else:
            # at this point we have a valid myprogram class instance (mp)
            print(mp)