Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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, 'conf{0}.py'.format(kind), 'exec') 

42 if fLOG: 

43 fLOG("[import_object] modname='{0}' code='{1}'".format( 

44 modname, code)) 

45 else: 

46 modname = ".".join(spl[:-2]) 

47 classname = spl[-2] 

48 code = 'from {0} import {1}\nmyfunc = {1}'.format(modname, classname) 

49 codeobj = compile(code, 'conf{0}2.py'.format(kind), 'exec') 

50 if fLOG: 

51 fLOG("[import_object] modname='{0}' code='{1}' classname='{2}'".format( 

52 modname, code, classname)) 

53 

54 context = {} 

55 with warnings.catch_warnings(): 

56 warnings.simplefilter("ignore") 

57 try: 

58 exec(codeobj, context, context) 

59 except Exception as e: 

60 mes = "Unable to compile and execute '{0}' due to \n{1}\ngiven:\n{2}".format( 

61 code.replace('\n', '\\n'), e, docname) 

62 if fLOG: 

63 fLOG("[import_object] failed due to {0}".format(e)) 

64 raise RuntimeError(mes) from e 

65 

66 myfunc = context["myfunc"] 

67 if fLOG: 

68 fLOG( 

69 "[import_object] imported '{0}' --> '{1}'".format(docname, str(myfunc))) 

70 if kind == "function": 

71 if not inspect.isfunction(myfunc) and 'built-in function' not in str(myfunc) and \ 

72 'built-in method' not in str(myfunc): 

73 # inspect.isfunction fails for C functions. 

74 raise TypeError("'{0}' is not a function".format(docname)) 

75 name = spl[-1] 

76 elif kind == "property": 

77 if not inspect.isclass(myfunc): 

78 raise TypeError("'{0}' is not a class".format(docname)) 

79 myfunc = getattr(myfunc, spl[-1]) 

80 if inspect.isfunction(myfunc) or inspect.ismethod(myfunc): 

81 raise TypeError( 

82 "'{0}' is not a property - {1}".format(docname, myfunc)) 

83 if (hasattr(_Types.prop, '__class__') and 

84 myfunc.__class__ is not _Types.prop.__class__): # pylint: disable=E1101 

85 raise TypeError( 

86 "'{0}' is not a property(*) - {1}".format(docname, myfunc)) 

87 if not isinstance(myfunc, property): 

88 raise TypeError( 

89 "'{0}' is not a static property(**) - {1}".format(docname, myfunc)) 

90 name = spl[-1] 

91 elif kind == "method": 

92 if not inspect.isclass(myfunc): 

93 raise TypeError("'{0}' is not a class".format(docname)) 

94 myfunc = getattr(myfunc, spl[-1]) 

95 if not inspect.isfunction(myfunc) and not inspect.ismethod(myfunc) and not name.endswith('__'): 

96 raise TypeError( 

97 "'{0}' is not a method - {1}".format(docname, myfunc)) 

98 if isinstance(myfunc, staticmethod): 

99 raise TypeError( 

100 "'{0}' is not a method(*) - {1}".format(docname, myfunc)) 

101 if hasattr(myfunc, "__code__") and sys.version_info >= (3, 4): 

102 if len(myfunc.__code__.co_varnames) == 0: 

103 raise TypeError( 

104 "'{0}' is not a method(**) - {1}".format(docname, myfunc)) 

105 if myfunc.__code__.co_varnames[0] != 'self': 

106 raise TypeError( 

107 "'{0}' is not a method(***) - {1}".format(docname, myfunc)) 

108 name = spl[-1] 

109 elif kind == "staticmethod": 

110 if not inspect.isclass(myfunc): 

111 raise TypeError("'{0}' is not a class".format(docname)) 

112 myfunc = getattr(myfunc, spl[-1]) 

113 if not inspect.isfunction(myfunc) and not inspect.ismethod(myfunc): 

114 raise TypeError( 

115 "'{0}' is not a static method - {1}".format(docname, myfunc)) 

116 if myfunc.__class__ is not _Types.stat.__class__: 

117 raise TypeError( 

118 "'{0}' is not a static method(*) - {1}".format(docname, myfunc)) 

119 name = spl[-1] 

120 elif kind == "class": 

121 if not inspect.isclass(myfunc): 

122 raise TypeError("'{0}' is not a class".format(docname)) 

