How can you convert all single-quoted strings to double-quoted strings in PyCharm?
Asked Answered
G

7

10

I want to do some code cleanup and make all of the strings within a module consistently double-quoted strings. My problem is there are tons of them and going through by hand (alt+enter, arrow, enter) is tedious and my regex-Fu is weak. (keep running into oddball cases where single quotes are in doc strings, etc)

Is there some faster way to convert all single-quoted strings to double-quoted strings in Pycharm?

Gardner answered 12/2, 2019 at 22:50 Comment(8)
Find and replace?Backwater
One option with many other benefits is to use the black python formatter, it opts for double-quoted strings by default and can easily be integrated with PyCharm.Strange
i would not recommend this, as, in general python (documentation, reprs, etc) tend to use single quotes. but if you really want to, you can use blackRestful
You could use VIM. ;) Press :, then %s/'/"/g and hit enter.Laceration
One answer (not Pycharm specific) is to do what black does, and manipulate a concrete syntax tree. black uses lib2to3, which ships with CPython. Another option is libcst. Putting aside the handling of f-strings and triple quoted strings, you should be able to parse a module into CST, change every string's quote type, and generate the source code again, with nothing else changing.Flyweight
What about triple quoted strings that uses single quotes? Should those be replaced as well?Ciccia
@AnnZen Personally I don't think they should. Pycharm offers the ability to change single and triple separately; which I think is a good call. The focus of my question is specifically aimed at converting single single-quoted strings to single double-quoted strings.Gardner
I missed this comment of yours (no triple-quoted strings). Will update my tool accordingly. I would be grateful if you tried it out and gave me your feedback.Accouchement
I
8

In Preferences, type double-quoted into the search bar. This will suggest Editor->Intentions. There's an option there for converting double-quoted strings to single-quoted strings and vice-versa. Check this to see if it is enabled.

When you select one instance of such a string, and click on the 'light bulb', this may, or may not, offer you the option to apply it to the entire file by tapping the right arrow.

If not, it may at least ease your pain in that you can simply click once on each instance and use the light bulb to fix that on. Finding documentation on Intentions isn't all that fruitful, regarding how to apply them across a file or include them in code inspection. This may be a difference between the Community edition and the Full Version.

Though, as @acushner's comment suggests, and in my personal experience and preference, single-quoting seems the more consistently-used styling, and arguably sub rosa canon.

(To be honest, it's so ingrained in me at this point that I find myself single-quoting prose. But I reckon that's a personal problem, and TMI. 'Be yourself.')

Imagism answered 12/2, 2019 at 23:45 Comment(4)
I use the full version and yeah, I would have expected to see the option to apply across all strings or the whole file but I do not see that. :(Gardner
I wish their Intentions were better documented, or the creation new rules, at least. I've pretty much jumped ship to VS Code at this point.Imagism
regarding personal experience and preferences: in my code all strings are single-quoted, except for f-strings, for a reason: they may contain other single-quoted strings, in dictionary keys and in expressions. For former PHP developers this comes naturally, since variable substitution only works in double-quoted strings in PHP.Accouchement
@WalterTross "variable substitution only works in double-quoted strings in PHP" And more commonly in most unix shell languages, including sh, bash, zsh, etc.Rustin
B
6

I think there's an easier solution not being discussed, which is PyCharm's find and replace feature to ignore comments and string literals, which seems like it would do exactly what you want without much work and with little room for edge cases: enter image description here

To ignore triple single quotes, you can use this regex, which I copied from this SO post:

(?<!')'(?!')

enter image description here

Brandon answered 20/3, 2022 at 2:38 Comment(11)
One solution would be to do what I suggested, and then find and replace """ to '''. @FlyweightBrandon
Sorry, why are there existing """ that you don't want changed to '''? Are they in docstrings?Brandon
I figured it out, gonna update original answer, thanks @FlyweightBrandon
Good call, thanksBrandon
1) Your regex will cause the closing quote of this string to be ignored: '\'foo\''; 2) Your solution still cannot be applied to a whole file, since it would blindly replace the enclosing single quotes of strings containing double quotes (like '"foo"'), without escaping them; 3) There is a bug in PyCharm that causes the opening quote of a prefixed string like an f-string to be considered "inside" the string, and thus ignored. Only the closing quote of f"foo{bar}" will be found!Accouchement
because of points 1) and 3) above, none of the quotes of f'foo \'{bar}\'' would be found by the recipe of this answerAccouchement
Point 3 is good, points 1 and 2 are pretty niche IMO. I like that your answer is correct but I feel like it's overengineered and there should be a middleground somewhere with regex and built-in toolsBrandon
I agree that point 1 is niche (and btw, the regex could be improved), but point 2 happens quite often in my codebase, I wouldn't say it's niche.Accouchement
I got the example of point 3 above wrong for this context, it should have been f'foo{bar}'Accouchement
4) in spite of the "Except Comments and String Literals" filter, the single quotes in f"foo{bar['baz']}" are found (they can't be simply replaced!)Accouchement
Walter's points make it clear that this isn't a perfect solution, given the amount of corrections that may need to be applied afterwards. Point 3 is really unfortunate, since the bug is Pycharm's fault, but f-strings are so prevalent now in so many codebases that breaking them all would be unacceptable, until the bug is fixed.Flyweight
A
3

I know you asked how to do it in PyCharm, but I'll propose a tool instead, since in my tests it works flawlessly. You can still run it from within PyCharm's terminal window, which makes this a valid answer to your question ;-).

