pyflyby 1.10.1__cp311-cp311-manylinux_2_24_x86_64.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.

Potentially problematic release.


This version of pyflyby might be problematic. Click here for more details.

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