Coverage for pyquickhelper/sphinxext/import_object_helper.py: 87%
133 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-03 02:21 +0200
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-03 02:21 +0200
1# -*- coding: utf-8 -*-
2"""
3@file
4@brief Defines a :epkg:`sphinx` extension which if all parameters are documented.
5"""
6import inspect
7from typing import Tuple
8import warnings
9import sys
12class _Types:
13 @property
14 def prop(self):
15 pass
17 @staticmethod
18 def stat():
19 pass
22def import_object(docname, kind, use_init=True, fLOG=None) -> Tuple[object, str]:
23 """
24 Extracts an object defined by its name including the module name.
26 @param docname full name of the object
27 (example: ``pyquickhelper.sphinxext.sphinx_docassert_extension.import_object``)
28 @param kind ``'function'`` or ``'class'`` or ``'kind'``
29 @param use_init return the constructor instead of the class
30 @param fLOG logging function
31 @return tuple(object, name)
32 @raises :epkg:`*py:RuntimeError` if cannot be imported,
33 :epkg:`*py:TypeError` if it is a method or a property,
34 :epkg:`*py:ValueError` if *kind* is unknown.
35 """
36 spl = docname.split(".")
37 name = spl[-1]
38 if kind not in ("method", "property", "staticmethod"):
39 modname = ".".join(spl[:-1])
40 code = 'from {0} import {1}\nmyfunc = {1}'.format(modname, name)
41 codeobj = compile(code, f'conf{kind}.py', 'exec')
42 if fLOG:
43 fLOG(f"[import_object] modname='{modname}' code='{code}'")
44 else:
45 modname = ".".join(spl[:-2])
46 classname = spl[-2]
47 code = 'from {0} import {1}\nmyfunc = {1}'.format(modname, classname)
48 codeobj = compile(code, f'conf{kind}2.py', 'exec')
49 if fLOG:
50 fLOG("[import_object] modname='{0}' code='{1}' classname='{2}'".format(
51 modname, code, classname))
53 context = {}
54 with warnings.catch_warnings():
55 warnings.simplefilter("ignore")
56 try:
57 exec(codeobj, context, context)
58 except Exception as e:
59 mes = "Unable to compile and execute '{0}' due to \n{1}\ngiven:\n{2}".format(
60 code.replace('\n', '\\n'), e, docname)
61 if fLOG:
62 fLOG(f"[import_object] failed due to {e}")
63 raise RuntimeError(mes) from e
65 myfunc = context["myfunc"]
66 if fLOG:
67 fLOG(
68 f"[import_object] imported '{docname}' --> '{str(myfunc)}'")
69 if kind == "function":
70 if not inspect.isfunction(myfunc) and 'built-in function' not in str(myfunc) and \
71 'built-in method' not in str(myfunc):
72 # inspect.isfunction fails for C functions.
73 raise TypeError(f"'{docname}' is not a function")
74 name = spl[-1]
75 elif kind == "property":
76 if not inspect.isclass(myfunc):
77 raise TypeError(f"'{docname}' is not a class")
78 myfunc = getattr(myfunc, spl[-1])
79 if inspect.isfunction(myfunc) or inspect.ismethod(myfunc):
80 raise TypeError(
81 f"'{docname}' is not a property - {myfunc}")
82 if (hasattr(_Types.prop, '__class__') and
83 myfunc.__class__ is not _Types.prop.__class__): # pylint: disable=E1101
84 raise TypeError(
85 f"'{docname}' is not a property(*) - {myfunc}")
86 if not isinstance(myfunc, property):
87 raise TypeError(
88 f"'{docname}' is not a static property(**) - {myfunc}")
89 name = spl[-1]
90 elif kind == "method":
91 if not inspect.isclass(myfunc):
92 raise TypeError(f"'{docname}' is not a class")
93 myfunc = getattr(myfunc, spl[-1])
94 if not inspect.isfunction(myfunc) and not inspect.ismethod(myfunc) and not name.endswith('__'):
95 raise TypeError(
96 f"'{docname}' is not a method - {myfunc}")
97 if isinstance(myfunc, staticmethod):
98 raise TypeError(
99 f"'{docname}' is not a method(*) - {myfunc}")
100 if hasattr(myfunc, "__code__") and sys.version_info >= (3, 4):
101 if len(myfunc.__code__.co_varnames) == 0:
102 raise TypeError(
103 f"'{docname}' is not a method(**) - {myfunc}")
104 if myfunc.__code__.co_varnames[0] != 'self':
105 raise TypeError(
106 f"'{docname}' is not a method(***) - {myfunc}")
107 name = spl[-1]
108 elif kind == "staticmethod":
109 if not inspect.isclass(myfunc):
110 raise TypeError(f"'{docname}' is not a class")
111 myfunc = getattr(myfunc, spl[-1])
112 if not inspect.isfunction(myfunc) and not inspect.ismethod(myfunc):
113 raise TypeError(
114 f"'{docname}' is not a static method - {myfunc}")
115 if myfunc.__class__ is not _Types.stat.__class__:
116 raise TypeError(
117 f"'{docname}' is not a static method(*) - {myfunc}")
118 name = spl[-1]
119 elif kind == "class":
120 if not inspect.isclass(myfunc):
121 raise TypeError(f"'{docname}' is not a class")
122 name = spl[-1]
123 myfunc = myfunc.__init__ if use_init else myfunc
124 else:
125 raise ValueError("Unknwon value for 'kind'")
127 return myfunc, name
130def import_any_object(docname, use_init=True, fLOG=None) -> Tuple[object, str, str]:
131 """
132 Extracts an object defined by its name including the module name.
134 :param docname: full name of the object
135 (example: ``pyquickhelper.sphinxext.sphinx_docassert_extension.import_object``)
136 :param use_init: return the constructor instead of the class
137 :param fLOG: logging function
138 :returns: tuple(object, name, kind)
139 :raises: :epkg:`*py:ImportError` if unable to import
141 Kind is among ``'function'`` or ``'class'`` or ``'kind'``.
142 """
143 myfunc = None
144 name = None
145 excs = []
146 for kind in ("function", "method", "staticmethod", "property", "class"):
147 try:
148 myfunc, name = import_object(
149 docname, kind, use_init=use_init, fLOG=fLOG)
150 if fLOG:
151 fLOG(
152 f"[import_any_object] ok '{kind}' for '{docname}' - use_unit={use_init}")
153 fLOG("[import_any_object] __doc__={0} __name__={1} __module__={2}".format(
154 hasattr(myfunc, '__doc__'), hasattr(myfunc, '__name__'),
155 hasattr(myfunc, '__module__')))
156 fLOG("[import_any_object] name='{0}' - module='{1}'".format(
157 name, getattr(myfunc, '__module__', None)))
158 return myfunc, name, kind
159 except Exception as e:
160 # not this kind
161 excs.append((kind, e))
162 if fLOG:
163 fLOG(
164 f"[import_any_object] not '{kind}' for '{docname}' (use_unit={use_init})")
166 sec = " ### ".join(f"{k}-{type(e)}-{e}".replace("\n", " ")
167 for k, e in excs)
168 raise ImportError(
169 f"Unable to import '{docname}'. Exceptions met: {sec}")
172def import_path(obj, class_name=None, err_msg=None, fLOG=None):
173 """
174 Determines the import path which is
175 the shortest way to import the function. In case the
176 following ``from module.submodule import function``
177 works, the import path will be ``module.submodule``.
179 :param obj: object
180 :param class_name: :epkg:`Python` does not really distinguish between
181 static method and functions. If not None, this parameter
182 should contain the name of the class which holds the static
183 method given in *obj*
184 :param err_msg: an error message to display if anything happens
185 :param fLOG: logging function
186 :returns: import path
187 :raises: :epkg:`*py:TypeError` if object is a property,
188 :epkg:`*py:RuntimeError` if cannot be imported
190 The function does not work for methods or properties.
191 It raises an exception or returns irrelevant results.
192 """
193 try:
194 _ = obj.__module__
195 except AttributeError:
196 # This is a method.
197 raise TypeError(f"obj is a method or a property ({obj})")
199 if class_name is None:
200 name = obj.__name__
201 else:
202 name = class_name
203 elements = obj.__module__.split('.')
204 found = None
205 for i in range(1, len(elements) + 1):
206 path = '.'.join(elements[:i])
207 code = f'from {path} import {name}'
208 codeobj = compile(code, f'import_path_{name}.py', 'exec')
209 with warnings.catch_warnings():
210 warnings.simplefilter("ignore")
211 context = {}
212 try:
213 exec(codeobj, context, context)
214 found = path
215 if fLOG:
216 fLOG(f"[import_path] succeeds: '{code}'")
217 break
218 except Exception:
219 if fLOG:
220 fLOG(f"[import_path] fails: '{code}'")
221 continue
223 if found is None:
224 raise RuntimeError("Unable to import object '{0}' ({1}). Full path: '{2}'{3}".format(
225 name, obj, '.'.join(elements), ("\n-----\n" + err_msg) if err_msg else ''))
226 return found