pyflyby 1.10.4__cp311-cp311-macosx_11_0_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyflyby/__init__.py +61 -0
- pyflyby/__main__.py +9 -0
- pyflyby/_autoimp.py +2228 -0
- pyflyby/_cmdline.py +591 -0
- pyflyby/_comms.py +221 -0
- pyflyby/_dbg.py +1383 -0
- pyflyby/_dynimp.py +154 -0
- pyflyby/_fast_iter_modules.cpython-311-darwin.so +0 -0
- pyflyby/_file.py +771 -0
- pyflyby/_flags.py +230 -0
- pyflyby/_format.py +186 -0
- pyflyby/_idents.py +227 -0
- pyflyby/_import_sorting.py +165 -0
- pyflyby/_importclns.py +658 -0
- pyflyby/_importdb.py +535 -0
- pyflyby/_imports2s.py +643 -0
- pyflyby/_importstmt.py +723 -0
- pyflyby/_interactive.py +2113 -0
- pyflyby/_livepatch.py +793 -0
- pyflyby/_log.py +107 -0
- pyflyby/_modules.py +646 -0
- pyflyby/_parse.py +1396 -0
- pyflyby/_py.py +2165 -0
- pyflyby/_saveframe.py +1145 -0
- pyflyby/_saveframe_reader.py +471 -0
- pyflyby/_util.py +458 -0
- pyflyby/_version.py +8 -0
- pyflyby/autoimport.py +20 -0
- pyflyby/etc/pyflyby/canonical.py +10 -0
- pyflyby/etc/pyflyby/common.py +27 -0
- pyflyby/etc/pyflyby/forget.py +10 -0
- pyflyby/etc/pyflyby/mandatory.py +10 -0
- pyflyby/etc/pyflyby/numpy.py +156 -0
- pyflyby/etc/pyflyby/std.py +335 -0
- pyflyby/importdb.py +19 -0
- pyflyby/libexec/pyflyby/colordiff +34 -0
- pyflyby/libexec/pyflyby/diff-colorize +148 -0
- pyflyby/share/emacs/site-lisp/pyflyby.el +112 -0
- pyflyby-1.10.4.data/scripts/collect-exports +76 -0
- pyflyby-1.10.4.data/scripts/collect-imports +58 -0
- pyflyby-1.10.4.data/scripts/find-import +38 -0
- pyflyby-1.10.4.data/scripts/prune-broken-imports +34 -0
- pyflyby-1.10.4.data/scripts/pyflyby-diff +34 -0
- pyflyby-1.10.4.data/scripts/reformat-imports +27 -0
- pyflyby-1.10.4.data/scripts/replace-star-imports +37 -0
- pyflyby-1.10.4.data/scripts/saveframe +299 -0
- pyflyby-1.10.4.data/scripts/tidy-imports +170 -0
- pyflyby-1.10.4.data/scripts/transform-imports +47 -0
- pyflyby-1.10.4.dist-info/METADATA +605 -0
- pyflyby-1.10.4.dist-info/RECORD +53 -0
- pyflyby-1.10.4.dist-info/WHEEL +6 -0
- pyflyby-1.10.4.dist-info/entry_points.txt +4 -0
- pyflyby-1.10.4.dist-info/licenses/LICENSE.txt +19 -0
pyflyby/_imports2s.py
ADDED
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
# pyflyby/_imports2s.py.
|
|
2
|
+
# Copyright (C) 2011-2018 Karl Chen.
|
|
3
|
+
# License: MIT http://opensource.org/licenses/MIT
|
|
4
|
+
|
|
5
|
+
from pyflyby._autoimp import scan_for_import_issues
|
|
6
|
+
from pyflyby._file import FileText, Filename
|
|
7
|
+
from pyflyby._flags import CompilerFlags
|
|
8
|
+
from pyflyby._importclns import ImportSet, NoSuchImportError
|
|
9
|
+
from pyflyby._importdb import ImportDB
|
|
10
|
+
from pyflyby._importstmt import ImportFormatParams, ImportStatement
|
|
11
|
+
from pyflyby._log import logger
|
|
12
|
+
from pyflyby._parse import PythonBlock
|
|
13
|
+
from pyflyby._util import ImportPathCtx, Inf, NullCtx, memoize
|
|
14
|
+
import re
|
|
15
|
+
|
|
16
|
+
from typing import Union, Optional, Literal
|
|
17
|
+
|
|
18
|
+
from textwrap import indent
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SourceToSourceTransformationBase:
|
|
22
|
+
|
|
23
|
+
input: PythonBlock
|
|
24
|
+
|
|
25
|
+
def __new__(cls, arg):
|
|
26
|
+
if isinstance(arg, cls):
|
|
27
|
+
return arg
|
|
28
|
+
if isinstance(arg, (PythonBlock, FileText, Filename, str)):
|
|
29
|
+
return cls._from_source_code(arg)
|
|
30
|
+
raise TypeError("%s: got unexpected %s"
|
|
31
|
+
% (cls.__name__, type(arg).__name__))
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def _from_source_code(cls, codeblock):
|
|
35
|
+
# TODO: don't do that.
|
|
36
|
+
self = object.__new__(cls)
|
|
37
|
+
if isinstance(codeblock, PythonBlock):
|
|
38
|
+
self.input = codeblock
|
|
39
|
+
elif isinstance(codeblock, FileText):
|
|
40
|
+
self.input = PythonBlock(codeblock)
|
|
41
|
+
else:
|
|
42
|
+
if not codeblock.endswith('\n'):
|
|
43
|
+
codeblock += '\n'
|
|
44
|
+
self.input = PythonBlock(codeblock)
|
|
45
|
+
self.preprocess()
|
|
46
|
+
return self
|
|
47
|
+
|
|
48
|
+
def preprocess(self):
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
def pretty_print(self, params=None):
|
|
52
|
+
raise NotImplementedError
|
|
53
|
+
|
|
54
|
+
def output(self, params=None) -> PythonBlock:
|
|
55
|
+
"""
|
|
56
|
+
Pretty-print and return as a `PythonBlock`.
|
|
57
|
+
|
|
58
|
+
:rtype:
|
|
59
|
+
`PythonBlock`
|
|
60
|
+
"""
|
|
61
|
+
result = self.pretty_print(params=params)
|
|
62
|
+
result = PythonBlock(result, filename=self.input.filename)
|
|
63
|
+
return result
|
|
64
|
+
|
|
65
|
+
def __repr__(self):
|
|
66
|
+
return f"<{self.__class__.__name__}\n{indent(str(self.pretty_print()),' ')}\n at 0x{hex(id(self))}>"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class SourceToSourceTransformation(SourceToSourceTransformationBase):
|
|
70
|
+
|
|
71
|
+
_output: PythonBlock
|
|
72
|
+
|
|
73
|
+
def preprocess(self):
|
|
74
|
+
assert isinstance(self.input, PythonBlock), self.input
|
|
75
|
+
self._output = self.input
|
|
76
|
+
|
|
77
|
+
def pretty_print(self, params=None):
|
|
78
|
+
return self._output.text
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class SourceToSourceImportBlockTransformation(SourceToSourceTransformationBase):
|
|
82
|
+
def preprocess(self):
|
|
83
|
+
self.importset = ImportSet(self.input, ignore_shadowed=True)
|
|
84
|
+
|
|
85
|
+
def pretty_print(self, params=None):
|
|
86
|
+
params = ImportFormatParams(params)
|
|
87
|
+
return self.importset.pretty_print(params)
|
|
88
|
+
|
|
89
|
+
def __repr__(self):
|
|
90
|
+
# Guard against partially initialized object...
|
|
91
|
+
import_set = getattr(self, "importset", None)
|
|
92
|
+
return f"<SourceToSourceImportBlockTransformation {import_set!r} @{hex(id(self))}>"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class LineNumberNotFoundError(Exception):
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
class LineNumberAmbiguousError(Exception):
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
class NoImportBlockError(Exception):
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
class ImportAlreadyExistsError(Exception):
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
class SourceToSourceFileImportsTransformation(SourceToSourceTransformationBase):
|
|
108
|
+
def preprocess(self):
|
|
109
|
+
# Group into blocks of imports and non-imports. Get a sequence of all
|
|
110
|
+
# imports for the transformers to operate on.
|
|
111
|
+
self.blocks = []
|
|
112
|
+
self.import_blocks = []
|
|
113
|
+
|
|
114
|
+
for is_imports, subblock in self.input.groupby(lambda ps: ps.is_import):
|
|
115
|
+
if is_imports:
|
|
116
|
+
trans = SourceToSourceImportBlockTransformation(subblock)
|
|
117
|
+
self.import_blocks.append(trans)
|
|
118
|
+
else:
|
|
119
|
+
trans = SourceToSourceTransformation(subblock)
|
|
120
|
+
self.blocks.append(trans)
|
|
121
|
+
|
|
122
|
+
def pretty_print(self, params=None):
|
|
123
|
+
params = ImportFormatParams(params)
|
|
124
|
+
result = [block.pretty_print(params=params) for block in self.blocks]
|
|
125
|
+
return FileText.concatenate(result)
|
|
126
|
+
|
|
127
|
+
def find_import_block_by_lineno(self, lineno: int):
|
|
128
|
+
"""
|
|
129
|
+
Find the import block containing the given line number.
|
|
130
|
+
|
|
131
|
+
:type lineno:
|
|
132
|
+
``int``
|
|
133
|
+
:rtype:
|
|
134
|
+
`SourceToSourceImportBlockTransformation`
|
|
135
|
+
"""
|
|
136
|
+
results = [
|
|
137
|
+
b
|
|
138
|
+
for b in self.import_blocks
|
|
139
|
+
if b.input.startpos.lineno <= lineno <= b.input.endpos.lineno]
|
|
140
|
+
if len(results) == 0:
|
|
141
|
+
raise LineNumberNotFoundError(lineno)
|
|
142
|
+
if len(results) > 1:
|
|
143
|
+
raise LineNumberAmbiguousError(lineno)
|
|
144
|
+
return results[0]
|
|
145
|
+
|
|
146
|
+
def remove_import(self, imp, lineno):
|
|
147
|
+
"""
|
|
148
|
+
Remove the given import.
|
|
149
|
+
|
|
150
|
+
:type imp:
|
|
151
|
+
`Import`
|
|
152
|
+
:type lineno:
|
|
153
|
+
``int``
|
|
154
|
+
"""
|
|
155
|
+
block = self.find_import_block_by_lineno(lineno)
|
|
156
|
+
try:
|
|
157
|
+
imports = block.importset.by_import_as[imp.import_as]
|
|
158
|
+
except KeyError:
|
|
159
|
+
raise NoSuchImportError
|
|
160
|
+
assert len(imports)
|
|
161
|
+
if len(imports) > 1:
|
|
162
|
+
raise Exception("Multiple imports to remove: %r" % (imports,))
|
|
163
|
+
imp = imports[0]
|
|
164
|
+
block.importset = block.importset.without_imports([imp])
|
|
165
|
+
return imp
|
|
166
|
+
|
|
167
|
+
def select_import_block_by_closest_prefix_match(self, imp, max_lineno):
|
|
168
|
+
"""
|
|
169
|
+
Heuristically pick an import block that ``imp`` "fits" best into. The
|
|
170
|
+
selection is based on the block that contains the import with the
|
|
171
|
+
longest common prefix.
|
|
172
|
+
|
|
173
|
+
:type imp:
|
|
174
|
+
`Import`
|
|
175
|
+
:param max_lineno:
|
|
176
|
+
Only return import blocks earlier than ``max_lineno``.
|
|
177
|
+
:rtype:
|
|
178
|
+
`SourceToSourceImportBlockTransformation`
|
|
179
|
+
"""
|
|
180
|
+
# Create a data structure that annotates blocks with data by which
|
|
181
|
+
# we'll sort.
|
|
182
|
+
annotated_blocks = [
|
|
183
|
+
( (max([0] + [len(imp.prefix_match(oimp))
|
|
184
|
+
for oimp in block.importset.imports]),
|
|
185
|
+
block.input.endpos.lineno),
|
|
186
|
+
block )
|
|
187
|
+
for block in self.import_blocks
|
|
188
|
+
if block.input.endpos.lineno <= max_lineno+1 ]
|
|
189
|
+
if not annotated_blocks:
|
|
190
|
+
raise NoImportBlockError()
|
|
191
|
+
annotated_blocks.sort()
|
|
192
|
+
if imp.split.module_name == '__future__':
|
|
193
|
+
# For __future__ imports, only add to an existing block that
|
|
194
|
+
# already contains __future__ import(s). If there are no existing
|
|
195
|
+
# import blocks containing __future__, don't return any result
|
|
196
|
+
# here, so that we will add a new one at the top.
|
|
197
|
+
if not annotated_blocks[-1][0][0] > 0:
|
|
198
|
+
raise NoImportBlockError
|
|
199
|
+
return annotated_blocks[-1][1]
|
|
200
|
+
|
|
201
|
+
def insert_new_blocks_after_comments(self, blocks):
|
|
202
|
+
blocks = [SourceToSourceTransformationBase(block) for block in blocks]
|
|
203
|
+
if isinstance(self.blocks[0], SourceToSourceImportBlockTransformation):
|
|
204
|
+
# Kludge. We should add an "output" attribute to
|
|
205
|
+
# SourceToSourceImportBlockTransformation and enumerate over that,
|
|
206
|
+
# instead of enumerating over the input below.
|
|
207
|
+
self.blocks[0:0] = blocks
|
|
208
|
+
return
|
|
209
|
+
# Get the "statements" in the first block.
|
|
210
|
+
statements = self.blocks[0].input.statements
|
|
211
|
+
# Find the insertion point.
|
|
212
|
+
for idx, statement in enumerate(statements):
|
|
213
|
+
if not statement.is_comment_or_blank_or_string_literal:
|
|
214
|
+
if idx == 0:
|
|
215
|
+
# First block starts with a noncomment, so insert before
|
|
216
|
+
# it.
|
|
217
|
+
self.blocks[0:0] = blocks
|
|
218
|
+
else:
|
|
219
|
+
# Found a non-comment after comment, so break it up and
|
|
220
|
+
# insert in the middle.
|
|
221
|
+
self.blocks[:1] = (
|
|
222
|
+
[SourceToSourceTransformation(
|
|
223
|
+
PythonBlock.concatenate(statements[:idx]))] +
|
|
224
|
+
blocks +
|
|
225
|
+
[SourceToSourceTransformation(
|
|
226
|
+
PythonBlock.concatenate(statements[idx:]))])
|
|
227
|
+
break
|
|
228
|
+
else:
|
|
229
|
+
# First block is entirely comments, so just insert after it.
|
|
230
|
+
self.blocks[1:1] = blocks
|
|
231
|
+
|
|
232
|
+
def insert_new_import_block(self):
|
|
233
|
+
"""
|
|
234
|
+
Adds a new empty imports block. It is added before the first
|
|
235
|
+
non-comment statement. Intended to be used when the input contains no
|
|
236
|
+
import blocks (before uses).
|
|
237
|
+
"""
|
|
238
|
+
block = SourceToSourceImportBlockTransformation("")
|
|
239
|
+
sepblock = SourceToSourceTransformation("")
|
|
240
|
+
sepblock._output = PythonBlock("\n")
|
|
241
|
+
self.insert_new_blocks_after_comments([block, sepblock])
|
|
242
|
+
self.import_blocks.insert(0, block)
|
|
243
|
+
return block
|
|
244
|
+
|
|
245
|
+
def add_import(self, imp, lineno=Inf):
|
|
246
|
+
"""
|
|
247
|
+
Add the specified import. Picks an existing global import block to
|
|
248
|
+
add to, or if none found, creates a new one near the beginning of the
|
|
249
|
+
module.
|
|
250
|
+
|
|
251
|
+
:type imp:
|
|
252
|
+
`Import`
|
|
253
|
+
:param lineno:
|
|
254
|
+
Line before which to add the import. ``Inf`` means no constraint.
|
|
255
|
+
"""
|
|
256
|
+
try:
|
|
257
|
+
block = self.select_import_block_by_closest_prefix_match(
|
|
258
|
+
imp, lineno)
|
|
259
|
+
except NoImportBlockError:
|
|
260
|
+
block = self.insert_new_import_block()
|
|
261
|
+
if imp in block.importset.imports:
|
|
262
|
+
raise ImportAlreadyExistsError(imp)
|
|
263
|
+
block.importset = block.importset.with_imports([imp])
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def reformat_import_statements(codeblock, params=None):
|
|
267
|
+
r"""
|
|
268
|
+
Reformat each top-level block of import statements within a block of code.
|
|
269
|
+
Blank lines, comments, etc. are left alone and separate blocks of imports.
|
|
270
|
+
|
|
271
|
+
Parse the entire code block into an ast, group into consecutive import
|
|
272
|
+
statements and other lines. Each import block consists entirely of
|
|
273
|
+
'import' (or 'from ... import') statements. Other lines, including blanks
|
|
274
|
+
and comment lines, are not touched.
|
|
275
|
+
|
|
276
|
+
>>> print(reformat_import_statements(
|
|
277
|
+
... 'from foo import bar2 as bar2x, bar1\n'
|
|
278
|
+
... 'import foo.bar3 as bar3x\n'
|
|
279
|
+
... 'import foo.bar4\n'
|
|
280
|
+
... '\n'
|
|
281
|
+
... 'import foo.bar0 as bar0\n').text.joined)
|
|
282
|
+
import foo.bar4
|
|
283
|
+
from foo import bar1, bar2 as bar2x, bar3 as bar3x
|
|
284
|
+
<BLANKLINE>
|
|
285
|
+
from foo import bar0
|
|
286
|
+
<BLANKLINE>
|
|
287
|
+
|
|
288
|
+
:type codeblock:
|
|
289
|
+
`PythonBlock` or convertible (``str``)
|
|
290
|
+
:type params:
|
|
291
|
+
`ImportFormatParams`
|
|
292
|
+
:rtype:
|
|
293
|
+
`PythonBlock`
|
|
294
|
+
"""
|
|
295
|
+
params = ImportFormatParams(params)
|
|
296
|
+
transformer = SourceToSourceFileImportsTransformation(codeblock)
|
|
297
|
+
return transformer.output(params=params)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def ImportPathForRelativeImportsCtx(codeblock):
|
|
301
|
+
"""
|
|
302
|
+
Context manager that temporarily modifies ``sys.path`` so that relative
|
|
303
|
+
imports for the given ``codeblock`` work as expected.
|
|
304
|
+
|
|
305
|
+
:type codeblock:
|
|
306
|
+
`PythonBlock`
|
|
307
|
+
"""
|
|
308
|
+
if not isinstance(codeblock, PythonBlock):
|
|
309
|
+
codeblock = PythonBlock(codeblock)
|
|
310
|
+
if not codeblock.filename:
|
|
311
|
+
return NullCtx()
|
|
312
|
+
if codeblock.flags & CompilerFlags("absolute_import"):
|
|
313
|
+
return NullCtx()
|
|
314
|
+
return ImportPathCtx(str(codeblock.filename.dir))
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def fix_unused_and_missing_imports(
|
|
318
|
+
codeblock: Union[PythonBlock, str, Filename],
|
|
319
|
+
add_missing: bool = True,
|
|
320
|
+
remove_unused: Union[Literal["AUTOMATIC"], bool] = "AUTOMATIC",
|
|
321
|
+
add_mandatory: bool = True,
|
|
322
|
+
db: Optional[ImportDB] = None,
|
|
323
|
+
params=None,
|
|
324
|
+
) -> PythonBlock:
|
|
325
|
+
r"""
|
|
326
|
+
Check for unused and missing imports, and fix them automatically.
|
|
327
|
+
|
|
328
|
+
Also formats imports.
|
|
329
|
+
|
|
330
|
+
In the example below, ``m1`` and ``m3`` are unused, so are automatically
|
|
331
|
+
removed. ``np`` was undefined, so an ``import numpy as np`` was
|
|
332
|
+
automatically added.
|
|
333
|
+
|
|
334
|
+
>>> codeblock = PythonBlock(
|
|
335
|
+
... 'from foo import m1, m2, m3, m4\n'
|
|
336
|
+
... 'm2, m4, np.foo', filename="/tmp/foo.py")
|
|
337
|
+
|
|
338
|
+
>>> print(fix_unused_and_missing_imports(codeblock, add_mandatory=False))
|
|
339
|
+
[PYFLYBY] /tmp/foo.py: removed unused 'from foo import m1'
|
|
340
|
+
[PYFLYBY] /tmp/foo.py: removed unused 'from foo import m3'
|
|
341
|
+
[PYFLYBY] /tmp/foo.py: added 'import numpy as np'
|
|
342
|
+
import numpy as np
|
|
343
|
+
from foo import m2, m4
|
|
344
|
+
m2, m4, np.foo
|
|
345
|
+
|
|
346
|
+
:type codeblock:
|
|
347
|
+
`PythonBlock` or convertible (``str``)
|
|
348
|
+
:rtype:
|
|
349
|
+
`PythonBlock`
|
|
350
|
+
"""
|
|
351
|
+
_codeblock: PythonBlock
|
|
352
|
+
if isinstance(codeblock, Filename):
|
|
353
|
+
_codeblock = PythonBlock(codeblock)
|
|
354
|
+
if not isinstance(codeblock, PythonBlock):
|
|
355
|
+
_codeblock = PythonBlock(codeblock)
|
|
356
|
+
else:
|
|
357
|
+
_codeblock = codeblock
|
|
358
|
+
if remove_unused == "AUTOMATIC":
|
|
359
|
+
fn = _codeblock.filename
|
|
360
|
+
remove_unused = not (fn and
|
|
361
|
+
(fn.base == "__init__.py"
|
|
362
|
+
or ".pyflyby" in str(fn).split("/")))
|
|
363
|
+
elif remove_unused is True or remove_unused is False:
|
|
364
|
+
pass
|
|
365
|
+
else:
|
|
366
|
+
raise ValueError("Invalid remove_unused=%r" % (remove_unused,))
|
|
367
|
+
params = ImportFormatParams(params)
|
|
368
|
+
db = ImportDB.interpret_arg(db, target_filename=_codeblock.filename)
|
|
369
|
+
# Do a first pass reformatting the imports to get rid of repeated or
|
|
370
|
+
# shadowed imports, e.g. L1 here:
|
|
371
|
+
# import foo # L1
|
|
372
|
+
# import foo # L2
|
|
373
|
+
# foo # L3
|
|
374
|
+
_codeblock = reformat_import_statements(_codeblock, params=params)
|
|
375
|
+
|
|
376
|
+
filename = _codeblock.filename
|
|
377
|
+
transformer = SourceToSourceFileImportsTransformation(_codeblock)
|
|
378
|
+
missing_imports, unused_imports = scan_for_import_issues(
|
|
379
|
+
_codeblock, find_unused_imports=remove_unused, parse_docstrings=True
|
|
380
|
+
)
|
|
381
|
+
logger.debug("missing_imports = %r", missing_imports)
|
|
382
|
+
logger.debug("unused_imports = %r", unused_imports)
|
|
383
|
+
if remove_unused and unused_imports:
|
|
384
|
+
# Go through imports to remove. [This used to be organized by going
|
|
385
|
+
# through import blocks and removing all relevant blocks from there,
|
|
386
|
+
# but if one removal caused problems the whole thing would fail. The
|
|
387
|
+
# CPU cost of calling without_imports() multiple times isn't worth
|
|
388
|
+
# that.]
|
|
389
|
+
# TODO: don't remove unused mandatory imports. [This isn't
|
|
390
|
+
# implemented yet because this isn't necessary for __future__ imports
|
|
391
|
+
# since they aren't reported as unused, and those are the only ones we
|
|
392
|
+
# have by default right now.]
|
|
393
|
+
for lineno, imp in unused_imports:
|
|
394
|
+
try:
|
|
395
|
+
imp = transformer.remove_import(imp, lineno)
|
|
396
|
+
except NoSuchImportError:
|
|
397
|
+
logger.error(
|
|
398
|
+
"%s: couldn't remove import %r", filename, imp,)
|
|
399
|
+
except LineNumberNotFoundError as e:
|
|
400
|
+
logger.debug(
|
|
401
|
+
"%s: unused import %r on line %d not global",
|
|
402
|
+
filename, str(imp), e.args[0])
|
|
403
|
+
else:
|
|
404
|
+
logger.info("%s: removed unused '%s'", filename, imp)
|
|
405
|
+
|
|
406
|
+
if add_missing and missing_imports:
|
|
407
|
+
missing_imports.sort(key=lambda k: (k[1], k[0]))
|
|
408
|
+
known = db.known_imports.by_import_as
|
|
409
|
+
# Decide on where to put each import to be added. Find the import
|
|
410
|
+
# block with the longest common prefix. Tie-break by preferring later
|
|
411
|
+
# blocks.
|
|
412
|
+
added_imports = set()
|
|
413
|
+
for lineno, ident in missing_imports:
|
|
414
|
+
import_as = ident.parts[0]
|
|
415
|
+
try:
|
|
416
|
+
imports = known[import_as]
|
|
417
|
+
except KeyError:
|
|
418
|
+
logger.warning(
|
|
419
|
+
"%s:%s: undefined name %r and no known import for it",
|
|
420
|
+
filename, lineno, import_as)
|
|
421
|
+
continue
|
|
422
|
+
if len(imports) != 1:
|
|
423
|
+
logger.error("%s: don't know which of %r to use",
|
|
424
|
+
filename, imports)
|
|
425
|
+
continue
|
|
426
|
+
imp_to_add = imports[0]
|
|
427
|
+
if imp_to_add in added_imports:
|
|
428
|
+
continue
|
|
429
|
+
transformer.add_import(imp_to_add, lineno)
|
|
430
|
+
added_imports.add(imp_to_add)
|
|
431
|
+
logger.info("%s: added %r", filename,
|
|
432
|
+
imp_to_add.pretty_print().strip())
|
|
433
|
+
|
|
434
|
+
if add_mandatory:
|
|
435
|
+
# Todo: allow not adding to empty __init__ files?
|
|
436
|
+
mandatory = db.mandatory_imports.imports
|
|
437
|
+
for imp in mandatory:
|
|
438
|
+
try:
|
|
439
|
+
transformer.add_import(imp)
|
|
440
|
+
except ImportAlreadyExistsError:
|
|
441
|
+
pass
|
|
442
|
+
else:
|
|
443
|
+
logger.info("%s: added mandatory %r",
|
|
444
|
+
filename, imp.pretty_print().strip())
|
|
445
|
+
|
|
446
|
+
return transformer.output(params=params)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def remove_broken_imports(codeblock, params=None):
|
|
450
|
+
"""
|
|
451
|
+
Try to execute each import, and remove the ones that don't work.
|
|
452
|
+
|
|
453
|
+
Also formats imports.
|
|
454
|
+
|
|
455
|
+
:type codeblock:
|
|
456
|
+
`PythonBlock` or convertible (``str``)
|
|
457
|
+
:rtype:
|
|
458
|
+
`PythonBlock`
|
|
459
|
+
"""
|
|
460
|
+
if not isinstance(codeblock, PythonBlock):
|
|
461
|
+
codeblock = PythonBlock(codeblock)
|
|
462
|
+
params = ImportFormatParams(params)
|
|
463
|
+
filename = codeblock.filename
|
|
464
|
+
transformer = SourceToSourceFileImportsTransformation(codeblock)
|
|
465
|
+
for block in transformer.import_blocks:
|
|
466
|
+
broken = []
|
|
467
|
+
for imp in list(block.importset.imports):
|
|
468
|
+
ns = {}
|
|
469
|
+
try:
|
|
470
|
+
exec(imp.pretty_print(), ns)
|
|
471
|
+
except Exception as e:
|
|
472
|
+
logger.info("%s: Could not import %r; removing it: %s: %s",
|
|
473
|
+
filename, imp.fullname, type(e).__name__, e)
|
|
474
|
+
broken.append(imp)
|
|
475
|
+
block.importset = block.importset.without_imports(broken)
|
|
476
|
+
return transformer.output(params=params)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def replace_star_imports(codeblock, params=None):
|
|
480
|
+
r"""
|
|
481
|
+
Replace lines such as::
|
|
482
|
+
|
|
483
|
+
from foo.bar import *
|
|
484
|
+
with
|
|
485
|
+
from foo.bar import f1, f2, f3
|
|
486
|
+
|
|
487
|
+
Note that this requires involves actually importing ``foo.bar``, which may
|
|
488
|
+
have side effects. (TODO: rewrite to avoid this?)
|
|
489
|
+
|
|
490
|
+
The result includes all imports from the ``email`` module. The result
|
|
491
|
+
excludes shadowed imports. In this example:
|
|
492
|
+
|
|
493
|
+
1. The original ``MIMEAudio`` import is shadowed, so it is removed.
|
|
494
|
+
2. The ``MIMEImage`` import in the ``email`` module is shadowed by a
|
|
495
|
+
subsequent import, so it is omitted.
|
|
496
|
+
|
|
497
|
+
>>> codeblock = PythonBlock('from keyword import *', filename="/tmp/x.py")
|
|
498
|
+
|
|
499
|
+
>>> print(replace_star_imports(codeblock)) # doctest: +SKIP
|
|
500
|
+
[PYFLYBY] /tmp/x.py: replaced 'from keyword import *' with 2 imports
|
|
501
|
+
from keyword import iskeyword, kwlist
|
|
502
|
+
<BLANKLINE>
|
|
503
|
+
|
|
504
|
+
Usually you'll want to remove unused imports after replacing star imports.
|
|
505
|
+
|
|
506
|
+
:type codeblock:
|
|
507
|
+
`PythonBlock` or convertible (``str``)
|
|
508
|
+
:rtype:
|
|
509
|
+
`PythonBlock`
|
|
510
|
+
"""
|
|
511
|
+
from pyflyby._modules import ModuleHandle
|
|
512
|
+
params = ImportFormatParams(params)
|
|
513
|
+
if not isinstance(codeblock, PythonBlock):
|
|
514
|
+
codeblock = PythonBlock(codeblock)
|
|
515
|
+
filename = codeblock.filename
|
|
516
|
+
transformer = SourceToSourceFileImportsTransformation(codeblock)
|
|
517
|
+
for block in transformer.import_blocks:
|
|
518
|
+
# Iterate over the import statements in ``block.input``. We do this
|
|
519
|
+
# instead of using ``block.importset`` because the latter doesn't
|
|
520
|
+
# preserve the order of inputs. The order is important for
|
|
521
|
+
# determining what's shadowed.
|
|
522
|
+
imports = [
|
|
523
|
+
imp
|
|
524
|
+
for s in block.input.statements
|
|
525
|
+
for imp in ImportStatement(s).imports
|
|
526
|
+
]
|
|
527
|
+
# Process "from ... import *" statements.
|
|
528
|
+
new_imports = []
|
|
529
|
+
for imp in imports:
|
|
530
|
+
if imp.split.member_name != "*":
|
|
531
|
+
new_imports.append(imp)
|
|
532
|
+
elif imp.split.module_name.startswith("."):
|
|
533
|
+
# The source contains e.g. "from .foo import *". Right now we
|
|
534
|
+
# don't have a good way to figure out the absolute module
|
|
535
|
+
# name, so we can't get at foo. That said, there's a decent
|
|
536
|
+
# chance that this is inside an __init__ anyway, which is one
|
|
537
|
+
# of the few justifiable use cases for star imports in library
|
|
538
|
+
# code.
|
|
539
|
+
logger.warning("%s: can't replace star imports in relative import: %s",
|
|
540
|
+
filename, imp.pretty_print().strip())
|
|
541
|
+
new_imports.append(imp)
|
|
542
|
+
else:
|
|
543
|
+
module = ModuleHandle(imp.split.module_name)
|
|
544
|
+
try:
|
|
545
|
+
with ImportPathForRelativeImportsCtx(codeblock):
|
|
546
|
+
exports = module.exports
|
|
547
|
+
except Exception as e:
|
|
548
|
+
logger.warning(
|
|
549
|
+
"%s: couldn't import '%s' to enumerate exports, "
|
|
550
|
+
"leaving unchanged: '%s'. %s: %s",
|
|
551
|
+
filename, module.name, imp, type(e).__name__, e)
|
|
552
|
+
new_imports.append(imp)
|
|
553
|
+
continue
|
|
554
|
+
if not exports:
|
|
555
|
+
# We found nothing in the target module. This probably
|
|
556
|
+
# means that module itself is just importing things from
|
|
557
|
+
# other modules. Currently we intentionally exclude those
|
|
558
|
+
# imports since usually we don't want them. TODO: do
|
|
559
|
+
# something better here.
|
|
560
|
+
logger.warning("%s: found nothing to import from %s, "
|
|
561
|
+
"leaving unchanged: '%s'",
|
|
562
|
+
filename, module, imp)
|
|
563
|
+
new_imports.append(imp)
|
|
564
|
+
else:
|
|
565
|
+
new_imports.extend(exports)
|
|
566
|
+
logger.info("%s: replaced %r with %d imports", filename,
|
|
567
|
+
imp.pretty_print().strip(), len(exports))
|
|
568
|
+
block.importset = ImportSet(new_imports, ignore_shadowed=True)
|
|
569
|
+
return transformer.output(params=params)
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def transform_imports(codeblock, transformations, params=None):
|
|
573
|
+
"""
|
|
574
|
+
Transform imports as specified by ``transformations``.
|
|
575
|
+
|
|
576
|
+
transform_imports() perfectly replaces all imports in top-level import
|
|
577
|
+
blocks.
|
|
578
|
+
|
|
579
|
+
For the rest of the code body, transform_imports() does a crude textual
|
|
580
|
+
string replacement. This is imperfect but handles most cases. There may
|
|
581
|
+
be some false positives, but this is difficult to avoid. Generally we do
|
|
582
|
+
want to do replacements even within in strings and comments.
|
|
583
|
+
|
|
584
|
+
>>> result = transform_imports("from m import x", {"m.x": "m.y.z"})
|
|
585
|
+
>>> print(result.text.joined.strip())
|
|
586
|
+
from m.y import z as x
|
|
587
|
+
|
|
588
|
+
:type codeblock:
|
|
589
|
+
`PythonBlock` or convertible (``str``)
|
|
590
|
+
:type transformations:
|
|
591
|
+
``dict`` from ``str`` to ``str``
|
|
592
|
+
:param transformations:
|
|
593
|
+
A map of import prefixes to replace, e.g. {"aa.bb": "xx.yy"}
|
|
594
|
+
:rtype:
|
|
595
|
+
`PythonBlock`
|
|
596
|
+
"""
|
|
597
|
+
if not isinstance(codeblock, PythonBlock):
|
|
598
|
+
codeblock = PythonBlock(codeblock)
|
|
599
|
+
params = ImportFormatParams(params)
|
|
600
|
+
transformer = SourceToSourceFileImportsTransformation(codeblock)
|
|
601
|
+
@memoize
|
|
602
|
+
def transform_import(imp):
|
|
603
|
+
# Transform a block of imports.
|
|
604
|
+
# TODO: optimize
|
|
605
|
+
# TODO: handle transformations containing both a.b=>x and a.b.c=>y
|
|
606
|
+
for k, v in transformations.items():
|
|
607
|
+
imp = imp.replace(k, v)
|
|
608
|
+
return imp
|
|
609
|
+
def transform_block(block):
|
|
610
|
+
# Do a crude string replacement in the PythonBlock.
|
|
611
|
+
if not isinstance(block, PythonBlock):
|
|
612
|
+
block = PythonBlock(block)
|
|
613
|
+
s = block.text.joined
|
|
614
|
+
for k, v in transformations.items():
|
|
615
|
+
s = re.sub("\\b%s\\b" % (re.escape(k)), v, s)
|
|
616
|
+
return PythonBlock(s, flags=block.flags)
|
|
617
|
+
# Loop over transformer blocks.
|
|
618
|
+
for block in transformer.blocks:
|
|
619
|
+
if isinstance(block, SourceToSourceImportBlockTransformation):
|
|
620
|
+
input_imports = block.importset.imports
|
|
621
|
+
output_imports = [ transform_import(imp) for imp in input_imports ]
|
|
622
|
+
block.importset = ImportSet(output_imports, ignore_shadowed=True)
|
|
623
|
+
else:
|
|
624
|
+
block._output = transform_block(block.input)
|
|
625
|
+
return transformer.output(params=params)
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def canonicalize_imports(codeblock, params=None, db=None):
|
|
629
|
+
"""
|
|
630
|
+
Transform ``codeblock`` as specified by ``__canonical_imports__`` in the
|
|
631
|
+
global import library.
|
|
632
|
+
|
|
633
|
+
:type codeblock:
|
|
634
|
+
`PythonBlock` or convertible (``str``)
|
|
635
|
+
:rtype:
|
|
636
|
+
`PythonBlock`
|
|
637
|
+
"""
|
|
638
|
+
if not isinstance(codeblock, PythonBlock):
|
|
639
|
+
codeblock = PythonBlock(codeblock)
|
|
640
|
+
params = ImportFormatParams(params)
|
|
641
|
+
db = ImportDB.interpret_arg(db, target_filename=codeblock.filename)
|
|
642
|
+
transformations = db.canonical_imports
|
|
643
|
+
return transform_imports(codeblock, transformations, params=params)
|