# -*- coding: utf-8 -*-

#########################################################################
# pyeole.inspect_utils - inspection utils
# Copyright © 2012 Pôle de Compétence EOLE <eole@ac-dijon.fr>
#
# License CeCILL:
#  * in french: http://www.cecill.info/licences/Licence_CeCILL_V2-fr.html
#  * in english http://www.cecill.info/licences/Licence_CeCILL_V2-en.html
#########################################################################

"""Convenient utlis around inspection.

This modules provides two utilities based on inspection:

  - :func:`format_caller`: returns a formatted string of caller
    informations as returned by :func:`get_caller_infos`.

  - :func:`get_caller_infos` returns informations on its parent caller
    (the caller of the :func:`get_caller_infos` user)

Implementation is based on:

  - :func:`warnings.warn` code

  - `Get full caller name
    <https://code.activestate.com/recipes/578352-get-full-caller-name-packagemodulefunction>`_

"""

import sys

import inspect

def get_caller_infos(stacklevel=2):
    """Get a caller informations.
    
    :data:`stacklevel` specifies how many levels of stack to skip
    while getting caller informations. :data:`stacklevel=1` means "who
    calls :func:`get_caller_infos`", :data:`stacklevel=2` "who calls
    the caller of :func:`get_caller_infos`", etc.

    The caller informations are return in a dictionary with the
    following keys:

      - :data:`modulename`: name of the module

      - :data:`classname`: name of the class

      - :data:`codename`: name of the function or method

      - :data:`filename`: name of the file where the call is made

      - :data:`lineno`: line in the 'filename' where the call is made

    Raise :exc:`ValueError` if :data:`stacklevel` exceed stack height.

    :param stacklevel: levels of stack to skip.
    :type stacklevel: `int`
    :return: caller informations.
    :rtype: `dict`

    """
    modulename = ''
    classname = ''
    codename = ''
    filename = ''
    lineno = 0

    stack = inspect.stack()
    start = 0 + stacklevel
    if len(stack) < start + 1:
      raise ValueError("Argument 'stacklevel' too high '%s' for stack lenght '%s'" 
                       % (stacklevel, len(stack)))

    parentframe = stack[start][0]    

    module = inspect.getmodule(parentframe)
    if module:
        modulename = module.__name__
    else:
        modulename = '__main__'

    # detect classname
    if 'self' in parentframe.f_locals:
        # I don't know any way to detect call from the object method
        # XXX: there seems to be no way to detect static method call - it will
        #      be just a function call
        classname = parentframe.f_locals['self'].__class__.__name__
    if parentframe.f_code.co_name != '<module>':
        codename = parentframe.f_code.co_name

    filename = parentframe.f_globals.get('__file__')
    if filename:
        lineno = parentframe.f_lineno
        fnl = filename.lower()
        if fnl.endswith((".pyc", ".pyo")):
            filename = filename[:-1]
    else:
        if modulename == "__main__":
            try:
                filename = sys.argv[0]
            except AttributeError:
                # embedded interpreters don't have sys.argv, see bug #839151
                filename = '__main__'
        if not filename:
            filename = modulename

    # Avoid bad interaction with garbage collector
    del parentframe

    return { 'modulename' : modulename,
             'classname'  : classname,
             'codename'   : codename,
             'filename'   : filename,
             'lineno'     : lineno,
    }

def format_caller(caller,
                  format_string='{modulename}.{classname}.{codename}'):
    """Format a string representation of caller informations.

    It return a formatted string with the default format::

      {modulename}.{classname}.{codename}

    Available keywords for format strings are thoses used in returned
    value of :func:`get_caller_infos`.

    Empty parts are removed from the string.

    :param caller: caller informations
    :type caller: `dict`
    :param format_string: Format string for string representation
    :type format_string: `str`
    :return: string representation of caller informations
    :rtype: `str`

    """
    formatted_string = format_string.format(**caller)
    # Remove empty parts
    return '.'.join([part for part in formatted_string.split('.') if part != ''])
