Skip to content

Commit 739d670

Browse files
committed
Add explicit -f/--fix=FIX option to futurize script (issue #39)
1 parent fe004f6 commit 739d670

File tree

2 files changed

+112
-31
lines changed

2 files changed

+112
-31
lines changed

libfuturize/fixes/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# projects that are happy to drop support for Py2.5 and below. Applying
77
# them first will reduce the size of the patch set for the real porting.
88
lib2to3_fix_names_stage1 = set([
9+
'lib2to3.fixes.fix_absolute_import', # FAKE: TESTING ONLY
910
'lib2to3.fixes.fix_apply',
1011
'lib2to3.fixes.fix_except',
1112
'lib2to3.fixes.fix_exitfunc',

libfuturize/main.py

Lines changed: 111 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,16 @@
4949
This adds the declaration ``from __future__ import unicode_literals`` to the
5050
top of each file, which implicitly declares all unadorned string literals to be
5151
unicode strings (``unicode`` on Py2).
52+
53+
All imports
54+
-----------
55+
56+
The --all-imports option forces adding all ``__future__`` imports and ``from
57+
future import standard_library``, even if they don't seem necessary for
58+
the current state of each module. (This can simplify testing, and can
59+
reduce the need to think about Py2 compatibility when editing the code
60+
further.)
61+
5262
"""
5363

5464
from __future__ import (absolute_import, print_function, unicode_literals)
@@ -57,6 +67,7 @@
5767
import sys
5868
import logging
5969
import optparse
70+
import os
6071

6172
from lib2to3.main import main, warn, StdoutRefactoringTool
6273
from lib2to3 import refactor
@@ -66,12 +77,20 @@
6677
libfuturize_fix_names_stage1,
6778
libfuturize_fix_names_stage2)
6879

80+
fixer_pkg = 'libfuturize.fixes'
81+
6982

7083
def main(args=None):
7184
"""Main program.
7285
86+
Args:
87+
fixer_pkg: the name of a package where the fixers are located.
88+
args: optional; a list of command line arguments. If omitted,
89+
sys.argv[1:] is used.
90+
7391
Returns a suggested exit status (0, 1, 2).
7492
"""
93+
7594
# Set up option parser
7695
parser = optparse.OptionParser(usage="futurize [options] file|dir ...")
7796
parser.add_option("-a", "--all-imports", action="store_true",
@@ -87,15 +106,15 @@ def main(args=None):
87106
parser.add_option("-u", "--unicode-literals", action="store_true",
88107
help="Add ``from __future__ import unicode_literals`` to implicitly convert all unadorned string literals '' into unicode strings")
89108
parser.add_option("-f", "--fix", action="append", default=[],
90-
help="Each FIX specifies a transformation; default: all")
109+
help="Each FIX specifies a transformation; default: all.\nEither use '-f division -f metaclass' etc. or use the fully-qualified module name: '-f lib2to3.fixes.fix_types -f libfuturize.fixes.fix_unicode_keep_u'")
91110
parser.add_option("-j", "--processes", action="store", default=1,
92111
type="int", help="Run 2to3 concurrently")
93112
parser.add_option("-x", "--nofix", action="append", default=[],
94113
help="Prevent a fixer from being run.")
95114
parser.add_option("-l", "--list-fixes", action="store_true",
96115
help="List available transformations")
97-
# parser.add_option("-p", "--print-function", action="store_true",
98-
# help="Modify the grammar so that print() is a function")
116+
parser.add_option("-p", "--print-function", action="store_true",
117+
help="Modify the grammar so that print() is a function")
99118
parser.add_option("-v", "--verbose", action="store_true",
100119
help="More verbose logging")
101120
parser.add_option("--no-diffs", action="store_true",
@@ -104,13 +123,51 @@ def main(args=None):
104123
help="Write back modified files")
105124
parser.add_option("-n", "--nobackups", action="store_true", default=False,
106125
help="Don't write backups for modified files.")
126+
parser.add_option("-o", "--output-dir", action="store", type="str",
127+
default="", help="Put output files in this directory "
128+
"instead of overwriting the input files. Requires -n.")
129+
parser.add_option("-W", "--write-unchanged-files", action="store_true",
130+
help="Also write files even if no changes were required"
131+
" (useful with --output-dir); implies -w.")
132+
parser.add_option("--add-suffix", action="store", type="str", default="",
133+
help="Append this string to all output filenames."
134+
" Requires -n if non-empty. "
135+
"ex: --add-suffix='3' will generate .py3 files.")
136+
137+
avail_fixes = set()
107138

108139
# Parse command line arguments
109140
refactor_stdin = False
110-
flags = {}
111141
options, args = parser.parse_args(args)
112-
fixer_pkg = 'libfuturize.fixes'
113-
avail_fixes = set()
142+
if options.write_unchanged_files:
143+
flags["write_unchanged_files"] = True
144+
if not options.write:
145+
warn("--write-unchanged-files/-W implies -w.")
146+
options.write = True
147+
# If we allowed these, the original files would be renamed to backup names
148+
# but not replaced.
149+
if options.output_dir and not options.nobackups:
150+
parser.error("Can't use --output-dir/-o without -n.")
151+
if options.add_suffix and not options.nobackups:
152+
parser.error("Can't use --add-suffix without -n.")
153+
154+
if not options.write and options.no_diffs:
155+
warn("not writing files and not printing diffs; that's not very useful")
156+
if not options.write and options.nobackups:
157+
parser.error("Can't use -n without -w")
158+
if "-" in args:
159+
refactor_stdin = True
160+
if options.write:
161+
print("Can't write to stdin.", file=sys.stderr)
162+
return 2
163+
# Is this ever necessary?
164+
if options.print_function:
165+
flags["print_function"] = True
166+
167+
# Set up logging handler
168+
level = logging.DEBUG if options.verbose else logging.INFO
169+
logging.basicConfig(format='%(name)s: %(message)s', level=level)
170+
114171
if options.stage1 or options.stage2:
115172
assert options.both_stages is None
116173
options.both_stages = False
@@ -123,16 +180,12 @@ def main(args=None):
123180
avail_fixes.update(lib2to3_fix_names_stage2)
124181
avail_fixes.update(libfuturize_fix_names_stage2)
125182

126-
# if options.tobytes:
127-
# avail_fixes.add('libfuturize.fixes.fix_bytes')
128183
if options.unicode_literals:
129184
avail_fixes.add('libfuturize.fixes.fix_unicode_literals_import')
130-
if not options.write and options.no_diffs:
131-
warn("not writing files and not printing diffs; that's not very useful")
132-
if not options.write and options.nobackups:
133-
parser.error("Can't use -n without -w")
185+
134186
if options.list_fixes:
135187
print("Available transformations for the -f/--fix option:")
188+
# for fixname in sorted(refactor.get_all_fix_names(fixer_pkg)):
136189
for fixname in sorted(avail_fixes):
137190
print(fixname)
138191
if not args:
@@ -142,24 +195,12 @@ def main(args=None):
142195
file=sys.stderr)
143196
print("Use --help to show usage.", file=sys.stderr)
144197
return 2
145-
if "-" in args:
146-
refactor_stdin = True
147-
if options.write:
148-
print("Can't write to stdin.", file=sys.stderr)
149-
return 2
150198

151-
# Is this ever necessary?
152-
# if options.print_function:
153-
# flags["print_function"] = True
154-
155-
# Set up logging handler
156-
level = logging.DEBUG if options.verbose else logging.INFO
157-
logging.basicConfig(format='%(name)s: %(message)s', level=level)
199+
flags = {}
158200

159-
# Initialize the refactoring tool
160201
unwanted_fixes = set(fixer_pkg + ".fix_" + fix for fix in options.nofix)
161202

162-
# The 'all-imports' option forces adding all imports __future__ and "from
203+
# The 'all-imports' option forces adding all __future__ imports and "from
163204
# future import standard_library", even if they don't seem necessary for
164205
# the current state of each module. (This can simplify testing, and can
165206
# reduce the need to think about Py2 compatibility when editing the code
@@ -175,11 +216,51 @@ def main(args=None):
175216
extra_fixes.add(prefix + 'fix_add__future__imports')
176217
extra_fixes.add(prefix + 'fix_add_future_standard_library_import')
177218
extra_fixes.add(prefix + 'fix_add_all_future_builtins')
219+
explicit = set()
220+
if options.fix:
221+
all_present = False
222+
for fix in options.fix:
223+
if fix == 'all':
224+
all_present = True
225+
else:
226+
if ".fix_" in fix:
227+
explicit.add(fix)
228+
else:
229+
# Infer the full module name for the fixer.
230+
# First ensure that no names clash (e.g.
231+
# lib2to3.fixes.fix_blah and libfuturize.fixes.fix_blah):
232+
found = [f for f in avail_fixes if 'fix_{}'.format(fix) in f]
233+
if len(found) > 1:
234+
print("Ambiguous fixer name. Choose a fully qualified "
235+
"module name instead from these:\n" +
236+
"\n".join(" " + myf for myf in found),
237+
file=sys.stderr)
238+
return 2
239+
explicit.add(found[0])
240+
requested = avail_fixes.union(explicit) if all_present else explicit
241+
else:
242+
requested = avail_fixes.union(explicit)
243+
fixer_names = requested | extra_fixes - unwanted_fixes
244+
245+
input_base_dir = os.path.commonprefix(args)
246+
if (input_base_dir and not input_base_dir.endswith(os.sep)
247+
and not os.path.isdir(input_base_dir)):
248+
# One or more similar names were passed, their directory is the base.
249+
# os.path.commonprefix() is ignorant of path elements, this corrects
250+
# for that weird API.
251+
input_base_dir = os.path.dirname(input_base_dir)
252+
if options.output_dir:
253+
input_base_dir = input_base_dir.rstrip(os.sep)
254+
logger.info('Output in %r will mirror the input directory %r layout.',
255+
options.output_dir, input_base_dir)
178256

179-
fixer_names = avail_fixes | extra_fixes - unwanted_fixes
180-
181-
rt = StdoutRefactoringTool(sorted(fixer_names), flags, set(),
182-
options.nobackups, not options.no_diffs)
257+
# Initialize the refactoring tool
258+
rt = StdoutRefactoringTool(
259+
sorted(fixer_names), flags, sorted(explicit),
260+
options.nobackups, not options.no_diffs,
261+
input_base_dir=input_base_dir,
262+
output_dir=options.output_dir,
263+
append_suffix=options.add_suffix)
183264

184265
# Refactor all files and directories passed as arguments
185266
if not rt.errors:
@@ -198,4 +279,3 @@ def main(args=None):
198279

199280
# Return error status (0 if rt.errors is zero)
200281
return int(bool(rt.errors))
201-

0 commit comments

Comments
 (0)