Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle %s strings with length and alignment #194

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion src/flynt/transform/percent_transformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
FORMAT_GROUP = f"[hlL]?[{FORMATS}]"
FORMAT_GROUP_MATCH = f"[hlL]?([{FORMATS}])"

PREFIX_GROUP = "[0-9]*[.]?[0-9]*"
PREFIX_GROUP = "[+-]?[0-9]*[.]?[0-9]*"

ANY_DICT = re.compile(r"(?<!%)%\([^)]+?\)")
DICT_PATTERN = re.compile(rf"(%\([^)]+\){PREFIX_GROUP}{FORMAT_GROUP})")
Expand Down Expand Up @@ -53,6 +53,32 @@ def formatted_value(
fmt_prefix = fmt_prefix.replace(".", "0")

if fmt_spec in conversion_methods:
if fmt_spec == "s" and fmt_prefix:
# Strings are right aligned in percent fmt by default, and indicate
# left alignment through a negative prefix.
#
# fstrings left align by default, and separate signs from alignment
#
# Python even accepts float values here, for both percent fmt
# and fstrings
#
# In order to not have to figure out what sort of number we are
# dealing with, just look at the leading - sign (if there is one)
# and remove it for the conversion
if aggressive and fmt_prefix.startswith("-"):
Copy link
Owner

@ikamensh ikamensh Aug 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if aggressive and fmt_prefix.startswith("-"):
if fmt_prefix.startswith("-"):

It seems that we have handled all possible cases within this branch (fmt_spec == "s" and fmt_prefix), so there is no room for error anymore; therefore we don't need aggressive flag? aggressive means "risky", some changes that are not guaranteed to be correct.

Is there any known/suspected case where outputs might differ for original and new f-string? to me it sounds exhaustive what you've suggested.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason I "locked" this behind the aggressive flag is that it changes the current behaviour. Arguably the current behaviour is wrong (a right aligned percentage string would become a left aligned fstring), but still.
If you feel that's not critical I'm fine with dropping the flag requirement.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my logic aggressive is not for compatibility with old version, people can freeze flynt version for this. Its to use some logic that might change how strings come out of formatting. Seems all cases are handled here.

# Left alignment
return ast_formatted_value(
val,
fmt_str=f"{fmt_prefix[1:]}",
conversion=conversion_methods[fmt_spec],
)
if aggressive and not fmt_prefix.startswith("-"):
# Right alignment
return ast_formatted_value(
val,
fmt_str=f">{fmt_prefix}",
conversion=conversion_methods[fmt_spec],
)
if not aggressive and fmt_prefix:
raise ConversionRefused(
"Default text alignment has changed between percent fmt and fstrings. "
Expand Down
18 changes: 18 additions & 0 deletions test/test_edits.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,24 @@ def test_call(state: State):

def test_string_specific_len(state: State):
s_in = """'%5s' % CLASS_NAMES[labels[j]]"""
s_expected = """f'{CLASS_NAMES[labels[j]]:>5}'"""

state.aggressive = True
s_out, count = code_editor.fstringify_code_by_line(s_in, state)
assert s_out == s_expected


def test_string_specific_len_right_aligned(state: State):
s_in = """'%5s' % CLASS_NAMES[labels[j]]"""
s_expected = """f'{CLASS_NAMES[labels[j]]:>5}'"""

state.aggressive = True
s_out, count = code_editor.fstringify_code_by_line(s_in, state)
assert s_out == s_expected
Comment on lines 41 to +56
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two tests are exactly the same? lets delete one.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, test_string_specific_len can go?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes.



def test_string_specific_len_left_aligned(state: State):
s_in = """'%-5s' % CLASS_NAMES[labels[j]]"""
s_expected = """f'{CLASS_NAMES[labels[j]]:5}'"""

state.aggressive = True
Expand Down
Loading