9
9
import sys
10
10
from types import ModuleType
11
11
12
+ # Prior to Python 3.7 threading support was optional
13
+ try :
14
+ import threading
15
+ except ImportError :
16
+ threading = None
17
+ else :
18
+ import functools
19
+
12
20
from .version import version as __version__ # NOQA:F401
13
21
14
22
@@ -83,6 +91,22 @@ def importobj(modpath, attrname):
83
91
return retval
84
92
85
93
94
+ def _synchronized (wrapped_function ):
95
+ """Decorator to synchronise __getattr__ calls."""
96
+ if threading is None :
97
+ return wrapped_function
98
+
99
+ # Lock shared between all instances of ApiModule to avoid possible deadlocks
100
+ lock = threading .RLock ()
101
+
102
+ @functools .wraps (wrapped_function )
103
+ def synchronized_wrapper_function (* args , ** kwargs ):
104
+ with lock :
105
+ return wrapped_function (* args , ** kwargs )
106
+
107
+ return synchronized_wrapper_function
108
+
109
+
86
110
class ApiModule (ModuleType ):
87
111
"""the magical lazy-loading module standing"""
88
112
@@ -105,7 +129,6 @@ def __init__(self, name, importspec, implprefix=None, attr=None):
105
129
self .__implprefix__ = implprefix or name
106
130
if attr :
107
131
for name , val in attr .items ():
108
- # print "setting", self.__name__, name, val
109
132
setattr (self , name , val )
110
133
for name , importspec in importspec .items ():
111
134
if isinstance (importspec , dict ):
@@ -139,19 +162,32 @@ def __repr__(self):
139
162
return "<ApiModule {!r} {}>" .format (self .__name__ , " " .join (repr_list ))
140
163
return "<ApiModule {!r}>" .format (self .__name__ )
141
164
142
- def __makeattr (self , name ):
165
+ @_synchronized
166
+ def __makeattr (self , name , isgetattr = False ):
143
167
"""lazily compute value for name or raise AttributeError if unknown."""
144
- # print "makeattr", self.__name__, name
145
168
target = None
146
169
if "__onfirstaccess__" in self .__map__ :
147
170
target = self .__map__ .pop ("__onfirstaccess__" )
148
171
importobj (* target )()
149
172
try :
150
173
modpath , attrname = self .__map__ [name ]
151
174
except KeyError :
175
+ # __getattr__ is called when the attribute does not exist, but it may have
176
+ # been set by the onfirstaccess call above. Infinite recursion is not
177
+ # possible as __onfirstaccess__ is removed before the call (unless the call
178
+ # adds __onfirstaccess__ to __map__ explicitly, which is not our problem)
152
179
if target is not None and name != "__onfirstaccess__" :
153
- # retry, onfirstaccess might have set attrs
154
180
return getattr (self , name )
181
+ # Attribute may also have been set during a concurrent call to __getattr__
182
+ # which executed after this call was already waiting on the lock. Check
183
+ # for a recently set attribute while avoiding infinite recursion:
184
+ # * Don't call __getattribute__ if __makeattr was called from a data
185
+ # descriptor such as the __doc__ or __dict__ properties, since data
186
+ # descriptors are called as part of object.__getattribute__
187
+ # * Only call __getattribute__ if there is a possibility something has set
188
+ # the attribute we're looking for since __getattr__ was called
189
+ if threading is not None and isgetattr :
190
+ return super (ApiModule , self ).__getattribute__ (name )
155
191
raise AttributeError (name )
156
192
else :
157
193
result = importobj (modpath , attrname )
@@ -162,7 +198,8 @@ def __makeattr(self, name):
162
198
pass # in a recursive-import situation a double-del can happen
163
199
return result
164
200
165
- __getattr__ = __makeattr
201
+ def __getattr__ (self , name ):
202
+ return self .__makeattr (name , isgetattr = True )
166
203
167
204
@property
168
205
def __dict__ (self ):
0 commit comments