The tool (source code below) is based on Python's tokenizer. This guarantees that only single quotes enclosing string literals can be replaced.

Furthermore, conversions that would not be straightforward and safe, and possibly not even desirable, are avoided. The file positions that may need manual intervention are reported.

With the help of the find command you can apply this tool to a whole source tree at once.

import argparse
import re
from tokenize import generate_tokens, STRING, untokenize

# Tool for the automatic (and manual) conversion of single-quoted strings to double-quoted ones

# Reference for string literals: https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals
LONG_STRING_REGEX  = re.compile(r"([fruFRU]*)'''(.*)'''", re.DOTALL)
SHORT_STRING_REGEX = re.compile(r"([fruFRU]*)'(.*)'")  # only if LONG_STRING_REGEX doesn't match

def single_to_double_quotes(infilename, outfilename, long_too):
    convs_done = 0
    manual_conv_starts = []
    output_tokens = []
    with open(infilename) as infile:
        for t in generate_tokens(infile.readline):
            if t.type == STRING:
                if m := LONG_STRING_REGEX.fullmatch(t.string):
                    if long_too:
                        prefix, value = m.groups()
                        if '"""' in value:
                            manual_conv_starts.append(t.start)
                        else:
                            t = (STRING, f'{prefix}"""{value}"""', t.start, t.end, t.line)
                            convs_done += 1
                elif m := SHORT_STRING_REGEX.fullmatch(t.string):
                    prefix, value = m.groups()
                    if '"' in value or "'" in value:
                        manual_conv_starts.append(t.start)
                    else:
                        t = (STRING, f'{prefix}"{value}"', t.start, t.end, t.line)
                        convs_done += 1
            output_tokens.append(t)

    with open(outfilename, 'w') as outfile:
        outfile.write(untokenize(output_tokens))
    return convs_done, manual_conv_starts

parser = argparse.ArgumentParser(description='safely convert single quotes to double quotes')
parser.add_argument('infilename', metavar='INPUT_FILE', help='the input file')
parser.add_argument('outfilename', metavar='OUTPUT_FILE', help='the output file')
parser.add_argument('--long', '-l', action='store_true', help='also convert "long" (triple-quoted) strings')
args = parser.parse_args()  # type: argparse.Namespace
infilename = args.infilename  # type: str
outfilename = args.outfilename  # type: str
long_too = bool(args.long)
print(f"input file: {infilename}")
print(f"long string conversion {('disabled', 'enabled')[long_too]}")
convs_done, manual_conv_starts = single_to_double_quotes(infilename, outfilename, long_too)
print(f"strings converted: {convs_done}")
print(f"possible manual conversions: {len(manual_conv_starts)}")
for start in manual_conv_starts:
    print(f"  {outfilename} line {start[0]} char {start[1] + 1}")
