Skip to content

Commit

Permalink
Add --replace-single-quotes to replace single quotes with double ones
Browse files Browse the repository at this point in the history
  • Loading branch information
wjsi committed Aug 15, 2024
1 parent 4294554 commit 3310eb9
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 18 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ Checks for the existence of private keys.

#### `double-quote-string-fixer`
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`
Makes sure files end in a newline and only a newline.
Expand Down
24 changes: 18 additions & 6 deletions pre_commit_hooks/string_fixer.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
else: # pragma: <3.12 cover
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:
return token_text

Expand All @@ -25,9 +25,11 @@ def handle_match(token_text: str) -> str:
meat = token_text[match.end():-1]
if '"' in meat or "'" in meat:
return token_text
elif replace_single_quotes:
return match.group().replace("'", '"') + meat + '"'
else:
return match.group().replace('"', "'") + meat + "'"
else:
else: # will this happen? # pragma: no cover
return token_text


Expand All @@ -39,7 +41,7 @@ def get_line_offsets_by_line_no(src: str) -> list[int]:
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:
contents = f.read()
line_offsets = get_line_offsets_by_line_no(contents)
Expand All @@ -58,7 +60,9 @@ def fix_strings(filename: str) -> int:
elif token_type == FSTRING_END: # pragma: >=3.12 cover
fstring_depth -= 1
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[
line_offsets[srow] + scol:
line_offsets[erow] + ecol
Expand All @@ -76,12 +80,20 @@ def fix_strings(filename: str) -> int:
def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser()
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)

retv = 0

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:
print(f'Fixing strings in {filename}')
retv |= return_value
Expand Down
55 changes: 43 additions & 12 deletions tests/string_fixer_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,20 @@

TESTS = (
# Base cases
("''", "''", 0),
('""', "''", 1),
(r'"\'"', r'"\'"', 0),
(r'"\""', r'"\""', 0),
(r"'\"\"'", r"'\"\"'", 0),
("''", "''", False, 0),
("''", '""', True, 1),
('""', "''", False, 1),
('""', '""', True, 0),
(r'"\'"', r'"\'"', False, 0),
(r'"\""', r'"\""', False, 0),
(r"'\"\"'", r"'\"\"'", False, 0),
# 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
(r'"\'"', r'"\'"', 0),
(r'"\'"', r'"\'"', False, 0),
# Docstring
('""" Foo """', '""" Foo """', 0),
('""" Foo """', '""" Foo """', False, 0),
(
textwrap.dedent(
"""
Expand All @@ -34,23 +37,51 @@
'\n
""",
),
False,
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(
"f'hello{\"world\"}'",
"f'hello{\"world\"}'",
False,
0,
id='ignore nested fstrings',
),
)


@pytest.mark.parametrize(('input_s', 'output', 'expected_retval'), TESTS)
def test_rewrite(input_s, output, expected_retval, tmpdir):
@pytest.mark.parametrize(
('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.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 retval == expected_retval

Expand Down

0 comments on commit 3310eb9

Please sign in to comment.