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.
- pyflyby/__init__.py +61 -0
- pyflyby/__main__.py +9 -0
- pyflyby/_autoimp.py +2229 -0
- pyflyby/_cmdline.py +548 -0
- pyflyby/_comms.py +221 -0
- pyflyby/_dbg.py +1367 -0
- pyflyby/_docxref.py +379 -0
- pyflyby/_dynimp.py +154 -0
- pyflyby/_fast_iter_modules.cpython-311-x86_64-linux-gnu.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 +680 -0
- pyflyby/_imports2s.py +643 -0
- pyflyby/_importstmt.py +723 -0
- pyflyby/_interactive.py +2113 -0
- pyflyby/_livepatch.py +793 -0
- pyflyby/_log.py +104 -0
- pyflyby/_modules.py +641 -0
- pyflyby/_parse.py +1381 -0
- pyflyby/_py.py +2166 -0
- pyflyby/_saveframe.py +1145 -0
- pyflyby/_saveframe_reader.py +471 -0
- pyflyby/_util.py +458 -0
- pyflyby/_version.py +7 -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 +108 -0
- pyflyby-1.10.1.data/scripts/collect-exports +76 -0
- pyflyby-1.10.1.data/scripts/collect-imports +58 -0
- pyflyby-1.10.1.data/scripts/find-import +38 -0
- pyflyby-1.10.1.data/scripts/list-bad-xrefs +34 -0
- pyflyby-1.10.1.data/scripts/prune-broken-imports +34 -0
- pyflyby-1.10.1.data/scripts/pyflyby-diff +34 -0
- pyflyby-1.10.1.data/scripts/reformat-imports +27 -0
- pyflyby-1.10.1.data/scripts/replace-star-imports +37 -0
- pyflyby-1.10.1.data/scripts/saveframe +299 -0
- pyflyby-1.10.1.data/scripts/tidy-imports +163 -0
- pyflyby-1.10.1.data/scripts/transform-imports +47 -0
- pyflyby-1.10.1.dist-info/METADATA +591 -0
- pyflyby-1.10.1.dist-info/RECORD +55 -0
- pyflyby-1.10.1.dist-info/WHEEL +5 -0
- pyflyby-1.10.1.dist-info/entry_points.txt +4 -0
- 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
|