pyflyby 1.10.1__cp311-cp311-manylinux_2_24_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pyflyby might be problematic. Click here for more details.

Files changed (55) hide show
  1. pyflyby/__init__.py +61 -0
  2. pyflyby/__main__.py +9 -0
  3. pyflyby/_autoimp.py +2229 -0
  4. pyflyby/_cmdline.py +548 -0
  5. pyflyby/_comms.py +221 -0
  6. pyflyby/_dbg.py +1367 -0
  7. pyflyby/_docxref.py +379 -0
  8. pyflyby/_dynimp.py +154 -0
  9. pyflyby/_fast_iter_modules.cpython-311-x86_64-linux-gnu.so +0 -0
  10. pyflyby/_file.py +771 -0
  11. pyflyby/_flags.py +230 -0
  12. pyflyby/_format.py +186 -0
  13. pyflyby/_idents.py +227 -0
  14. pyflyby/_import_sorting.py +165 -0
  15. pyflyby/_importclns.py +658 -0
  16. pyflyby/_importdb.py +680 -0
  17. pyflyby/_imports2s.py +643 -0
  18. pyflyby/_importstmt.py +723 -0
  19. pyflyby/_interactive.py +2113 -0
  20. pyflyby/_livepatch.py +793 -0
  21. pyflyby/_log.py +104 -0
  22. pyflyby/_modules.py +641 -0
  23. pyflyby/_parse.py +1381 -0
  24. pyflyby/_py.py +2166 -0
  25. pyflyby/_saveframe.py +1145 -0
  26. pyflyby/_saveframe_reader.py +471 -0
  27. pyflyby/_util.py +458 -0
  28. pyflyby/_version.py +7 -0
  29. pyflyby/autoimport.py +20 -0
  30. pyflyby/etc/pyflyby/canonical.py +10 -0
  31. pyflyby/etc/pyflyby/common.py +27 -0
  32. pyflyby/etc/pyflyby/forget.py +10 -0
  33. pyflyby/etc/pyflyby/mandatory.py +10 -0
  34. pyflyby/etc/pyflyby/numpy.py +156 -0
  35. pyflyby/etc/pyflyby/std.py +335 -0
  36. pyflyby/importdb.py +19 -0
  37. pyflyby/libexec/pyflyby/colordiff +34 -0
  38. pyflyby/libexec/pyflyby/diff-colorize +148 -0
  39. pyflyby/share/emacs/site-lisp/pyflyby.el +108 -0
  40. pyflyby-1.10.1.data/scripts/collect-exports +76 -0
  41. pyflyby-1.10.1.data/scripts/collect-imports +58 -0
  42. pyflyby-1.10.1.data/scripts/find-import +38 -0
  43. pyflyby-1.10.1.data/scripts/list-bad-xrefs +34 -0
  44. pyflyby-1.10.1.data/scripts/prune-broken-imports +34 -0
  45. pyflyby-1.10.1.data/scripts/pyflyby-diff +34 -0
  46. pyflyby-1.10.1.data/scripts/reformat-imports +27 -0
  47. pyflyby-1.10.1.data/scripts/replace-star-imports +37 -0
  48. pyflyby-1.10.1.data/scripts/saveframe +299 -0
  49. pyflyby-1.10.1.data/scripts/tidy-imports +163 -0
  50. pyflyby-1.10.1.data/scripts/transform-imports +47 -0
  51. pyflyby-1.10.1.dist-info/METADATA +591 -0
  52. pyflyby-1.10.1.dist-info/RECORD +55 -0
  53. pyflyby-1.10.1.dist-info/WHEEL +5 -0
  54. pyflyby-1.10.1.dist-info/entry_points.txt +4 -0
  55. pyflyby-1.10.1.dist-info/licenses/LICENSE.txt +23 -0
pyflyby/_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