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

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 

10 

11 

12class _Types: 

13 @property 

14 def prop(self): 

15 pass 

16 

17 @staticmethod 

18 def stat(): 

19 pass 

20 

21 

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. 

25 

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)) 

52 

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 

64 

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'") 

126 

127 return myfunc, name 

128 

129 

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. 

133 

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 

140 

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})") 

165 

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}") 

170 

171 

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``. 

178 

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 

189 

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})") 

198 

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 

222 

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