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/_file.py
ADDED
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
# pyflyby/_file.py.
|
|
2
|
+
# Copyright (C) 2011, 2012, 2013, 2014, 2015, 2018 Karl Chen.
|
|
3
|
+
# License: MIT http://opensource.org/licenses/MIT
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from functools import cached_property, total_ordering
|
|
7
|
+
import io
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import sys
|
|
11
|
+
from typing import ClassVar, List, Optional, Tuple, Union
|
|
12
|
+
|
|
13
|
+
from pyflyby._util import cmp, memoize
|
|
14
|
+
|
|
15
|
+
if sys.version_info < (3,10):
|
|
16
|
+
NoneType = type(None)
|
|
17
|
+
else:
|
|
18
|
+
from types import NoneType
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class UnsafeFilenameError(ValueError):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# TODO: statcache
|
|
26
|
+
|
|
27
|
+
@total_ordering
|
|
28
|
+
class Filename(object):
|
|
29
|
+
"""
|
|
30
|
+
A filename.
|
|
31
|
+
|
|
32
|
+
>>> Filename('/etc/passwd')
|
|
33
|
+
Filename('/etc/passwd')
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
_filename: str
|
|
37
|
+
STDIN: Filename
|
|
38
|
+
|
|
39
|
+
def __new__(cls, arg):
|
|
40
|
+
if isinstance(arg, cls):
|
|
41
|
+
# TODO make this assert False
|
|
42
|
+
return cls._from_filename(arg._filename)
|
|
43
|
+
if isinstance(arg, str):
|
|
44
|
+
return cls._from_filename(arg)
|
|
45
|
+
raise TypeError
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def _from_filename(cls, filename: str):
|
|
49
|
+
if not isinstance(filename, str):
|
|
50
|
+
raise TypeError
|
|
51
|
+
filename = os.path.abspath(filename)
|
|
52
|
+
if not filename:
|
|
53
|
+
raise UnsafeFilenameError("(empty string)")
|
|
54
|
+
# we only allow filename with given character set
|
|
55
|
+
match = re.search("[^a-zA-Z0-9_=+{}/.,~@-]", filename)
|
|
56
|
+
if match:
|
|
57
|
+
raise UnsafeFilenameError((filename, match))
|
|
58
|
+
if re.search("(^|/)~", filename):
|
|
59
|
+
raise UnsafeFilenameError(filename)
|
|
60
|
+
self = object.__new__(cls)
|
|
61
|
+
self._filename = filename
|
|
62
|
+
return self
|
|
63
|
+
|
|
64
|
+
def __str__(self):
|
|
65
|
+
return self._filename
|
|
66
|
+
|
|
67
|
+
def __repr__(self):
|
|
68
|
+
return "%s(%r)" % (type(self).__name__, self._filename)
|
|
69
|
+
|
|
70
|
+
def __truediv__(self, x):
|
|
71
|
+
return type(self)(os.path.join(self._filename, x))
|
|
72
|
+
|
|
73
|
+
def __hash__(self):
|
|
74
|
+
return hash(self._filename)
|
|
75
|
+
|
|
76
|
+
def __eq__(self, o):
|
|
77
|
+
if self is o:
|
|
78
|
+
return True
|
|
79
|
+
if not isinstance(o, Filename):
|
|
80
|
+
return NotImplemented
|
|
81
|
+
return self._filename == o._filename
|
|
82
|
+
|
|
83
|
+
def __ne__(self, other):
|
|
84
|
+
return not (self == other)
|
|
85
|
+
|
|
86
|
+
# The rest are defined by total_ordering
|
|
87
|
+
def __lt__(self, o):
|
|
88
|
+
if not isinstance(o, Filename):
|
|
89
|
+
return NotImplemented
|
|
90
|
+
return self._filename < o._filename
|
|
91
|
+
|
|
92
|
+
def __cmp__(self, o):
|
|
93
|
+
if self is o:
|
|
94
|
+
return 0
|
|
95
|
+
if not isinstance(o, Filename):
|
|
96
|
+
return NotImplemented
|
|
97
|
+
return cmp(self._filename, o._filename)
|
|
98
|
+
|
|
99
|
+
@cached_property
|
|
100
|
+
def ext(self):
|
|
101
|
+
"""
|
|
102
|
+
Returns the extension of this filename, including the dot.
|
|
103
|
+
Returns ``None`` if no extension.
|
|
104
|
+
|
|
105
|
+
:rtype:
|
|
106
|
+
``str`` or ``None``
|
|
107
|
+
"""
|
|
108
|
+
lhs, dot, rhs = self._filename.rpartition('.')
|
|
109
|
+
if not dot:
|
|
110
|
+
return None
|
|
111
|
+
return dot + rhs
|
|
112
|
+
|
|
113
|
+
@cached_property
|
|
114
|
+
def base(self):
|
|
115
|
+
return os.path.basename(self._filename)
|
|
116
|
+
|
|
117
|
+
@cached_property
|
|
118
|
+
def dir(self):
|
|
119
|
+
return type(self)(os.path.dirname(self._filename))
|
|
120
|
+
|
|
121
|
+
@cached_property
|
|
122
|
+
def real(self):
|
|
123
|
+
return type(self)(os.path.realpath(self._filename))
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def realpath(self):
|
|
127
|
+
return type(self)(os.path.realpath(self._filename))
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def exists(self):
|
|
131
|
+
return os.path.exists(self._filename)
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def islink(self):
|
|
135
|
+
return os.path.islink(self._filename)
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def isdir(self):
|
|
139
|
+
return os.path.isdir(self._filename)
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def isfile(self):
|
|
143
|
+
return os.path.isfile(self._filename)
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def isreadable(self):
|
|
147
|
+
return os.access(self._filename, os.R_OK)
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def iswritable(self):
|
|
151
|
+
return os.access(self._filename, os.W_OK)
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def isexecutable(self):
|
|
155
|
+
return os.access(self._filename, os.X_OK)
|
|
156
|
+
|
|
157
|
+
def startswith(self, prefix):
|
|
158
|
+
prefix = Filename(prefix)
|
|
159
|
+
if self == prefix:
|
|
160
|
+
return True
|
|
161
|
+
return self._filename.startswith("%s/" % (prefix,))
|
|
162
|
+
|
|
163
|
+
def list(self, ignore_unsafe=True):
|
|
164
|
+
filenames = [os.path.join(self._filename, f)
|
|
165
|
+
for f in sorted(os.listdir(self._filename))]
|
|
166
|
+
result = []
|
|
167
|
+
for f in filenames:
|
|
168
|
+
try:
|
|
169
|
+
f = Filename(f)
|
|
170
|
+
except UnsafeFilenameError:
|
|
171
|
+
if ignore_unsafe:
|
|
172
|
+
continue
|
|
173
|
+
else:
|
|
174
|
+
raise
|
|
175
|
+
result.append(f)
|
|
176
|
+
return result
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def ancestors(self):
|
|
180
|
+
"""
|
|
181
|
+
Return ancestors of self, from self to /.
|
|
182
|
+
|
|
183
|
+
>>> Filename("/aa/bb").ancestors
|
|
184
|
+
(Filename('/aa/bb'), Filename('/aa'), Filename('/'))
|
|
185
|
+
|
|
186
|
+
:rtype:
|
|
187
|
+
``tuple`` of ``Filename`` s
|
|
188
|
+
"""
|
|
189
|
+
result = [self]
|
|
190
|
+
while True:
|
|
191
|
+
dir = result[-1].dir
|
|
192
|
+
if dir == result[-1]:
|
|
193
|
+
break
|
|
194
|
+
result.append(dir)
|
|
195
|
+
return tuple(result)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@memoize
|
|
199
|
+
def _get_PATH():
|
|
200
|
+
PATH = os.environ.get("PATH", "").split(os.pathsep)
|
|
201
|
+
result = []
|
|
202
|
+
for path in PATH:
|
|
203
|
+
if not path:
|
|
204
|
+
continue
|
|
205
|
+
try:
|
|
206
|
+
result.append(Filename(path))
|
|
207
|
+
except UnsafeFilenameError:
|
|
208
|
+
continue
|
|
209
|
+
return tuple(result)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def which(program):
|
|
213
|
+
"""
|
|
214
|
+
Find ``program`` on $PATH.
|
|
215
|
+
|
|
216
|
+
:type program:
|
|
217
|
+
``str``
|
|
218
|
+
:rtype:
|
|
219
|
+
`Filename`
|
|
220
|
+
:return:
|
|
221
|
+
Program on $PATH, or ``None`` if not found.
|
|
222
|
+
"""
|
|
223
|
+
# See if it exists in the current directory.
|
|
224
|
+
candidate = Filename(program)
|
|
225
|
+
if candidate.isreadable:
|
|
226
|
+
return candidate
|
|
227
|
+
for path in _get_PATH():
|
|
228
|
+
candidate = path / program
|
|
229
|
+
if candidate.isexecutable:
|
|
230
|
+
return candidate
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
Filename.STDIN = Filename("/dev/stdin")
|
|
236
|
+
|
|
237
|
+
@total_ordering
|
|
238
|
+
class FilePos(object):
|
|
239
|
+
"""
|
|
240
|
+
A (lineno, colno) position within a `FileText`.
|
|
241
|
+
Both lineno and colno are 1-indexed.
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
lineno: int
|
|
245
|
+
colno: int
|
|
246
|
+
|
|
247
|
+
_ONE_ONE: ClassVar[FilePos]
|
|
248
|
+
|
|
249
|
+
def __new__(cls, *args):
|
|
250
|
+
if len(args) == 0:
|
|
251
|
+
return cls._ONE_ONE
|
|
252
|
+
if len(args) == 1:
|
|
253
|
+
arg, = args
|
|
254
|
+
if isinstance(arg, cls):
|
|
255
|
+
return arg
|
|
256
|
+
elif arg is None:
|
|
257
|
+
return cls._ONE_ONE
|
|
258
|
+
elif isinstance(arg, tuple):
|
|
259
|
+
args = arg
|
|
260
|
+
# Fall through
|
|
261
|
+
else:
|
|
262
|
+
raise TypeError
|
|
263
|
+
lineno, colno = cls._intint(args)
|
|
264
|
+
if lineno == colno == 1:
|
|
265
|
+
return cls._ONE_ONE # space optimization
|
|
266
|
+
if lineno < 1:
|
|
267
|
+
raise ValueError(
|
|
268
|
+
"FilePos: invalid lineno=%d; should be >= 1" % lineno,)
|
|
269
|
+
if colno < 1:
|
|
270
|
+
raise ValueError(
|
|
271
|
+
"FilePos: invalid colno=%d; should be >= 1" % colno,)
|
|
272
|
+
return cls._from_lc(lineno, colno)
|
|
273
|
+
|
|
274
|
+
@staticmethod
|
|
275
|
+
def _intint(args):
|
|
276
|
+
if (type(args) is tuple and
|
|
277
|
+
len(args) == 2 and
|
|
278
|
+
type(args[0]) is type(args[1]) is int):
|
|
279
|
+
return args
|
|
280
|
+
else:
|
|
281
|
+
raise TypeError("Expected (int,int); got %r" % (args,))
|
|
282
|
+
|
|
283
|
+
@classmethod
|
|
284
|
+
def _from_lc(cls, lineno:int, colno:int):
|
|
285
|
+
self = object.__new__(cls)
|
|
286
|
+
self.lineno = lineno
|
|
287
|
+
self.colno = colno
|
|
288
|
+
return self
|
|
289
|
+
|
|
290
|
+
def __add__(self, delta):
|
|
291
|
+
'''
|
|
292
|
+
"Add" a coordinate (line,col) delta to this ``FilePos``.
|
|
293
|
+
|
|
294
|
+
Note that addition here may be a non-obvious. If there is any line
|
|
295
|
+
movement, then the existing column number is ignored, and the new
|
|
296
|
+
column is the new column delta + 1 (to convert into 1-based numbers).
|
|
297
|
+
|
|
298
|
+
:rtype:
|
|
299
|
+
`FilePos`
|
|
300
|
+
'''
|
|
301
|
+
ldelta, cdelta = self._intint(delta)
|
|
302
|
+
assert ldelta >= 0 and cdelta >= 0
|
|
303
|
+
if ldelta == 0:
|
|
304
|
+
return FilePos(self.lineno, self.colno + cdelta)
|
|
305
|
+
else:
|
|
306
|
+
return FilePos(self.lineno + ldelta, 1 + cdelta)
|
|
307
|
+
|
|
308
|
+
def __str__(self):
|
|
309
|
+
return "(%d,%d)" % (self.lineno, self.colno)
|
|
310
|
+
|
|
311
|
+
def __repr__(self):
|
|
312
|
+
return "FilePos%s" % (self,)
|
|
313
|
+
|
|
314
|
+
@property
|
|
315
|
+
def _data(self):
|
|
316
|
+
return (self.lineno, self.colno)
|
|
317
|
+
|
|
318
|
+
def __eq__(self, other):
|
|
319
|
+
if self is other:
|
|
320
|
+
return True
|
|
321
|
+
if not isinstance(other, FilePos):
|
|
322
|
+
return NotImplemented
|
|
323
|
+
return self._data == other._data
|
|
324
|
+
|
|
325
|
+
def __ne__(self, other):
|
|
326
|
+
return not (self == other)
|
|
327
|
+
|
|
328
|
+
def __cmp__(self, other):
|
|
329
|
+
if self is other:
|
|
330
|
+
return 0
|
|
331
|
+
if not isinstance(other, FilePos):
|
|
332
|
+
return NotImplemented
|
|
333
|
+
return cmp(self._data, other._data)
|
|
334
|
+
|
|
335
|
+
# The rest are defined by total_ordering
|
|
336
|
+
def __lt__(self, other):
|
|
337
|
+
if self is other:
|
|
338
|
+
return 0
|
|
339
|
+
if not isinstance(other, FilePos):
|
|
340
|
+
return NotImplemented
|
|
341
|
+
return self._data < other._data
|
|
342
|
+
|
|
343
|
+
def __hash__(self):
|
|
344
|
+
return hash(self._data)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
FilePos._ONE_ONE = FilePos._from_lc(1, 1)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
@total_ordering
|
|
352
|
+
class FileText:
|
|
353
|
+
"""
|
|
354
|
+
Represents a contiguous sequence of lines from a file.
|
|
355
|
+
"""
|
|
356
|
+
|
|
357
|
+
filename: Optional[Filename]
|
|
358
|
+
startpos: FilePos
|
|
359
|
+
_lines: Optional[Tuple[str, ...]] = None
|
|
360
|
+
|
|
361
|
+
def __new__(cls, arg, filename=None, startpos=None):
|
|
362
|
+
"""
|
|
363
|
+
Return a new ``FileText`` instance.
|
|
364
|
+
|
|
365
|
+
:type arg:
|
|
366
|
+
``FileText``, ``Filename``, ``str``, or tuple of ``str``
|
|
367
|
+
:param arg:
|
|
368
|
+
If a sequence of lines, then each should end with a newline and have
|
|
369
|
+
no other newlines. Otherwise, something that can be interpreted or
|
|
370
|
+
converted into a sequence of lines.
|
|
371
|
+
:type filename:
|
|
372
|
+
`Filename`
|
|
373
|
+
:param filename:
|
|
374
|
+
Filename to attach to this ``FileText``, if not already given by
|
|
375
|
+
``arg``.
|
|
376
|
+
:type startpos:
|
|
377
|
+
``FilePos``
|
|
378
|
+
:param startpos:
|
|
379
|
+
Starting file position (lineno & colno) of this ``FileText``, if not
|
|
380
|
+
already given by ``arg``.
|
|
381
|
+
:rtype:
|
|
382
|
+
``FileText``
|
|
383
|
+
"""
|
|
384
|
+
if isinstance(filename, str):
|
|
385
|
+
filename = Filename(filename)
|
|
386
|
+
if isinstance(arg, cls):
|
|
387
|
+
if filename is startpos is None:
|
|
388
|
+
return arg
|
|
389
|
+
return arg.alter(filename=filename, startpos=startpos)
|
|
390
|
+
elif isinstance(arg, Filename):
|
|
391
|
+
return cls(read_file(arg), filename=filename, startpos=startpos)
|
|
392
|
+
elif hasattr(arg, "__text__"):
|
|
393
|
+
return FileText(arg.__text__(), filename=filename, startpos=startpos)
|
|
394
|
+
elif isinstance(arg, str):
|
|
395
|
+
self = object.__new__(cls)
|
|
396
|
+
self._lines = tuple(arg.split('\n'))
|
|
397
|
+
else:
|
|
398
|
+
raise TypeError("%s: unexpected %s"
|
|
399
|
+
% (cls.__name__, type(arg).__name__))
|
|
400
|
+
|
|
401
|
+
assert isinstance(filename, (Filename, NoneType))
|
|
402
|
+
startpos = FilePos(startpos)
|
|
403
|
+
self.filename = filename
|
|
404
|
+
self.startpos = startpos
|
|
405
|
+
return self
|
|
406
|
+
|
|
407
|
+
def get_comments(self) -> list[Optional[str]]:
|
|
408
|
+
"""Return the comment string for each line (if any).
|
|
409
|
+
|
|
410
|
+
:return:
|
|
411
|
+
The comment string for each line in the statement. If no
|
|
412
|
+
comment is present, None is returned for that line
|
|
413
|
+
"""
|
|
414
|
+
comments: list[Optional[str]] = []
|
|
415
|
+
if self._lines:
|
|
416
|
+
for line in self._lines:
|
|
417
|
+
split = line.split("#", maxsplit=1)[1:]
|
|
418
|
+
if split:
|
|
419
|
+
comments.append(split[0])
|
|
420
|
+
else:
|
|
421
|
+
comments.append(None)
|
|
422
|
+
return comments
|
|
423
|
+
|
|
424
|
+
@classmethod
|
|
425
|
+
def _from_lines(cls, lines, filename: Optional[Filename], startpos: FilePos):
|
|
426
|
+
assert type(lines) is tuple
|
|
427
|
+
assert len(lines) > 0
|
|
428
|
+
assert isinstance(lines[0], str)
|
|
429
|
+
assert not lines[-1].endswith("\n")
|
|
430
|
+
assert isinstance(startpos, FilePos), repr(startpos)
|
|
431
|
+
assert isinstance(filename, (Filename, type(None))), repr(filename)
|
|
432
|
+
self = object.__new__(cls)
|
|
433
|
+
self._lines = tuple(lines)
|
|
434
|
+
self.filename = filename
|
|
435
|
+
self.startpos = startpos
|
|
436
|
+
return self
|
|
437
|
+
|
|
438
|
+
@cached_property
|
|
439
|
+
def lines(self) -> Tuple[str, ...]:
|
|
440
|
+
r"""
|
|
441
|
+
Lines that have been split by newline.
|
|
442
|
+
|
|
443
|
+
These strings do NOT contain '\n'.
|
|
444
|
+
|
|
445
|
+
If the input file ended in '\n', then the last item will be the empty
|
|
446
|
+
string. This is to avoid having to check lines[-1].endswith('\n')
|
|
447
|
+
everywhere.
|
|
448
|
+
|
|
449
|
+
:rtype:
|
|
450
|
+
``tuple`` of ``str``
|
|
451
|
+
"""
|
|
452
|
+
if self._lines is not None:
|
|
453
|
+
return self._lines
|
|
454
|
+
# Used if only initialized with 'joined'.
|
|
455
|
+
# We use str.split() instead of str.splitlines() because the latter
|
|
456
|
+
# doesn't distinguish between strings that end in newline or not
|
|
457
|
+
# (or requires extra work to process if we use splitlines(True)).
|
|
458
|
+
return tuple(self.joined.split('\n'))
|
|
459
|
+
|
|
460
|
+
@cached_property
|
|
461
|
+
def joined(self) -> str:
|
|
462
|
+
return '\n'.join(self.lines)
|
|
463
|
+
|
|
464
|
+
@classmethod
|
|
465
|
+
def from_filename(cls, filename):
|
|
466
|
+
return cls.from_lines(Filename(filename))
|
|
467
|
+
|
|
468
|
+
def alter(self, filename: Optional[Filename] = None, startpos=None):
|
|
469
|
+
if filename is not None:
|
|
470
|
+
assert isinstance(filename, Filename)
|
|
471
|
+
else:
|
|
472
|
+
filename = self.filename
|
|
473
|
+
if startpos is not None:
|
|
474
|
+
startpos = FilePos(startpos)
|
|
475
|
+
else:
|
|
476
|
+
startpos = self.startpos
|
|
477
|
+
if filename == self.filename and startpos == self.startpos:
|
|
478
|
+
return self
|
|
479
|
+
else:
|
|
480
|
+
result = object.__new__(type(self))
|
|
481
|
+
result._lines = self._lines
|
|
482
|
+
result.filename = filename
|
|
483
|
+
result.startpos = startpos
|
|
484
|
+
return result
|
|
485
|
+
|
|
486
|
+
@cached_property
|
|
487
|
+
def endpos(self):
|
|
488
|
+
"""
|
|
489
|
+
The position after the last character in the text.
|
|
490
|
+
|
|
491
|
+
:rtype:
|
|
492
|
+
``FilePos``
|
|
493
|
+
"""
|
|
494
|
+
startpos = self.startpos
|
|
495
|
+
lines = self.lines
|
|
496
|
+
lineno = startpos.lineno + len(lines) - 1
|
|
497
|
+
if len(lines) == 1:
|
|
498
|
+
colno = startpos.colno + len(lines[-1])
|
|
499
|
+
else:
|
|
500
|
+
colno = 1 + len(lines[-1])
|
|
501
|
+
return FilePos(lineno, colno)
|
|
502
|
+
|
|
503
|
+
def _lineno_to_index(self, lineno):
|
|
504
|
+
lineindex = lineno - self.startpos.lineno
|
|
505
|
+
# Check that the lineindex is in range. We don't allow pointing at
|
|
506
|
+
# the line after the last line because we already ensured that
|
|
507
|
+
# self.lines contains an extra empty string if necessary, to indicate
|
|
508
|
+
# a trailing newline in the file.
|
|
509
|
+
if not 0 <= lineindex < len(self.lines):
|
|
510
|
+
raise IndexError(
|
|
511
|
+
"Line number %d out of range [%d, %d)"
|
|
512
|
+
% (lineno, self.startpos.lineno, self.endpos.lineno))
|
|
513
|
+
return lineindex
|
|
514
|
+
|
|
515
|
+
def _colno_to_index(self, lineindex, colno):
|
|
516
|
+
coloffset = self.startpos.colno if lineindex == 0 else 1
|
|
517
|
+
colindex = colno - coloffset
|
|
518
|
+
line = self.lines[lineindex]
|
|
519
|
+
# Check that the colindex is in range. We do allow pointing at the
|
|
520
|
+
# character after the last (non-newline) character in the line.
|
|
521
|
+
if not 0 <= colindex <= len(line):
|
|
522
|
+
raise IndexError(
|
|
523
|
+
"Column number %d on line %d out of range [%d, %d]"
|
|
524
|
+
% (colno, lineindex+self.startpos.lineno,
|
|
525
|
+
coloffset, coloffset+len(line)))
|
|
526
|
+
return colindex
|
|
527
|
+
|
|
528
|
+
def __getitem__(self, arg):
|
|
529
|
+
"""
|
|
530
|
+
Return the line(s) with the given line number(s).
|
|
531
|
+
If slicing, returns an instance of ``FileText``.
|
|
532
|
+
|
|
533
|
+
Note that line numbers are indexed based on ``self.startpos.lineno``
|
|
534
|
+
(which is 1 at the start of the file).
|
|
535
|
+
|
|
536
|
+
>>> FileText("a\\nb\\nc\\nd")[2]
|
|
537
|
+
'b'
|
|
538
|
+
|
|
539
|
+
>>> FileText("a\\nb\\nc\\nd")[2:4]
|
|
540
|
+
FileText('b\\nc\\n', startpos=(2,1))
|
|
541
|
+
|
|
542
|
+
>>> FileText("a\\nb\\nc\\nd")[0]
|
|
543
|
+
Traceback (most recent call last):
|
|
544
|
+
...
|
|
545
|
+
IndexError: Line number 0 out of range [1, 4)
|
|
546
|
+
|
|
547
|
+
When slicing, the input arguments can also be given as ``FilePos``
|
|
548
|
+
arguments or (lineno,colno) tuples. These are 1-indexed at the start
|
|
549
|
+
of the file.
|
|
550
|
+
|
|
551
|
+
>>> FileText("a\\nb\\nc\\nd")[(2,2):4]
|
|
552
|
+
FileText('\\nc\\n', startpos=(2,2))
|
|
553
|
+
|
|
554
|
+
:rtype:
|
|
555
|
+
``str`` or `FileText`
|
|
556
|
+
"""
|
|
557
|
+
L = self._lineno_to_index
|
|
558
|
+
C = self._colno_to_index
|
|
559
|
+
if isinstance(arg, slice):
|
|
560
|
+
if arg.step is not None and arg.step != 1:
|
|
561
|
+
raise ValueError("steps not supported")
|
|
562
|
+
# Interpret start (lineno,colno) into indexes.
|
|
563
|
+
if arg.start is None:
|
|
564
|
+
start_lineindex = 0
|
|
565
|
+
start_colindex = 0
|
|
566
|
+
elif isinstance(arg.start, int):
|
|
567
|
+
start_lineindex = L(arg.start)
|
|
568
|
+
start_colindex = 0
|
|
569
|
+
else:
|
|
570
|
+
startpos = FilePos(arg.start)
|
|
571
|
+
start_lineindex = L(startpos.lineno)
|
|
572
|
+
start_colindex = C(start_lineindex, startpos.colno)
|
|
573
|
+
# Interpret stop (lineno,colno) into indexes.
|
|
574
|
+
if arg.stop is None:
|
|
575
|
+
stop_lineindex = len(self.lines)
|
|
576
|
+
stop_colindex = len(self.lines[-1])
|
|
577
|
+
elif isinstance(arg.stop, int):
|
|
578
|
+
stop_lineindex = L(arg.stop)
|
|
579
|
+
stop_colindex = 0
|
|
580
|
+
else:
|
|
581
|
+
stoppos = FilePos(arg.stop)
|
|
582
|
+
stop_lineindex = L(stoppos.lineno)
|
|
583
|
+
stop_colindex = C(stop_lineindex, stoppos.colno)
|
|
584
|
+
# {start,stop}_{lineindex,colindex} are now 0-indexed
|
|
585
|
+
# [open,closed) ranges.
|
|
586
|
+
assert 0 <= start_lineindex <= stop_lineindex < len(self.lines)
|
|
587
|
+
assert 0 <= start_colindex <= len(self.lines[start_lineindex])
|
|
588
|
+
assert 0 <= stop_colindex <= len(self.lines[stop_lineindex])
|
|
589
|
+
# Optimization: return entire range
|
|
590
|
+
if (start_lineindex == 0 and
|
|
591
|
+
start_colindex == 0 and
|
|
592
|
+
stop_lineindex == len(self.lines)-1 and
|
|
593
|
+
stop_colindex == len(self.lines[-1])):
|
|
594
|
+
return self
|
|
595
|
+
# Get the lines we care about. We always include an extra entry
|
|
596
|
+
# at the end which we'll chop to the desired number of characters.
|
|
597
|
+
result_split = list(self.lines[start_lineindex:stop_lineindex+1])
|
|
598
|
+
# Clip the starting and ending strings. We do the end clip first
|
|
599
|
+
# in case the result has only one line.
|
|
600
|
+
result_split[-1] = result_split[-1][:stop_colindex]
|
|
601
|
+
result_split[0] = result_split[0][start_colindex:]
|
|
602
|
+
# Compute the new starting line and column numbers.
|
|
603
|
+
result_lineno = start_lineindex + self.startpos.lineno
|
|
604
|
+
if start_lineindex == 0:
|
|
605
|
+
result_colno = start_colindex + self.startpos.colno
|
|
606
|
+
else:
|
|
607
|
+
result_colno = start_colindex + 1
|
|
608
|
+
result_startpos = FilePos(result_lineno, result_colno)
|
|
609
|
+
return FileText._from_lines(tuple(result_split),
|
|
610
|
+
filename=self.filename,
|
|
611
|
+
startpos=result_startpos)
|
|
612
|
+
elif isinstance(arg, int):
|
|
613
|
+
# Return a single line.
|
|
614
|
+
lineindex = L(arg)
|
|
615
|
+
return self.lines[lineindex]
|
|
616
|
+
else:
|
|
617
|
+
raise TypeError("bad type %r" % (type(arg),))
|
|
618
|
+
|
|
619
|
+
@classmethod
|
|
620
|
+
def concatenate(cls, args):
|
|
621
|
+
"""
|
|
622
|
+
Concatenate a bunch of `FileText` arguments. Uses the ``filename``
|
|
623
|
+
and ``startpos`` from the first argument.
|
|
624
|
+
|
|
625
|
+
:rtype:
|
|
626
|
+
`FileText`
|
|
627
|
+
"""
|
|
628
|
+
args = [FileText(x) for x in args]
|
|
629
|
+
if len(args) == 1:
|
|
630
|
+
return args[0]
|
|
631
|
+
return FileText(
|
|
632
|
+
''.join([l.joined for l in args]),
|
|
633
|
+
filename=args[0].filename if args else None,
|
|
634
|
+
startpos=args[0].startpos if args else None)
|
|
635
|
+
|
|
636
|
+
def __repr__(self):
|
|
637
|
+
r = "%s(%r" % (type(self).__name__, self.joined,)
|
|
638
|
+
if self.filename is not None:
|
|
639
|
+
r += ", filename=%r" % (str(self.filename),)
|
|
640
|
+
if self.startpos != FilePos():
|
|
641
|
+
r += ", startpos=%s" % (self.startpos,)
|
|
642
|
+
r += ")"
|
|
643
|
+
return r
|
|
644
|
+
|
|
645
|
+
def __str__(self):
|
|
646
|
+
return self.joined
|
|
647
|
+
|
|
648
|
+
def __eq__(self, o):
|
|
649
|
+
if self is o:
|
|
650
|
+
return True
|
|
651
|
+
if not isinstance(o, FileText):
|
|
652
|
+
return NotImplemented
|
|
653
|
+
return (self.filename == o.filename and
|
|
654
|
+
self.joined == o.joined and
|
|
655
|
+
self.startpos == o.startpos)
|
|
656
|
+
|
|
657
|
+
def __ne__(self, other):
|
|
658
|
+
return not (self == other)
|
|
659
|
+
|
|
660
|
+
# The rest are defined by total_ordering
|
|
661
|
+
def __lt__(self, o):
|
|
662
|
+
if not isinstance(o, FileText):
|
|
663
|
+
return NotImplemented
|
|
664
|
+
return ((self.filename, self.joined, self.startpos) <
|
|
665
|
+
(o .filename, o .joined, o .startpos))
|
|
666
|
+
|
|
667
|
+
def __cmp__(self, o):
|
|
668
|
+
if self is o:
|
|
669
|
+
return 0
|
|
670
|
+
if not isinstance(o, FileText):
|
|
671
|
+
return NotImplemented
|
|
672
|
+
return cmp((self.filename, self.joined, self.startpos),
|
|
673
|
+
(o .filename, o .joined, o .startpos))
|
|
674
|
+
|
|
675
|
+
def __hash__(self):
|
|
676
|
+
h = hash((self.filename, self.joined, self.startpos))
|
|
677
|
+
self.__hash__ = lambda: h
|
|
678
|
+
return h
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def read_file(filename: Filename) -> FileText:
|
|
682
|
+
assert isinstance(filename, Filename)
|
|
683
|
+
if filename == Filename.STDIN:
|
|
684
|
+
data = sys.stdin.read()
|
|
685
|
+
else:
|
|
686
|
+
with io.open(str(filename), 'r') as f:
|
|
687
|
+
data = f.read()
|
|
688
|
+
return FileText(data, filename=filename)
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def write_file(filename: Filename, data):
|
|
692
|
+
assert isinstance(filename, Filename)
|
|
693
|
+
data = FileText(data)
|
|
694
|
+
with open(str(filename), 'w') as f:
|
|
695
|
+
f.write(data.joined)
|
|
696
|
+
|
|
697
|
+
def atomic_write_file(filename: Filename, data):
|
|
698
|
+
assert isinstance(filename, Filename)
|
|
699
|
+
data = FileText(data)
|
|
700
|
+
temp_filename = Filename("%s.tmp.%s" % (filename, os.getpid(),))
|
|
701
|
+
write_file(temp_filename, data)
|
|
702
|
+
try:
|
|
703
|
+
st = os.stat(str(filename)) # OSError if file didn't exit before
|
|
704
|
+
os.chmod(str(temp_filename), st.st_mode)
|
|
705
|
+
os.chown(str(temp_filename), -1, st.st_gid) # OSError if not member of group
|
|
706
|
+
except OSError:
|
|
707
|
+
pass
|
|
708
|
+
os.rename(str(temp_filename), str(filename))
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
def expand_py_files_from_args(
|
|
712
|
+
pathnames: Union[List[Filename], Filename], on_error=lambda filename: None
|
|
713
|
+
):
|
|
714
|
+
"""
|
|
715
|
+
Enumerate ``*.py`` files, recursively.
|
|
716
|
+
|
|
717
|
+
Arguments that are files are always included.
|
|
718
|
+
Arguments that are directories are recursively searched for ``*.py`` files.
|
|
719
|
+
|
|
720
|
+
:type pathnames:
|
|
721
|
+
``list`` of `Filename` s
|
|
722
|
+
:type on_error:
|
|
723
|
+
callable
|
|
724
|
+
:param on_error:
|
|
725
|
+
Function that is called for arguments directly specified in ``pathnames``
|
|
726
|
+
that don't exist or are otherwise inaccessible.
|
|
727
|
+
:rtype:
|
|
728
|
+
``list`` of `Filename` s
|
|
729
|
+
"""
|
|
730
|
+
if not isinstance(pathnames, (tuple, list)):
|
|
731
|
+
# July 2024 DeprecationWarning
|
|
732
|
+
# this seem to be used only internally, maybe deprecate not passing a list.
|
|
733
|
+
pathnames = [pathnames]
|
|
734
|
+
for f in pathnames:
|
|
735
|
+
assert isinstance(f, Filename)
|
|
736
|
+
result = []
|
|
737
|
+
# Check for problematic arguments. Note that we intentionally only do
|
|
738
|
+
# this for directly specified arguments, not for recursively traversed
|
|
739
|
+
# arguments.
|
|
740
|
+
stack = []
|
|
741
|
+
for pathname in reversed(pathnames):
|
|
742
|
+
if pathname.isfile:
|
|
743
|
+
stack.append((pathname, True))
|
|
744
|
+
elif pathname.isdir:
|
|
745
|
+
stack.append((pathname, False))
|
|
746
|
+
else:
|
|
747
|
+
on_error(pathname)
|
|
748
|
+
while stack:
|
|
749
|
+
pathname, isfile = stack.pop(-1)
|
|
750
|
+
if isfile:
|
|
751
|
+
result.append(pathname)
|
|
752
|
+
continue
|
|
753
|
+
for f in reversed(pathname.list()):
|
|
754
|
+
# Check inclusions/exclusions for recursion. Note that we
|
|
755
|
+
# intentionally do this in the recursive step rather than the
|
|
756
|
+
# base step because if the user specification includes
|
|
757
|
+
# e.g. .pyflyby, we do want to include it; however, we don't
|
|
758
|
+
# want to recurse into .pyflyby ourselves.
|
|
759
|
+
if f.base.startswith("."):
|
|
760
|
+
continue
|
|
761
|
+
if f.base == "__pycache__":
|
|
762
|
+
continue
|
|
763
|
+
if f.isfile:
|
|
764
|
+
if f.ext == ".py":
|
|
765
|
+
stack.append((f, True))
|
|
766
|
+
elif f.isdir:
|
|
767
|
+
stack.append((f, False))
|
|
768
|
+
else:
|
|
769
|
+
# Silently ignore non-files/dirs from traversal.
|
|
770
|
+
pass
|
|
771
|
+
return result
|