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.
Files changed (53) hide show
  1. pyflyby/__init__.py +61 -0
  2. pyflyby/__main__.py +9 -0
  3. pyflyby/_autoimp.py +2228 -0
  4. pyflyby/_cmdline.py +591 -0
  5. pyflyby/_comms.py +221 -0
  6. pyflyby/_dbg.py +1383 -0
  7. pyflyby/_dynimp.py +154 -0
  8. pyflyby/_fast_iter_modules.cpython-311-darwin.so +0 -0
  9. pyflyby/_file.py +771 -0
  10. pyflyby/_flags.py +230 -0
  11. pyflyby/_format.py +186 -0
  12. pyflyby/_idents.py +227 -0
  13. pyflyby/_import_sorting.py +165 -0
  14. pyflyby/_importclns.py +658 -0
  15. pyflyby/_importdb.py +535 -0
  16. pyflyby/_imports2s.py +643 -0
  17. pyflyby/_importstmt.py +723 -0
  18. pyflyby/_interactive.py +2113 -0
  19. pyflyby/_livepatch.py +793 -0
  20. pyflyby/_log.py +107 -0
  21. pyflyby/_modules.py +646 -0
  22. pyflyby/_parse.py +1396 -0
  23. pyflyby/_py.py +2165 -0
  24. pyflyby/_saveframe.py +1145 -0
  25. pyflyby/_saveframe_reader.py +471 -0
  26. pyflyby/_util.py +458 -0
  27. pyflyby/_version.py +8 -0
  28. pyflyby/autoimport.py +20 -0
  29. pyflyby/etc/pyflyby/canonical.py +10 -0
  30. pyflyby/etc/pyflyby/common.py +27 -0
  31. pyflyby/etc/pyflyby/forget.py +10 -0
  32. pyflyby/etc/pyflyby/mandatory.py +10 -0
  33. pyflyby/etc/pyflyby/numpy.py +156 -0
  34. pyflyby/etc/pyflyby/std.py +335 -0
  35. pyflyby/importdb.py +19 -0
  36. pyflyby/libexec/pyflyby/colordiff +34 -0
  37. pyflyby/libexec/pyflyby/diff-colorize +148 -0
  38. pyflyby/share/emacs/site-lisp/pyflyby.el +112 -0
  39. pyflyby-1.10.4.data/scripts/collect-exports +76 -0
  40. pyflyby-1.10.4.data/scripts/collect-imports +58 -0
  41. pyflyby-1.10.4.data/scripts/find-import +38 -0
  42. pyflyby-1.10.4.data/scripts/prune-broken-imports +34 -0
  43. pyflyby-1.10.4.data/scripts/pyflyby-diff +34 -0
  44. pyflyby-1.10.4.data/scripts/reformat-imports +27 -0
  45. pyflyby-1.10.4.data/scripts/replace-star-imports +37 -0
  46. pyflyby-1.10.4.data/scripts/saveframe +299 -0
  47. pyflyby-1.10.4.data/scripts/tidy-imports +170 -0
  48. pyflyby-1.10.4.data/scripts/transform-imports +47 -0
  49. pyflyby-1.10.4.dist-info/METADATA +605 -0
  50. pyflyby-1.10.4.dist-info/RECORD +53 -0
  51. pyflyby-1.10.4.dist-info/WHEEL +6 -0
  52. pyflyby-1.10.4.dist-info/entry_points.txt +4 -0
  53. 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
+ )