print(f"output file: {outfilename}")

Example output (without -l option):

input file: single_to_double_quotes_TEST.py
long string conversion disabled
strings converted: 5
possible manual conversions: 3
  single_to_double_quotes_CONV.py line 3 char 5
  single_to_double_quotes_CONV.py line 4 char 5
  single_to_double_quotes_CONV.py line 12 char 7
output file: single_to_double_quotes_CONV.py

single_to_double_quotes_TEST.py:

n = 123
s = 'abc'
t = 'd"e"f'
u = 'g\'h\'i'
l = '''
"let's see"
'''
w, x, y, z = '''xyz''', '''"""yz''', '''x"""z''', '''xy"""'''
print(f'{s=}')
d = {'a': 1, 'b': 2, 'c': 3}
print(f"{d['a']=}")
print(f'{d["a"]=}')

single_to_double_quotes_CONV.py:

n = 123
s = "abc"
t = 'd"e"f'
u = 'g\'h\'i'
l = '''
"let's see"
'''
w, x, y, z = '''xyz''', '''"""yz''', '''x"""z''', '''xy"""'''
print(f"{s=}")
d = {"a": 1, "b": 2, "c": 3}
print(f"{d['a']=}")
print(f'{d["a"]=}')

On a copy of your source tree, like on a git checkout, you can do the following:

find . -name \*.py -exec python3 PATH/TO/TOOL.py {} {} \;

This will convert each file onto itself.

In case you are not familiar with argparse: start from

python3 TOOL.py -h

I recommend you modify the tool to best fit your needs.

Accouchement answered 19/3, 2022 at 0:18 Comment(4)
I'm working on a generalization of this tool, will publish it in githubAccouchement
I would be interested in seeing that. If you're willing, share your link to github.Gardner
@MarcelWilson lemme polish it a bit, I'll get back to youAccouchement
@MarcelWilson: Making the tool accept and handle a complete configuration proved to be a real feat. How to allow the user a clear choice between, e.g., '"don\'t do it" they said', "\"don't do it\" they said", '''"don't do it" they said''' and """"don't do it" they said"""? How to deal with, e.g., f"""{d['''"don't do it" they said''']}!""". I had a couple of working solutions, but they were not good enough to be published, and now I ran out of time. I'm sorry, I have to postpone this to some longer vacation.Accouchement
L
3

This can be done using black formatting

Install black by doing pip install black

Then do black . to perform the formatting.

This will not just take care of the single quotes, but will format your code to a much readable way. There are a lot of options that you can provide to the black cli to customize how black does the formatting.

You can check those options here: https://black.readthedocs.io/en/stable/usage_and_configuration/the_basics.html#command-line-options

Lafferty answered 22/3, 2022 at 13:16 Comment(2)
This answer is so underrated! Thanks!Amphoteric
That would work except the file in question should not be completely reformatted. I wish black had a way to run specific formatting only; (e.g. black . --quotes-only)Gardner
R
0

I don't know if it helps anyone but I prefer to select one word with a double click instead of selecting the whole string between quotes and then press Alt + Enter. This way you open the actions menu and select "Convert single quoted string to double quotes" or vice versa. You don't even have to double click since Pycharm is smart enough to select the first pair of quotes. I find this very comfortable. I don't know if it is smart enough for very complicated nested strings but you can try. It would be nice though, if it would let us select multiple lines and fix them too but that's what black is here for, right?

Rinaldo answered 21/11, 2023 at 7:47 Comment(0)
S
0

install black in global

pip install black

1、use file watcher

add black to file watcher enter image description here enable it enter image description here 2、or use blackconnect

enter image description here

3、I just found pycharm already support some usage

enter image description here

Snobbery answered 1/8 at 10:7 Comment(0)
J
-5

Ctrl+R Will bring up the find/replace dialog. Just put ' in the find field and " in the replace field.

Jarita answered 12/2, 2019 at 23:7 Comment(1)
That would also convert all apostrophes that occur in the middle of text sentences though (e.g. in comment lines, docstrings and log/print statements). I doubt that's desirable.Maupassant

© 2022 - 2024 — McMap. All rights reserved.