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/_importdb.py ADDED
@@ -0,0 +1,680 @@
1
+ # pyflyby/_importdb.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
+
8
+
9
+ from collections import defaultdict
10
+ import os
11
+ import re
12
+ import sys
13
+ import warnings
14
+
15
+ from pathlib import Path
16
+
17
+ from typing import Any, Dict, List, Tuple, Union
18
+
19
+ from pyflyby._file import (Filename, UnsafeFilenameError,
20
+ expand_py_files_from_args)
21
+ from pyflyby._idents import dotted_prefixes
22
+ from pyflyby._importclns import ImportMap, ImportSet
23
+ from pyflyby._importstmt import Import, ImportStatement
24
+ from pyflyby._log import logger
25
+ from pyflyby._parse import PythonBlock
26
+ from pyflyby._util import cached_attribute, memoize, stable_unique
27
+
28
+ if sys.version_info <= (3, 12):
29
+ from typing_extensions import Self
30
+ else:
31
+ from typing import Self
32
+
33
+ SUPPORT_DEPRECATED_BEHAVIOR = False
34
+
35
+ @memoize
36
+ def _find_etc_dirs():
37
+ result = []
38
+ dirs = Filename(__file__).real.dir.ancestors[:-1]
39
+ for dir in dirs:
40
+ candidate = dir / "etc/pyflyby"
41
+ if candidate.isdir:
42
+ result.append(candidate)
43
+ break
44
+ global_dir = Filename("/etc/pyflyby")
45
+ if global_dir.exists:
46
+ result.append(global_dir)
47
+ return result
48
+
49
+
50
+ def _get_env_var(env_var_name, default):
51
+ '''
52
+ Get an environment variable and split on ":", replacing ``-`` with the
53
+ default.
54
+ '''
55
+ assert re.match("^[A-Z_]+$", env_var_name)
56
+ assert isinstance(default, (tuple, list))
57
+ value = list(filter(None, os.environ.get(env_var_name, '').split(':')))
58
+ if not value:
59
+ return default
60
+ # Replace '-' with ``default``
61
+ try:
62
+ idx = value.index('-')
63
+ except ValueError:
64
+ pass
65
+ else:
66
+ value[idx:idx+1] = default
67
+ return value
68
+
69
+
70
+ def _get_python_path(env_var_name, default_path, target_dirname):
71
+ '''
72
+ Expand an environment variable specifying pyflyby input config files.
73
+
74
+ - Default to ``default_path`` if the environment variable is undefined.
75
+ - Process colon delimiters.
76
+ - Replace "-" with ``default_path``.
77
+ - Expand triple dots.
78
+ - Recursively traverse directories.
79
+
80
+ :rtype:
81
+ ``tuple`` of ``Filename`` s
82
+ '''
83
+ pathnames = _get_env_var(env_var_name, default_path)
84
+ if pathnames == ["EMPTY"]:
85
+ # The special code PYFLYBY_PATH=EMPTY means we intentionally want to
86
+ # use an empty PYFLYBY_PATH (and don't fall back to the default path,
87
+ # nor warn about an empty path).
88
+ return ()
89
+ for p in pathnames:
90
+ if re.match("/|[.]/|[.][.][.]/|~/", p):
91
+ continue
92
+ raise ValueError(
93
+ "{env_var_name} components should start with / or ./ or ~/ or .../. "
94
+ "Use {env_var_name}=./{p} instead of {env_var_name}={p} if you really "
95
+ "want to use the current directory."
96
+ .format(env_var_name=env_var_name, p=p))
97
+ pathnames = [os.path.expanduser(p) for p in pathnames]
98
+ pathnames = _expand_tripledots(pathnames, target_dirname)
99
+ for fn in pathnames:
100
+ assert isinstance(fn, Filename)
101
+ pathnames = stable_unique(pathnames)
102
+ for p in pathnames:
103
+ assert isinstance(p, Filename)
104
+ pathnames = expand_py_files_from_args(pathnames)
105
+ if not pathnames:
106
+ logger.warning(
107
+ "No import libraries found (%s=%r, default=%r)"
108
+ % (env_var_name, os.environ.get(env_var_name), default_path))
109
+ return tuple(pathnames)
110
+
111
+
112
+ # TODO: stop memoizing here after using StatCache. Actually just inline into
113
+ # _ancestors_on_same_partition
114
+ @memoize
115
+ def _get_st_dev(filename: Filename):
116
+ assert isinstance(filename, Filename)
117
+ try:
118
+ return os.stat(str(filename)).st_dev
119
+ except OSError:
120
+ return None
121
+
122
+
123
+ def _ancestors_on_same_partition(filename):
124
+ """
125
+ Generate ancestors of ``filename`` that exist and are on the same partition
126
+ as the first existing ancestor of ``filename``.
127
+
128
+ For example, suppose a partition is mounted on /u/homer; /u is a different
129
+ partition. Suppose /u/homer/aa exists but /u/homer/aa/bb does not exist.
130
+ Then::
131
+
132
+ >>> _ancestors_on_same_partition(Filename("/u/homer/aa/bb/cc")) # doctest: +SKIP
133
+ [Filename("/u/homer", Filename("/u/homer/aa")]
134
+
135
+ :rtype:
136
+ ``list`` of ``Filename``
137
+ """
138
+ result = []
139
+ dev = None
140
+ for f in filename.ancestors:
141
+ this_dev = _get_st_dev(f)
142
+ if this_dev is None:
143
+ continue
144
+ if dev is None:
145
+ dev = this_dev
146
+ elif dev != this_dev:
147
+ break
148
+ result.append(f)
149
+ return result
150
+
151
+
152
+ def _expand_tripledots(pathnames, target_dirname):
153
+ """
154
+ Expand pathnames of the form ``".../foo/bar"`` as "../../foo/bar",
155
+ "../foo/bar", "./foo/bar" etc., up to the oldest ancestor with the same
156
+ st_dev.
157
+
158
+ For example, suppose a partition is mounted on /u/homer; /u is a different
159
+ partition. Then::
160
+
161
+ >>> _expand_tripledots(["/foo", ".../tt"], "/u/homer/aa") # doctest: +SKIP
162
+ [Filename("/foo"), Filename("/u/homer/tt"), Filename("/u/homer/aa/tt")]
163
+
164
+ :type pathnames:
165
+ sequence of ``str`` (not ``Filename``)
166
+ :type target_dirname:
167
+ `Filename`
168
+ :rtype:
169
+ ``list`` of `Filename`
170
+ """
171
+ assert isinstance(target_dirname, Filename)
172
+ if not isinstance(pathnames, (tuple, list)):
173
+ pathnames = [pathnames]
174
+ result = []
175
+ for pathname in pathnames:
176
+ if not pathname.startswith(".../"):
177
+ result.append(Filename(pathname))
178
+ continue
179
+ suffix = pathname[4:]
180
+ expanded = []
181
+ for p in _ancestors_on_same_partition(target_dirname):
182
+ try:
183
+ expanded.append(p / suffix)
184
+ except UnsafeFilenameError:
185
+ continue
186
+ result.extend(expanded[::-1])
187
+ return result
188
+
189
+
190
+ class ImportDB:
191
+ """
192
+ A database of known, mandatory, canonical imports.
193
+
194
+ @iattr known_imports:
195
+ Set of known imports. For use by tidy-imports and autoimporter.
196
+ @iattr mandatory_imports:
197
+ Set of imports that must be added by tidy-imports.
198
+ @iattr canonical_imports:
199
+ Map of imports that tidy-imports transforms on every run.
200
+ @iattr forget_imports:
201
+ Set of imports to remove from known_imports, mandatory_imports,
202
+ canonical_imports.
203
+ """
204
+
205
+ forget_imports : ImportSet
206
+ known_imports : ImportSet
207
+ mandatory_imports: ImportSet
208
+ canonical_imports: ImportMap
209
+
210
+ _default_cache: Dict[Any, Any] = {}
211
+
212
+ def __new__(cls, *args):
213
+ if len(args) != 1:
214
+ raise TypeError
215
+ arg, = args
216
+ if isinstance(arg, cls):
217
+ return arg
218
+ if isinstance(arg, ImportSet):
219
+ return cls._from_data(arg, [], [], [])
220
+ return cls._from_args(arg) # PythonBlock, Filename, etc
221
+
222
+
223
+
224
+
225
+ @classmethod
226
+ def clear_default_cache(cls):
227
+ """
228
+ Clear the class cache of default ImportDBs.
229
+
230
+ Subsequent calls to ImportDB.get_default() will not reuse previously
231
+ cached results. Existing ImportDB instances are not affected by this
232
+ call.
233
+ """
234
+ if logger.debug_enabled:
235
+ allpyfiles = set()
236
+ for tup in cls._default_cache:
237
+ if tup[0] != 2:
238
+ continue
239
+ for tup2 in tup[1:]:
240
+ for f in tup2:
241
+ assert isinstance(f, Filename)
242
+ if f.ext == ".py":
243
+ allpyfiles.add(f)
244
+ nfiles = len(allpyfiles)
245
+ logger.debug("ImportDB: Clearing default cache of %d files", nfiles)
246
+ cls._default_cache.clear()
247
+
248
+ @classmethod
249
+ def get_default(cls, target_filename: Union[Filename, str], /):
250
+ """
251
+ Return the default import library for the given target filename.
252
+
253
+ This will read various .../.pyflyby files as specified by
254
+ $PYFLYBY_PATH as well as older deprecated environment variables.
255
+
256
+ Memoized.
257
+
258
+ :param target_filename:
259
+ The target filename for which to get the import database. Note that
260
+ the target filename itself is not read. Instead, the target
261
+ filename is relevant because we look for .../.pyflyby based on the
262
+ target filename.
263
+ :rtype:
264
+ `ImportDB`
265
+ """
266
+ # We're going to canonicalize target_filename in a number of steps.
267
+ # At each step, see if we've seen the input so far. We do the cache
268
+ # checking incrementally since the steps involve syscalls. Since this
269
+ # is going to potentially be executed inside the IPython interactive
270
+ # loop, we cache as much as possible.
271
+ # TODO: Consider refreshing periodically. Check if files have
272
+ # been touched, and if so, return new data. Check file timestamps at
273
+ # most once every 60 seconds.
274
+ cache_keys:List[Tuple[Any,...]] = []
275
+ if target_filename is None:
276
+ target_filename = "."
277
+
278
+ if isinstance(target_filename, Filename):
279
+ target_filename = str(target_filename)
280
+
281
+ assert isinstance(target_filename, str), (
282
+ target_filename,
283
+ type(target_filename),
284
+ )
285
+
286
+ target_path = Path(target_filename).resolve()
287
+
288
+ parents: List[Path]
289
+ if target_path.is_dir():
290
+ parents = [target_path]
291
+ else:
292
+ parents = []
293
+
294
+ # filter safe parents
295
+ safe_parent = None
296
+ for p in parents + list(target_path.parents):
297
+ try:
298
+ safe_parent = Filename(str(p))
299
+ break
300
+ except UnsafeFilenameError:
301
+ pass
302
+ if safe_parent is None:
303
+ raise ValueError("No know path are safe")
304
+
305
+ target_dirname = safe_parent
306
+
307
+ if target_filename.startswith("/dev"):
308
+ try:
309
+ target_dirname = Filename(".")
310
+ except UnsafeFilenameError:
311
+ pass
312
+ # TODO: with StatCache
313
+ while True:
314
+ key = (
315
+ 1,
316
+ target_dirname,
317
+ os.getenv("PYFLYBY_PATH"),
318
+ os.getenv("PYFLYBY_KNOWN_IMPORTS_PATH"),
319
+ os.getenv("PYFLYBY_MANDATORY_IMPORTS_PATH"),
320
+ )
321
+ cache_keys.append(key)
322
+ if key in cls._default_cache:
323
+ return cls._default_cache[key]
324
+ if target_dirname.isdir:
325
+ break
326
+ target_dirname = target_dirname.dir
327
+ try:
328
+ target_dirname = target_dirname.real
329
+ except UnsafeFilenameError:
330
+ pass
331
+ if target_dirname != cache_keys[-1][0]:
332
+ cache_keys.append((1,
333
+ target_dirname,
334
+ os.getenv("PYFLYBY_PATH"),
335
+ os.getenv("PYFLYBY_KNOWN_IMPORTS_PATH"),
336
+ os.getenv("PYFLYBY_MANDATORY_IMPORTS_PATH")))
337
+ try:
338
+ return cls._default_cache[cache_keys[-1]]
339
+ except KeyError:
340
+ pass
341
+ DEFAULT_PYFLYBY_PATH = []
342
+ DEFAULT_PYFLYBY_PATH += [str(p) for p in _find_etc_dirs()]
343
+ DEFAULT_PYFLYBY_PATH += [
344
+ ".../.pyflyby",
345
+ "~/.pyflyby",
346
+ ]
347
+ logger.debug("DEFAULT_PYFLYBY_PATH=%s", DEFAULT_PYFLYBY_PATH)
348
+ filenames = _get_python_path("PYFLYBY_PATH", DEFAULT_PYFLYBY_PATH,
349
+ target_dirname)
350
+ mandatory_imports_filenames = ()
351
+ if SUPPORT_DEPRECATED_BEHAVIOR:
352
+ PYFLYBY_PATH = _get_env_var("PYFLYBY_PATH", DEFAULT_PYFLYBY_PATH)
353
+ # If the old deprecated environment variables are set, then heed
354
+ # them.
355
+ if os.getenv("PYFLYBY_KNOWN_IMPORTS_PATH"):
356
+ # Use PYFLYBY_PATH as the default for
357
+ # PYFLYBY_KNOWN_IMPORTS_PATH. Note that the default is
358
+ # relevant even though we only enter this code path when the
359
+ # variable is set to anything, because the env var can
360
+ # reference "-" to include the default.
361
+ # Before pyflyby version 0.8, the default value would have
362
+ # been
363
+ # [d/"known_imports" for d in PYFLYBY_PATH]
364
+ # Instead of using that, we just use PYFLYBY_PATH directly as
365
+ # the default. This simplifies things and avoids need for a
366
+ # "known_imports=>." symlink for backwards compatibility. It
367
+ # means that ~/.pyflyby/**/*.py (as opposed to only
368
+ # ~/.pyflyby/known_imports/**/*.py) would be included.
369
+ # Although this differs slightly from the old behavior, it
370
+ # matches the behavior of the newer PYFLYBY_PATH; matching the
371
+ # new behavior seems higher utility than exactly matching the
372
+ # old behavior. Files under ~/.pyflyby/mandatory_imports will
373
+ # be included in known_imports as well, but that should not
374
+ # cause any problems.
375
+ default_path = PYFLYBY_PATH
376
+ # Expand $PYFLYBY_KNOWN_IMPORTS_PATH.
377
+ filenames = _get_python_path(
378
+ "PYFLYBY_KNOWN_IMPORTS_PATH", default_path, target_dirname
379
+ )
380
+ warnings.warn(
381
+ "The environment variable PYFLYBY_KNOWN_IMPORTS_PATH was"
382
+ " deprecated since 2014. But never emitted a warning,"
383
+ " please use PYFLYBY_PATH or open an issue"
384
+ " if you are still requiring PYFLYBY_KNOWN_IMPORTS_PATH",
385
+ DeprecationWarning,
386
+ )
387
+ logger.debug(
388
+ "The environment variable PYFLYBY_KNOWN_IMPORTS_PATH is deprecated. "
389
+ "Use PYFLYBY_PATH.")
390
+ if os.getenv("PYFLYBY_MANDATORY_IMPORTS_PATH"):
391
+ # Compute the "default" path.
392
+ # Note that we still calculate the erstwhile default value,
393
+ # even though it's no longer the defaults, in order to still
394
+ # allow the "-" in the variable.
395
+ default_path = [
396
+ os.path.join(d,"mandatory_imports") for d in PYFLYBY_PATH]
397
+ # Expand $PYFLYBY_MANDATORY_IMPORTS_PATH.
398
+ mandatory_imports_filenames = _get_python_path(
399
+ "PYFLYBY_MANDATORY_IMPORTS_PATH", default_path, target_dirname
400
+ )
401
+ warnings.warn(
402
+ "The environment variable PYFLYBY_MANDATORY_IMPORTS_PATH was"
403
+ " deprecated since 2014 but never emitted a warning."
404
+ " Use PYFLYBY_PATH and write __mandatory_imports__=['...']"
405
+ " in your files.",
406
+ DeprecationWarning,
407
+ )
408
+ logger.debug(
409
+ "The environment variable PYFLYBY_MANDATORY_IMPORTS_PATH is deprecated. "
410
+ "Use PYFLYBY_PATH and write __mandatory_imports__=['...'] in your files.")
411
+ cache_keys.append((2, filenames, mandatory_imports_filenames))
412
+ try:
413
+ return cls._default_cache[cache_keys[-1]]
414
+ except KeyError:
415
+ pass
416
+ result = cls._from_filenames(filenames, mandatory_imports_filenames)
417
+ for k in cache_keys:
418
+ cls._default_cache[k] = result
419
+ return result
420
+
421
+ @classmethod
422
+ def interpret_arg(cls, arg, target_filename) -> ImportDB:
423
+ if arg is None:
424
+ return cls.get_default(target_filename)
425
+ else:
426
+ return cls(arg)
427
+
428
+ @classmethod
429
+ def _from_data(cls, known_imports, mandatory_imports,
430
+ canonical_imports, forget_imports):
431
+ self = object.__new__(cls)
432
+ self.forget_imports = ImportSet(forget_imports )
433
+ self.known_imports = ImportSet(known_imports ).without_imports(forget_imports)
434
+ self.mandatory_imports = ImportSet(mandatory_imports).without_imports(forget_imports)
435
+ # TODO: provide more fine-grained control about canonical_imports.
436
+ self.canonical_imports = ImportMap(canonical_imports).without_imports(forget_imports)
437
+ return self
438
+
439
+ def __or__(self, other:'Self') -> 'Self':
440
+ assert isinstance(other, ImportDB)
441
+ return self._from_data(
442
+ known_imports = self.known_imports | other.known_imports,
443
+ mandatory_imports = self.mandatory_imports | other.mandatory_imports,
444
+ canonical_imports = self.canonical_imports | other.canonical_imports,
445
+ forget_imports = self.forget_imports | other.forget_imports
446
+ )
447
+
448
+
449
+ @classmethod
450
+ def _from_args(cls, args):
451
+ # TODO: support merging input ImportDBs. For now we support
452
+ # `PythonBlock` s and convertibles such as `Filename`.
453
+ return cls._from_code(args)
454
+
455
+ @classmethod
456
+ def _from_code(cls, blocks,
457
+ _mandatory_imports_blocks_deprecated=(),
458
+ _forget_imports_blocks_deprecated=(),
459
+ ):
460
+ """
461
+ Load an import database from code.
462
+
463
+ >>> ImportDB._from_code('''
464
+ ... import foo, bar as barf
465
+ ... from xx import yy
466
+ ... __mandatory_imports__ = ['__future__.division',
467
+ ... 'import aa . bb . cc as dd']
468
+ ... __forget_imports__ = ['xx.yy', 'from xx import zz']
469
+ ... __canonical_imports__ = {'bad.baad': 'good.goood'}
470
+ ... ''')
471
+ ImportDB('''
472
+ import bar as barf
473
+ import foo
474
+ <BLANKLINE>
475
+ __mandatory_imports__ = [
476
+ 'from __future__ import division',
477
+ 'from aa.bb import cc as dd',
478
+ ]
479
+ <BLANKLINE>
480
+ __canonical_imports__ = {
481
+ 'bad.baad': 'good.goood',
482
+ }
483
+ <BLANKLINE>
484
+ __forget_imports__ = [
485
+ 'from xx import yy',
486
+ 'from xx import zz',
487
+ ]
488
+ ''')
489
+
490
+ :rtype:
491
+ `ImportDB`
492
+ """
493
+ if not isinstance(blocks, (tuple, list)):
494
+ blocks = [blocks]
495
+ if not isinstance(_mandatory_imports_blocks_deprecated, (tuple, list)):
496
+ _mandatory_imports_blocks_deprecated = [_mandatory_imports_blocks_deprecated]
497
+ if not isinstance(_forget_imports_blocks_deprecated, (tuple, list)):
498
+ _forget_imports_blocks_deprecated = [_forget_imports_blocks_deprecated]
499
+ known_imports = []
500
+ mandatory_imports = []
501
+ canonical_imports = []
502
+ forget_imports = []
503
+ blocks = [PythonBlock(b) for b in blocks]
504
+ for block in blocks:
505
+ for statement in block.statements:
506
+ if statement.is_comment_or_blank:
507
+ continue
508
+ if statement.is_import:
509
+ known_imports.extend(ImportStatement(statement).imports)
510
+ continue
511
+ try:
512
+ name, value = statement.get_assignment_literal_value()
513
+ if name == "__mandatory_imports__":
514
+ mandatory_imports.append(cls._parse_import_set(value))
515
+ elif name == "__canonical_imports__":
516
+ canonical_imports.append(cls._parse_import_map(value))
517
+ elif name == "__forget_imports__":
518
+ forget_imports.append(cls._parse_import_set(value))
519
+ else:
520
+ raise ValueError(
521
+ "Unknown assignment to %r (expected one of "
522
+ "__mandatory_imports__, __canonical_imports__, "
523
+ "__forget_imports__)" % (name,))
524
+ except ValueError as e:
525
+ raise ValueError(
526
+ "While parsing %s: error in %r: %s"
527
+ % (block.filename, statement, e))
528
+ for block in _mandatory_imports_blocks_deprecated:
529
+ mandatory_imports.append(ImportSet(block))
530
+ for block in _forget_imports_blocks_deprecated:
531
+ forget_imports.append(ImportSet(block))
532
+ return cls._from_data(known_imports,
533
+ mandatory_imports,
534
+ canonical_imports,
535
+ forget_imports)
536
+
537
+ @classmethod
538
+ def _from_filenames(cls, filenames, _mandatory_filenames_deprecated=[]):
539
+ """
540
+ Load an import database from filenames.
541
+
542
+ This function exists to support deprecated behavior.
543
+ When we stop supporting the old behavior, we will delete this function.
544
+
545
+ :type filenames:
546
+ Sequence of `Filename` s
547
+ :param filenames:
548
+ Filenames of files to read.
549
+ :rtype:
550
+ `ImportDB`
551
+ """
552
+ if _mandatory_filenames_deprecated:
553
+ warnings.warn(
554
+ "_mandatory_filenames_deprecated has been deprecated in Pyflyby"
555
+ " 1.9.4 and will removed in future versions",
556
+ DeprecationWarning,
557
+ stacklevel=1,
558
+ )
559
+ if not isinstance(filenames, (tuple, list)):
560
+ # TODO DeprecationWarning July 2024,
561
+ # this is internal deprecate not passing a list;
562
+ filenames = [filenames]
563
+ for f in filenames:
564
+ assert isinstance(f, Filename)
565
+ logger.debug(
566
+ "ImportDB: loading %r, mandatory=%r",
567
+ [str(f) for f in filenames],
568
+ [str(f) for f in _mandatory_filenames_deprecated],
569
+ )
570
+ if SUPPORT_DEPRECATED_BEHAVIOR:
571
+ # Before 2014-10, pyflyby read the following:
572
+ # * known_imports from $PYFLYBY_PATH/known_imports/**/*.py or
573
+ # $PYFLYBY_KNOWN_IMPORTS_PATH/**/*.py,
574
+ # * mandatory_imports from $PYFLYBY_PATH/mandatory_imports/**/*.py or
575
+ # $PYFLYBY_MANDATORY_IMPORTS_PATH/**/*.py, and
576
+ # * forget_imports from $PYFLYBY_PATH/known_imports/**/__remove__.py
577
+ # After 2014-10, pyflyby reads the following:
578
+ # * $PYFLYBY_PATH/**/*.py
579
+ # (with directives inside the file)
580
+ # For backwards compatibility, for now we continue supporting the
581
+ # old, deprecated behavior.
582
+ blocks = []
583
+ mandatory_imports_blocks = [
584
+ Filename(f) for f in _mandatory_filenames_deprecated]
585
+ forget_imports_blocks = []
586
+ for filename in filenames:
587
+ if filename.base == "__remove__.py":
588
+ forget_imports_blocks.append(filename)
589
+ elif "mandatory_imports" in str(filename).split("/"):
590
+ mandatory_imports_blocks.append(filename)
591
+ else:
592
+ blocks.append(filename)
593
+ return cls._from_code(
594
+ blocks, mandatory_imports_blocks, forget_imports_blocks)
595
+ else:
596
+ return cls._from_code(filenames)
597
+
598
+ @classmethod
599
+ def _parse_import_set(cls, arg):
600
+ if isinstance(arg, str):
601
+ arg = [arg]
602
+ if not isinstance(arg, (tuple, list)):
603
+ raise ValueError("Expected a list, not a %s" % (type(arg).__name__,))
604
+ for item in arg:
605
+ if not isinstance(item, str):
606
+ raise ValueError(
607
+ "Expected a list of str, not %s" % (type(item).__name__,))
608
+ return ImportSet(arg)
609
+
610
+ @classmethod
611
+ def _parse_import_map(cls, arg):
612
+ if isinstance(arg, str):
613
+ arg = [arg]
614
+ if not isinstance(arg, dict):
615
+ raise ValueError("Expected a dict, not a %s" % (type(arg).__name__,))
616
+ for k, v in arg.items():
617
+ if not isinstance(k, str):
618
+ raise ValueError(
619
+ "Expected a dict of str, not %s" % (type(k).__name__,))
620
+ if not isinstance(v, str):
621
+ raise ValueError(
622
+ "Expected a dict of str, not %s" % (type(v).__name__,))
623
+ return ImportMap(arg)
624
+
625
+ @cached_attribute
626
+ def by_fullname_or_import_as(self) -> Dict[str, Tuple[Import, ...]]:
627
+ """
628
+ Map from ``fullname`` and ``import_as`` to `Import` s.
629
+
630
+ >>> import pprint
631
+ >>> db = ImportDB('from aa.bb import cc as dd')
632
+ >>> pprint.pprint(db.by_fullname_or_import_as)
633
+ {'aa': (Import('import aa'),),
634
+ 'aa.bb': (Import('import aa.bb'),),
635
+ 'dd': (Import('from aa.bb import cc as dd'),)}
636
+
637
+ :rtype:
638
+ ``dict`` mapping from ``str`` to tuple of `Import` s
639
+ """
640
+ # TODO: make known_imports take into account the below forget_imports,
641
+ # then move this function into ImportSet
642
+ d = defaultdict(set)
643
+ for imp in self.known_imports.imports:
644
+ # Given an import like "from foo.bar import quux as QUUX", add the
645
+ # following entries:
646
+ # - "QUUX" => "from foo.bar import quux as QUUX"
647
+ # - "foo.bar" => "import foo.bar"
648
+ # - "foo" => "import foo"
649
+ # We don't include an entry labeled "quux" because the user has
650
+ # implied he doesn't want to pollute the global namespace with
651
+ # "quux", only "QUUX".
652
+ d[imp.import_as].add(imp)
653
+ for prefix in dotted_prefixes(imp.fullname)[:-1]:
654
+ d[prefix].add(Import.from_parts(prefix, prefix))
655
+ return dict( (k, tuple(sorted(v - set(self.forget_imports.imports))))
656
+ for k, v in d.items())
657
+
658
+ def __repr__(self):
659
+ printed = self.pretty_print()
660
+ lines = "".join(" "+line for line in printed.splitlines(True))
661
+ return "%s('''\n%s''')" % (type(self).__name__, lines)
662
+
663
+ def pretty_print(self):
664
+ s = self.known_imports.pretty_print()
665
+ if self.mandatory_imports:
666
+ s += "\n__mandatory_imports__ = [\n"
667
+ for imp in self.mandatory_imports.imports:
668
+ s += " '%s',\n" % imp
669
+ s += "]\n"
670
+ if self.canonical_imports:
671
+ s += "\n__canonical_imports__ = {\n"
672
+ for k, v in sorted(self.canonical_imports.items()):
673
+ s += " '%s': '%s',\n" % (k, v)
674
+ s += "}\n"
675
+ if self.forget_imports:
676
+ s += "\n__forget_imports__ = [\n"
677
+ for imp in self.forget_imports.imports:
678
+ s += " '%s',\n" % imp
679
+ s += "]\n"
680
+ return s