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