22#
33# Copyright (c) Schneider Electric Industries, 2019. All right reserved.
44import sys
5- from inspect import isfunction
5+ from inspect import isfunction , getmro
66from itertools import islice
77
88try :
@@ -258,7 +258,8 @@ class InitDescriptor(object):
258258
259259 Inspired by https://stackoverflow.com/a/3412743/7262247
260260 """
261- __slots__ = 'fields' , 'user_init_is_injected' , 'user_init_fun' , 'user_init_args_before' , 'ancestor_fields_first'
261+ __slots__ = 'fields' , 'user_init_is_injected' , 'user_init_fun' , 'user_init_args_before' , 'ancestor_fields_first' , \
262+ 'ownercls'
262263
263264 def __init__ (self , fields = None , user_init_is_injected = False , user_init_fun = None , user_init_args_before = True ,
264265 ancestor_fields_first = None ):
@@ -273,11 +274,15 @@ def __init__(self, fields=None, user_init_is_injected=False, user_init_fun=None,
273274 elif fields is not None :
274275 raise ValueError ("`ancestor_fields_first` is only applicable when `fields` is empty" )
275276 self .ancestor_fields_first = ancestor_fields_first
277+ self .ownercls = None
276278
277- # not useful and may slow things down anyway
278- # def __set_name__(self, owner, name):
279- # if name != '__init__':
280- # raise ValueError("this should not happen")
279+ def __set_name__ (self , owner , name ):
280+ """
281+ There is a python issue with init descriptors with super() access. To fix it we need to
282+ remember the owner class type separately as we cant' trust the one received in __get__.
283+ See https://github.com/smarie/python-pyfields/issues/53
284+ """
285+ self .ownercls = owner
281286
282287 def __get__ (self , obj , objtype ):
283288 # type: (...) -> Callable
@@ -287,28 +292,47 @@ def __get__(self, obj, objtype):
287292 it creates the `__init__` method, replaces itself with it, and returns it. Subsequent calls will directly
288293 be routed to the new init method and not here.
289294 """
290- if objtype is not None :
291- # <objtype>.__init__ has been accessed. Create the modified init
292- fields = self .fields
293- if fields is None :
294- # fields have not been provided explicitly, collect them all.
295- fields = get_fields (objtype , include_inherited = True , ancestors_first = self .ancestor_fields_first ,
296- _auto_fix_fields = not PY36 )
297- elif not PY36 :
298- # take this opportunity to apply all field names including inherited
299- # TODO set back inherited = False when the bug with class-level access is solved -> make_init will be ok
300- get_fields (objtype , include_inherited = True , ancestors_first = self .ancestor_fields_first ,
301- _auto_fix_fields = True )
302-
303- # create the init method
304- new_init = create_init (fields = fields , inject_fields = self .user_init_is_injected ,
305- user_init_fun = self .user_init_fun , user_init_args_before = self .user_init_args_before )
306-
307- # replace it forever in the class
308- setattr (objtype , '__init__' , new_init )
309-
310- # return the new init
311- return new_init .__get__ (obj , objtype )
295+ # objtype is not reliable: when called through super() it does not contain the right class.
296+ # see https://github.com/smarie/python-pyfields/issues/53
297+ if self .ownercls is not None :
298+ objtype = self .ownercls
299+ elif objtype is not None :
300+ # workaround in case of python < 3.6: at least, when a subclass init is created, make sure that all super
301+ # classes init have their owner class properly set, .
302+ # That way, when the subclass __init__ will be called, containing potential calls to super(), the parents'
303+ # __init__ method descriptors will be correctly configured.
304+ for _c in reversed (getmro (objtype )[1 :- 1 ]):
305+ try :
306+ _init_member = _c .__dict__ ['__init__' ]
307+ except KeyError :
308+ continue
309+ else :
310+ if isinstance (_init_member , InitDescriptor ):
311+ if _init_member .ownercls is None :
312+ # call __set_name__ explicitly (python < 3.6) to register the descriptor with the class
313+ _init_member .__set_name__ (_c , '__init__' )
314+
315+ # <objtype>.__init__ has been accessed. Create the modified init
316+ fields = self .fields
317+ if fields is None :
318+ # fields have not been provided explicitly, collect them all.
319+ fields = get_fields (objtype , include_inherited = True , ancestors_first = self .ancestor_fields_first ,
320+ _auto_fix_fields = not PY36 )
321+ elif not PY36 :
322+ # take this opportunity to apply all field names including inherited
323+ # TODO set back inherited = False when the bug with class-level access is solved -> make_init will be ok
324+ get_fields (objtype , include_inherited = True , ancestors_first = self .ancestor_fields_first ,
325+ _auto_fix_fields = True )
326+
327+ # create the init method
328+ new_init = create_init (fields = fields , inject_fields = self .user_init_is_injected ,
329+ user_init_fun = self .user_init_fun , user_init_args_before = self .user_init_args_before )
330+
331+ # replace it forever in the class
332+ setattr (objtype , '__init__' , new_init )
333+
334+ # return the new init
335+ return new_init .__get__ (obj , objtype )
312336
313337
314338class InjectedInitFieldsArg (object ):
0 commit comments