"""
This module encapsulates all functionality necessary for running the todo_catalog operations.
Executing the fucntion is exposed to the command line via the ``get_todo`` call specifiied in
the ``[options.entry_points]`` section of ``setup.cfg``::
console_scripts =
get_todo = todo_catalog.catalog:run
"""
import os
import argparse
import logging
import re
import sys
import configparser
from todo_catalog import __version__
__author__ = "Ryan"
__copyright__ = "Ryan"
__license__ = "MIT"
_logger = logging.getLogger(__name__)
# ---- Python API ----
# The functions defined in this section can be imported by users in their
# Python scripts/interactive interpreter, e.g. via
# `from todo_catalog.catalog import get_config`,
# when using this Python module as a library.
[docs]def get_config(args):
"""Return configuration parameters
If both args and a ``config`` file are detected, the arg values will take priority
Args:
args (argparse.Namespace): An argparse Namespace of args passed at runtime
Raises:
Exception if config file provided but no DEFAULTS section is present
Returns:
dict: a dictionary of configuration parameters
"""
# TODO (ryan@gensci.org) Split into separate functions
# Creating default return dict with iterables where needed
result = {
"root": args.dir,
'file_ext': [],
'files_to_ignore': [],
'dirs_to_ignore': []
}
# Assigning file-specified configurations, if exists
if os.path.isfile(os.path.join(args.dir, args.config)):
conf = configparser.ConfigParser()
conf.read(os.path.join(args.dir, args.config))
if 'DEFAULTS' not in conf.sections():
raise Exception("DEFAULTS section required if config file is provided.")
defaults = conf['DEFAULTS']
result['file_ext'] = [f.strip()
for f in defaults.get('file_ext', '').split(',') if f]
result['files_to_ignore'] = [f.strip()
for f in defaults.get('files_to_ignore', '').split(',') if f]
result['dirs_to_ignore'] = [f.strip()
for f in defaults.get('dirs_to_ignore', '').split(',') if f]
# Assinging command line args if provided.
result['file_ext'] = [s.strip() for s in args.file_ext.split(
',')] if args.file_ext else result['file_ext']
result['files_to_ignore'] = [s.strip() for s in args.files_to_ignore.split(
',')] if args.files_to_ignore else result['files_to_ignore']
result['dirs_to_ignore'] = [s.strip() for s in args.dirs_to_ignore.split(
',')] if args.dirs_to_ignore else result['dirs_to_ignore']
return result
[docs]def walk_dir(config):
"""Traverse directory to scan for TODO comments
Args:
config (dict): Configuration dictionary
"""
# TODO (ryan@gensci.org) Figure out how to identify and ignore docstrings
with open(os.path.join(config['root'], 'TODO.md'), "w") as td:
found = False
for (root, dirs, files) in os.walk(config['root'], topdown=True):
# Pruning the files and directories based on config
dirs[:] = [d for d in dirs if d not in config['dirs_to_ignore']]
files[:] = [f for f in files if f not in config['files_to_ignore']]
files[:] = [f for f in files if f.endswith(tuple(config['file_ext']))]
for filename in files:
# Generating useful file name with nested directories but
# without computer specific components (e.g /home/user)
pfile = os.path.join(root, filename)
pfile = pfile.replace(config['root'] + "/", "")
with open(os.path.join(root, filename), "r") as f:
for n, line in enumerate(f, 1):
if "TODO" in line:
found = True
comment = re.match(r".*TODO:*\s*(.*)", line).group(1)
log_comment(td, pfile, n, comment)
if not found:
root = os.path.split(config['root'])[1]
td.write("# No TODO Comments found in {}".format(root))
print("""
No TODO comments found.
If you think this is incorrect, check configuration settings used.
""")
# ---- CLI ----
# The functions defined in this section are wrappers around the main Python
# API allowing them to be called directly from the terminal as a CLI
# executable/script.
[docs]def parse_args(args):
"""Parse command line parameters
Args:
args (List[str]): command line parameters as list of strings
(for example ``["--help"]``).
Returns:
:obj:`argparse.Namespace`: command line parameters namespace
"""
parser = argparse.ArgumentParser(description="Catalog TODO comments in project")
# -- Generic CLA args --
parser.add_argument(
"--version",
action="version",
version="todo_catalog {ver}".format(ver=__version__),
)
parser.add_argument(
"-v",
"--verbose",
dest="loglevel",
help="set loglevel to INFO",
action="store_const",
const=logging.INFO,
)
parser.add_argument(
"-vv",
"--very-verbose",
dest="loglevel",
help="set loglevel to DEBUG",
action="store_const",
const=logging.DEBUG,
)
# -- Application specific args --
parser.add_argument(
"-d",
"--dir",
default=os.getcwd(),
help="Root directory for traversal. Defaults to current dir."
)
parser.add_argument(
"-f",
"--file_ext",
default="",
help="File extensions to scan, defaults to .py"
)
parser.add_argument(
"-fi",
"--files-to-ignore",
default="",
help="Files to ignore during scan"
)
parser.add_argument(
"-di",
"--dirs-to-ignore",
default="",
help="Directories to ignore during scan"
)
parser.add_argument(
"-c",
"--config",
default="todo_config",
help="Specify a config file name in the root directory"
)
return parser.parse_args(args)
def __setup_logging(loglevel):
"""Setup basic logging
Args:
loglevel (int): minimum loglevel for emitting messages
"""
logformat = "[%(asctime)s] %(levelname)s:%(name)s:%(message)s"
logging.basicConfig(
level=loglevel, stream=sys.stdout, format=logformat, datefmt="%Y-%m-%d %H:%M:%S"
)
[docs]def main(args):
"""Wrapper allowing component functions to be called with string arguments in a CLI fashion
Generates ``TODO.md`` file from TODO comments found in a project, based on the configuration
parameters provided either by a ``config`` file (see
Args:
args (List[str]): command line parameters as list of strings
(for example ``["--verbose", "42"]``).
"""
args = parse_args(args)
__setup_logging(args.loglevel)
_logger.debug(args)
config = get_config(args)
_logger.debug(config)
walk_dir(config)
[docs]def run():
"""Calls :func:`main` passing the CLI arguments extracted from :obj:`sys.argv`
This function can be used as entry point to create console scripts with setuptools.
"""
main(sys.argv[1:])
if __name__ == "__main__":
# ^ This is a guard statement that will prevent the following code from
# being executed in the case someone imports this file instead of
# executing it as a script.
# https://docs.python.org/3/library/__main__.html
# After installing your project with pip, users can also run your Python
# modules as scripts via the ``-m`` flag, as defined in PEP 338::
#
# python -m todo_catalog.skeleton 42
#
run()