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/_importstmt.py
ADDED
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
# pyflyby/_importstmt.py.
|
|
2
|
+
# Copyright (C) 2011, 2012, 2013, 2014 Karl Chen.
|
|
3
|
+
# License: MIT http://opensource.org/licenses/MIT
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
import ast
|
|
8
|
+
from collections import namedtuple
|
|
9
|
+
from functools import total_ordering
|
|
10
|
+
|
|
11
|
+
from pyflyby._flags import CompilerFlags
|
|
12
|
+
from pyflyby._format import FormatParams, pyfill
|
|
13
|
+
from pyflyby._idents import is_identifier
|
|
14
|
+
from pyflyby._parse import PythonStatement
|
|
15
|
+
from pyflyby._util import (Inf, cached_attribute, cmp,
|
|
16
|
+
longest_common_prefix)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
from typing import Dict, Tuple, Optional, Union
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def read_black_config() -> Dict:
|
|
24
|
+
"""Read the black configuration from ``pyproject.toml``"""
|
|
25
|
+
from black.files import find_pyproject_toml, parse_pyproject_toml
|
|
26
|
+
|
|
27
|
+
pyproject_path = find_pyproject_toml((".",))
|
|
28
|
+
|
|
29
|
+
raw_config = parse_pyproject_toml(pyproject_path) if pyproject_path else {}
|
|
30
|
+
|
|
31
|
+
config = {}
|
|
32
|
+
for key in [
|
|
33
|
+
"line_length",
|
|
34
|
+
"skip_magic_trailing_comma",
|
|
35
|
+
"skip_string_normalization",
|
|
36
|
+
]:
|
|
37
|
+
if key in raw_config:
|
|
38
|
+
config[key] = raw_config[key]
|
|
39
|
+
if "target_version" in raw_config:
|
|
40
|
+
target_version = raw_config["target_version"]
|
|
41
|
+
if isinstance(target_version, str):
|
|
42
|
+
config["target_version"] = target_version
|
|
43
|
+
elif isinstance(target_version, list):
|
|
44
|
+
# Convert TOML list to a Python set
|
|
45
|
+
config["target_version"] = set(target_version)
|
|
46
|
+
else:
|
|
47
|
+
raise ValueError(
|
|
48
|
+
f"Invalid config for black = {target_version!r} in {pyproject_path}"
|
|
49
|
+
)
|
|
50
|
+
return config
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ImportFormatParams(FormatParams):
|
|
54
|
+
align_imports:Union[bool, set, list, tuple, str] = True
|
|
55
|
+
"""
|
|
56
|
+
Whether and how to align 'from modulename import aliases...'. If ``True``,
|
|
57
|
+
then the 'import' keywords will be aligned within a block. If an integer,
|
|
58
|
+
then the 'import' keyword will always be at that column. They will be
|
|
59
|
+
wrapped if necessary.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
from_spaces:int = 1
|
|
63
|
+
"""
|
|
64
|
+
The number of spaces after the 'from' keyword. (Must be at least 1.)
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
separate_from_imports:bool = True
|
|
68
|
+
"""
|
|
69
|
+
Whether all 'from ... import ...' in an import block should come after
|
|
70
|
+
'import ...' statements. ``separate_from_imports = False`` works well with
|
|
71
|
+
``from_spaces = 3``. ('from __future__ import ...' always comes first.)
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
align_future:bool = False
|
|
75
|
+
"""
|
|
76
|
+
Whether 'from __future__ import ...' statements should be aligned with
|
|
77
|
+
others. If False, uses a single space after the 'from' and 'import'
|
|
78
|
+
keywords.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class NonImportStatementError(TypeError):
|
|
83
|
+
"""
|
|
84
|
+
Unexpectedly got a statement that wasn't an import.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
ImportSplit = namedtuple("ImportSplit",
|
|
88
|
+
"module_name member_name import_as")
|
|
89
|
+
"""
|
|
90
|
+
Representation of a single import at the token level::
|
|
91
|
+
|
|
92
|
+
from [...]<module_name> import <member_name> as <import_as>
|
|
93
|
+
|
|
94
|
+
If <module_name> is ``None``, then there is no "from" clause; instead just::
|
|
95
|
+
import <member_name> as <import_as>
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
@total_ordering
|
|
99
|
+
class Import:
|
|
100
|
+
"""
|
|
101
|
+
Representation of the desire to import a single name into the current
|
|
102
|
+
namespace.
|
|
103
|
+
|
|
104
|
+
>>> Import.from_parts(".foo.bar", "bar")
|
|
105
|
+
Import('from .foo import bar')
|
|
106
|
+
|
|
107
|
+
>>> Import("from . import foo")
|
|
108
|
+
Import('from . import foo')
|
|
109
|
+
|
|
110
|
+
>>> Import("from . import foo").fullname
|
|
111
|
+
'.foo'
|
|
112
|
+
|
|
113
|
+
>>> Import("import foo . bar")
|
|
114
|
+
Import('import foo.bar')
|
|
115
|
+
|
|
116
|
+
>>> Import("import foo . bar as baz")
|
|
117
|
+
Import('from foo import bar as baz')
|
|
118
|
+
|
|
119
|
+
>>> Import("import foo . bar as bar")
|
|
120
|
+
Import('from foo import bar')
|
|
121
|
+
|
|
122
|
+
>>> Import("foo.bar")
|
|
123
|
+
Import('from foo import bar')
|
|
124
|
+
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
fullname:str
|
|
128
|
+
import_as:str
|
|
129
|
+
comment: Optional[str] = None
|
|
130
|
+
|
|
131
|
+
def __new__(cls, arg):
|
|
132
|
+
if isinstance(arg, cls):
|
|
133
|
+
return arg
|
|
134
|
+
if isinstance(arg, ImportSplit):
|
|
135
|
+
return cls.from_split(arg)
|
|
136
|
+
if isinstance(arg, (ImportStatement, PythonStatement)):
|
|
137
|
+
return cls._from_statement(arg)
|
|
138
|
+
if isinstance(arg, str):
|
|
139
|
+
return cls._from_identifier_or_statement(arg)
|
|
140
|
+
raise TypeError
|
|
141
|
+
|
|
142
|
+
@classmethod
|
|
143
|
+
def from_parts(cls, fullname, import_as, comment=None):
|
|
144
|
+
assert isinstance(fullname, str)
|
|
145
|
+
assert isinstance(import_as, str)
|
|
146
|
+
self = object.__new__(cls)
|
|
147
|
+
self.fullname = fullname
|
|
148
|
+
self.import_as = import_as
|
|
149
|
+
self.comment = comment
|
|
150
|
+
return self
|
|
151
|
+
|
|
152
|
+
@classmethod
|
|
153
|
+
def _from_statement(cls, statement):
|
|
154
|
+
"""
|
|
155
|
+
:type statement:
|
|
156
|
+
`ImportStatement` or convertible (`PythonStatement`, ``str``)
|
|
157
|
+
:rtype:
|
|
158
|
+
`Import`
|
|
159
|
+
"""
|
|
160
|
+
statement = ImportStatement(statement)
|
|
161
|
+
imports = statement.imports
|
|
162
|
+
if len(imports) != 1:
|
|
163
|
+
raise ValueError(
|
|
164
|
+
"Got %d imports instead of 1 in %r" % (len(imports), statement))
|
|
165
|
+
return imports[0]
|
|
166
|
+
|
|
167
|
+
@classmethod
|
|
168
|
+
def _from_identifier_or_statement(cls, arg):
|
|
169
|
+
"""
|
|
170
|
+
Parse either a raw identifier or a statement.
|
|
171
|
+
|
|
172
|
+
>>> Import._from_identifier_or_statement('foo.bar.baz')
|
|
173
|
+
Import('from foo.bar import baz')
|
|
174
|
+
|
|
175
|
+
>>> Import._from_identifier_or_statement('import foo.bar.baz')
|
|
176
|
+
Import('import foo.bar.baz')
|
|
177
|
+
|
|
178
|
+
:rtype:
|
|
179
|
+
`Import`
|
|
180
|
+
"""
|
|
181
|
+
if is_identifier(arg, dotted=True):
|
|
182
|
+
return cls.from_parts(arg, arg.split('.')[-1])
|
|
183
|
+
else:
|
|
184
|
+
return cls._from_statement(arg)
|
|
185
|
+
|
|
186
|
+
@cached_attribute
|
|
187
|
+
def split(self):
|
|
188
|
+
"""
|
|
189
|
+
Split this `Import` into a ``ImportSplit`` which represents the
|
|
190
|
+
token-level ``module_name``, ``member_name``, ``import_as``.
|
|
191
|
+
|
|
192
|
+
Note that at the token level, ``import_as`` can be ``None`` to represent
|
|
193
|
+
that the import statement doesn't have an "as ..." clause, whereas the
|
|
194
|
+
``import_as`` attribute on an ``Import`` object is never ``None``.
|
|
195
|
+
|
|
196
|
+
>>> Import.from_parts(".foo.bar", "bar").split
|
|
197
|
+
ImportSplit(module_name='.foo', member_name='bar', import_as=None)
|
|
198
|
+
|
|
199
|
+
>>> Import("from . import foo").split
|
|
200
|
+
ImportSplit(module_name='.', member_name='foo', import_as=None)
|
|
201
|
+
|
|
202
|
+
>>> Import.from_parts(".foo", "foo").split
|
|
203
|
+
ImportSplit(module_name='.', member_name='foo', import_as=None)
|
|
204
|
+
|
|
205
|
+
>>> Import.from_parts("foo.bar", "foo.bar").split
|
|
206
|
+
ImportSplit(module_name=None, member_name='foo.bar', import_as=None)
|
|
207
|
+
|
|
208
|
+
:rtype:
|
|
209
|
+
`ImportSplit`
|
|
210
|
+
"""
|
|
211
|
+
if self.import_as == self.fullname:
|
|
212
|
+
return ImportSplit(None, self.fullname, None)
|
|
213
|
+
level = 0
|
|
214
|
+
qname = self.fullname
|
|
215
|
+
for level, char in enumerate(qname):
|
|
216
|
+
if char != '.':
|
|
217
|
+
break
|
|
218
|
+
prefix = qname[:level]
|
|
219
|
+
qname = qname[level:]
|
|
220
|
+
if '.' in qname:
|
|
221
|
+
module_name, member_name = qname.rsplit(".", 1)
|
|
222
|
+
else:
|
|
223
|
+
module_name = ''
|
|
224
|
+
member_name = qname
|
|
225
|
+
module_name = prefix + module_name
|
|
226
|
+
import_as = self.import_as
|
|
227
|
+
if import_as == member_name:
|
|
228
|
+
import_as = None
|
|
229
|
+
return ImportSplit(module_name or None, member_name, import_as)
|
|
230
|
+
|
|
231
|
+
@classmethod
|
|
232
|
+
def from_split(cls, impsplit, comment=None):
|
|
233
|
+
"""
|
|
234
|
+
Construct an `Import` instance from ``module_name``, ``member_name``,
|
|
235
|
+
``import_as``.
|
|
236
|
+
|
|
237
|
+
:rtype:
|
|
238
|
+
`Import`
|
|
239
|
+
"""
|
|
240
|
+
module_name, member_name, import_as = ImportSplit(*impsplit)
|
|
241
|
+
if import_as is None:
|
|
242
|
+
import_as = member_name
|
|
243
|
+
if module_name is None:
|
|
244
|
+
result = cls.from_parts(member_name, import_as, comment)
|
|
245
|
+
else:
|
|
246
|
+
fullname = "%s%s%s" % (
|
|
247
|
+
module_name,
|
|
248
|
+
"" if module_name.endswith(".") else ".",
|
|
249
|
+
member_name)
|
|
250
|
+
result = cls.from_parts(fullname, import_as, comment)
|
|
251
|
+
# result.split will usually be the same as impsplit, but could be
|
|
252
|
+
# different if the input was 'import foo.bar as baz', which we
|
|
253
|
+
# canonicalize to 'from foo import bar as baz'.
|
|
254
|
+
return result
|
|
255
|
+
|
|
256
|
+
def prefix_match(self, imp):
|
|
257
|
+
"""
|
|
258
|
+
Return the longest common prefix between ``self`` and ``imp``.
|
|
259
|
+
|
|
260
|
+
>>> Import("import ab.cd.ef").prefix_match(Import("import ab.cd.xy"))
|
|
261
|
+
('ab', 'cd')
|
|
262
|
+
|
|
263
|
+
:type imp:
|
|
264
|
+
`Import`
|
|
265
|
+
:rtype:
|
|
266
|
+
``tuple`` of ``str``
|
|
267
|
+
"""
|
|
268
|
+
imp = Import(imp)
|
|
269
|
+
n1 = self.fullname.split('.')
|
|
270
|
+
n2 = imp.fullname.split('.')
|
|
271
|
+
return tuple(longest_common_prefix(n1, n2))
|
|
272
|
+
|
|
273
|
+
def replace(self, prefix, replacement):
|
|
274
|
+
"""
|
|
275
|
+
Return a new ``Import`` that replaces ``prefix`` with ``replacement``.
|
|
276
|
+
|
|
277
|
+
>>> Import("from aa.bb import cc").replace("aa.bb", "xx.yy")
|
|
278
|
+
Import('from xx.yy import cc')
|
|
279
|
+
|
|
280
|
+
>>> Import("from aa import bb").replace("aa.bb", "xx.yy")
|
|
281
|
+
Import('from xx import yy as bb')
|
|
282
|
+
|
|
283
|
+
:rtype:
|
|
284
|
+
``Import``
|
|
285
|
+
"""
|
|
286
|
+
prefix_parts = prefix.split('.')
|
|
287
|
+
replacement_parts = replacement.split('.')
|
|
288
|
+
fullname_parts = self.fullname.split('.')
|
|
289
|
+
if fullname_parts[:len(prefix_parts)] != prefix_parts:
|
|
290
|
+
# No prefix match.
|
|
291
|
+
return self
|
|
292
|
+
fullname_parts[:len(prefix_parts)] = replacement_parts
|
|
293
|
+
import_as_parts = self.import_as.split('.')
|
|
294
|
+
if import_as_parts[:len(prefix_parts)] == prefix_parts:
|
|
295
|
+
import_as_parts[:len(prefix_parts)] = replacement_parts
|
|
296
|
+
return self.from_parts('.'.join(fullname_parts),
|
|
297
|
+
'.'.join(import_as_parts))
|
|
298
|
+
|
|
299
|
+
@cached_attribute
|
|
300
|
+
def flags(self):
|
|
301
|
+
"""
|
|
302
|
+
If this is a __future__ import, then the compiler_flag associated with
|
|
303
|
+
it. Otherwise, 0.
|
|
304
|
+
"""
|
|
305
|
+
if self.split.module_name == "__future__":
|
|
306
|
+
return CompilerFlags(self.split.member_name)
|
|
307
|
+
else:
|
|
308
|
+
return CompilerFlags.from_int(0)
|
|
309
|
+
|
|
310
|
+
@property
|
|
311
|
+
def _data(self):
|
|
312
|
+
return (self.fullname, self.import_as)
|
|
313
|
+
|
|
314
|
+
def pretty_print(self, params=FormatParams()):
|
|
315
|
+
return ImportStatement([self]).pretty_print(params)
|
|
316
|
+
|
|
317
|
+
def __str__(self):
|
|
318
|
+
return self.pretty_print(FormatParams(max_line_length=Inf)).rstrip()
|
|
319
|
+
|
|
320
|
+
def __repr__(self):
|
|
321
|
+
return "%s(%r)" % (type(self).__name__, str(self))
|
|
322
|
+
|
|
323
|
+
def __hash__(self):
|
|
324
|
+
return hash(self._data)
|
|
325
|
+
|
|
326
|
+
def __cmp__(self, other):
|
|
327
|
+
if self is other:
|
|
328
|
+
return 0
|
|
329
|
+
if not isinstance(other, Import):
|
|
330
|
+
return NotImplemented
|
|
331
|
+
return cmp(self._data, other._data)
|
|
332
|
+
|
|
333
|
+
def __eq__(self, other):
|
|
334
|
+
if self is other:
|
|
335
|
+
return True
|
|
336
|
+
if not isinstance(other, Import):
|
|
337
|
+
return NotImplemented
|
|
338
|
+
return self._data == other._data
|
|
339
|
+
|
|
340
|
+
def __ne__(self, other):
|
|
341
|
+
return not (self == other)
|
|
342
|
+
|
|
343
|
+
# The rest are defined by total_ordering
|
|
344
|
+
def __lt__(self, other):
|
|
345
|
+
if self is other:
|
|
346
|
+
return False
|
|
347
|
+
if not isinstance(other, Import):
|
|
348
|
+
return NotImplemented
|
|
349
|
+
return self._data < other._data
|
|
350
|
+
|
|
351
|
+
def _validate_alias(arg) -> Tuple[str, Optional[str]]:
|
|
352
|
+
"""
|
|
353
|
+
Ensure each alias is a tuple (str, None|str), and return it.
|
|
354
|
+
|
|
355
|
+
"""
|
|
356
|
+
assert isinstance(arg, tuple)
|
|
357
|
+
# Pyright does not seem to be able to infer the length from a
|
|
358
|
+
# the unpacking.
|
|
359
|
+
assert len(arg) == 2
|
|
360
|
+
a0, a1 = arg
|
|
361
|
+
assert isinstance(a0, str)
|
|
362
|
+
assert isinstance(a1, (str, type(None)))
|
|
363
|
+
return arg
|
|
364
|
+
|
|
365
|
+
@total_ordering
|
|
366
|
+
class ImportStatement:
|
|
367
|
+
"""
|
|
368
|
+
Token-level representation of an import statement containing multiple
|
|
369
|
+
imports from a single module. Corresponds to an ``ast.ImportFrom`` or
|
|
370
|
+
``ast.Import``.
|
|
371
|
+
"""
|
|
372
|
+
|
|
373
|
+
aliases : Tuple[Tuple[str, Optional[str]],...]
|
|
374
|
+
fromname : Optional[str]
|
|
375
|
+
comments : Optional[list[Optional[str]]] = None
|
|
376
|
+
|
|
377
|
+
def __new__(cls, arg):
|
|
378
|
+
if isinstance(arg, cls):
|
|
379
|
+
return arg
|
|
380
|
+
if isinstance(arg, str):
|
|
381
|
+
return cls._from_str(arg)
|
|
382
|
+
if isinstance(arg, PythonStatement):
|
|
383
|
+
return cls._from_statement(arg)
|
|
384
|
+
if isinstance(arg, (ast.ImportFrom, ast.Import)):
|
|
385
|
+
return cls._from_ast_node(arg)
|
|
386
|
+
if isinstance(arg, Import):
|
|
387
|
+
return cls._from_imports([arg])
|
|
388
|
+
if isinstance(arg, (tuple, list)) and len(arg):
|
|
389
|
+
if isinstance(arg[0], Import):
|
|
390
|
+
return cls._from_imports(arg)
|
|
391
|
+
raise TypeError
|
|
392
|
+
|
|
393
|
+
@classmethod
|
|
394
|
+
def from_parts(
|
|
395
|
+
cls,
|
|
396
|
+
fromname: Optional[str],
|
|
397
|
+
aliases: Tuple[Tuple[str, Optional[str]], ...],
|
|
398
|
+
comments: Optional[list[Optional[str]]] = None,
|
|
399
|
+
):
|
|
400
|
+
assert isinstance(aliases, tuple)
|
|
401
|
+
assert len(aliases) > 0
|
|
402
|
+
|
|
403
|
+
self = object.__new__(cls)
|
|
404
|
+
self.fromname = fromname
|
|
405
|
+
self.aliases = tuple(_validate_alias(a) for a in aliases)
|
|
406
|
+
self.comments = comments
|
|
407
|
+
return self
|
|
408
|
+
|
|
409
|
+
@classmethod
|
|
410
|
+
def _from_str(cls, code:str, /):
|
|
411
|
+
"""
|
|
412
|
+
>>> ImportStatement._from_str("from foo import bar, bar2, bar")
|
|
413
|
+
ImportStatement('from foo import bar, bar2, bar')
|
|
414
|
+
|
|
415
|
+
>>> ImportStatement._from_str("from foo import bar as bar")
|
|
416
|
+
ImportStatement('from foo import bar as bar')
|
|
417
|
+
|
|
418
|
+
>>> ImportStatement._from_str("from foo.bar import baz")
|
|
419
|
+
ImportStatement('from foo.bar import baz')
|
|
420
|
+
|
|
421
|
+
>>> ImportStatement._from_str("import foo.bar")
|
|
422
|
+
ImportStatement('import foo.bar')
|
|
423
|
+
|
|
424
|
+
>>> ImportStatement._from_str("from .foo import bar")
|
|
425
|
+
ImportStatement('from .foo import bar')
|
|
426
|
+
|
|
427
|
+
>>> ImportStatement._from_str("from . import bar, bar2")
|
|
428
|
+
ImportStatement('from . import bar, bar2')
|
|
429
|
+
|
|
430
|
+
:type statement:
|
|
431
|
+
`PythonStatement`
|
|
432
|
+
:rtype:
|
|
433
|
+
`ImportStatement`
|
|
434
|
+
"""
|
|
435
|
+
return cls._from_statement(
|
|
436
|
+
PythonStatement(code)
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
@classmethod
|
|
440
|
+
def _from_statement(cls, statement):
|
|
441
|
+
stmt = PythonStatement.from_statement(statement)
|
|
442
|
+
return cls._from_ast_node(
|
|
443
|
+
stmt.ast_node,
|
|
444
|
+
comments=stmt.text.get_comments()
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
@classmethod
|
|
448
|
+
def _from_ast_node(cls, node, comments: Optional[list[Optional[str]]] = None):
|
|
449
|
+
"""
|
|
450
|
+
Construct an `ImportStatement` from an `ast` node.
|
|
451
|
+
|
|
452
|
+
:rtype:
|
|
453
|
+
`ImportStatement`
|
|
454
|
+
"""
|
|
455
|
+
if isinstance(node, ast.ImportFrom):
|
|
456
|
+
if node.module is None:
|
|
457
|
+
module = ''
|
|
458
|
+
else:
|
|
459
|
+
assert isinstance(node.module, str)
|
|
460
|
+
module = node.module
|
|
461
|
+
fromname = '.' * node.level + module
|
|
462
|
+
elif isinstance(node, ast.Import):
|
|
463
|
+
fromname = None
|
|
464
|
+
else:
|
|
465
|
+
raise NonImportStatementError(
|
|
466
|
+
'Expected ImportStatement, got {node}'.format(node=node)
|
|
467
|
+
)
|
|
468
|
+
aliases = tuple( (alias.name, alias.asname) for alias in node.names )
|
|
469
|
+
return cls.from_parts(fromname, aliases, comments)
|
|
470
|
+
|
|
471
|
+
@classmethod
|
|
472
|
+
def _from_imports(cls, imports):
|
|
473
|
+
"""
|
|
474
|
+
Construct an `ImportStatement` from a sequence of ``Import`` s. They
|
|
475
|
+
must all have the same ``fromname``.
|
|
476
|
+
|
|
477
|
+
:type imports:
|
|
478
|
+
Sequence of `Import` s
|
|
479
|
+
:rtype:
|
|
480
|
+
`ImportStatement`
|
|
481
|
+
"""
|
|
482
|
+
if not all(isinstance(imp, Import) for imp in imports):
|
|
483
|
+
raise TypeError
|
|
484
|
+
if not len(imports) > 0:
|
|
485
|
+
raise ValueError
|
|
486
|
+
module_names = set(imp.split.module_name for imp in imports)
|
|
487
|
+
if len(module_names) > 1:
|
|
488
|
+
raise ValueError(
|
|
489
|
+
"Inconsistent module names %r" % (sorted(module_names),))
|
|
490
|
+
|
|
491
|
+
return cls.from_parts(
|
|
492
|
+
fromname=list(module_names)[0],
|
|
493
|
+
aliases=tuple(imp.split[1:] for imp in imports),
|
|
494
|
+
comments=[imp.comment for imp in imports]
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
@cached_attribute
|
|
498
|
+
def imports(self):
|
|
499
|
+
"""
|
|
500
|
+
Return a sequence of `Import` s.
|
|
501
|
+
|
|
502
|
+
If the returned sequence of imports has only one entry, any single line
|
|
503
|
+
comment will be included with it. Otherwise, line comments are not included
|
|
504
|
+
because there's too much ambiguity about where they should be placed otherwise.
|
|
505
|
+
|
|
506
|
+
:rtype:
|
|
507
|
+
``tuple`` of `Import` s
|
|
508
|
+
"""
|
|
509
|
+
result = []
|
|
510
|
+
for alias in self.aliases:
|
|
511
|
+
result.append(
|
|
512
|
+
Import.from_split(
|
|
513
|
+
(self.fromname, alias[0], alias[1]),
|
|
514
|
+
comment=self.get_valid_comment(),
|
|
515
|
+
)
|
|
516
|
+
)
|
|
517
|
+
return tuple(result)
|
|
518
|
+
|
|
519
|
+
def get_valid_comment(self):
|
|
520
|
+
"""Get the comment for the ImportStatment, if possible.
|
|
521
|
+
|
|
522
|
+
A comment is only valid if there is a single comment.
|
|
523
|
+
|
|
524
|
+
# 1. The ImportStatement has a single alias
|
|
525
|
+
# 2. There is a single string comment in self.comments
|
|
526
|
+
|
|
527
|
+
:rtype:
|
|
528
|
+
``Optional[str]`` containing the valid comment, if any
|
|
529
|
+
"""
|
|
530
|
+
if self.comments and len(self.aliases) == 1:
|
|
531
|
+
valid = [comment for comment in self.comments if comment is not None]
|
|
532
|
+
if len(valid) == 1:
|
|
533
|
+
return valid[0]
|
|
534
|
+
return None
|
|
535
|
+
|
|
536
|
+
@property
|
|
537
|
+
def module(self) -> Tuple[str, ...]:
|
|
538
|
+
"""
|
|
539
|
+
|
|
540
|
+
return the import module as a list of string (which would be joined by
|
|
541
|
+
dot in the original import form.
|
|
542
|
+
|
|
543
|
+
This is useful for sorting purposes
|
|
544
|
+
|
|
545
|
+
Note that this may contain some empty string in particular with relative
|
|
546
|
+
imports
|
|
547
|
+
"""
|
|
548
|
+
if self.fromname:
|
|
549
|
+
return tuple(self.fromname.split('.'))
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
assert len(self.aliases) == 1, self.aliases
|
|
553
|
+
|
|
554
|
+
return tuple(self.aliases[0][0].split('.'))
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def _cmp(self):
|
|
558
|
+
"""
|
|
559
|
+
Comparison function for sorting.
|
|
560
|
+
|
|
561
|
+
We want to sort:
|
|
562
|
+
- by the root module
|
|
563
|
+
- whether it is an "import ... as", or "from ... import as" import
|
|
564
|
+
- then lexicographically
|
|
565
|
+
|
|
566
|
+
"""
|
|
567
|
+
return (self.module[0], 0 if self.fromname is not None else 1, self.fromname)
|
|
568
|
+
|
|
569
|
+
@cached_attribute
|
|
570
|
+
def flags(self):
|
|
571
|
+
"""
|
|
572
|
+
If this is a __future__ import, then the bitwise-ORed of the
|
|
573
|
+
compiler_flag values associated with the features. Otherwise, 0.
|
|
574
|
+
"""
|
|
575
|
+
return CompilerFlags(*[imp.flags for imp in self.imports])
|
|
576
|
+
|
|
577
|
+
def pretty_print(self, params=FormatParams(),
|
|
578
|
+
import_column=None, from_spaces=1):
|
|
579
|
+
"""
|
|
580
|
+
Pretty-print into a single string.
|
|
581
|
+
|
|
582
|
+
ImportStatement objects represent python import statements, which can span
|
|
583
|
+
multiple lines or multiple aliases. Here we append comments
|
|
584
|
+
|
|
585
|
+
- If the output is one line
|
|
586
|
+
- If there is only one comment
|
|
587
|
+
|
|
588
|
+
This way we avoid worrying about combining comments from multiple lines,
|
|
589
|
+
or where to place comments if the resulting output is more than one line
|
|
590
|
+
|
|
591
|
+
:type params:
|
|
592
|
+
`FormatParams`
|
|
593
|
+
:param modulename_ljust:
|
|
594
|
+
Number of characters to left-justify the 'from' name.
|
|
595
|
+
:rtype:
|
|
596
|
+
``str``
|
|
597
|
+
"""
|
|
598
|
+
s0 = ''
|
|
599
|
+
s = ''
|
|
600
|
+
assert from_spaces >= 1
|
|
601
|
+
if self.fromname is not None:
|
|
602
|
+
s += "from%s%s " % (' ' * from_spaces, self.fromname)
|
|
603
|
+
if import_column is not None:
|
|
604
|
+
if len(s) > import_column:
|
|
605
|
+
# The caller wants the 'import' statement lined up left of
|
|
606
|
+
# where the current end of the line is. So wrap it
|
|
607
|
+
# specially like this::
|
|
608
|
+
# from foo import ...
|
|
609
|
+
# from foo.bar.baz \
|
|
610
|
+
# import ...
|
|
611
|
+
s0 = s + '\\\n'
|
|
612
|
+
s = ' ' * import_column
|
|
613
|
+
else:
|
|
614
|
+
s = s.ljust(import_column)
|
|
615
|
+
s += "import "
|
|
616
|
+
tokens = []
|
|
617
|
+
for importname, asname in self.aliases:
|
|
618
|
+
if asname is not None:
|
|
619
|
+
t = "%s as %s" % (importname, asname)
|
|
620
|
+
else:
|
|
621
|
+
t = "%s" % (importname,)
|
|
622
|
+
|
|
623
|
+
tokens.append(t)
|
|
624
|
+
res = s0 + pyfill(s, tokens, params=params)
|
|
625
|
+
|
|
626
|
+
comment = self.get_valid_comment()
|
|
627
|
+
if comment is not None:
|
|
628
|
+
# Only append to text on the first line
|
|
629
|
+
lines = res.split('\n')
|
|
630
|
+
if len(lines) == 2:
|
|
631
|
+
lines[0] += f" #{comment}"
|
|
632
|
+
res = "\n".join(lines)
|
|
633
|
+
|
|
634
|
+
if params.use_black:
|
|
635
|
+
res = self.run_black(res, params)
|
|
636
|
+
|
|
637
|
+
return res
|
|
638
|
+
|
|
639
|
+
@staticmethod
|
|
640
|
+
def run_black(src_contents: str, params:FormatParams) -> str:
|
|
641
|
+
"""Run the black formatter for the Python source code given as a string
|
|
642
|
+
|
|
643
|
+
This is adapted from https://github.com/akaihola/darker
|
|
644
|
+
|
|
645
|
+
"""
|
|
646
|
+
from black import format_str, FileMode
|
|
647
|
+
from black.mode import TargetVersion
|
|
648
|
+
|
|
649
|
+
black_config = read_black_config()
|
|
650
|
+
mode = dict()
|
|
651
|
+
|
|
652
|
+
if params.max_line_length is None:
|
|
653
|
+
mode["line_length"] = black_config.get("line_length", params.max_line_length_default)
|
|
654
|
+
else:
|
|
655
|
+
mode["line_length"] = params.max_line_length
|
|
656
|
+
|
|
657
|
+
if "target_version" in black_config:
|
|
658
|
+
if isinstance(black_config["target_version"], set):
|
|
659
|
+
target_versions_in = black_config["target_version"]
|
|
660
|
+
else:
|
|
661
|
+
target_versions_in = {black_config["target_version"]}
|
|
662
|
+
all_target_versions = {
|
|
663
|
+
tgt_v.name.lower(): tgt_v for tgt_v in TargetVersion
|
|
664
|
+
}
|
|
665
|
+
bad_target_versions = target_versions_in - set(all_target_versions)
|
|
666
|
+
if bad_target_versions:
|
|
667
|
+
raise ValueError(
|
|
668
|
+
f"Invalid target version(s) {bad_target_versions}"
|
|
669
|
+
)
|
|
670
|
+
mode["target_versions"] = {
|
|
671
|
+
all_target_versions[n] for n in target_versions_in
|
|
672
|
+
}
|
|
673
|
+
if "skip_magic_trailing_comma" in black_config:
|
|
674
|
+
mode["magic_trailing_comma"] = not black_config[
|
|
675
|
+
"skip_magic_trailing_comma"
|
|
676
|
+
]
|
|
677
|
+
if "skip_string_normalization" in black_config:
|
|
678
|
+
# The ``black`` command line argument is
|
|
679
|
+
# ``--skip-string-normalization``, but the parameter for
|
|
680
|
+
# ``black.Mode`` needs to be the opposite boolean of
|
|
681
|
+
# ``skip-string-normalization``, hence the inverse boolean
|
|
682
|
+
mode["string_normalization"] = not black_config["skip_string_normalization"]
|
|
683
|
+
|
|
684
|
+
# The custom handling of empty and all-whitespace files below will be unnecessary if
|
|
685
|
+
# https://github.com/psf/black/pull/2484 lands in Black.
|
|
686
|
+
contents_for_black = src_contents
|
|
687
|
+
return format_str(contents_for_black, mode=FileMode(**mode))
|
|
688
|
+
|
|
689
|
+
@property
|
|
690
|
+
def _data(self):
|
|
691
|
+
return (self.fromname, self.aliases)
|
|
692
|
+
|
|
693
|
+
def __str__(self):
|
|
694
|
+
return self.pretty_print(FormatParams(max_line_length=Inf)).rstrip()
|
|
695
|
+
|
|
696
|
+
def __repr__(self):
|
|
697
|
+
return "%s(%r)" % (type(self).__name__, str(self))
|
|
698
|
+
|
|
699
|
+
def __cmp__(self, other):
|
|
700
|
+
if self is other:
|
|
701
|
+
return 0
|
|
702
|
+
if not isinstance(other, ImportStatement):
|
|
703
|
+
return NotImplemented
|
|
704
|
+
return cmp(self._data, other._data)
|
|
705
|
+
|
|
706
|
+
def __eq__(self, other):
|
|
707
|
+
if self is other:
|
|
708
|
+
return True
|
|
709
|
+
if not isinstance(other, ImportStatement):
|
|
710
|
+
return NotImplemented
|
|
711
|
+
return self._data == other._data
|
|
712
|
+
|
|
713
|
+
def __ne__(self, other):
|
|
714
|
+
return not (self == other)
|
|
715
|
+
|
|
716
|
+
# The rest are defined by total_ordering
|
|
717
|
+
def __lt__(self, other):
|
|
718
|
+
if not isinstance(other, ImportStatement):
|
|
719
|
+
return NotImplemented
|
|
720
|
+
return self._data < other._data
|
|
721
|
+
|
|
722
|
+
def __hash__(self):
|
|
723
|
+
return hash(self._data)
|