Source code for sphinx_runpython.import_object_helper

# -*- coding: utf-8 -*-
"""
@file
@brief Defines a :epkg:`sphinx` extension which if all parameters are documented.
"""
import inspect
import warnings
import sys
from typing import Tuple


class _Types:
    @property
    def prop(self):
        pass

    @staticmethod
    def stat():
        pass


def import_object(docname, kind, use_init=True) -> Tuple[object, str]:
    """
    Extracts an object defined by its name including the module name.

    :param docname: full name of the object
    :param kind: ``'function'`` or ``'class'`` or ``'kind'``
    :param use_init: return the constructor instead of the class
    :return: tuple(object, name)
    :raises: :epkg:`*py:RuntimeError` if cannot be imported,
             :epkg:`*py:TypeError` if it is a method or a property,
             :epkg:`*py:ValueError` if *kind* is unknown.
    """
    spl = docname.split(".")
    name = spl[-1]
    if kind not in ("method", "property", "staticmethod"):
        modname = ".".join(spl[:-1])
        code = "from {0} import {1}\nmyfunc = {1}".format(modname, name)
        codeobj = compile(code, f"conf{kind}.py", "exec")
    else:
        modname = ".".join(spl[:-2])
        classname = spl[-2]
        code = "from {0} import {1}\nmyfunc = {1}".format(modname, classname)
        codeobj = compile(code, f"conf{kind}2.py", "exec")

    context = {}
    with warnings.catch_warnings():
        warnings.simplefilter("ignore")
        try:
            exec(codeobj, context, context)
        except Exception as e:
            mes = (
                "Unable to compile and execute '{0}' due to \n{1}\ngiven:\n{2}".format(
                    code.replace("\n", "\\n"), e, docname
                )
            )
            raise RuntimeError(mes) from e

    myfunc = context["myfunc"]
    if kind == "function":
        if (
            not inspect.isfunction(myfunc)
            and "built-in function" not in str(myfunc)
            and "built-in method" not in str(myfunc)
            and (
                not hasattr(myfunc, "func_code") or ".pyx" not in str(myfunc.func_code)
            )
        ):
            # inspect.isfunction fails for C functions.
            raise TypeError(f"'{docname}' is not a function")
        name = spl[-1]
    elif kind == "property":
        if not inspect.isclass(myfunc):
            raise TypeError(f"'{docname}' is not a class")
        myfunc = getattr(myfunc, spl[-1])
        if inspect.isfunction(myfunc) or inspect.ismethod(myfunc):
            raise TypeError(f"'{docname}' is not a property - {myfunc}")
        if (
            hasattr(_Types.prop, "__class__")
            and myfunc.__class__ is not _Types.prop.__class__
        ):
            raise TypeError(f"'{docname}' is not a property(*) - {myfunc}")
        if not isinstance(myfunc, property):
            raise TypeError(f"'{docname}' is not a static property(**) - {myfunc}")
        name = spl[-1]
    elif kind == "method":
        if not inspect.isclass(myfunc):
            raise TypeError(f"'{docname}' is not a class")
        myfunc = getattr(myfunc, spl[-1])
        if (
            not inspect.isfunction(myfunc)
            and not inspect.ismethod(myfunc)
            and not name.endswith("__")
        ):
            raise TypeError(f"'{docname}' is not a method - {myfunc}")
        if isinstance(myfunc, staticmethod):
            raise TypeError(f"'{docname}' is not a method(*) - {myfunc}")
        if hasattr(myfunc, "__code__") and sys.version_info >= (3, 4):
            if len(myfunc.__code__.co_varnames) == 0:
                raise TypeError(f"'{docname}' is not a method(**) - {myfunc}")
            if myfunc.__code__.co_varnames[0] != "self":
                raise TypeError(f"'{docname}' is not a method(***) - {myfunc}")
        name = spl[-1]
    elif kind == "staticmethod":
        if not inspect.isclass(myfunc):
            raise TypeError(f"'{docname}' is not a class")
        myfunc = getattr(myfunc, spl[-1])
        if not inspect.isfunction(myfunc) and not inspect.ismethod(myfunc):
            raise TypeError(f"'{docname}' is not a static method - {myfunc}")
        if myfunc.__class__ is not _Types.stat.__class__:
            raise TypeError(f"'{docname}' is not a static method(*) - {myfunc}")
        name = spl[-1]
    elif kind == "class":
        if not inspect.isclass(myfunc):
            raise TypeError(f"'{docname}' is not a class")
        name = spl[-1]
        myfunc = myfunc.__init__ if use_init else myfunc
    else:
        raise ValueError("Unknwon value for 'kind'")

    return myfunc, name


[docs]def import_any_object(docname: str, use_init: bool = True) -> Tuple[object, str, str]: """ Extracts an object defined by its name including the module name. :param docname: full name of the object :param use_init: return the constructor instead of the class :returns: tuple(object, name, kind) :raises: :epkg:`*py:ImportError` if unable to import Kind is among ``'function'`` or ``'class'`` or ``'kind'``. """ myfunc = None name = None excs = [] for kind in ("function", "method", "staticmethod", "property", "class"): try: myfunc, name = import_object(docname, kind, use_init=use_init) return myfunc, name, kind except Exception as e: # not this kind excs.append((kind, e)) sec = " ### ".join(f"{k}-{type(e)}-{e}".replace("\n", " ") for k, e in excs) raise ImportError(f"Unable to import '{docname}'. Exceptions met: {sec}")
def import_path(obj, class_name=None, err_msg=None): """ Determines the import path which is the shortest way to import the function. In case the following ``from module.submodule import function`` works, the import path will be ``module.submodule``. :param obj: object :param class_name: :epkg:`Python` does not really distinguish between static method and functions. If not None, this parameter should contain the name of the class which holds the static method given in *obj* :param err_msg: an error message to display if anything happens :returns: import path :raises: :epkg:`*py:TypeError` if object is a property, :epkg:`*py:RuntimeError` if cannot be imported The function does not work for methods or properties. It raises an exception or returns irrelevant results. """ try: _ = obj.__module__ except AttributeError: # This is a method. raise TypeError(f"obj is a method or a property ({obj})") if class_name is None: name = obj.__name__ else: name = class_name elements = obj.__module__.split(".") found = None for i in range(1, len(elements) + 1): path = ".".join(elements[:i]) code = f"from {path} import {name}" codeobj = compile(code, f"import_path_{name}.py", "exec") with warnings.catch_warnings(): warnings.simplefilter("ignore") context = {} try: exec(codeobj, context, context) found = path break except Exception: continue if found is None: raise RuntimeError( "Unable to import object '{0}' ({1}). Full path: '{2}'{3}".format( name, obj, ".".join(elements), ("\n-----\n" + err_msg) if err_msg else "", ) ) return found