pyflyby 1.10.4__cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_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.
- 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-312-x86_64-linux-gnu.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/_cmdline.py
ADDED
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
# pyflyby/_cmdline.py.
|
|
2
|
+
# Copyright (C) 2011, 2012, 2013, 2014, 2015, 2018 Karl Chen.
|
|
3
|
+
# License: MIT http://opensource.org/licenses/MIT
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
from builtins import input
|
|
8
|
+
import optparse
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import signal
|
|
12
|
+
import sys
|
|
13
|
+
from textwrap import dedent
|
|
14
|
+
import traceback
|
|
15
|
+
from typing import List
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from pyflyby._file import (FileText, Filename, atomic_write_file,
|
|
19
|
+
expand_py_files_from_args, read_file)
|
|
20
|
+
from pyflyby._importstmt import ImportFormatParams
|
|
21
|
+
from pyflyby._log import logger
|
|
22
|
+
from pyflyby._util import cached_attribute, indent
|
|
23
|
+
|
|
24
|
+
if sys.version_info < (3, 11):
|
|
25
|
+
from tomli import loads
|
|
26
|
+
else:
|
|
27
|
+
from tomllib import loads
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ConfigurationError(Exception):
|
|
31
|
+
"""Exception class indicating a configuration error."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def hfmt(s):
|
|
35
|
+
return dedent(s).strip()
|
|
36
|
+
|
|
37
|
+
def maindoc():
|
|
38
|
+
import __main__
|
|
39
|
+
return (__main__.__doc__ or '').strip()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _sigpipe_handler(*args):
|
|
43
|
+
# The parent process piped our stdout and closed the pipe before we
|
|
44
|
+
# finished writing, e.g. "tidy-imports ... | head" or "tidy-imports ... |
|
|
45
|
+
# less". Exit quietly - squelch the "close failed in file object
|
|
46
|
+
# destructor" message would otherwise be raised.
|
|
47
|
+
raise SystemExit(1)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def parse_args(addopts=None, import_format_params=False, modify_action_params=False):
|
|
51
|
+
"""
|
|
52
|
+
Do setup for a top-level script and parse arguments.
|
|
53
|
+
"""
|
|
54
|
+
### Setup.
|
|
55
|
+
# Register a SIGPIPE handler.
|
|
56
|
+
signal.signal(signal.SIGPIPE, _sigpipe_handler)
|
|
57
|
+
### Parse args.
|
|
58
|
+
parser = optparse.OptionParser(usage='\n'+maindoc())
|
|
59
|
+
|
|
60
|
+
def log_level_callbacker(level):
|
|
61
|
+
def callback(option, opt_str, value, parser):
|
|
62
|
+
logger.set_level(level)
|
|
63
|
+
return callback
|
|
64
|
+
|
|
65
|
+
def debug_callback(option, opt_str, value, parser):
|
|
66
|
+
logger.set_level("DEBUG")
|
|
67
|
+
|
|
68
|
+
parser.add_option("--debug", action="callback",
|
|
69
|
+
callback=debug_callback,
|
|
70
|
+
help="Debug mode (noisy and fail fast).")
|
|
71
|
+
|
|
72
|
+
parser.add_option("--verbose", action="callback",
|
|
73
|
+
callback=log_level_callbacker("DEBUG"),
|
|
74
|
+
help="Be noisy.")
|
|
75
|
+
|
|
76
|
+
parser.add_option("--quiet", action="callback",
|
|
77
|
+
callback=log_level_callbacker("ERROR"),
|
|
78
|
+
help="Be quiet.")
|
|
79
|
+
|
|
80
|
+
parser.add_option("--version", action="callback",
|
|
81
|
+
callback=lambda *args: print_version_and_exit(),
|
|
82
|
+
help="Print pyflyby version and exit.")
|
|
83
|
+
|
|
84
|
+
if modify_action_params:
|
|
85
|
+
group = optparse.OptionGroup(parser, "Action options")
|
|
86
|
+
action_diff = action_external_command('pyflyby-diff')
|
|
87
|
+
def parse_action(v):
|
|
88
|
+
V = v.strip().upper()
|
|
89
|
+
if V == 'PRINT':
|
|
90
|
+
return action_print
|
|
91
|
+
elif V == 'REPLACE':
|
|
92
|
+
return action_replace
|
|
93
|
+
elif V == 'QUERY':
|
|
94
|
+
return action_query()
|
|
95
|
+
elif V == "DIFF":
|
|
96
|
+
return action_diff
|
|
97
|
+
elif V.startswith("QUERY:"):
|
|
98
|
+
return action_query(v[6:])
|
|
99
|
+
elif V.startswith("EXECUTE:"):
|
|
100
|
+
return action_external_command(v[8:])
|
|
101
|
+
elif V == "IFCHANGED":
|
|
102
|
+
return action_ifchanged
|
|
103
|
+
elif V == "EXIT1":
|
|
104
|
+
return action_exit1
|
|
105
|
+
else:
|
|
106
|
+
raise Exception(
|
|
107
|
+
"Bad argument %r to --action; "
|
|
108
|
+
"expected PRINT or REPLACE or QUERY or IFCHANGED or EXIT1 "
|
|
109
|
+
"or EXECUTE:..." % (v,))
|
|
110
|
+
|
|
111
|
+
def set_actions(actions):
|
|
112
|
+
actions = tuple(actions)
|
|
113
|
+
parser.values.actions = actions
|
|
114
|
+
|
|
115
|
+
def action_callback(option, opt_str, value, parser):
|
|
116
|
+
action_args = value.split(',')
|
|
117
|
+
set_actions([parse_action(v) for v in action_args])
|
|
118
|
+
|
|
119
|
+
def action_callbacker(actions):
|
|
120
|
+
def callback(option, opt_str, value, parser):
|
|
121
|
+
set_actions(actions)
|
|
122
|
+
return callback
|
|
123
|
+
|
|
124
|
+
group.add_option(
|
|
125
|
+
"--actions", type='string', action='callback',
|
|
126
|
+
callback=action_callback,
|
|
127
|
+
metavar='PRINT|REPLACE|IFCHANGED|QUERY|DIFF|EXIT1:EXECUTE:mycommand',
|
|
128
|
+
help=hfmt('''
|
|
129
|
+
Comma-separated list of action(s) to take. If PRINT, print
|
|
130
|
+
the changed file to stdout. If REPLACE, then modify the
|
|
131
|
+
file in-place. If EXECUTE:mycommand, then execute
|
|
132
|
+
'mycommand oldfile tmpfile'. If DIFF, then execute
|
|
133
|
+
'pyflyby-diff'. If QUERY, then query user to continue.
|
|
134
|
+
If IFCHANGED, then continue actions only if file was
|
|
135
|
+
changed. If EXIT1, then exit with exit code 1 after all
|
|
136
|
+
files/actions are processed.'''))
|
|
137
|
+
group.add_option(
|
|
138
|
+
"--print", "-p", action='callback',
|
|
139
|
+
callback=action_callbacker([action_print]),
|
|
140
|
+
help=hfmt('''
|
|
141
|
+
Equivalent to --action=PRINT (default when stdin or stdout is
|
|
142
|
+
not a tty) '''))
|
|
143
|
+
group.add_option(
|
|
144
|
+
"--diff", "-d", action='callback',
|
|
145
|
+
callback=action_callbacker([action_diff]),
|
|
146
|
+
help=hfmt('''Equivalent to --action=DIFF'''))
|
|
147
|
+
group.add_option(
|
|
148
|
+
"--replace", "-r", action='callback',
|
|
149
|
+
callback=action_callbacker([action_ifchanged, action_replace]),
|
|
150
|
+
help=hfmt('''Equivalent to --action=IFCHANGED,REPLACE'''))
|
|
151
|
+
group.add_option(
|
|
152
|
+
"--diff-replace", "-R", action='callback',
|
|
153
|
+
callback=action_callbacker([action_ifchanged, action_diff, action_replace]),
|
|
154
|
+
help=hfmt('''Equivalent to --action=IFCHANGED,DIFF,REPLACE'''))
|
|
155
|
+
actions_interactive = [
|
|
156
|
+
action_ifchanged, action_diff,
|
|
157
|
+
action_query("Replace {filename}?"), action_replace]
|
|
158
|
+
group.add_option(
|
|
159
|
+
"--interactive", "-i", action='callback',
|
|
160
|
+
callback=action_callbacker(actions_interactive),
|
|
161
|
+
help=hfmt('''
|
|
162
|
+
Equivalent to --action=IFCHANGED,DIFF,QUERY,REPLACE (default
|
|
163
|
+
when stdin & stdout are ttys) '''))
|
|
164
|
+
if os.isatty(0) and os.isatty(1):
|
|
165
|
+
default_actions = actions_interactive
|
|
166
|
+
else:
|
|
167
|
+
default_actions = [action_print]
|
|
168
|
+
parser.set_default('actions', tuple(default_actions))
|
|
169
|
+
parser.add_option_group(group)
|
|
170
|
+
|
|
171
|
+
parser.add_option(
|
|
172
|
+
'--symlinks', action='callback', nargs=1, type=str,
|
|
173
|
+
dest='symlinks', callback=symlink_callback, help="--symlinks should be one of: " + symlinks_help,
|
|
174
|
+
)
|
|
175
|
+
parser.set_defaults(symlinks='error')
|
|
176
|
+
|
|
177
|
+
if import_format_params:
|
|
178
|
+
group = optparse.OptionGroup(parser, "Pretty-printing options")
|
|
179
|
+
group.add_option('--align-imports', '--align', type='str', default="32",
|
|
180
|
+
metavar='N',
|
|
181
|
+
help=hfmt('''
|
|
182
|
+
Whether and how to align the 'import' keyword in
|
|
183
|
+
'from modulename import aliases...'. If 0, then
|
|
184
|
+
don't align. If 1, then align within each block
|
|
185
|
+
of imports. If an integer > 1, then align at
|
|
186
|
+
that column, wrapping with a backslash if
|
|
187
|
+
necessary. If a comma-separated list of integers
|
|
188
|
+
(tab stops), then pick the column that results in
|
|
189
|
+
the fewest number of lines total per block.'''))
|
|
190
|
+
group.add_option('--from-spaces', type='int', default=3, metavar='N',
|
|
191
|
+
help=hfmt('''
|
|
192
|
+
The number of spaces after the 'from' keyword.
|
|
193
|
+
(Must be at least 1; default is 3.)'''))
|
|
194
|
+
group.add_option('--separate-from-imports', action='store_true',
|
|
195
|
+
default=False,
|
|
196
|
+
help=hfmt('''
|
|
197
|
+
Separate 'from ... import ...'
|
|
198
|
+
statements from 'import ...' statements.'''))
|
|
199
|
+
group.add_option('--no-separate-from-imports', action='store_false',
|
|
200
|
+
dest='separate_from_imports',
|
|
201
|
+
help=hfmt('''
|
|
202
|
+
(Default) Don't separate 'from ... import ...'
|
|
203
|
+
statements from 'import ...' statements.'''))
|
|
204
|
+
group.add_option('--align-future', action='store_true',
|
|
205
|
+
default=False,
|
|
206
|
+
help=hfmt('''
|
|
207
|
+
Align the 'from __future__ import ...' statement
|
|
208
|
+
like others.'''))
|
|
209
|
+
group.add_option('--no-align-future', action='store_false',
|
|
210
|
+
dest='align_future',
|
|
211
|
+
help=hfmt('''
|
|
212
|
+
(Default) Don't align the 'from __future__ import
|
|
213
|
+
...' statement.'''))
|
|
214
|
+
group.add_option('--width', type='int', default=None, metavar='N',
|
|
215
|
+
help=hfmt('''
|
|
216
|
+
Maximum line length (default: 79).'''))
|
|
217
|
+
group.add_option('--black', action='store_true', default=False,
|
|
218
|
+
help=hfmt('''
|
|
219
|
+
Use black to format imports. If this option is
|
|
220
|
+
used, all other formatting options are ignored,
|
|
221
|
+
except width'''))
|
|
222
|
+
group.add_option('--hanging-indent', type='choice', default='never',
|
|
223
|
+
choices=['never','auto','always'],
|
|
224
|
+
metavar='never|auto|always',
|
|
225
|
+
dest='hanging_indent',
|
|
226
|
+
help=hfmt('''
|
|
227
|
+
How to wrap import statements that don't fit on
|
|
228
|
+
one line.
|
|
229
|
+
If --hanging-indent=always, then always indent
|
|
230
|
+
imported tokens at column 4 on the next line.
|
|
231
|
+
If --hanging-indent=never (default), then align
|
|
232
|
+
import tokens after "import (" (by default column
|
|
233
|
+
40); do so even if some symbols are so long that
|
|
234
|
+
this would exceed the width (by default 79)).
|
|
235
|
+
If --hanging-indent=auto, then use hanging indent
|
|
236
|
+
only if it is necessary to prevent exceeding the
|
|
237
|
+
width (by default 79).
|
|
238
|
+
'''))
|
|
239
|
+
group.add_option('--uniform', '-u', action="store_true",
|
|
240
|
+
help=hfmt('''
|
|
241
|
+
(Default) Shortcut for --no-separate-from-imports
|
|
242
|
+
--from-spaces=3 --align-imports=32.'''))
|
|
243
|
+
group.add_option('--unaligned', '-n', action="store_true",
|
|
244
|
+
help=hfmt('''
|
|
245
|
+
Shortcut for --separate-from-imports
|
|
246
|
+
--from-spaces=1 --align-imports=0.'''))
|
|
247
|
+
|
|
248
|
+
parser.add_option_group(group)
|
|
249
|
+
|
|
250
|
+
if addopts is not None:
|
|
251
|
+
addopts(parser)
|
|
252
|
+
# This is the only way to provide a default value for an option with a
|
|
253
|
+
# callback.
|
|
254
|
+
if modify_action_params:
|
|
255
|
+
args = ["--symlinks=error"] + sys.argv[1:]
|
|
256
|
+
else:
|
|
257
|
+
args = None
|
|
258
|
+
|
|
259
|
+
options, args = parser.parse_args(args=args)
|
|
260
|
+
|
|
261
|
+
# Set these manually rather than in a callback option because callback
|
|
262
|
+
# options don't get triggered by OptionParser.set_default (which is
|
|
263
|
+
# used when setting values via pyproject.toml)
|
|
264
|
+
if getattr(options, "unaligned", False):
|
|
265
|
+
parser.values.separate_from_imports = True
|
|
266
|
+
parser.values.from_spaces = 1
|
|
267
|
+
parser.values.align_imports = '0'
|
|
268
|
+
|
|
269
|
+
if getattr(options, "uniform", False):
|
|
270
|
+
parser.values.separate_from_imports = False
|
|
271
|
+
parser.values.from_spaces = 3
|
|
272
|
+
parser.values.align_imports = '32'
|
|
273
|
+
|
|
274
|
+
if import_format_params:
|
|
275
|
+
align_imports_args = [int(x.strip())
|
|
276
|
+
for x in options.align_imports.split(",")]
|
|
277
|
+
if len(align_imports_args) == 1 and align_imports_args[0] == 1:
|
|
278
|
+
align_imports = True
|
|
279
|
+
elif len(align_imports_args) == 1 and align_imports_args[0] == 0:
|
|
280
|
+
align_imports = False
|
|
281
|
+
else:
|
|
282
|
+
align_imports = tuple(sorted(set(align_imports_args)))
|
|
283
|
+
options.params = ImportFormatParams(
|
|
284
|
+
align_imports =align_imports,
|
|
285
|
+
from_spaces =options.from_spaces,
|
|
286
|
+
separate_from_imports =options.separate_from_imports,
|
|
287
|
+
max_line_length =options.width,
|
|
288
|
+
use_black =options.black,
|
|
289
|
+
align_future =options.align_future,
|
|
290
|
+
hanging_indent =options.hanging_indent,
|
|
291
|
+
)
|
|
292
|
+
return options, args
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _default_on_error(filename):
|
|
296
|
+
raise SystemExit("bad filename %s" % (filename,))
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def filename_args(args: List[str], on_error=_default_on_error):
|
|
300
|
+
"""
|
|
301
|
+
Return list of filenames given command-line arguments.
|
|
302
|
+
|
|
303
|
+
:rtype:
|
|
304
|
+
``list`` of `Filename`
|
|
305
|
+
"""
|
|
306
|
+
if args:
|
|
307
|
+
for a in args:
|
|
308
|
+
assert isinstance(a, str)
|
|
309
|
+
return expand_py_files_from_args([Filename(f) for f in args], on_error)
|
|
310
|
+
elif not os.isatty(0):
|
|
311
|
+
return [Filename.STDIN]
|
|
312
|
+
else:
|
|
313
|
+
syntax()
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def print_version_and_exit(extra=None):
|
|
317
|
+
from pyflyby._version import __version__
|
|
318
|
+
msg = "pyflyby %s" % (__version__,)
|
|
319
|
+
progname = os.path.realpath(sys.argv[0])
|
|
320
|
+
if os.path.exists(progname):
|
|
321
|
+
msg += " (%s)" % (os.path.basename(progname),)
|
|
322
|
+
print(msg)
|
|
323
|
+
if extra:
|
|
324
|
+
print(extra)
|
|
325
|
+
raise SystemExit(0)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def syntax(message=None, usage=None):
|
|
329
|
+
if message:
|
|
330
|
+
logger.error(message)
|
|
331
|
+
outmsg = ((usage or maindoc()) +
|
|
332
|
+
'\n\nFor usage, see: %s --help' % (sys.argv[0],))
|
|
333
|
+
print(outmsg, file=sys.stderr)
|
|
334
|
+
raise SystemExit(1)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
class AbortActions(Exception):
|
|
338
|
+
pass
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
class Exit1(Exception):
|
|
342
|
+
pass
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class Modifier(object):
|
|
346
|
+
def __init__(self, modifier, filename):
|
|
347
|
+
self.modifier = modifier
|
|
348
|
+
self.filename = filename
|
|
349
|
+
self._tmpfiles = []
|
|
350
|
+
|
|
351
|
+
@cached_attribute
|
|
352
|
+
def input_content(self):
|
|
353
|
+
return read_file(self.filename)
|
|
354
|
+
|
|
355
|
+
# TODO: refactor to avoid having these heavy-weight things inside a
|
|
356
|
+
# cached_attribute, which causes annoyance while debugging.
|
|
357
|
+
@cached_attribute
|
|
358
|
+
def output_content(self):
|
|
359
|
+
return FileText(self.modifier(self.input_content), filename=self.filename)
|
|
360
|
+
|
|
361
|
+
def _tempfile(self):
|
|
362
|
+
from tempfile import NamedTemporaryFile
|
|
363
|
+
f = NamedTemporaryFile()
|
|
364
|
+
self._tmpfiles.append(f)
|
|
365
|
+
return f, Filename(f.name)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
@cached_attribute
|
|
369
|
+
def output_content_filename(self):
|
|
370
|
+
f, fname = self._tempfile()
|
|
371
|
+
f.write(bytes(self.output_content.joined, "utf-8"))
|
|
372
|
+
f.flush()
|
|
373
|
+
return fname
|
|
374
|
+
|
|
375
|
+
@cached_attribute
|
|
376
|
+
def input_content_filename(self):
|
|
377
|
+
if isinstance(self.filename, Filename):
|
|
378
|
+
return self.filename
|
|
379
|
+
# If the input was stdin, and the user wants a diff, then we need to
|
|
380
|
+
# write it to a temp file.
|
|
381
|
+
f, fname = self._tempfile()
|
|
382
|
+
f.write(bytes(self.input_content, "utf-8"))
|
|
383
|
+
f.flush()
|
|
384
|
+
return fname
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def __del__(self):
|
|
388
|
+
for f in self._tmpfiles:
|
|
389
|
+
f.close()
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def process_actions(filenames:List[str], actions, modify_function,
|
|
393
|
+
reraise_exceptions=(), exclude=()):
|
|
394
|
+
|
|
395
|
+
if not isinstance(exclude, (list, tuple)):
|
|
396
|
+
raise ConfigurationError(
|
|
397
|
+
"Exclusions must be a list of filenames/patterns to exclude."
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
errors = []
|
|
401
|
+
def on_error_filename_arg(arg):
|
|
402
|
+
print("%s: bad filename %s" % (sys.argv[0], arg), file=sys.stderr)
|
|
403
|
+
errors.append("%s: bad filename" % (arg,))
|
|
404
|
+
filename_objs = filename_args(filenames, on_error=on_error_filename_arg)
|
|
405
|
+
exit_code = 0
|
|
406
|
+
for filename in filename_objs:
|
|
407
|
+
|
|
408
|
+
# Log any matching exclusion patterns before ignoring, if applicable
|
|
409
|
+
matching_excludes = []
|
|
410
|
+
for pattern in exclude:
|
|
411
|
+
if Path(str(filename)).match(str(pattern)):
|
|
412
|
+
matching_excludes.append(pattern)
|
|
413
|
+
if any(matching_excludes):
|
|
414
|
+
msg = f"{filename} matches exclusion pattern"
|
|
415
|
+
if len(matching_excludes) == 1:
|
|
416
|
+
msg += f": {matching_excludes[0]}"
|
|
417
|
+
else:
|
|
418
|
+
msg += f"s: {matching_excludes}"
|
|
419
|
+
logger.info(msg)
|
|
420
|
+
continue
|
|
421
|
+
|
|
422
|
+
try:
|
|
423
|
+
m = Modifier(modify_function, filename)
|
|
424
|
+
for action in actions:
|
|
425
|
+
action(m)
|
|
426
|
+
except AbortActions:
|
|
427
|
+
continue
|
|
428
|
+
except reraise_exceptions:
|
|
429
|
+
raise
|
|
430
|
+
except Exit1:
|
|
431
|
+
exit_code = 1
|
|
432
|
+
except Exception as e:
|
|
433
|
+
errors.append("%s: %s: %s" % (filename, type(e).__name__, e))
|
|
434
|
+
type_e = type(e)
|
|
435
|
+
try:
|
|
436
|
+
tb = sys.exc_info()[2]
|
|
437
|
+
if str(filename) not in str(e):
|
|
438
|
+
try:
|
|
439
|
+
e = type_e("While processing %s: %s" % (filename, e))
|
|
440
|
+
pass
|
|
441
|
+
except TypeError:
|
|
442
|
+
# Exception takes more than one argument
|
|
443
|
+
pass
|
|
444
|
+
if logger.debug_enabled:
|
|
445
|
+
raise
|
|
446
|
+
traceback.print_exception(type(e), e, tb)
|
|
447
|
+
finally:
|
|
448
|
+
tb = None # avoid refcycles involving tb
|
|
449
|
+
continue
|
|
450
|
+
if errors:
|
|
451
|
+
msg = "\n%s: encountered the following problems:\n" % (sys.argv[0],)
|
|
452
|
+
for er in errors:
|
|
453
|
+
lines = er.splitlines()
|
|
454
|
+
msg += " " + lines[0] + '\n'.join(
|
|
455
|
+
(" %s"%line for line in lines[1:]))
|
|
456
|
+
raise SystemExit(msg)
|
|
457
|
+
else:
|
|
458
|
+
raise SystemExit(exit_code)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def action_print(m):
|
|
462
|
+
output_content = m.output_content
|
|
463
|
+
sys.stdout.write(output_content.joined)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def action_ifchanged(m):
|
|
467
|
+
if m.output_content.joined == m.input_content.joined:
|
|
468
|
+
logger.debug("unmodified: %s", m.filename)
|
|
469
|
+
raise AbortActions
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def action_replace(m):
|
|
473
|
+
if m.filename == Filename.STDIN:
|
|
474
|
+
raise Exception("Can't replace stdio in-place")
|
|
475
|
+
logger.info("%s: *** modified ***", m.filename)
|
|
476
|
+
atomic_write_file(m.filename, m.output_content)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def action_exit1(m):
|
|
480
|
+
logger.debug("action_exit1")
|
|
481
|
+
raise Exit1
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def action_external_command(command):
|
|
485
|
+
import subprocess
|
|
486
|
+
def action(m):
|
|
487
|
+
bindir = os.path.dirname(os.path.realpath(sys.argv[0]))
|
|
488
|
+
env = os.environ
|
|
489
|
+
env['PATH'] = env['PATH'] + ":" + bindir
|
|
490
|
+
fullcmd = "%s %s %s" % (
|
|
491
|
+
command, m.input_content_filename, m.output_content_filename)
|
|
492
|
+
logger.debug("Executing external command: %s", fullcmd)
|
|
493
|
+
ret = subprocess.call(fullcmd, shell=True, env=env)
|
|
494
|
+
logger.debug("External command returned %d", ret)
|
|
495
|
+
return action
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def action_query(prompt="Proceed?"):
|
|
499
|
+
def action(m):
|
|
500
|
+
p = prompt.format(filename=m.filename)
|
|
501
|
+
print()
|
|
502
|
+
print("%s [y/N] " % (p), end="")
|
|
503
|
+
try:
|
|
504
|
+
if input().strip().lower().startswith('y'):
|
|
505
|
+
return True
|
|
506
|
+
except KeyboardInterrupt:
|
|
507
|
+
print("KeyboardInterrupt", file=sys.stderr)
|
|
508
|
+
raise SystemExit(1)
|
|
509
|
+
print("Aborted")
|
|
510
|
+
raise AbortActions
|
|
511
|
+
return action
|
|
512
|
+
|
|
513
|
+
def symlink_callback(option, opt_str, value, parser):
|
|
514
|
+
parser.values.actions = tuple(i for i in parser.values.actions if i not in
|
|
515
|
+
symlink_callbacks.values())
|
|
516
|
+
if value in symlink_callbacks:
|
|
517
|
+
parser.values.actions = (symlink_callbacks[value],) + parser.values.actions
|
|
518
|
+
else:
|
|
519
|
+
raise optparse.OptionValueError("--symlinks must be one of 'error', 'follow', 'skip', or 'replace'. Got %r" % value)
|
|
520
|
+
|
|
521
|
+
symlinks_help = """\
|
|
522
|
+
--symlinks=error (default; gives an error on symlinks),
|
|
523
|
+
--symlinks=follow (follows symlinks),
|
|
524
|
+
--symlinks=skip (skips symlinks),
|
|
525
|
+
--symlinks=replace (replaces symlinks with the target file\
|
|
526
|
+
"""
|
|
527
|
+
|
|
528
|
+
# Warning, the symlink actions will only work if they are run first.
|
|
529
|
+
# Otherwise, output_content may already be cached
|
|
530
|
+
def symlink_error(m):
|
|
531
|
+
if m.filename == Filename.STDIN:
|
|
532
|
+
return symlink_follow(m)
|
|
533
|
+
if m.filename.islink:
|
|
534
|
+
raise SystemExit("""\
|
|
535
|
+
Error: %s appears to be a symlink. Use one of the following options to allow symlinks:
|
|
536
|
+
%s
|
|
537
|
+
""" % (m.filename, indent(symlinks_help, ' ')))
|
|
538
|
+
|
|
539
|
+
def symlink_follow(m):
|
|
540
|
+
if m.filename == Filename.STDIN:
|
|
541
|
+
return
|
|
542
|
+
if m.filename.islink:
|
|
543
|
+
logger.info("Following symlink %s" % m.filename)
|
|
544
|
+
m.filename = m.filename.realpath
|
|
545
|
+
|
|
546
|
+
def symlink_skip(m):
|
|
547
|
+
if m.filename == Filename.STDIN:
|
|
548
|
+
return symlink_follow(m)
|
|
549
|
+
if m.filename.islink:
|
|
550
|
+
logger.info("Skipping symlink %s" % m.filename)
|
|
551
|
+
raise AbortActions
|
|
552
|
+
|
|
553
|
+
def symlink_replace(m):
|
|
554
|
+
if m.filename == Filename.STDIN:
|
|
555
|
+
return symlink_follow(m)
|
|
556
|
+
if m.filename.islink:
|
|
557
|
+
logger.info("Replacing symlink %s" % m.filename)
|
|
558
|
+
# The current behavior automatically replaces symlinks, so do nothing
|
|
559
|
+
|
|
560
|
+
symlink_callbacks = {
|
|
561
|
+
'error': symlink_error,
|
|
562
|
+
'follow': symlink_follow,
|
|
563
|
+
'skip': symlink_skip,
|
|
564
|
+
'replace': symlink_replace,
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
def _get_pyproj_toml_file():
|
|
568
|
+
"""Try to find the location of the current project pyproject.toml
|
|
569
|
+
in cwd or parents directories.
|
|
570
|
+
|
|
571
|
+
If no pyproject.toml can be found, None is returned.
|
|
572
|
+
"""
|
|
573
|
+
cwd = Path(os.getcwd())
|
|
574
|
+
|
|
575
|
+
for pth in [cwd] + list(cwd.parents):
|
|
576
|
+
pyproj_toml = pth /'pyproject.toml'
|
|
577
|
+
if pyproj_toml.exists() and pyproj_toml.is_file():
|
|
578
|
+
return pyproj_toml
|
|
579
|
+
|
|
580
|
+
return None
|
|
581
|
+
|
|
582
|
+
def _get_pyproj_toml_config():
|
|
583
|
+
"""Return the toml contents of the current pyproject.toml.
|
|
584
|
+
|
|
585
|
+
If no pyproject.toml can be found in cwd or parent directories,
|
|
586
|
+
None is returned.
|
|
587
|
+
"""
|
|
588
|
+
pyproject_toml = _get_pyproj_toml_file()
|
|
589
|
+
if pyproject_toml is not None:
|
|
590
|
+
return loads(pyproject_toml.read_text())
|
|
591
|
+
return None
|