7
7
"""
8
8
from __future__ import annotations
9
9
10
- import functools
10
+ __all__ = [ "initpkg" , "ApiModule" , "AliasModule" ]
11
11
import sys
12
- import threading
13
- from types import ModuleType
14
12
from typing import Any
15
- from typing import Callable
16
- from typing import cast
17
- from typing import Iterable
18
13
19
14
from ._alias_module import AliasModule
20
- from ._importing import _py_abspath
21
15
from ._importing import distribution_version as distribution_version # NOQA:F401
22
- from ._importing import importobj
16
+ from ._module import _initpkg
17
+ from ._module import ApiModule
23
18
from ._version import version as __version__ # NOQA:F401
24
19
25
- _PRESERVED_MODULE_ATTRS = {
26
- "__file__" ,
27
- "__version__" ,
28
- "__loader__" ,
29
- "__path__" ,
30
- "__package__" ,
31
- "__doc__" ,
32
- "__spec__" ,
33
- "__dict__" ,
34
- }
35
-
36
20
37
21
def initpkg (
38
22
pkgname : str ,
39
23
exportdefs : dict [str , Any ],
40
- attr : dict [str , Any ] | None = None ,
24
+ attr : dict [str , object ] | None = None ,
41
25
eager : bool = False ,
42
26
) -> ApiModule :
43
27
"""initialize given package from the export definitions."""
@@ -53,175 +37,3 @@ def initpkg(
53
37
getattr (module , "__dict__" )
54
38
55
39
return mod
56
-
57
-
58
- def _initpkg (mod : ModuleType | None , pkgname , exportdefs , attr = None ) -> ApiModule :
59
- """Helper for initpkg.
60
-
61
- Python 3.3+ uses finer grained locking for imports, and checks sys.modules before
62
- acquiring the lock to avoid the overhead of the fine-grained locking. This
63
- introduces a race condition when a module is imported by multiple threads
64
- concurrently - some threads will see the initial module and some the replacement
65
- ApiModule. We avoid this by updating the existing module in-place.
66
-
67
- """
68
- if mod is None :
69
- d = {"__file__" : None , "__spec__" : None }
70
- d .update (attr )
71
- mod = ApiModule (pkgname , exportdefs , implprefix = pkgname , attr = d )
72
- sys .modules [pkgname ] = mod
73
- return mod
74
- else :
75
- f = getattr (mod , "__file__" , None )
76
- if f :
77
- f = _py_abspath (f )
78
- mod .__file__ = f
79
- if hasattr (mod , "__path__" ):
80
- mod .__path__ = [_py_abspath (p ) for p in mod .__path__ ]
81
- if "__doc__" in exportdefs and hasattr (mod , "__doc__" ):
82
- del mod .__doc__
83
- for name in dir (mod ):
84
- if name not in _PRESERVED_MODULE_ATTRS :
85
- delattr (mod , name )
86
-
87
- # Updating class of existing module as per importlib.util.LazyLoader
88
- mod .__class__ = ApiModule
89
- apimod = cast (ApiModule , mod )
90
- ApiModule .__init__ (apimod , pkgname , exportdefs , implprefix = pkgname , attr = attr )
91
- return apimod
92
-
93
-
94
- def _synchronized (wrapped_function ):
95
- """Decorator to synchronise __getattr__ calls."""
96
-
97
- # Lock shared between all instances of ApiModule to avoid possible deadlocks
98
- lock = threading .RLock ()
99
-
100
- @functools .wraps (wrapped_function )
101
- def synchronized_wrapper_function (* args , ** kwargs ):
102
- with lock :
103
- return wrapped_function (* args , ** kwargs )
104
-
105
- return synchronized_wrapper_function
106
-
107
-
108
- class ApiModule (ModuleType ):
109
- """the magical lazy-loading module standing"""
110
-
111
- def __docget (self ) -> str | None :
112
- try :
113
- return self .__doc
114
- except AttributeError :
115
- if "__doc__" in self .__map__ :
116
- return cast (str , self .__makeattr ("__doc__" ))
117
- else :
118
- return None
119
-
120
- def __docset (self , value : str ) -> None :
121
- self .__doc = value
122
-
123
- __doc__ = property (__docget , __docset ) # type: ignore
124
- __map__ : dict [str , tuple [str , str ]]
125
-
126
- def __init__ (
127
- self ,
128
- name : str ,
129
- importspec : dict [str , Any ],
130
- implprefix : str | None = None ,
131
- attr : dict [str , Any ] | None = None ,
132
- ) -> None :
133
- super ().__init__ (name )
134
- self .__name__ = name
135
- self .__all__ = [x for x in importspec if x != "__onfirstaccess__" ]
136
- self .__map__ = {}
137
- self .__implprefix__ = implprefix or name
138
- if attr :
139
- for name , val in attr .items ():
140
- setattr (self , name , val )
141
- for name , importspec in importspec .items ():
142
- if isinstance (importspec , dict ):
143
- subname = f"{ self .__name__ } .{ name } "
144
- apimod = ApiModule (subname , importspec , implprefix )
145
- sys .modules [subname ] = apimod
146
- setattr (self , name , apimod )
147
- else :
148
- parts = importspec .split (":" )
149
- modpath = parts .pop (0 )
150
- attrname = parts and parts [0 ] or ""
151
- if modpath [0 ] == "." :
152
- modpath = implprefix + modpath
153
-
154
- if not attrname :
155
- subname = f"{ self .__name__ } .{ name } "
156
- apimod = AliasModule (subname , modpath )
157
- sys .modules [subname ] = apimod
158
- if "." not in name :
159
- setattr (self , name , apimod )
160
- else :
161
- self .__map__ [name ] = (modpath , attrname )
162
-
163
- def __repr__ (self ):
164
- repr_list = [f"<ApiModule { self .__name__ !r} " ]
165
- if hasattr (self , "__version__" ):
166
- repr_list .append (f" version={ self .__version__ !r} " )
167
- if hasattr (self , "__file__" ):
168
- repr_list .append (f" from { self .__file__ !r} " )
169
- repr_list .append (">" )
170
- return "" .join (repr_list )
171
-
172
- @_synchronized
173
- def __makeattr (self , name , isgetattr = False ):
174
- """lazily compute value for name or raise AttributeError if unknown."""
175
- target = None
176
- if "__onfirstaccess__" in self .__map__ :
177
- target = self .__map__ .pop ("__onfirstaccess__" )
178
- fn = cast (Callable [[], None ], importobj (* target ))
179
- fn ()
180
- try :
181
- modpath , attrname = self .__map__ [name ]
182
- except KeyError :
183
- # __getattr__ is called when the attribute does not exist, but it may have
184
- # been set by the onfirstaccess call above. Infinite recursion is not
185
- # possible as __onfirstaccess__ is removed before the call (unless the call
186
- # adds __onfirstaccess__ to __map__ explicitly, which is not our problem)
187
- if target is not None and name != "__onfirstaccess__" :
188
- return getattr (self , name )
189
- # Attribute may also have been set during a concurrent call to __getattr__
190
- # which executed after this call was already waiting on the lock. Check
191
- # for a recently set attribute while avoiding infinite recursion:
192
- # * Don't call __getattribute__ if __makeattr was called from a data
193
- # descriptor such as the __doc__ or __dict__ properties, since data
194
- # descriptors are called as part of object.__getattribute__
195
- # * Only call __getattribute__ if there is a possibility something has set
196
- # the attribute we're looking for since __getattr__ was called
197
- if threading is not None and isgetattr :
198
- return super ().__getattribute__ (name )
199
- raise AttributeError (name )
200
- else :
201
- result = importobj (modpath , attrname )
202
- setattr (self , name , result )
203
- # in a recursive-import situation a double-del can happen
204
- self .__map__ .pop (name , None )
205
- return result
206
-
207
- def __getattr__ (self , name ):
208
- return self .__makeattr (name , isgetattr = True )
209
-
210
- def __dir__ (self ) -> Iterable [str ]:
211
- yield from super ().__dir__ ()
212
- yield from self .__map__
213
-
214
- @property
215
- def __dict__ (self ) -> dict [str , Any ]: # type: ignore
216
- # force all the content of the module
217
- # to be loaded when __dict__ is read
218
- dictdescr = ModuleType .__dict__ ["__dict__" ] # type: ignore
219
- ns : dict [str , Any ] = dictdescr .__get__ (self )
220
- if ns is not None :
221
- hasattr (self , "some" )
222
- for name in self .__all__ :
223
- try :
224
- self .__makeattr (name )
225
- except AttributeError :
226
- pass
227
- return ns
0 commit comments