Add --replace-single-quotes to replace single quotes with double ones

This commit is contained in:
Wenjun Si 2024-08-15 13:21:52 +08:00
parent 429455474b
commit 3310eb9734
3 changed files with 62 additions and 18 deletions

View file

@ -112,6 +112,7 @@ Checks for the existence of private keys.
#### `double-quote-string-fixer` #### `double-quote-string-fixer`
This hook replaces double quoted strings with single quoted strings. This hook replaces double quoted strings with single quoted strings.
- `--replace-single-quotes` - replaces single quoted strings with double quoted strings.
#### `end-of-file-fixer` #### `end-of-file-fixer`
Makes sure files end in a newline and only a newline. Makes sure files end in a newline and only a newline.

View file

@ -13,10 +13,10 @@ if sys.version_info >= (3, 12): # pragma: >=3.12 cover
else: # pragma: <3.12 cover else: # pragma: <3.12 cover
FSTRING_START = FSTRING_END = -1 FSTRING_START = FSTRING_END = -1
START_QUOTE_RE = re.compile('^[a-zA-Z]*"') START_QUOTE_RE = re.compile("^[a-zA-Z]*['\"]")
def handle_match(token_text: str) -> str: def handle_match(token_text: str, replace_single_quotes: bool = False) -> str:
if '"""' in token_text or "'''" in token_text: if '"""' in token_text or "'''" in token_text:
return token_text return token_text
@ -25,9 +25,11 @@ def handle_match(token_text: str) -> str:
meat = token_text[match.end():-1] meat = token_text[match.end():-1]
if '"' in meat or "'" in meat: if '"' in meat or "'" in meat:
return token_text return token_text
elif replace_single_quotes:
return match.group().replace("'", '"') + meat + '"'
else: else:
return match.group().replace('"', "'") + meat + "'" return match.group().replace('"', "'") + meat + "'"
else: else: # will this happen? # pragma: no cover
return token_text return token_text
@ -39,7 +41,7 @@ def get_line_offsets_by_line_no(src: str) -> list[int]:
return offsets return offsets
def fix_strings(filename: str) -> int: def fix_strings(filename: str, replace_single_quotes: bool = False) -> int:
with open(filename, encoding='UTF-8', newline='') as f: with open(filename, encoding='UTF-8', newline='') as f:
contents = f.read() contents = f.read()
line_offsets = get_line_offsets_by_line_no(contents) line_offsets = get_line_offsets_by_line_no(contents)
@ -58,7 +60,9 @@ def fix_strings(filename: str) -> int:
elif token_type == FSTRING_END: # pragma: >=3.12 cover elif token_type == FSTRING_END: # pragma: >=3.12 cover
fstring_depth -= 1 fstring_depth -= 1
elif fstring_depth == 0 and token_type == tokenize.STRING: elif fstring_depth == 0 and token_type == tokenize.STRING:
new_text = handle_match(token_text) new_text = handle_match(
token_text, replace_single_quotes=replace_single_quotes
)
splitcontents[ splitcontents[
line_offsets[srow] + scol: line_offsets[srow] + scol:
line_offsets[erow] + ecol line_offsets[erow] + ecol
@ -76,12 +80,20 @@ def fix_strings(filename: str) -> int:
def main(argv: Sequence[str] | None = None) -> int: def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*', help='Filenames to fix') parser.add_argument('filenames', nargs='*', help='Filenames to fix')
parser.add_argument(
'--replace-single-quotes',
action='store_true',
default=False,
help='Replace single quotes into double quotes',
)
args = parser.parse_args(argv) args = parser.parse_args(argv)
retv = 0 retv = 0
for filename in args.filenames: for filename in args.filenames:
return_value = fix_strings(filename) return_value = fix_strings(
filename, replace_single_quotes=args.replace_single_quotes
)
if return_value != 0: if return_value != 0:
print(f'Fixing strings in {filename}') print(f'Fixing strings in {filename}')
retv |= return_value retv |= return_value

View file

@ -8,17 +8,20 @@ from pre_commit_hooks.string_fixer import main
TESTS = ( TESTS = (
# Base cases # Base cases
("''", "''", 0), ("''", "''", False, 0),
('""', "''", 1), ("''", '""', True, 1),
(r'"\'"', r'"\'"', 0), ('""', "''", False, 1),
(r'"\""', r'"\""', 0), ('""', '""', True, 0),
(r"'\"\"'", r"'\"\"'", 0), (r'"\'"', r'"\'"', False, 0),
(r'"\""', r'"\""', False, 0),
(r"'\"\"'", r"'\"\"'", False, 0),
# String somewhere in the line # String somewhere in the line
('x = "foo"', "x = 'foo'", 1), ('x = "foo"', "x = 'foo'", False, 1),
("x = 'foo'", 'x = "foo"', True, 1),
# Test escaped characters # Test escaped characters
(r'"\'"', r'"\'"', 0), (r'"\'"', r'"\'"', False, 0),
# Docstring # Docstring
('""" Foo """', '""" Foo """', 0), ('""" Foo """', '""" Foo """', False, 0),
( (
textwrap.dedent( textwrap.dedent(
""" """
@ -34,23 +37,51 @@ TESTS = (
'\n '\n
""", """,
), ),
False,
1, 1,
), ),
('"foo""bar"', "'foo''bar'", 1), (
textwrap.dedent(
"""
x = ' \\
foo \\
'\n
""",
),
textwrap.dedent(
"""
x = " \\
foo \\
"\n
""",
),
True,
1,
),
('"foo""bar"', "'foo''bar'", False, 1),
("'foo''bar'", '"foo""bar"', True, 1),
pytest.param( pytest.param(
"f'hello{\"world\"}'", "f'hello{\"world\"}'",
"f'hello{\"world\"}'", "f'hello{\"world\"}'",
False,
0, 0,
id='ignore nested fstrings', id='ignore nested fstrings',
), ),
) )
@pytest.mark.parametrize(('input_s', 'output', 'expected_retval'), TESTS) @pytest.mark.parametrize(
def test_rewrite(input_s, output, expected_retval, tmpdir): ('input_s', 'output', 'reversed_case', 'expected_retval'), TESTS
)
def test_rewrite(input_s, output, reversed_case, expected_retval, tmpdir):
path = tmpdir.join('file.py') path = tmpdir.join('file.py')
path.write(input_s) path.write(input_s)
retval = main([str(path)])
argv = [str(path)]
if reversed_case:
argv.append("--replace-single-quotes")
retval = main(argv)
assert path.read() == output assert path.read() == output
assert retval == expected_retval assert retval == expected_retval