@@ -211,41 +211,257 @@ def __exit__(self, exc_type, exc_val, exc_tb):
211211"""Marker for automatic defaults"""
212212
213213
214- def get_function_host (func ):
214+ def get_function_host (func , fallback_to_module = True ):
215215 """
216216 Returns the module or class where func is defined. Approximate method based on qname but "good enough"
217217
218218 :param func:
219+ :param fallback_to_module: if True and an HostNotConstructedYet error is caught, the host module is returned
219220 :return:
220221 """
221- host = get_class_that_defined_method (func )
222+ host = None
223+ try :
224+ host = get_class_that_defined_method (func )
225+ except HostNotConstructedYet :
226+ # ignore if `fallback_to_module=True`
227+ if not fallback_to_module :
228+ raise
229+
222230 if host is None :
223231 host = import_module (func .__module__ )
224232 # assert func in host
225233
226234 return host
227235
228236
229- def get_class_that_defined_method (meth ):
230- """ Adapted from https://stackoverflow.com/a/25959545/7262247 , to support python 2 too """
231- if isinstance (meth , functools .partial ):
232- return get_class_that_defined_method (meth .func )
237+ def needs_binding (f , return_bound = False ):
238+ # type: (...) -> Union[bool, Tuple[bool, Callable]]
239+ """Utility to check if a function needs to be bound to be used """
240+
241+ # detect non-callables
242+ if isinstance (f , staticmethod ):
243+ # only happens if the method is provided as Foo.__dict__['b'], not as Foo.b
244+ # binding is really easy here: pass any class
245+
246+ # no need for the actual class
247+ # bound = f.__get__(get_class_that_defined_method(f.__func__))
248+
249+ # f.__func__ (python 3) or f.__get__(object) (py2 and py3) work
250+ return (True , f .__get__ (object )) if return_bound else True
233251
234- if inspect .ismethod (meth ) or (inspect .isbuiltin (meth ) and getattr (meth , '__self__' , None ) is not None
235- and getattr (meth .__self__ , '__class__' , None )):
236- for cls in inspect .getmro (meth .__self__ .__class__ ):
237- if meth .__name__ in cls .__dict__ :
238- return cls
239- meth = getattr (meth , '__func__' , meth ) # fallback to __qualname__ parsing
252+ elif isinstance (f , classmethod ):
253+ # only happens if the method is provided as Foo.__dict__['b'], not as Foo.b
254+ if not return_bound :
255+ return True
256+ else :
257+ host_cls = get_class_that_defined_method (f .__func__ )
258+ bound = f .__get__ (host_cls , host_cls )
259+ return True , bound
260+
261+ else :
262+ # note that for the two above cases callable(f) returns False !
263+ if not callable (f ) and (PY3 or not inspect .ismethoddescriptor (f )):
264+ raise TypeError ("`f` is not a callable !" )
240265
241- if inspect .isfunction (meth ):
242- cls = getattr (inspect .getmodule (meth ),
243- qname (meth ).split ('.<locals>' , 1 )[0 ].rsplit ('.' , 1 )[0 ],
244- None )
245- if isinstance (cls , type ):
246- return cls
266+ if isinstance (f , functools .partial ) or fixed_ismethod (f ) or is_bound_builtin_method (f ):
267+ # already bound, although TODO the functools.partial one is a shortcut that should be analyzed more deeply
268+ return (False , f ) if return_bound else False
247269
248- return getattr (meth , '__objclass__' , None ) # handle special descriptor objects
270+ else :
271+ # can be a static method, a class method, a descriptor...
272+ if not PY3 :
273+ host_cls = getattr (f , "im_class" , None )
274+ if host_cls is None :
275+ # defined outside a class: no need for binding
276+ return (False , f ) if return_bound else False
277+ else :
278+ bound_obj = getattr (f , "im_self" , None )
279+ if bound_obj is None :
280+ # unbound method
281+ if return_bound :
282+ # bind it on an instance
283+ return True , f .__get__ (host_cls (), host_cls ) # functools.partial(f, host_cls())
284+ else :
285+ return True
286+ else :
287+ # yes: already bound, no binding needed
288+ return (False , f ) if return_bound else False
289+ else :
290+ try :
291+ qname = f .__qualname__
292+ except AttributeError :
293+ return (False , f ) if return_bound else False
294+ else :
295+ if qname == f .__name__ :
296+ # not nested - plain old function in a module
297+ return (False , f ) if return_bound else False
298+ else :
299+ # NESTED in a class or a function or ...
300+ qname_parts = qname .split ("." )
301+
302+ # normal unbound method (since we already eliminated bound ones above with fixed_ismethod(f))
303+ # or static method accessed on an instance or on a class (!)
304+ # or descriptor-created method
305+ # if "__get__" in qname_parts:
306+ # # a method generated by a descriptor - should be already bound but...
307+ # #
308+ # # see https://docs.python.org/3/reference/datamodel.html#object.__set_name__
309+ # # The attribute __objclass__ may indicate that an instance of the given type (or a subclass)
310+ # # is expected or required as the first positional argument
311+ # cls_needed = getattr(f, '__objclass__', None)
312+ # if cls_needed is not None:
313+ # return (True, functools.partial(f, cls_needed())) if return_bound else True
314+ # else:
315+ # return (False, f) if return_bound else False
316+
317+ if qname_parts [- 2 ] == "<locals>" :
318+ # a function generated by another function. most probably does not require binding
319+ # since `get_class_that_defined_method` does not support those (as PEP3155 states)
320+ # we have no choice but to make this assumption.
321+ return (False , f ) if return_bound else False
322+
323+ else :
324+ # unfortunately in order to detect static methods we have no choice: we need the host class
325+ host_cls = get_class_that_defined_method (f )
326+ if host_cls is None :
327+ get_class_that_defined_method (f ) # for debugging, do it again
328+ raise NotImplementedError ("This case does not seem covered, please report" )
329+
330+ # is it a static method (on instance or class, it is the same),
331+ # an unbound classmethod, or an unbound method ?
332+ # To answer we need to go back to the definition
333+ func_def = inspect .getattr_static (host_cls , f .__name__ )
334+ # assert inspect.getattr(host_cls, f.__name__) is f
335+ if isinstance (func_def , staticmethod ):
336+ return (False , f ) if return_bound else False
337+ elif isinstance (func_def , classmethod ):
338+ # unbound class method
339+ if return_bound :
340+ # bind it on the class
341+ return True , f .__get__ (host_cls , host_cls ) # functools.partial(f, host_cls)
342+ else :
343+ return True
344+ else :
345+ # unbound method
346+ if return_bound :
347+ # bind it on an instance
348+ return True , f .__get__ (host_cls (), host_cls ) # functools.partial(f, host_cls())
349+ else :
350+ return True
351+
352+
353+ def is_static_method (cls , func_name , func = None ):
354+ """ Adapted from https://stackoverflow.com/a/64436801/7262247
355+
356+ indeed isinstance(staticmethod) does not work if the method is already bound
357+
358+ :param cls:
359+ :param func_name:
360+ :param func: optional, if you have it already
361+ :return:
362+ """
363+ if func is not None :
364+ assert getattr (cls , func_name ) is func
365+
366+ return isinstance (inspect .getattr_static (cls , func_name ), staticmethod )
367+
368+
369+ def is_class_method (cls , func_name , func = None ):
370+ """ Adapted from https://stackoverflow.com/a/64436801/7262247
371+
372+ indeed isinstance(classmethod) does not work if the method is already bound
373+
374+ :param cls:
375+ :param func_name:
376+ :param func: optional, if you have it already
377+ :return:
378+ """
379+ if func is not None :
380+ assert getattr (cls , func_name ) is func
381+
382+ return isinstance (inspect .getattr_static (cls , func_name ), classmethod )
383+
384+
385+ def is_bound_builtin_method (meth ):
386+ """Helper returning True if meth is a bound built-in method"""
387+ return (inspect .isbuiltin (meth )
388+ and getattr (meth , '__self__' , None ) is not None
389+ and getattr (meth .__self__ , '__class__' , None ))
390+
391+
392+ class HostNotConstructedYet (Exception ):
393+ """Raised by `get_class_that_defined_method` in the situation where the host class is not in the host module yet."""
394+ pass
395+
396+
397+ if PY3 :
398+ # this does not need fixing
399+ fixed_ismethod = inspect .ismethod
400+
401+ def get_class_that_defined_method (meth ):
402+ """from https://stackoverflow.com/a/25959545/7262247
403+
404+ Improved to support nesting, and to raise an Exception if __qualname__ does
405+ not properly work (instead of returning None which may be misleading)
406+
407+ And yes PEP3155 states that __qualname__ should be used for such introspection.
408+ See https://www.python.org/dev/peps/pep-3155/#rationale
409+ """
410+ if isinstance (meth , functools .partial ):
411+ return get_class_that_defined_method (meth .func )
412+
413+ if inspect .ismethod (meth ) or is_bound_builtin_method (meth ):
414+ for cls in inspect .getmro (meth .__self__ .__class__ ):
415+ if meth .__name__ in cls .__dict__ :
416+ return cls
417+ meth = getattr (meth , '__func__' , meth ) # fallback to __qualname__ parsing
418+
419+ if inspect .isfunction (meth ):
420+ host = inspect .getmodule (meth )
421+ host_part = meth .__qualname__ .split ('.<locals>' , 1 )[0 ]
422+ # note: the local part of qname is not walkable see https://www.python.org/dev/peps/pep-3155/#limitations
423+ for item in host_part .split ('.' )[:- 1 ]:
424+ try :
425+ host = getattr (host , item )
426+ except AttributeError :
427+ # non-resolvable __qualname__
428+ raise HostNotConstructedYet (
429+ "__qualname__ is not resolvable, this can happen if the host class of this method "
430+ "%r has not yet been created. PEP3155 does not seem to tell us what we should do "
431+ "in this case." % meth
432+ )
433+ if host is None :
434+ raise ValueError ("__qualname__ leads to `None`, this is strange and not PEP3155 compliant, please "
435+ "report" )
436+
437+ if isinstance (host , type ):
438+ return host
439+
440+ return getattr (meth , '__objclass__' , None ) # handle special descriptor objects
441+
442+ else :
443+ def fixed_ismethod (f ):
444+ """inspect.ismethod does not have the same contract in python 2: it returns True even for bound methods"""
445+ return hasattr (f , '__self__' ) and f .__self__ is not None
446+
447+ def get_class_that_defined_method (meth ):
448+ """from https://stackoverflow.com/a/961057/7262247
449+
450+ Adapted to support partial
451+ """
452+ if isinstance (meth , functools .partial ):
453+ return get_class_that_defined_method (meth .func )
454+
455+ try :
456+ _mro = inspect .getmro (meth .im_class )
457+ except AttributeError :
458+ # no host class
459+ return None
460+ else :
461+ for cls in _mro :
462+ if meth .__name__ in cls .__dict__ :
463+ return cls
464+ return None
249465
250466
251467if PY3 :
0 commit comments