pyflyby 1.10.4__cp311-cp311-macosx_11_0_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyflyby/__init__.py +61 -0
- pyflyby/__main__.py +9 -0
- pyflyby/_autoimp.py +2228 -0
- pyflyby/_cmdline.py +591 -0
- pyflyby/_comms.py +221 -0
- pyflyby/_dbg.py +1383 -0
- pyflyby/_dynimp.py +154 -0
- pyflyby/_fast_iter_modules.cpython-311-darwin.so +0 -0
- pyflyby/_file.py +771 -0
- pyflyby/_flags.py +230 -0
- pyflyby/_format.py +186 -0
- pyflyby/_idents.py +227 -0
- pyflyby/_import_sorting.py +165 -0
- pyflyby/_importclns.py +658 -0
- pyflyby/_importdb.py +535 -0
- pyflyby/_imports2s.py +643 -0
- pyflyby/_importstmt.py +723 -0
- pyflyby/_interactive.py +2113 -0
- pyflyby/_livepatch.py +793 -0
- pyflyby/_log.py +107 -0
- pyflyby/_modules.py +646 -0
- pyflyby/_parse.py +1396 -0
- pyflyby/_py.py +2165 -0
- pyflyby/_saveframe.py +1145 -0
- pyflyby/_saveframe_reader.py +471 -0
- pyflyby/_util.py +458 -0
- pyflyby/_version.py +8 -0
- pyflyby/autoimport.py +20 -0
- pyflyby/etc/pyflyby/canonical.py +10 -0
- pyflyby/etc/pyflyby/common.py +27 -0
- pyflyby/etc/pyflyby/forget.py +10 -0
- pyflyby/etc/pyflyby/mandatory.py +10 -0
- pyflyby/etc/pyflyby/numpy.py +156 -0
- pyflyby/etc/pyflyby/std.py +335 -0
- pyflyby/importdb.py +19 -0
- pyflyby/libexec/pyflyby/colordiff +34 -0
- pyflyby/libexec/pyflyby/diff-colorize +148 -0
- pyflyby/share/emacs/site-lisp/pyflyby.el +112 -0
- pyflyby-1.10.4.data/scripts/collect-exports +76 -0
- pyflyby-1.10.4.data/scripts/collect-imports +58 -0
- pyflyby-1.10.4.data/scripts/find-import +38 -0
- pyflyby-1.10.4.data/scripts/prune-broken-imports +34 -0
- pyflyby-1.10.4.data/scripts/pyflyby-diff +34 -0
- pyflyby-1.10.4.data/scripts/reformat-imports +27 -0
- pyflyby-1.10.4.data/scripts/replace-star-imports +37 -0
- pyflyby-1.10.4.data/scripts/saveframe +299 -0
- pyflyby-1.10.4.data/scripts/tidy-imports +170 -0
- pyflyby-1.10.4.data/scripts/transform-imports +47 -0
- pyflyby-1.10.4.dist-info/METADATA +605 -0
- pyflyby-1.10.4.dist-info/RECORD +53 -0
- pyflyby-1.10.4.dist-info/WHEEL +6 -0
- pyflyby-1.10.4.dist-info/entry_points.txt +4 -0
- pyflyby-1.10.4.dist-info/licenses/LICENSE.txt +19 -0
pyflyby/_modules.py
ADDED
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
# pyflyby/_modules.py.
|
|
2
|
+
# Copyright (C) 2011, 2012, 2013, 2014, 2015 Karl Chen.
|
|
3
|
+
# License: MIT http://opensource.org/licenses/MIT
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import ast
|
|
8
|
+
from functools import cached_property, total_ordering
|
|
9
|
+
import hashlib
|
|
10
|
+
import importlib
|
|
11
|
+
import itertools
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import pathlib
|
|
15
|
+
import pkgutil
|
|
16
|
+
import platformdirs
|
|
17
|
+
import textwrap
|
|
18
|
+
|
|
19
|
+
from pyflyby._fast_iter_modules \
|
|
20
|
+
import _iter_file_finder_modules
|
|
21
|
+
from pyflyby._file import FileText, Filename
|
|
22
|
+
from pyflyby._idents import DottedIdentifier, is_identifier
|
|
23
|
+
from pyflyby._log import logger
|
|
24
|
+
from pyflyby._util import (ExcludeImplicitCwdFromPathCtx, cmp,
|
|
25
|
+
memoize, prefixes)
|
|
26
|
+
|
|
27
|
+
import re
|
|
28
|
+
import shutil
|
|
29
|
+
import sys
|
|
30
|
+
import types
|
|
31
|
+
from typing import Any, Dict, Generator, Union
|
|
32
|
+
|
|
33
|
+
class ErrorDuringImportError(ImportError):
|
|
34
|
+
"""
|
|
35
|
+
Exception raised by import_module if the module exists but an exception
|
|
36
|
+
occurred while attempting to import it. That nested exception could be
|
|
37
|
+
ImportError, e.g. if a module tries to import another module that doesn't
|
|
38
|
+
exist.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def rebuild_import_cache():
|
|
42
|
+
"""Force the import cache to be rebuilt.
|
|
43
|
+
|
|
44
|
+
The cache is deleted before calling _fast_iter_modules, which repopulates the cache.
|
|
45
|
+
"""
|
|
46
|
+
for path in pathlib.Path(
|
|
47
|
+
platformdirs.user_cache_dir(appname='pyflyby', appauthor=False)
|
|
48
|
+
).iterdir():
|
|
49
|
+
_remove_import_cache_dir(path)
|
|
50
|
+
_fast_iter_modules()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _remove_import_cache_dir(path: pathlib.Path):
|
|
54
|
+
"""Remove an import cache directory.
|
|
55
|
+
|
|
56
|
+
Import cache directories exist in <user cache dir>/pyflyby/, and they should
|
|
57
|
+
contain just a single file which itself contains a JSON blob of cached import names.
|
|
58
|
+
We therefore only delete the requested path if it is a directory.
|
|
59
|
+
|
|
60
|
+
Parameters
|
|
61
|
+
----------
|
|
62
|
+
path : pathlib.Path
|
|
63
|
+
Import cache directory path to remove
|
|
64
|
+
"""
|
|
65
|
+
if path.is_dir():
|
|
66
|
+
# Only directories are valid import cache entries
|
|
67
|
+
try:
|
|
68
|
+
shutil.rmtree(str(path))
|
|
69
|
+
except Exception as e:
|
|
70
|
+
logger.error(
|
|
71
|
+
f"Failed to remove cache directory at {path} - please "
|
|
72
|
+
"consider removing this directory manually. Error:\n"
|
|
73
|
+
f"{textwrap.indent(str(e), prefix=' ')}"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@memoize
|
|
78
|
+
def import_module(module_name):
|
|
79
|
+
module_name = str(module_name)
|
|
80
|
+
logger.debug("Importing %r", module_name)
|
|
81
|
+
try:
|
|
82
|
+
result = __import__(module_name, fromlist=['dummy'])
|
|
83
|
+
if result.__name__ != module_name:
|
|
84
|
+
logger.debug("Note: import_module(%r).__name__ == %r",
|
|
85
|
+
module_name, result.__name__)
|
|
86
|
+
return result
|
|
87
|
+
except ImportError as e:
|
|
88
|
+
# We got an ImportError. Figure out whether this is due to the module
|
|
89
|
+
# not existing, or whether the module exists but caused an ImportError
|
|
90
|
+
# (perhaps due to trying to import another problematic module).
|
|
91
|
+
# Do this by looking at the exception traceback. If the previous
|
|
92
|
+
# frame in the traceback is this function (because locals match), then
|
|
93
|
+
# it should be the internal import machinery reporting that the module
|
|
94
|
+
# doesn't exist. Re-raise the exception as-is.
|
|
95
|
+
# If some sys.meta_path or other import hook isn't compatible with
|
|
96
|
+
# such a check, here are some things we could do:
|
|
97
|
+
# - Use pkgutil.find_loader() after the fact to check if the module
|
|
98
|
+
# is supposed to exist. Note that we shouldn't rely solely on
|
|
99
|
+
# this before attempting to import, because find_loader() doesn't
|
|
100
|
+
# work with meta_path.
|
|
101
|
+
# - Write a memoized global function that compares in the current
|
|
102
|
+
# environment the difference between attempting to import a
|
|
103
|
+
# non-existent module vs a problematic module, and returns a
|
|
104
|
+
# function that uses the working discriminators.
|
|
105
|
+
real_importerror1 = type(e) is ImportError
|
|
106
|
+
real_importerror2 = (sys.exc_info()[2].tb_frame.f_locals is locals())
|
|
107
|
+
m = re.match("^No module named (.*)$", str(e))
|
|
108
|
+
real_importerror3 = (m and m.group(1) == module_name
|
|
109
|
+
or module_name.endswith("."+m.group(1)))
|
|
110
|
+
logger.debug("import_module(%r): real ImportError: %s %s %s",
|
|
111
|
+
module_name,
|
|
112
|
+
real_importerror1, real_importerror2, real_importerror3)
|
|
113
|
+
if real_importerror1 and real_importerror2 and real_importerror3:
|
|
114
|
+
raise
|
|
115
|
+
raise ErrorDuringImportError(
|
|
116
|
+
"Error while attempting to import %s: %s: %s"
|
|
117
|
+
% (module_name, type(e).__name__, e)) from e
|
|
118
|
+
except Exception as e:
|
|
119
|
+
raise ErrorDuringImportError(
|
|
120
|
+
"Error while attempting to import %s: %s: %s"
|
|
121
|
+
% (module_name, type(e).__name__, e)) from e
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _my_iter_modules(path, prefix=''):
|
|
125
|
+
# Modified version of pkgutil.ImpImporter.iter_modules(), patched to
|
|
126
|
+
# handle inaccessible subdirectories.
|
|
127
|
+
if path is None:
|
|
128
|
+
return
|
|
129
|
+
try:
|
|
130
|
+
filenames = os.listdir(path)
|
|
131
|
+
except OSError:
|
|
132
|
+
return # silently ignore inaccessible paths
|
|
133
|
+
filenames.sort() # handle packages before same-named modules
|
|
134
|
+
yielded = {}
|
|
135
|
+
import inspect
|
|
136
|
+
for fn in filenames:
|
|
137
|
+
modname = inspect.getmodulename(fn)
|
|
138
|
+
if modname=='__init__' or modname in yielded:
|
|
139
|
+
continue
|
|
140
|
+
subpath = os.path.join(path, fn)
|
|
141
|
+
ispkg = False
|
|
142
|
+
try:
|
|
143
|
+
if not modname and os.path.isdir(path) and '.' not in fn:
|
|
144
|
+
modname = fn
|
|
145
|
+
for fn in os.listdir(subpath):
|
|
146
|
+
subname = inspect.getmodulename(fn)
|
|
147
|
+
if subname=='__init__':
|
|
148
|
+
ispkg = True
|
|
149
|
+
break
|
|
150
|
+
else:
|
|
151
|
+
continue # not a package
|
|
152
|
+
except OSError:
|
|
153
|
+
continue # silently ignore inaccessible subdirectories
|
|
154
|
+
if modname and '.' not in modname:
|
|
155
|
+
yielded[modname] = 1
|
|
156
|
+
yield prefix + modname, ispkg
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def pyc_to_py(filename):
|
|
160
|
+
if filename.endswith(".pyc") or filename.endswith(".pyo"):
|
|
161
|
+
filename = filename[:-1]
|
|
162
|
+
return filename
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@total_ordering
|
|
166
|
+
class ModuleHandle(object):
|
|
167
|
+
"""
|
|
168
|
+
A handle to a module.
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
name: DottedIdentifier
|
|
172
|
+
|
|
173
|
+
def __new__(cls, arg):
|
|
174
|
+
if isinstance(arg, cls):
|
|
175
|
+
return arg
|
|
176
|
+
if isinstance(arg, Filename):
|
|
177
|
+
return cls._from_filename(arg)
|
|
178
|
+
if isinstance(arg, (str, DottedIdentifier)):
|
|
179
|
+
return cls._from_modulename(arg)
|
|
180
|
+
if isinstance(arg, types.ModuleType):
|
|
181
|
+
return cls._from_module(arg)
|
|
182
|
+
raise TypeError("ModuleHandle: unexpected %s" % (type(arg).__name__,))
|
|
183
|
+
|
|
184
|
+
_cls_cache:Dict[Any, Any] = {}
|
|
185
|
+
|
|
186
|
+
@classmethod
|
|
187
|
+
def _from_modulename(cls, modulename):
|
|
188
|
+
modulename = DottedIdentifier(modulename)
|
|
189
|
+
try:
|
|
190
|
+
return cls._cls_cache[modulename]
|
|
191
|
+
except KeyError:
|
|
192
|
+
pass
|
|
193
|
+
self = object.__new__(cls)
|
|
194
|
+
self.name = modulename
|
|
195
|
+
cls._cls_cache[modulename] = self
|
|
196
|
+
return self
|
|
197
|
+
|
|
198
|
+
@classmethod
|
|
199
|
+
def _from_module(cls, module):
|
|
200
|
+
if not isinstance(module, types.ModuleType):
|
|
201
|
+
raise TypeError
|
|
202
|
+
self = cls._from_modulename(module.__name__)
|
|
203
|
+
assert self.module is module
|
|
204
|
+
return self
|
|
205
|
+
|
|
206
|
+
@classmethod
|
|
207
|
+
def _from_filename(cls, filename):
|
|
208
|
+
filename = Filename(filename)
|
|
209
|
+
raise NotImplementedError(
|
|
210
|
+
"TODO: look at sys.path to guess module name")
|
|
211
|
+
|
|
212
|
+
@cached_property
|
|
213
|
+
def parent(self):
|
|
214
|
+
if not self.name.parent:
|
|
215
|
+
return None
|
|
216
|
+
return ModuleHandle(self.name.parent)
|
|
217
|
+
|
|
218
|
+
@cached_property
|
|
219
|
+
def ancestors(self):
|
|
220
|
+
return tuple(ModuleHandle(m) for m in self.name.prefixes)
|
|
221
|
+
|
|
222
|
+
@cached_property
|
|
223
|
+
def module(self):
|
|
224
|
+
"""
|
|
225
|
+
Return the module instance.
|
|
226
|
+
|
|
227
|
+
:rtype:
|
|
228
|
+
``types.ModuleType``
|
|
229
|
+
:raise ErrorDuringImportError:
|
|
230
|
+
The module should exist but an error occurred while attempting to
|
|
231
|
+
import it.
|
|
232
|
+
:raise ImportError:
|
|
233
|
+
The module doesn't exist.
|
|
234
|
+
"""
|
|
235
|
+
# First check if prefix component is importable.
|
|
236
|
+
if self.parent:
|
|
237
|
+
self.parent.module
|
|
238
|
+
# Import.
|
|
239
|
+
return import_module(self.name)
|
|
240
|
+
|
|
241
|
+
@cached_property
|
|
242
|
+
def exists(self):
|
|
243
|
+
"""
|
|
244
|
+
Return whether the module exists, according to pkgutil.
|
|
245
|
+
Note that this doesn't work for things that are only known by using
|
|
246
|
+
sys.meta_path.
|
|
247
|
+
"""
|
|
248
|
+
name = str(self.name)
|
|
249
|
+
if name in sys.modules:
|
|
250
|
+
return True
|
|
251
|
+
if self.parent and not self.parent.exists:
|
|
252
|
+
return False
|
|
253
|
+
|
|
254
|
+
import importlib.util
|
|
255
|
+
find = importlib.util.find_spec
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
pkg = find(name)
|
|
259
|
+
except Exception:
|
|
260
|
+
# Catch all exceptions, not just ImportError. If the __init__.py
|
|
261
|
+
# for the parent package of the module raises an exception, it'll
|
|
262
|
+
# propagate to here.
|
|
263
|
+
pkg = None
|
|
264
|
+
return pkg is not None
|
|
265
|
+
|
|
266
|
+
@cached_property
|
|
267
|
+
def filename(self):
|
|
268
|
+
"""
|
|
269
|
+
Return the filename, if appropriate.
|
|
270
|
+
|
|
271
|
+
The module itself will not be imported, but if the module is not a
|
|
272
|
+
top-level module/package, accessing this attribute may cause the
|
|
273
|
+
parent package to be imported.
|
|
274
|
+
|
|
275
|
+
:rtype:
|
|
276
|
+
`Filename`
|
|
277
|
+
"""
|
|
278
|
+
if sys.version_info > (3, 12):
|
|
279
|
+
from importlib.util import find_spec
|
|
280
|
+
try:
|
|
281
|
+
mod = find_spec(str(self.name))
|
|
282
|
+
if mod is None or mod.origin is None:
|
|
283
|
+
return None
|
|
284
|
+
else:
|
|
285
|
+
assert isinstance(mod.origin, str)
|
|
286
|
+
return Filename(mod.origin)
|
|
287
|
+
except ModuleNotFoundError:
|
|
288
|
+
return None
|
|
289
|
+
assert False
|
|
290
|
+
|
|
291
|
+
# Use the loader mechanism to find the filename. We do so instead of
|
|
292
|
+
# using self.module.__file__, because the latter forces importing a
|
|
293
|
+
# module, which may be undesirable.
|
|
294
|
+
|
|
295
|
+
import pkgutil
|
|
296
|
+
try:
|
|
297
|
+
#TODO: deprecated and will be removed in 3.14
|
|
298
|
+
loader = pkgutil.get_loader(str(self.name))
|
|
299
|
+
except ImportError:
|
|
300
|
+
return None
|
|
301
|
+
if not loader:
|
|
302
|
+
return None
|
|
303
|
+
# Get the filename using loader.get_filename(). Note that this does
|
|
304
|
+
# more than just loader.filename: for example, it adds /__init__.py
|
|
305
|
+
# for packages.
|
|
306
|
+
if not hasattr(loader, 'get_filename'):
|
|
307
|
+
return None
|
|
308
|
+
filename = loader.get_filename()
|
|
309
|
+
if not filename:
|
|
310
|
+
return None
|
|
311
|
+
return Filename(pyc_to_py(filename))
|
|
312
|
+
|
|
313
|
+
@cached_property
|
|
314
|
+
def text(self):
|
|
315
|
+
return FileText(self.filename)
|
|
316
|
+
|
|
317
|
+
def __text__(self):
|
|
318
|
+
return self.text
|
|
319
|
+
|
|
320
|
+
@cached_property
|
|
321
|
+
def block(self):
|
|
322
|
+
from pyflyby._parse import PythonBlock
|
|
323
|
+
return PythonBlock(self.text)
|
|
324
|
+
|
|
325
|
+
@staticmethod
|
|
326
|
+
@memoize
|
|
327
|
+
def list() -> list[str]:
|
|
328
|
+
"""Enumerate all top-level packages/modules.
|
|
329
|
+
|
|
330
|
+
The current working directory is excluded for autoimporting; if we autoimported
|
|
331
|
+
random python scripts in the current directory, we could accidentally execute
|
|
332
|
+
code with side effects.
|
|
333
|
+
|
|
334
|
+
Also exclude any module names that are not legal python module names (e.g.
|
|
335
|
+
"try.py" or "123.py").
|
|
336
|
+
|
|
337
|
+
:return: A list of all importable module names
|
|
338
|
+
"""
|
|
339
|
+
with ExcludeImplicitCwdFromPathCtx():
|
|
340
|
+
return [mod.name for mod in _fast_iter_modules() if is_identifier(mod.name)]
|
|
341
|
+
|
|
342
|
+
@cached_property
|
|
343
|
+
def submodules(self):
|
|
344
|
+
"""
|
|
345
|
+
Enumerate the importable submodules of this module.
|
|
346
|
+
|
|
347
|
+
>>> ModuleHandle("email").submodules # doctest:+ELLIPSIS
|
|
348
|
+
(..., ModuleHandle('email.encoders'), ..., ModuleHandle('email.mime'), ...)
|
|
349
|
+
|
|
350
|
+
:rtype:
|
|
351
|
+
``tuple`` of `ModuleHandle` s
|
|
352
|
+
"""
|
|
353
|
+
import pkgutil
|
|
354
|
+
module = self.module
|
|
355
|
+
try:
|
|
356
|
+
path = module.__path__
|
|
357
|
+
except AttributeError:
|
|
358
|
+
return ()
|
|
359
|
+
# Enumerate the modules at a given path. Prefer to use ``pkgutil`` if
|
|
360
|
+
# we can. However, if it fails due to OSError, use our own version
|
|
361
|
+
# which is robust to that.
|
|
362
|
+
try:
|
|
363
|
+
submodule_names = [t[1] for t in pkgutil.iter_modules(path)]
|
|
364
|
+
except OSError:
|
|
365
|
+
submodule_names = [t[0] for p in path for t in _my_iter_modules(p)]
|
|
366
|
+
return tuple(ModuleHandle("%s.%s" % (self.name,m))
|
|
367
|
+
for m in sorted(set(submodule_names)))
|
|
368
|
+
|
|
369
|
+
@staticmethod
|
|
370
|
+
def _member_from_node(node):
|
|
371
|
+
extractors = {
|
|
372
|
+
# Top-level assignments (as opposed to member assignments
|
|
373
|
+
# whose targets are of type ast.Attribute).
|
|
374
|
+
ast.Assign: lambda x: [t.id for t in x.targets if isinstance(t, ast.Name)],
|
|
375
|
+
ast.ClassDef: lambda x: [x.name],
|
|
376
|
+
ast.FunctionDef: lambda x: [x.name],
|
|
377
|
+
}
|
|
378
|
+
if isinstance(node, tuple(extractors.keys())):
|
|
379
|
+
return extractors[type(node)](node)
|
|
380
|
+
return []
|
|
381
|
+
|
|
382
|
+
@cached_property
|
|
383
|
+
def exports(self):
|
|
384
|
+
"""
|
|
385
|
+
Get symbols exported by this module.
|
|
386
|
+
|
|
387
|
+
Note that this will not recognize symbols that are dynamically
|
|
388
|
+
introduced to the module's namespace or __all__ list.
|
|
389
|
+
|
|
390
|
+
:rtype:
|
|
391
|
+
`ImportSet` or ``None``
|
|
392
|
+
:return:
|
|
393
|
+
Exports, or ``None`` if nothing exported.
|
|
394
|
+
"""
|
|
395
|
+
from pyflyby._importclns import ImportStatement, ImportSet
|
|
396
|
+
|
|
397
|
+
filename = getattr(self, 'filename', None)
|
|
398
|
+
if not filename or not filename.exists:
|
|
399
|
+
# Try to load the module to get the filename
|
|
400
|
+
filename = Filename(self.module.__file__)
|
|
401
|
+
text = FileText(filename)
|
|
402
|
+
|
|
403
|
+
ast_mod = ast.parse(str(text), str(filename)).body
|
|
404
|
+
|
|
405
|
+
# First, add members that are explicitly defined in the module
|
|
406
|
+
members = list(itertools.chain(*[self._member_from_node(n) \
|
|
407
|
+
for n in ast_mod]))
|
|
408
|
+
|
|
409
|
+
# If __all__ is defined, try to use it
|
|
410
|
+
all_is_good = False # pun intended
|
|
411
|
+
all_members = []
|
|
412
|
+
if "__all__" in members:
|
|
413
|
+
# Iterate through the nodes and reconstruct the
|
|
414
|
+
# value of __all__
|
|
415
|
+
for n in ast_mod:
|
|
416
|
+
if isinstance(n, ast.Assign):
|
|
417
|
+
if "__all__" in self._member_from_node(n):
|
|
418
|
+
try:
|
|
419
|
+
all_members = list(ast.literal_eval(n.value))
|
|
420
|
+
all_is_good = True
|
|
421
|
+
except (ValueError, TypeError):
|
|
422
|
+
all_is_good = False
|
|
423
|
+
elif isinstance(n, ast.AugAssign) and \
|
|
424
|
+
isinstance(n.target, ast.Name) and \
|
|
425
|
+
n.target.id == "__all__" and all_is_good:
|
|
426
|
+
try:
|
|
427
|
+
all_members += list(ast.literal_eval(n.value))
|
|
428
|
+
except (ValueError, TypeError):
|
|
429
|
+
all_is_good = False
|
|
430
|
+
if not all(type(s) == str for s in members):
|
|
431
|
+
raise Exception(
|
|
432
|
+
"Module %r contains non-string entries in __all__"
|
|
433
|
+
% (str(self.name),))
|
|
434
|
+
|
|
435
|
+
if all_is_good:
|
|
436
|
+
members = all_members
|
|
437
|
+
else:
|
|
438
|
+
# Add "from" imports that belong to submodules
|
|
439
|
+
# (note: this will fail to recognize implicit relative imports)
|
|
440
|
+
imp_nodes = [n for n in ast_mod if isinstance(n, ast.ImportFrom)]
|
|
441
|
+
for imp_node in imp_nodes:
|
|
442
|
+
if imp_node.level == 0:
|
|
443
|
+
from_mod = DottedIdentifier(imp_node.module)
|
|
444
|
+
if not from_mod.startswith(self.name):
|
|
445
|
+
continue
|
|
446
|
+
elif imp_node.level == 1 and \
|
|
447
|
+
filename.base == "__init__.py":
|
|
448
|
+
# Special case: a relative import can be from a submodule only if
|
|
449
|
+
# our module's filename is __init__.py.
|
|
450
|
+
from_mod = self.name
|
|
451
|
+
if imp_node.module:
|
|
452
|
+
from_mod += imp_node.module
|
|
453
|
+
else:
|
|
454
|
+
continue
|
|
455
|
+
for n in imp_node.names:
|
|
456
|
+
m = n.asname or n.name
|
|
457
|
+
if n.name != "*" and not ModuleHandle(from_mod + m).exists:
|
|
458
|
+
members.append(m)
|
|
459
|
+
|
|
460
|
+
# Filter by non-private.
|
|
461
|
+
members = [n for n in members if not n.startswith("_")]
|
|
462
|
+
|
|
463
|
+
# Filter out artificially added "deep" members.
|
|
464
|
+
members = tuple([(n, None) for n in members if "." not in n])
|
|
465
|
+
if not members:
|
|
466
|
+
return None
|
|
467
|
+
return ImportSet(
|
|
468
|
+
[ ImportStatement.from_parts(str(self.name), members) ])
|
|
469
|
+
|
|
470
|
+
def __str__(self):
|
|
471
|
+
return str(self.name)
|
|
472
|
+
|
|
473
|
+
def __repr__(self):
|
|
474
|
+
return "%s(%r)" % (type(self).__name__, str(self.name))
|
|
475
|
+
|
|
476
|
+
def __hash__(self):
|
|
477
|
+
return hash(self.name)
|
|
478
|
+
|
|
479
|
+
def __cmp__(self, o):
|
|
480
|
+
if self is o:
|
|
481
|
+
return 0
|
|
482
|
+
if not isinstance(o, ModuleHandle):
|
|
483
|
+
return NotImplemented
|
|
484
|
+
return cmp(self.name, o.name)
|
|
485
|
+
|
|
486
|
+
def __eq__(self, o):
|
|
487
|
+
if self is o:
|
|
488
|
+
return True
|
|
489
|
+
if not isinstance(o, ModuleHandle):
|
|
490
|
+
return NotImplemented
|
|
491
|
+
return self.name == o.name
|
|
492
|
+
|
|
493
|
+
def __ne__(self, other):
|
|
494
|
+
return not (self == other)
|
|
495
|
+
|
|
496
|
+
# The rest are defined by total_ordering
|
|
497
|
+
def __lt__(self, o):
|
|
498
|
+
if not isinstance(o, ModuleHandle):
|
|
499
|
+
return NotImplemented
|
|
500
|
+
return self.name < o.name
|
|
501
|
+
|
|
502
|
+
def __getitem__(self, x):
|
|
503
|
+
if isinstance(x, slice):
|
|
504
|
+
return type(self)(self.name[x])
|
|
505
|
+
raise TypeError
|
|
506
|
+
|
|
507
|
+
@classmethod
|
|
508
|
+
def containing(cls, identifier):
|
|
509
|
+
"""
|
|
510
|
+
Try to find the module that defines a name such as ``a.b.c`` by trying
|
|
511
|
+
to import ``a``, ``a.b``, and ``a.b.c``.
|
|
512
|
+
|
|
513
|
+
:return:
|
|
514
|
+
The name of the 'deepest' module (most commonly it would be ``a.b``
|
|
515
|
+
in this example).
|
|
516
|
+
:rtype:
|
|
517
|
+
`Module`
|
|
518
|
+
"""
|
|
519
|
+
# In the code below we catch "Exception" rather than just ImportError
|
|
520
|
+
# or AttributeError since importing and __getattr__ing can raise other
|
|
521
|
+
# exceptions.
|
|
522
|
+
identifier = DottedIdentifier(identifier)
|
|
523
|
+
try:
|
|
524
|
+
module = ModuleHandle(identifier[:1])
|
|
525
|
+
result = module.module
|
|
526
|
+
except Exception as e:
|
|
527
|
+
raise ImportError(e)
|
|
528
|
+
# TODO: as far as I can tell the code here is never reached, or haven't
|
|
529
|
+
# been in quite some time as the line below was invalid on Python 3 since 2011
|
|
530
|
+
# zip(...)[...] fails as zip is not indexable.
|
|
531
|
+
# the only place that seem to be using this method is XrefScanner.
|
|
532
|
+
for part, prefix in list(zip(identifier, prefixes(identifier)))[1:]:
|
|
533
|
+
try:
|
|
534
|
+
result = getattr(result, str(part))
|
|
535
|
+
except Exception:
|
|
536
|
+
try:
|
|
537
|
+
module = cls(prefix)
|
|
538
|
+
result = module.module
|
|
539
|
+
except Exception as e:
|
|
540
|
+
raise ImportError(e)
|
|
541
|
+
else:
|
|
542
|
+
if isinstance(result, types.ModuleType):
|
|
543
|
+
module = cls(result)
|
|
544
|
+
logger.debug("Imported %r to get %r", module, identifier)
|
|
545
|
+
return module
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def _format_path(path: Union[str, pathlib.Path]) -> str:
|
|
549
|
+
"""Format a path for printing as a log message.
|
|
550
|
+
|
|
551
|
+
If the path is a child of $HOME, the prefix is replaced with "~" for brevity.
|
|
552
|
+
Otherwise the original path is returned.
|
|
553
|
+
|
|
554
|
+
Parameters
|
|
555
|
+
----------
|
|
556
|
+
path : Union[str, pathlib.Path]
|
|
557
|
+
Path to format
|
|
558
|
+
|
|
559
|
+
Returns
|
|
560
|
+
-------
|
|
561
|
+
str
|
|
562
|
+
Formatted output path
|
|
563
|
+
"""
|
|
564
|
+
path = pathlib.Path(path)
|
|
565
|
+
home = pathlib.Path.home()
|
|
566
|
+
|
|
567
|
+
if path.is_relative_to(home):
|
|
568
|
+
return str(pathlib.Path("~").joinpath(path.relative_to(home)))
|
|
569
|
+
return str(path)
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
SUFFIXES = sorted(importlib.machinery.all_suffixes())
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def _cached_module_finder(
|
|
576
|
+
importer: importlib.machinery.FileFinder, prefix: str = ""
|
|
577
|
+
) -> Generator[tuple[str, bool], None, None]:
|
|
578
|
+
"""Yield the modules found by the importer.
|
|
579
|
+
|
|
580
|
+
The importer path's mtime is recorded; if the path and mtime have a corresponding
|
|
581
|
+
cache file, the modules recorded in the cache file are returned. Otherwise, the
|
|
582
|
+
cache is rebuilt.
|
|
583
|
+
|
|
584
|
+
Parameters
|
|
585
|
+
----------
|
|
586
|
+
importer : importlib.machinery.FileFinder
|
|
587
|
+
FileFinder importer that points to a path under which imports can be found
|
|
588
|
+
prefix : str
|
|
589
|
+
String to affix to the beginning of each module name
|
|
590
|
+
|
|
591
|
+
Returns
|
|
592
|
+
-------
|
|
593
|
+
Generator[tuple[str, bool], None, None]
|
|
594
|
+
Tuples containing (prefix+module name, a bool indicating whether the module is a
|
|
595
|
+
package or not)
|
|
596
|
+
"""
|
|
597
|
+
if os.environ.get("PYFLYBY_DISABLE_CACHE", "0") == "1":
|
|
598
|
+
modules = _iter_file_finder_modules(importer, SUFFIXES)
|
|
599
|
+
for module, ispkg in modules:
|
|
600
|
+
yield prefix + module, ispkg
|
|
601
|
+
return
|
|
602
|
+
|
|
603
|
+
cache_dir = pathlib.Path(
|
|
604
|
+
platformdirs.user_cache_dir(appname='pyflyby', appauthor=False)
|
|
605
|
+
) / hashlib.sha256(str(importer.path).encode()).hexdigest()
|
|
606
|
+
cache_file = cache_dir / str(os.stat(importer.path).st_mtime_ns)
|
|
607
|
+
|
|
608
|
+
if cache_file.exists():
|
|
609
|
+
with open(cache_file) as fp:
|
|
610
|
+
modules = json.load(fp)
|
|
611
|
+
else:
|
|
612
|
+
# Generate the cache dir if it doesn't exist, and remove any existing cache
|
|
613
|
+
# files for the given import path
|
|
614
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
615
|
+
for path in cache_dir.iterdir():
|
|
616
|
+
_remove_import_cache_dir(path)
|
|
617
|
+
|
|
618
|
+
if os.environ.get("PYFLYBY_SUPPRESS_CACHE_REBUILD_LOGS", "1") != "1":
|
|
619
|
+
logger.info(f"Rebuilding cache for {_format_path(importer.path)}...")
|
|
620
|
+
|
|
621
|
+
modules = _iter_file_finder_modules(importer, SUFFIXES)
|
|
622
|
+
with open(cache_file, 'w') as fp:
|
|
623
|
+
json.dump(modules, fp)
|
|
624
|
+
|
|
625
|
+
for module, ispkg in modules:
|
|
626
|
+
yield prefix + module, ispkg
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def _fast_iter_modules() -> Generator[pkgutil.ModuleInfo, None, None]:
|
|
630
|
+
"""Return an iterator over all importable python modules.
|
|
631
|
+
|
|
632
|
+
This function patches `pkgutil.iter_importer_modules` for
|
|
633
|
+
`importlib.machinery.FileFinder` types, causing `pkgutil.iter_importer_modules` to
|
|
634
|
+
call our own custom _iter_file_finder_modules instead of
|
|
635
|
+
pkgutil._iter_file_finder_modules.
|
|
636
|
+
|
|
637
|
+
:return: The modules that are importable by python
|
|
638
|
+
"""
|
|
639
|
+
pkgutil.iter_importer_modules.register( # type: ignore[attr-defined]
|
|
640
|
+
importlib.machinery.FileFinder, _cached_module_finder
|
|
641
|
+
)
|
|
642
|
+
yield from pkgutil.iter_modules()
|
|
643
|
+
pkgutil.iter_importer_modules.register( # type: ignore[attr-defined]
|
|
644
|
+
importlib.machinery.FileFinder,
|
|
645
|
+
pkgutil._iter_file_finder_modules, # type: ignore[attr-defined]
|
|
646
|
+
)
|