OLD | NEW |
1 # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. | 1 # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr | 2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr |
3 # | 3 # |
4 # This file is part of astroid. | 4 # This file is part of astroid. |
5 # | 5 # |
6 # astroid is free software: you can redistribute it and/or modify it | 6 # astroid is free software: you can redistribute it and/or modify it |
7 # under the terms of the GNU Lesser General Public License as published by the | 7 # under the terms of the GNU Lesser General Public License as published by the |
8 # Free Software Foundation, either version 2.1 of the License, or (at your | 8 # Free Software Foundation, either version 2.1 of the License, or (at your |
9 # option) any later version. | 9 # option) any later version. |
10 # | 10 # |
11 # astroid is distributed in the hope that it will be useful, but | 11 # astroid is distributed in the hope that it will be useful, but |
12 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | 12 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
13 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License | 13 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License |
14 # for more details. | 14 # for more details. |
15 # | 15 # |
16 # You should have received a copy of the GNU Lesser General Public License along | 16 # You should have received a copy of the GNU Lesser General Public License along |
17 # with astroid. If not, see <http://www.gnu.org/licenses/>. | 17 # with astroid. If not, see <http://www.gnu.org/licenses/>. |
18 """astroid manager: avoid multiple astroid build of a same module when | 18 """astroid manager: avoid multiple astroid build of a same module when |
19 possible by providing a class responsible to get astroid representation | 19 possible by providing a class responsible to get astroid representation |
20 from various source and using a cache of built modules) | 20 from various source and using a cache of built modules) |
21 """ | 21 """ |
| 22 from __future__ import print_function |
22 | 23 |
23 __docformat__ = "restructuredtext en" | 24 __docformat__ = "restructuredtext en" |
24 | 25 |
| 26 import collections |
| 27 import imp |
25 import os | 28 import os |
26 from os.path import dirname, join, isdir, exists | 29 from os.path import dirname, join, isdir, exists |
27 from warnings import warn | 30 from warnings import warn |
| 31 import zipimport |
28 | 32 |
29 from logilab.common.configuration import OptionsProviderMixIn | 33 from logilab.common.configuration import OptionsProviderMixIn |
30 | 34 |
31 from astroid.exceptions import AstroidBuildingException | 35 from astroid.exceptions import AstroidBuildingException |
32 from astroid.modutils import NoSourceFile, is_python_source, \ | 36 from astroid import modutils |
33 file_from_modpath, load_module_from_name, modpath_from_file, \ | |
34 get_module_files, get_source_file, zipimport | |
35 | 37 |
36 | 38 |
37 def astroid_wrapper(func, modname): | 39 def astroid_wrapper(func, modname): |
38 """wrapper to give to AstroidManager.project_from_files""" | 40 """wrapper to give to AstroidManager.project_from_files""" |
39 print 'parsing %s...' % modname | 41 print('parsing %s...' % modname) |
40 try: | 42 try: |
41 return func(modname) | 43 return func(modname) |
42 except AstroidBuildingException, exc: | 44 except AstroidBuildingException as exc: |
43 print exc | 45 print(exc) |
44 except Exception, exc: | 46 except Exception as exc: |
45 import traceback | 47 import traceback |
46 traceback.print_exc() | 48 traceback.print_exc() |
47 | 49 |
48 def _silent_no_wrap(func, modname): | 50 def _silent_no_wrap(func, modname): |
49 """silent wrapper that doesn't do anything; can be used for tests""" | 51 """silent wrapper that doesn't do anything; can be used for tests""" |
50 return func(modname) | 52 return func(modname) |
51 | 53 |
52 def safe_repr(obj): | 54 def safe_repr(obj): |
53 try: | 55 try: |
54 return repr(obj) | 56 return repr(obj) |
(...skipping 23 matching lines...) Expand all Loading... |
78 ) | 80 ) |
79 brain = {} | 81 brain = {} |
80 def __init__(self): | 82 def __init__(self): |
81 self.__dict__ = AstroidManager.brain | 83 self.__dict__ = AstroidManager.brain |
82 if not self.__dict__: | 84 if not self.__dict__: |
83 OptionsProviderMixIn.__init__(self) | 85 OptionsProviderMixIn.__init__(self) |
84 self.load_defaults() | 86 self.load_defaults() |
85 # NOTE: cache entries are added by the [re]builder | 87 # NOTE: cache entries are added by the [re]builder |
86 self.astroid_cache = {} | 88 self.astroid_cache = {} |
87 self._mod_file_cache = {} | 89 self._mod_file_cache = {} |
88 self.transforms = {} | 90 self.transforms = collections.defaultdict(list) |
| 91 self._failed_import_hooks = [] |
| 92 self.always_load_extensions = False |
| 93 self.extension_package_whitelist = set() |
89 | 94 |
90 def ast_from_file(self, filepath, modname=None, fallback=True, source=False)
: | 95 def ast_from_file(self, filepath, modname=None, fallback=True, source=False)
: |
91 """given a module name, return the astroid object""" | 96 """given a module name, return the astroid object""" |
92 try: | 97 try: |
93 filepath = get_source_file(filepath, include_no_ext=True) | 98 filepath = modutils.get_source_file(filepath, include_no_ext=True) |
94 source = True | 99 source = True |
95 except NoSourceFile: | 100 except modutils.NoSourceFile: |
96 pass | 101 pass |
97 if modname is None: | 102 if modname is None: |
98 try: | 103 try: |
99 modname = '.'.join(modpath_from_file(filepath)) | 104 modname = '.'.join(modutils.modpath_from_file(filepath)) |
100 except ImportError: | 105 except ImportError: |
101 modname = filepath | 106 modname = filepath |
102 if modname in self.astroid_cache and self.astroid_cache[modname].file ==
filepath: | 107 if modname in self.astroid_cache and self.astroid_cache[modname].file ==
filepath: |
103 return self.astroid_cache[modname] | 108 return self.astroid_cache[modname] |
104 if source: | 109 if source: |
105 from astroid.builder import AstroidBuilder | 110 from astroid.builder import AstroidBuilder |
106 return AstroidBuilder(self).file_build(filepath, modname) | 111 return AstroidBuilder(self).file_build(filepath, modname) |
107 elif fallback and modname: | 112 elif fallback and modname: |
108 return self.ast_from_module_name(modname) | 113 return self.ast_from_module_name(modname) |
109 raise AstroidBuildingException('unable to get astroid for file %s' % | 114 raise AstroidBuildingException('unable to get astroid for file %s' % |
110 filepath) | 115 filepath) |
111 | 116 |
| 117 def _build_stub_module(self, modname): |
| 118 from astroid.builder import AstroidBuilder |
| 119 return AstroidBuilder(self).string_build('', modname) |
| 120 |
| 121 def _can_load_extension(self, modname): |
| 122 if self.always_load_extensions: |
| 123 return True |
| 124 if modutils.is_standard_module(modname): |
| 125 return True |
| 126 parts = modname.split('.') |
| 127 return any( |
| 128 '.'.join(parts[:x]) in self.extension_package_whitelist |
| 129 for x in range(1, len(parts) + 1)) |
| 130 |
112 def ast_from_module_name(self, modname, context_file=None): | 131 def ast_from_module_name(self, modname, context_file=None): |
113 """given a module name, return the astroid object""" | 132 """given a module name, return the astroid object""" |
114 if modname in self.astroid_cache: | 133 if modname in self.astroid_cache: |
115 return self.astroid_cache[modname] | 134 return self.astroid_cache[modname] |
116 if modname == '__main__': | 135 if modname == '__main__': |
117 from astroid.builder import AstroidBuilder | 136 return self._build_stub_module(modname) |
118 return AstroidBuilder(self).string_build('', modname) | |
119 old_cwd = os.getcwd() | 137 old_cwd = os.getcwd() |
120 if context_file: | 138 if context_file: |
121 os.chdir(dirname(context_file)) | 139 os.chdir(dirname(context_file)) |
122 try: | 140 try: |
123 filepath = self.file_from_module_name(modname, context_file) | 141 filepath, mp_type = self.file_from_module_name(modname, context_file
) |
124 if filepath is not None and not is_python_source(filepath): | 142 if mp_type == modutils.PY_ZIPMODULE: |
125 module = self.zip_import_data(filepath) | 143 module = self.zip_import_data(filepath) |
126 if module is not None: | 144 if module is not None: |
127 return module | 145 return module |
128 if filepath is None or not is_python_source(filepath): | 146 elif mp_type in (imp.C_BUILTIN, imp.C_EXTENSION): |
| 147 if mp_type == imp.C_EXTENSION and not self._can_load_extension(m
odname): |
| 148 return self._build_stub_module(modname) |
129 try: | 149 try: |
130 module = load_module_from_name(modname) | 150 module = modutils.load_module_from_name(modname) |
131 except Exception, ex: | 151 except Exception as ex: |
132 msg = 'Unable to load module %s (%s)' % (modname, ex) | 152 msg = 'Unable to load module %s (%s)' % (modname, ex) |
133 raise AstroidBuildingException(msg) | 153 raise AstroidBuildingException(msg) |
134 return self.ast_from_module(module, modname) | 154 return self.ast_from_module(module, modname) |
| 155 elif mp_type == imp.PY_COMPILED: |
| 156 raise AstroidBuildingException("Unable to load compiled module %
s" % (modname,)) |
| 157 if filepath is None: |
| 158 raise AstroidBuildingException("Unable to load module %s" % (mod
name,)) |
135 return self.ast_from_file(filepath, modname, fallback=False) | 159 return self.ast_from_file(filepath, modname, fallback=False) |
| 160 except AstroidBuildingException as e: |
| 161 for hook in self._failed_import_hooks: |
| 162 try: |
| 163 return hook(modname) |
| 164 except AstroidBuildingException: |
| 165 pass |
| 166 raise e |
136 finally: | 167 finally: |
137 os.chdir(old_cwd) | 168 os.chdir(old_cwd) |
138 | 169 |
139 def zip_import_data(self, filepath): | 170 def zip_import_data(self, filepath): |
140 if zipimport is None: | 171 if zipimport is None: |
141 return None | 172 return None |
142 from astroid.builder import AstroidBuilder | 173 from astroid.builder import AstroidBuilder |
143 builder = AstroidBuilder(self) | 174 builder = AstroidBuilder(self) |
144 for ext in ('.zip', '.egg'): | 175 for ext in ('.zip', '.egg'): |
145 try: | 176 try: |
146 eggpath, resource = filepath.rsplit(ext + '/', 1) | 177 eggpath, resource = filepath.rsplit(ext + os.path.sep, 1) |
147 except ValueError: | 178 except ValueError: |
148 continue | 179 continue |
149 try: | 180 try: |
150 importer = zipimport.zipimporter(eggpath + ext) | 181 importer = zipimport.zipimporter(eggpath + ext) |
151 zmodname = resource.replace('/', '.') | 182 zmodname = resource.replace(os.path.sep, '.') |
152 if importer.is_package(resource): | 183 if importer.is_package(resource): |
153 zmodname = zmodname + '.__init__' | 184 zmodname = zmodname + '.__init__' |
154 module = builder.string_build(importer.get_source(resource), | 185 module = builder.string_build(importer.get_source(resource), |
155 zmodname, filepath) | 186 zmodname, filepath) |
156 return module | 187 return module |
157 except: | 188 except: |
158 continue | 189 continue |
159 return None | 190 return None |
160 | 191 |
161 def file_from_module_name(self, modname, contextfile): | 192 def file_from_module_name(self, modname, contextfile): |
162 try: | 193 try: |
163 value = self._mod_file_cache[(modname, contextfile)] | 194 value = self._mod_file_cache[(modname, contextfile)] |
164 except KeyError: | 195 except KeyError: |
165 try: | 196 try: |
166 value = file_from_modpath(modname.split('.'), | 197 value = modutils.file_info_from_modpath( |
167 context_file=contextfile) | 198 modname.split('.'), context_file=contextfile) |
168 except ImportError, ex: | 199 except ImportError as ex: |
169 msg = 'Unable to load module %s (%s)' % (modname, ex) | 200 msg = 'Unable to load module %s (%s)' % (modname, ex) |
170 value = AstroidBuildingException(msg) | 201 value = AstroidBuildingException(msg) |
171 self._mod_file_cache[(modname, contextfile)] = value | 202 self._mod_file_cache[(modname, contextfile)] = value |
172 if isinstance(value, AstroidBuildingException): | 203 if isinstance(value, AstroidBuildingException): |
173 raise value | 204 raise value |
174 return value | 205 return value |
175 | 206 |
176 def ast_from_module(self, module, modname=None): | 207 def ast_from_module(self, module, modname=None): |
177 """given an imported module, return the astroid object""" | 208 """given an imported module, return the astroid object""" |
178 modname = modname or module.__name__ | 209 modname = modname or module.__name__ |
179 if modname in self.astroid_cache: | 210 if modname in self.astroid_cache: |
180 return self.astroid_cache[modname] | 211 return self.astroid_cache[modname] |
181 try: | 212 try: |
182 # some builtin modules don't have __file__ attribute | 213 # some builtin modules don't have __file__ attribute |
183 filepath = module.__file__ | 214 filepath = module.__file__ |
184 if is_python_source(filepath): | 215 if modutils.is_python_source(filepath): |
185 return self.ast_from_file(filepath, modname) | 216 return self.ast_from_file(filepath, modname) |
186 except AttributeError: | 217 except AttributeError: |
187 pass | 218 pass |
188 from astroid.builder import AstroidBuilder | 219 from astroid.builder import AstroidBuilder |
189 return AstroidBuilder(self).module_build(module, modname) | 220 return AstroidBuilder(self).module_build(module, modname) |
190 | 221 |
191 def ast_from_class(self, klass, modname=None): | 222 def ast_from_class(self, klass, modname=None): |
192 """get astroid for the given class""" | 223 """get astroid for the given class""" |
193 if modname is None: | 224 if modname is None: |
194 try: | 225 try: |
195 modname = klass.__module__ | 226 modname = klass.__module__ |
196 except AttributeError: | 227 except AttributeError: |
197 raise AstroidBuildingException( | 228 raise AstroidBuildingException( |
198 'Unable to get module for class %s' % safe_repr(klass)) | 229 'Unable to get module for class %s' % safe_repr(klass)) |
199 modastroid = self.ast_from_module_name(modname) | 230 modastroid = self.ast_from_module_name(modname) |
200 return modastroid.getattr(klass.__name__)[0] # XXX | 231 return modastroid.getattr(klass.__name__)[0] # XXX |
201 | 232 |
202 | 233 |
203 def infer_ast_from_something(self, obj, context=None): | 234 def infer_ast_from_something(self, obj, context=None): |
204 """infer astroid for the given class""" | 235 """infer astroid for the given class""" |
205 if hasattr(obj, '__class__') and not isinstance(obj, type): | 236 if hasattr(obj, '__class__') and not isinstance(obj, type): |
206 klass = obj.__class__ | 237 klass = obj.__class__ |
207 else: | 238 else: |
208 klass = obj | 239 klass = obj |
209 try: | 240 try: |
210 modname = klass.__module__ | 241 modname = klass.__module__ |
211 except AttributeError: | 242 except AttributeError: |
212 raise AstroidBuildingException( | 243 raise AstroidBuildingException( |
213 'Unable to get module for %s' % safe_repr(klass)) | 244 'Unable to get module for %s' % safe_repr(klass)) |
214 except Exception, ex: | 245 except Exception as ex: |
215 raise AstroidBuildingException( | 246 raise AstroidBuildingException( |
216 'Unexpected error while retrieving module for %s: %s' | 247 'Unexpected error while retrieving module for %s: %s' |
217 % (safe_repr(klass), ex)) | 248 % (safe_repr(klass), ex)) |
218 try: | 249 try: |
219 name = klass.__name__ | 250 name = klass.__name__ |
220 except AttributeError: | 251 except AttributeError: |
221 raise AstroidBuildingException( | 252 raise AstroidBuildingException( |
222 'Unable to get name for %s' % safe_repr(klass)) | 253 'Unable to get name for %s' % safe_repr(klass)) |
223 except Exception, ex: | 254 except Exception as ex: |
224 raise AstroidBuildingException( | 255 raise AstroidBuildingException( |
225 'Unexpected error while retrieving name for %s: %s' | 256 'Unexpected error while retrieving name for %s: %s' |
226 % (safe_repr(klass), ex)) | 257 % (safe_repr(klass), ex)) |
227 # take care, on living object __module__ is regularly wrong :( | 258 # take care, on living object __module__ is regularly wrong :( |
228 modastroid = self.ast_from_module_name(modname) | 259 modastroid = self.ast_from_module_name(modname) |
229 if klass is obj: | 260 if klass is obj: |
230 for infered in modastroid.igetattr(name, context): | 261 for infered in modastroid.igetattr(name, context): |
231 yield infered | 262 yield infered |
232 else: | 263 else: |
233 for infered in modastroid.igetattr(name, context): | 264 for infered in modastroid.igetattr(name, context): |
234 yield infered.instanciate_class() | 265 yield infered.instanciate_class() |
235 | 266 |
236 def project_from_files(self, files, func_wrapper=astroid_wrapper, | 267 def project_from_files(self, files, func_wrapper=astroid_wrapper, |
237 project_name=None, black_list=None): | 268 project_name=None, black_list=None): |
238 """return a Project from a list of files or modules""" | 269 """return a Project from a list of files or modules""" |
239 # build the project representation | 270 # build the project representation |
240 project_name = project_name or self.config.project | 271 project_name = project_name or self.config.project |
241 black_list = black_list or self.config.black_list | 272 black_list = black_list or self.config.black_list |
242 project = Project(project_name) | 273 project = Project(project_name) |
243 for something in files: | 274 for something in files: |
244 if not exists(something): | 275 if not exists(something): |
245 fpath = file_from_modpath(something.split('.')) | 276 fpath = modutils.file_from_modpath(something.split('.')) |
246 elif isdir(something): | 277 elif isdir(something): |
247 fpath = join(something, '__init__.py') | 278 fpath = join(something, '__init__.py') |
248 else: | 279 else: |
249 fpath = something | 280 fpath = something |
250 astroid = func_wrapper(self.ast_from_file, fpath) | 281 astroid = func_wrapper(self.ast_from_file, fpath) |
251 if astroid is None: | 282 if astroid is None: |
252 continue | 283 continue |
253 # XXX why is first file defining the project.path ? | 284 # XXX why is first file defining the project.path ? |
254 project.path = project.path or astroid.file | 285 project.path = project.path or astroid.file |
255 project.add_module(astroid) | 286 project.add_module(astroid) |
256 base_name = astroid.name | 287 base_name = astroid.name |
257 # recurse in package except if __init__ was explicitly given | 288 # recurse in package except if __init__ was explicitly given |
258 if astroid.package and something.find('__init__') == -1: | 289 if astroid.package and something.find('__init__') == -1: |
259 # recurse on others packages / modules if this is a package | 290 # recurse on others packages / modules if this is a package |
260 for fpath in get_module_files(dirname(astroid.file), | 291 for fpath in modutils.get_module_files(dirname(astroid.file), |
261 black_list): | 292 black_list): |
262 astroid = func_wrapper(self.ast_from_file, fpath) | 293 astroid = func_wrapper(self.ast_from_file, fpath) |
263 if astroid is None or astroid.name == base_name: | 294 if astroid is None or astroid.name == base_name: |
264 continue | 295 continue |
265 project.add_module(astroid) | 296 project.add_module(astroid) |
266 return project | 297 return project |
267 | 298 |
268 def register_transform(self, node_class, transform, predicate=None): | 299 def register_transform(self, node_class, transform, predicate=None): |
269 """Register `transform(node)` function to be applied on the given | 300 """Register `transform(node)` function to be applied on the given |
270 Astroid's `node_class` if `predicate` is None or return a true value | 301 Astroid's `node_class` if `predicate` is None or returns true |
271 when called with the node as argument. | 302 when called with the node as argument. |
272 | 303 |
273 The transform function may return a value which is then used to | 304 The transform function may return a value which is then used to |
274 substitute the original node in the tree. | 305 substitute the original node in the tree. |
275 """ | 306 """ |
276 self.transforms.setdefault(node_class, []).append((transform, predicate)
) | 307 self.transforms[node_class].append((transform, predicate)) |
277 | 308 |
278 def unregister_transform(self, node_class, transform, predicate=None): | 309 def unregister_transform(self, node_class, transform, predicate=None): |
279 """Unregister the given transform.""" | 310 """Unregister the given transform.""" |
280 self.transforms[node_class].remove((transform, predicate)) | 311 self.transforms[node_class].remove((transform, predicate)) |
281 | 312 |
| 313 def register_failed_import_hook(self, hook): |
| 314 """"Registers a hook to resolve imports that cannot be found otherwise. |
| 315 |
| 316 `hook` must be a function that accepts a single argument `modname` which |
| 317 contains the name of the module or package that could not be imported. |
| 318 If `hook` can resolve the import, must return a node of type `astroid.Mo
dule`, |
| 319 otherwise, it must raise `AstroidBuildingException`. |
| 320 """ |
| 321 self._failed_import_hooks.append(hook) |
| 322 |
282 def transform(self, node): | 323 def transform(self, node): |
283 """Call matching transforms for the given node if any and return the | 324 """Call matching transforms for the given node if any and return the |
284 transformed node. | 325 transformed node. |
285 """ | 326 """ |
286 cls = node.__class__ | 327 cls = node.__class__ |
287 if cls not in self.transforms: | 328 if cls not in self.transforms: |
288 # no transform registered for this class of node | 329 # no transform registered for this class of node |
289 return node | 330 return node |
290 | 331 |
291 transforms = self.transforms[cls] | 332 transforms = self.transforms[cls] |
292 orig_node = node # copy the reference | 333 orig_node = node # copy the reference |
293 for transform_func, predicate in transforms: | 334 for transform_func, predicate in transforms: |
294 if predicate is None or predicate(node): | 335 if predicate is None or predicate(node): |
295 ret = transform_func(node) | 336 ret = transform_func(node) |
296 # if the transformation function returns something, it's | 337 # if the transformation function returns something, it's |
297 # expected to be a replacement for the node | 338 # expected to be a replacement for the node |
298 if ret is not None: | 339 if ret is not None: |
299 if node is not orig_node: | 340 if node is not orig_node: |
300 # node has already be modified by some previous | 341 # node has already be modified by some previous |
301 # transformation, warn about it | 342 # transformation, warn about it |
302 warn('node %s substituted multiple times' % node) | 343 warn('node %s substituted multiple times' % node) |
303 node = ret | 344 node = ret |
304 return node | 345 return node |
305 | 346 |
306 def cache_module(self, module): | 347 def cache_module(self, module): |
307 """Cache a module if no module with the same name is known yet.""" | 348 """Cache a module if no module with the same name is known yet.""" |
308 self.astroid_cache.setdefault(module.name, module) | 349 self.astroid_cache.setdefault(module.name, module) |
309 | 350 |
310 def clear_cache(self): | 351 def clear_cache(self): |
| 352 # XXX clear transforms |
311 self.astroid_cache.clear() | 353 self.astroid_cache.clear() |
312 # force bootstrap again, else we may ends up with cache inconsistency | 354 # force bootstrap again, else we may ends up with cache inconsistency |
313 # between the manager and CONST_PROXY, making | 355 # between the manager and CONST_PROXY, making |
314 # unittest_lookup.LookupTC.test_builtin_lookup fail depending on the | 356 # unittest_lookup.LookupTC.test_builtin_lookup fail depending on the |
315 # test order | 357 # test order |
316 from astroid.raw_building import astroid_bootstrapping | 358 from astroid.raw_building import astroid_bootstrapping |
317 astroid_bootstrapping() | 359 astroid_bootstrapping() |
318 | 360 |
319 | 361 |
320 class Project(object): | 362 class Project(object): |
(...skipping 17 matching lines...) Expand all Loading... |
338 return self.locals[name] | 380 return self.locals[name] |
339 | 381 |
340 def get_children(self): | 382 def get_children(self): |
341 return self.modules | 383 return self.modules |
342 | 384 |
343 def __repr__(self): | 385 def __repr__(self): |
344 return '<Project %r at %s (%s modules)>' % (self.name, id(self), | 386 return '<Project %r at %s (%s modules)>' % (self.name, id(self), |
345 len(self.modules)) | 387 len(self.modules)) |
346 | 388 |
347 | 389 |
OLD | NEW |