Skip to content

Commit cdd5402

Browse files
authored
Merge pull request #54 from smarie/issue_53
[WIP] Fix for issue #53
2 parents ddb4a93 + 9ad890a commit cdd5402

File tree

3 files changed

+81
-28
lines changed

3 files changed

+81
-28
lines changed

docs/changelog.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
### 1.0.3 - bugfix
4+
5+
* Fixed bug with `super().__init__` not behaving as expected. Fixed [#53](https://github.com/smarie/python-pyfields/issues/53)
6+
37
### 1.0.2 - bugfixes
48

59
* User-provided `nonable` status was wrongly overriden automatically when the field was attached to the class. Fixed [#51](https://github.com/smarie/python-pyfields/issues/51)

pyfields/init_makers.py

Lines changed: 52 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
#
33
# Copyright (c) Schneider Electric Industries, 2019. All right reserved.
44
import sys
5-
from inspect import isfunction
5+
from inspect import isfunction, getmro
66
from itertools import islice
77

88
try:
@@ -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

314338
class InjectedInitFieldsArg(object):
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Authors: Sylvain Marie <[email protected]>
2+
#
3+
# Copyright (c) Schneider Electric Industries, 2019. All right reserved.
4+
5+
from pyfields import field, init_fields, get_field
6+
7+
8+
def test_issue_53():
9+
10+
class A(object):
11+
a = field(str, check_type=True)
12+
13+
@init_fields()
14+
def __init__(self):
15+
pass
16+
17+
class B(A):
18+
b = field(str, check_type=True)
19+
20+
@init_fields()
21+
def __init__(self):
22+
super(B, self).__init__(a=self.a)
23+
24+
# note that with the issue, this was raising an exception
25+
print(B('a', 'b'))

0 commit comments

Comments
 (0)