Source code for gleandoc.gleandoc

#!/usr/bin/env python3

"""See top level package docstring for documentation"""

import ast
import importlib
import logging
import os
import pathlib
import sys

myself = pathlib.Path(__file__).stem

# configure library-specific logger
logger = logging.getLogger(myself)
logging.getLogger(myself).addHandler(logging.NullHandler())

logging.basicConfig(level=logging.DEBUG)

########################################################################


[docs]def docstring(name=os.path.basename(os.getcwd())): """ Return the doctring for a module - The default name is the basename of the current working directory - For example: if /var/tmp, name = 'tmp' - First try relative import in current directory - Then try general import (read docstring from installed package) Parameters ---------- name : str or object, default=os.path.basename(os.getcwd()) Name of entity from which to attempt docstring extraction Returns ------- str, default='' The extracted docstring """ logger.info(f"searching for docstring belonging to {name}") try: # logger.info(f"trying relative import in working directory...") # hit = importlib.import_module(name, package='.') # logger.info(f"relative import: loaded module from {hit.__file__}") # return hit.__doc__ cwd = os.getcwd() python_file = f"{cwd}/{name}/__init__.py" logger.info(f"attempting abstract syntax tree parse: {python_file}") parsed = ast.parse(open(python_file).read()) logger.info('abstract syntax tree parse succeeded') doc = ast.get_docstring(parsed) logger.info('retrieved docstring from abstract syntax tree') return doc except Exception as e: logger.info('failed parse') logger.debug(f"exception details: {e}") try: logger.info('trying general import...') hit = importlib.import_module(name) logger.info(f"general import: loaded module from {hit.__file__}") return hit.__doc__ except ModuleNotFoundError as e: logger.info(f"failed general import of {name}") logger.debug(f"exception details: {e}") except AttributeError as e: logger.info(f"import succeeded for {name}") logger.info(f"however, {name} lacks __doc__ attribute") logger.debug(f"exception details: {e}") logger.info(f"unable to extract docstring for {name}") logger.info('returning empty string') return ''
[docs]def interpolate(input, output): """Interpolate docstring into template and write file""" try: template = open(input).read() except Exception: logger.error(f"failed reading {input}") sys.exit(1) try: doc = docstring() interpolated = template.format(**{'__doc__': doc}) except Exception: logger.error(f"failed interpolating {input}") sys.exit(1) try: if os.path.exists(output): logger.warning(f"replacing {output}") open(output, 'wt').write(interpolated) logger.info(f"wrote {output}") except Exception: logger.error(f"failed writing {output}") sys.exit(1)
[docs]def usage(): usagemsg = f"""Usage: {myself} [-h] [NAME] Extract docstring from module [NAME] -h, --help show this help message and exit - If unspecified, NAME defaults to the basename of the current directory - This is designed for use in build systems to construct README files Alternative two argument usage: {myself} TEMPLATE README Interpolate docstring into TEMPLATE and write results to README - In this mode, always derives NAME from basename of current directory - Template uses style similar to f-string - Supported variables which will be interpolated include: {{__doc__}} - For literal (single) braces, use double braces: {{{{ or }}}} """ print(usagemsg)
[docs]def main(): if len(sys.argv) == 1: print(docstring()) elif len(sys.argv) == 2: argv1 = sys.argv[1] if argv1 == '-h' or argv1 == '--help': usage() sys.exit(0) else: print(docstring(name=argv1)) elif len(sys.argv) == 3: interpolate(input=sys.argv[1], output=sys.argv[2]) else: logger.error('too many arguments\n') usage() sys.exit(1)
if __name__ == '__main__': main()