Skip to content

Instantly share code, notes, and snippets.

@tomschr
Last active April 21, 2024 08:13
Show Gist options
  • Save tomschr/10837c2f7286b13d3ebc59e40d943188 to your computer and use it in GitHub Desktop.
Save tomschr/10837c2f7286b13d3ebc59e40d943188 to your computer and use it in GitHub Desktop.
Template for an example CLI program with argparse, docopts and logging
#!/usr/bin/env python3
import argparse
import logging
from logging.config import dictConfig
import sys
__version__ = "0.2.0"
__author__ = "Tux Penguin <[email protected]>"
#: The logger name; can also set to "__name__"
LOGGERNAME = "examplelog"
#: The dictionary, passed to :class:`logging.config.dictConfig`,
#: is used to setup your logging formatters, handlers, and loggers
#: For details, see https://docs.python.org/3.4/library/logging.config.html#configuration-dictionary-schema
DEFAULT_LOGGING_DICT = {
'version': 1,
'disable_existing_loggers': True,
'formatters': {
'standard': {'format': '[%(levelname)s] %(funcName)s: %(message)s'},
'file': {'format': '[%(levelname)s] %(asctime)s (%(funcName)s): %(message)s',
#: Depending on your wanted precision, disable this line
'datefmt': '%Y-%m-%d %H:%M:%S',
},
},
'handlers': {
'console': {
'level': 'NOTSET', # will be set later
'formatter': 'standard',
'class': 'logging.StreamHandler',
},
'fh': {
'level': 'DEBUG', # we want all in the log file
# Change the formatting here, if you want a different output in your log file
'formatter': 'file',
'class': 'logging.FileHandler',
'filename': '/tmp/log.txt',
'mode': 'w', # use "a" if you want to append log output or remove this lien
},
},
'loggers': {
LOGGERNAME: {
'handlers': ['console', 'fh'], # Remove "fh" if you don't want log files
'level': 'INFO',
# 'propagate': True
},
# Set the root logger's log level:
'': {
'level': 'NOTSET',
}
}
}
#: Map verbosity level (int) to log level
LOGLEVELS = {None: logging.WARNING, # 0
0: logging.ERROR,
1: logging.WARNING,
2: logging.INFO,
3: logging.DEBUG,
}
#: Change root logger level from WARNING (default) to NOTSET
#: in order for all messages to be delegated.
logging.getLogger().setLevel(logging.NOTSET)
#: Instantiate our logger
log = logging.getLogger(LOGGERNAME)
def parsecli(cliargs=None) -> argparse.Namespace:
"""Parse CLI with :class:`argparse.ArgumentParser` and return parsed result
:param cliargs: Arguments to parse or None (=use sys.argv)
:return: parsed CLI result
"""
parser = argparse.ArgumentParser(description=__doc__,
epilog="Version %s written by %s " % (__version__, __author__)
)
parser.add_argument('-v', '--verbose',
action='count',
default=0,
help="increase verbosity level")
parser.add_argument('--version',
action='version',
version='%(prog)s ' + __version__
)
parser.add_argument("DIR",
help="Searches the directory for files"
)
args = parser.parse_args(args=cliargs)
# Setup logging and the log level according to the "-v" option
dictConfig(DEFAULT_LOGGING_DICT)
# Setup logging and the log level according to the "-v" option
loglevel = LOGLEVELS.get(args.verbose, logging.DEBUG)
# Set console logger to the requested log level
for handler in log.handlers:
if handler.name == "console":
handler.setLevel(loglevel)
log.debug("CLI result: %s", args)
return args
def main(cliargs=None) -> int:
"""Entry point for the application script
:param cliargs: Arguments to parse or None (=use :class:`sys.argv`)
:return: error code
"""
try:
args = parsecli(cliargs)
# do some useful things here...
# If everything was good, return without error:
log.info("I'm an info message")
log.debug("I'm a debug message.")
log.warning("I'm a warning message.")
log.error("I'm an error message.")
log.fatal("I'm a really fatal massage!")
return 0
# List possible exceptions here and return error codes
except Exception as error: # FIXME: add a more specific exception here!
log.fatal(error)
# Use whatever return code is appropriate for your specific exception
return 10
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/env python3
"""
Does some fancy stuff
Usage:
{proc} [-h | --help]
{proc} [-v ...] INPUT OUTPUT
Options:
-h, --help Shows this help
-v Raise verbosity level
Arguments:
DIR Searches the directory for files
"""
__version__ = "0.1.0"
__author__ = "Tux Penguin <[email protected]>"
from docopt import docopt
import logging
from logging.config import dictConfig
from os.path import basename
import sys
#: The dictionary, passed to :class:`logging.config.dictConfig`,
#: is used to setup your logging formatters, handlers, and loggers
#: For details, see https://docs.python.org/3.4/library/logging.config.html#configuration-dictionary-schema
DEFAULT_LOGGING_DICT = {
'version': 1,
'disable_existing_loggers': True,
'formatters': {
'standard': {'format': '[%(levelname)s] %(funcName)s: %(message)s'},
},
'handlers': {
'console': {
'level': 'NOTSET', # will be set later
'formatter': 'standard',
'class': 'logging.StreamHandler',
},
'fh': {
'level': 'DEBUG', # we want all in the log file
# Change the formatting here, if you want a different output in your log file
'formatter': 'standard',
'class': 'logging.FileHandler',
'filename': '/tmp/log.txt',
'mode': 'w', # use "a" if you want to append log output
},
},
'loggers': {
__name__: {
'handlers': ['console', 'fh'], # Remove "fh" if you don't want log files
'level': 'INFO',
# 'propagate': True
},
# Set the root logger's log level:
'': {
'level': 'NOTSET',
}
}
}
#: Map verbosity level (int) to log level
LOGLEVELS = {None: logging.WARNING, # 0
0: logging.ERROR,
1: logging.WARNING,
2: logging.INFO,
3: logging.DEBUG,
}
#: Change root logger level from WARNING (default) to NOTSET
#: in order for all messages to be delegated.
logging.getLogger().setLevel(logging.NOTSET)
#: Instantiate our logger
log = logging.getLogger(__name__)
#: Use best practice from Hitchhiker's Guide
#: see https://docs.python-guide.org/writing/logging/#logging-in-a-library
log.addHandler(logging.NullHandler())
def parsecli(cliargs=None) -> dict:
"""Parse CLI arguments with docopt
:param list cliargs: List of commandline arguments or None (=use sys.argv)
:type clicargs: list | None
:return: result dictionary from docopt
"""
version = f"{__name__} {__version__}"
args = docopt(__doc__.format(proc=basename(sys.argv[0])),
argv=cliargs,
version=version)
# Setup logging and the log level according to the "-v" option
dictConfig(DEFAULT_LOGGING_DICT)
# Setup logging and the log level according to the "-v" option
loglevel = LOGLEVELS.get(args['-v'], logging.DEBUG)
# Set console logger to the requested log level
for handler in log.handlers:
if handler.name == "console":
handler.setLevel(loglevel)
log.debug("CLI result: %s", args)
return args
def main(cliargs=None) -> int:
"""Entry point for the application script
:param list cliargs: Arguments to parse or None (=use sys.argv)
:type clicargs: list | None
:return: error codes
"""
try:
args = parsecli(cliargs)
# do some useful things here...
# If everything was good, return without error:
log.info("I'm an info message")
log.debug("I'm a debug message.")
log.warning("I'm a warning message.")
log.error("I'm an error message.")
log.fatal("I'm a really fatal massage!")
return 0
# List possible exceptions here and turn exceptions into return codes
except Exception as error: # FIXME: add a more specific exception here!
log.fatal(error)
# Use whatever return code is appropriate for your specific exception
return 10
if __name__ == "__main__":
sys.exit(main())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment