#!/usr/bin/python3

"""
pydocmd generates Python Module / script documentation in the Markdown (md)
format. It was written to automatically generate documentation that can be put
on Github or Bitbucket.

It is as of yet not very complete and is more of a Proof-of-concept than a
fully-fledged tool. Markdown is also a very restricted format and every
implementation works subtly, or completely, different. This means output
may be different on different converters.

Usage
-----

    ./pydocmd file.py > file.md

Example output
--------------

* https://github.com/fboender/pydocmd/wiki/Example-output-1
* https://github.com/fboender/pydocmd/wiki/Example-output-2

"""

import sys
import os
import imp
import inspect
import pprint


__author__ = "Ferry Boender"
__copyright__ = "Copyright 2014, Ferry Boender"
__license__ = "MIT (expat) License"
__version__ = "1.0"
__maintainer__ = "Ferry Boender"
__email__ = "ferry.boender@gmail.com"


def fmt_doc(doc, indent=''):
    """
    Format a doc-string.
    """
    s = ''
    for line in doc.lstrip().splitlines():
        s += '%s%s\n' % (indent, line.strip())
    return s.rstrip()

def insp_file(file_name):
    """
    Inspect a file and return module information
    """
    mod_inst = imp.load_source('', file_name)
    if not mod_inst:
        sys.stderr.write("Failed to import '%s'\n" % (file_name))
        sys.exit(2)

    mod_name = inspect.getmodulename(file_name)
    if not mod_name:
        mod_name = os.path.splitext(os.path.basename(file_name))[0]

    return insp_mod(mod_name, mod_inst)

def insp_mod(mod_name, mod_inst):
    """
    Inspect a module return doc, vars, functions and classes.
    """
    info = {
        'name': mod_name,
        'inst': mod_inst,
        'author': {},
        'doc': '',
        'vars': [],
        'functions': [],
        'classes': [],
    }

    # Get module documentation
    mod_doc = inspect.getdoc(mod_inst)
    if mod_doc:
        info['doc'] = fmt_doc(mod_doc)

    for attr_name in ['author', 'copyright', 'license', 'version', 'maintainer', 'email']:
        if hasattr(mod_inst, '__%s__' % (attr_name)):
            info['author'][attr_name] = getattr(mod_inst, '__%s__' % (attr_name))

    # Get module global vars
    for member_name, member_inst in inspect.getmembers(mod_inst):
        if not member_name.startswith('_') and \
           not inspect.isfunction(member_inst) and \
           not inspect.isclass(member_inst) and \
           not inspect.ismodule(member_inst) and \
           member_name not in mod_inst.__builtins__:
            info['vars'].append( (member_name, member_inst) )

    # Get module functions
    functions = inspect.getmembers(mod_inst, inspect.isfunction)
    if functions:
        for func_name, func_inst in functions:
            info['functions'].append(insp_method(func_name, func_inst))

    classes = inspect.getmembers(mod_inst, inspect.isclass)
    if classes:
        for class_name, class_inst in classes:
            info['classes'].append(insp_class(class_name, class_inst))

    return info

def insp_class(class_name, class_inst):
    """
    Inspect class and return doc, methods.
    """
    info = {
        'name': class_name,
        'inst': class_inst,
        'doc': '',
        'methods': [],
    }

    # Get class documentation
    class_doc = inspect.getdoc(class_inst)
    if class_doc:
        info['doc'] = fmt_doc(class_doc)

    # Get class methods
    methods = inspect.getmembers(class_inst, inspect.ismethod)
    for method_name, method_inst in methods:
        info['methods'].append(insp_method(method_name, method_inst))

    return info

def insp_method(method_name, method_inst):
    """
    Inspect a method and return arguments, doc.
    """
    info = {
        'name': method_name,
        'inst': method_inst,
        'args': [],
        'doc': ''
    }

    # Get method arguments
    method_args = inspect.getargspec(method_inst)
    for arg in method_args.args:
        if arg != 'self':
            info['args'].append(arg)

    # Apply default argumument values to arguments
    if method_args.defaults:
        a_pos = len(info['args']) - len(method_args.defaults)
        for pos, default in enumerate(method_args.defaults):
            info['args'][a_pos + pos] = '%s=%s' % (info['args'][a_pos + pos], default)

    # Print method documentation
    method_doc = inspect.getdoc(method_inst)
    if method_doc:
        info['doc'] = fmt_doc(method_doc)

    return info


def out_file(file_i):
    print("%s\n%s\n" % (file_i['name'], '=' * len(file_i['name'])))
    print(file_i['doc'] + '\n')

    author = ''
    if 'author' in file_i['author']:
        author += file_i['author']['author'] + ' '
    if 'email' in file_i['author']:
        author += '<%s>' % (file_i['author']['email'])
    if author:
        print("* __Author__: %s" % (author))

    author_attrs = [
        ('Version', 'version'),
        ('Copyright', 'copyright'),
        ('License', 'license'),
    ]
    for attr_friendly, attr_name in author_attrs:
        if attr_name in file_i['author']:
            print("* __%s__: %s" % (attr_friendly, file_i['author'][attr_name]))

    print("\nVariables\n----------\n")
    if not file_i['vars']:
        print("This file does not define any variables\n")
    for var_name, var_inst in file_i['vars']:
        print("* `%s`: %s" % (var_name, var_inst))

    print("\nFunctions\n----------\n")
    if not file_i['functions']:
        print("This file does not define any top-level functions\n")
    for function_i in file_i['functions']:
        if function_i['name'].startswith('_'):
            continue
        print("### def `%s(%s)`\n" % (function_i['name'], ', '.join(function_i['args'])))
        if function_i['doc']:
            print("%s" % (function_i['doc']))
        else:
            print("No documentation for this function")

    print("\nClasses\n----------\n")
    if not file_i['classes']:
        print("This file does not define any classes functions\n")
    for class_i in file_i['classes']:
        print("### class `%s()`\n" % (class_i['name']))
        if class_i['doc']:
            print("%s\n" % (class_i['doc']))
        else:
            print("No documentation for this class\n")

        print("Methods:\n")
        for method_i in class_i['methods']:
            if method_i['name'] != '__init__' and method_i['name'].startswith('_'):
                continue
            print("#### def `%s(%s)`\n" % (method_i['name'], ', '.join(method_i['args'])))
            print("%s\n" % (method_i['doc']))


if __name__ == '__main__':
    try:
        in_f = sys.argv[1]
    except IndexError:
        sys.stderr.write('Usage: %s <file.py>\n' % (sys.argv[0]))
        sys.exit(1)

    file_i = insp_file(in_f)
    out_file(file_i)