123 name = spl[-1] 

124 myfunc = myfunc.__init__ if use_init else myfunc 

125 else: 

126 raise ValueError("Unknwon value for 'kind'") 

127 

128 return myfunc, name 

129 

130 

131def import_any_object(docname, use_init=True, fLOG=None) -> Tuple[object, str, str]: 

132 """ 

133 Extracts an object defined by its name including the module name. 

134 

135 :param docname: full name of the object 

136 (example: ``pyquickhelper.sphinxext.sphinx_docassert_extension.import_object``) 

137 :param use_init: return the constructor instead of the class 

138 :param fLOG: logging function 

139 :returns: tuple(object, name, kind) 

140 :raises: :epkg:`*py:ImportError` if unable to import 

141 

142 Kind is among ``'function'`` or ``'class'`` or ``'kind'``. 

143 """ 

144 myfunc = None 

145 name = None 

146 excs = [] 

147 for kind in ("function", "method", "staticmethod", "property", "class"): 

148 try: 

149 myfunc, name = import_object( 

150 docname, kind, use_init=use_init, fLOG=fLOG) 

151 if fLOG: 

152 fLOG( 

153 "[import_any_object] ok '{0}' for '{1}' - use_unit={2}".format(kind, docname, use_init)) 

154 fLOG("[import_any_object] __doc__={0} __name__={1} __module__={2}".format( 

155 hasattr(myfunc, '__doc__'), hasattr(myfunc, '__name__'), 

156 hasattr(myfunc, '__module__'))) 

157 fLOG("[import_any_object] name='{0}' - module='{1}'".format( 

158 name, getattr(myfunc, '__module__', None))) 

159 return myfunc, name, kind 

160 except Exception as e: 

161 # not this kind 

162 excs.append((kind, e)) 

163 if fLOG: 

164 fLOG( 

165 "[import_any_object] not '{0}' for '{1}' (use_unit={2})".format(kind, docname, use_init)) 

166 

167 sec = " ### ".join("{0}-{1}-{2}".format(k, type(e), e).replace("\n", " ") 

168 for k, e in excs) 

169 raise ImportError( 

170 "Unable to import '{0}'. Exceptions met: {1}".format(docname, sec)) 

171 

172 

173def import_path(obj, class_name=None, err_msg=None, fLOG=None): 

174 """ 

175 Determines the import path which is 

176 the shortest way to import the function. In case the 

177 following ``from module.submodule import function`` 

178 works, the import path will be ``module.submodule``. 

179 

180 :param obj: object 

181 :param class_name: :epkg:`Python` does not really distinguish between 

182 static method and functions. If not None, this parameter 

183 should contain the name of the class which holds the static 

184 method given in *obj* 

185 :param err_msg: an error message to display if anything happens 

186 :param fLOG: logging function 

187 :returns: import path 

188 :raises: :epkg:`*py:TypeError` if object is a property, 

189 :epkg:`*py:RuntimeError` if cannot be imported 

190 

191 The function does not work for methods or properties. 

192 It raises an exception or returns irrelevant results. 

193 """ 

194 try: 

195 _ = obj.__module__ 

196 except AttributeError: 

197 # This is a method. 

198 raise TypeError("obj is a method or a property ({0})".format(obj)) 

199 

200 if class_name is None: 

201 name = obj.__name__ 

202 else: 

203 name = class_name 

204 elements = obj.__module__.split('.') 

205 found = None 

206 for i in range(1, len(elements) + 1): 

207 path = '.'.join(elements[:i]) 

208 code = 'from {0} import {1}'.format(path, name) 

209 codeobj = compile(code, 'import_path_{0}.py'.format(name), 'exec') 

210 with warnings.catch_warnings(): 

211 warnings.simplefilter("ignore") 

212 context = {} 

213 try: 

214 exec(codeobj, context, context) 

215 found = path 

216 if fLOG: 

217 fLOG("[import_path] succeeds: '{0}'".format(code)) 

218 break 

219 except Exception: 

220 if fLOG: 

221 fLOG("[import_path] fails: '{0}'".format(code)) 

222 continue 

223 

224 if found is None: 

225 raise RuntimeError("Unable to import object '{0}' ({1}). Full path: '{2}'{3}".format( 

226 name, obj, '.'.join(elements), ("\n-----\n" + err_msg) if err_msg else '')) 

227 return found