You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
239 lines
7.5 KiB
239 lines
7.5 KiB
#!/usr/bin/env python
|
|
|
|
# Copyright (C) 2013-2018 Steven Myint
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining
|
|
# a copy of this software and associated documentation files (the
|
|
# "Software"), to deal in the Software without restriction, including
|
|
# without limitation the rights to use, copy, modify, merge, publish,
|
|
# distribute, sublicense, and/or sell copies of the Software, and to
|
|
# permit persons to whom the Software is furnished to do so, subject to
|
|
# the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be
|
|
# included in all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
|
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
# SOFTWARE.
|
|
|
|
"""Modifies strings to all use the same quote where possible."""
|
|
|
|
from __future__ import print_function
|
|
from __future__ import unicode_literals
|
|
|
|
import io
|
|
import os
|
|
import signal
|
|
import sys
|
|
import tokenize
|
|
|
|
import untokenize
|
|
|
|
|
|
__version__ = '0.5'
|
|
|
|
|
|
try:
|
|
unicode
|
|
except NameError:
|
|
unicode = str
|
|
|
|
|
|
def format_code(source, preferred_quote="'"):
|
|
"""Return source code with quotes unified."""
|
|
try:
|
|
return _format_code(source, preferred_quote)
|
|
except (tokenize.TokenError, IndentationError):
|
|
return source
|
|
|
|
|
|
def _format_code(source, preferred_quote):
|
|
"""Return source code with quotes unified."""
|
|
if not source:
|
|
return source
|
|
|
|
modified_tokens = []
|
|
|
|
sio = io.StringIO(source)
|
|
for (token_type,
|
|
token_string,
|
|
start,
|
|
end,
|
|
line) in tokenize.generate_tokens(sio.readline):
|
|
|
|
if token_type == tokenize.STRING:
|
|
token_string = unify_quotes(token_string,
|
|
preferred_quote=preferred_quote)
|
|
|
|
modified_tokens.append(
|
|
(token_type, token_string, start, end, line))
|
|
|
|
return untokenize.untokenize(modified_tokens)
|
|
|
|
|
|
def unify_quotes(token_string, preferred_quote):
|
|
"""Return string with quotes changed to preferred_quote if possible."""
|
|
bad_quote = {'"': "'",
|
|
"'": '"'}[preferred_quote]
|
|
|
|
allowed_starts = {
|
|
'': bad_quote,
|
|
'f': 'f' + bad_quote,
|
|
'r': 'r' + bad_quote,
|
|
'u': 'u' + bad_quote,
|
|
'b': 'b' + bad_quote
|
|
}
|
|
|
|
if not any(token_string.startswith(start)
|
|
for start in allowed_starts.values()):
|
|
return token_string
|
|
|
|
if token_string.count(bad_quote) != 2:
|
|
return token_string
|
|
|
|
if preferred_quote in token_string:
|
|
return token_string
|
|
|
|
assert token_string.endswith(bad_quote)
|
|
assert len(token_string) >= 2
|
|
for prefix, start in allowed_starts.items():
|
|
if token_string.startswith(start):
|
|
chars_to_strip_from_front = len(start)
|
|
return '{prefix}{preferred_quote}{token}{preferred_quote}'.format(
|
|
prefix=prefix,
|
|
preferred_quote=preferred_quote,
|
|
token=token_string[chars_to_strip_from_front:-1]
|
|
)
|
|
|
|
|
|
def open_with_encoding(filename, encoding, mode='r'):
|
|
"""Return opened file with a specific encoding."""
|
|
return io.open(filename, mode=mode, encoding=encoding,
|
|
newline='') # Preserve line endings
|
|
|
|
|
|
def detect_encoding(filename):
|
|
"""Return file encoding."""
|
|
try:
|
|
with open(filename, 'rb') as input_file:
|
|
from lib2to3.pgen2 import tokenize as lib2to3_tokenize
|
|
encoding = lib2to3_tokenize.detect_encoding(input_file.readline)[0]
|
|
|
|
# Check for correctness of encoding.
|
|
with open_with_encoding(filename, encoding) as input_file:
|
|
input_file.read()
|
|
|
|
return encoding
|
|
except (SyntaxError, LookupError, UnicodeDecodeError):
|
|
return 'latin-1'
|
|
|
|
|
|
def format_file(filename, args, standard_out):
|
|
"""Run format_code() on a file.
|
|
|
|
Returns `True` if any changes are needed and they are not being done
|
|
in-place.
|
|
|
|
"""
|
|
encoding = detect_encoding(filename)
|
|
with open_with_encoding(filename, encoding=encoding) as input_file:
|
|
source = input_file.read()
|
|
formatted_source = format_code(
|
|
source,
|
|
preferred_quote=args.quote)
|
|
|
|
if source != formatted_source:
|
|
if args.in_place:
|
|
with open_with_encoding(filename, mode='w',
|
|
encoding=encoding) as output_file:
|
|
output_file.write(formatted_source)
|
|
else:
|
|
import difflib
|
|
diff = difflib.unified_diff(
|
|
source.splitlines(),
|
|
formatted_source.splitlines(),
|
|
'before/' + filename,
|
|
'after/' + filename,
|
|
lineterm='')
|
|
standard_out.write('\n'.join(list(diff) + ['']))
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def _main(argv, standard_out, standard_error):
|
|
"""Run quotes unifying on files.
|
|
|
|
Returns `1` if any quoting changes are still needed, otherwise
|
|
`None`.
|
|
|
|
"""
|
|
import argparse
|
|
parser = argparse.ArgumentParser(description=__doc__, prog='unify')
|
|
parser.add_argument('-i', '--in-place', action='store_true',
|
|
help='make changes to files instead of printing diffs')
|
|
parser.add_argument('-c', '--check-only', action='store_true',
|
|
help='exit with a status code of 1 if any changes are'
|
|
' still needed')
|
|
parser.add_argument('-r', '--recursive', action='store_true',
|
|
help='drill down directories recursively')
|
|
parser.add_argument('--quote', help='preferred quote', choices=["'", '"'],
|
|
default="'")
|
|
parser.add_argument('--version', action='version',
|
|
version='%(prog)s ' + __version__)
|
|
parser.add_argument('files', nargs='+',
|
|
help='files to format')
|
|
|
|
args = parser.parse_args(argv[1:])
|
|
|
|
filenames = list(set(args.files))
|
|
changes_needed = False
|
|
failure = False
|
|
while filenames:
|
|
name = filenames.pop(0)
|
|
if args.recursive and os.path.isdir(name):
|
|
for root, directories, children in os.walk(unicode(name)):
|
|
filenames += [os.path.join(root, f) for f in children
|
|
if f.endswith('.py') and
|
|
not f.startswith('.')]
|
|
directories[:] = [d for d in directories
|
|
if not d.startswith('.')]
|
|
else:
|
|
try:
|
|
if format_file(name, args=args, standard_out=standard_out):
|
|
changes_needed = True
|
|
except IOError as exception:
|
|
print(unicode(exception), file=standard_error)
|
|
failure = True
|
|
|
|
if failure or (args.check_only and changes_needed):
|
|
return 1
|
|
|
|
|
|
def main(): # pragma: no cover
|
|
"""Return exit status."""
|
|
try:
|
|
# Exit on broken pipe.
|
|
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
|
except AttributeError:
|
|
# SIGPIPE is not available on Windows.
|
|
pass
|
|
|
|
try:
|
|
return _main(sys.argv,
|
|
standard_out=sys.stdout,
|
|
standard_error=sys.stderr)
|
|
except KeyboardInterrupt:
|
|
return 2
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main()) # pragma: no cover
|