Repository: asottile/pyupgrade

Issue:
f-string conversion with advanced string formatting & None
Failing test case using pyupgrade 2.25.0:

```
$ echo 'a=b=c=None; print("Query:%8s %s %s" % (a, b, c))'  > test.py ; pyupgrade --py36-plus test.py ; cat test.py
Rewriting test.py
a=b=c=None; print(f"Query:{a:>8} {b} {c}")
```

```pycon
>>> a=b=c=None; print("Query:%8s %s %s" % (a, b, c))
Query:    None None None
>>> a=b=c=None; print(f"Query:{a:>8} {b} {c}")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported format string passed to NoneType.__format__
```

The problem is while ``f"{None}"`` works, advanced string format arguments do not:

```pycon
>>> "%8s" % None
'    None'
```

```pycon
>>> f"{None:>8}"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported format string passed to NoneType.__format__
```

Possible workaround:

```python
>>> a=b=c=None; print(f"Query:{str(a):>8} {b} {c}")
Query:    None None None
```

This issue was found in https://github.com/biopython/biopython/pull/3724

Comments:

Patch:
diff --git a/pyupgrade/_plugins/percent_format.py b/pyupgrade/_plugins/percent_format.py
index 8dc2c76..1b0599d 100644
--- a/pyupgrade/_plugins/percent_format.py
+++ b/pyupgrade/_plugins/percent_format.py
@@ -128,8 +128,6 @@ def _percent_to_format(s: str) -> str:
             if conversion == '%':
                 return s + '%'
             parts = [s, '{']
-            if width and conversion == 's' and not conversion_flag:
-                conversion_flag = '>'
             if conversion == 's':
                 conversion = ''
             if key:
@@ -276,6 +274,9 @@ def visit_BinOp(
                 # no equivalent in format
                 if conversion in {'a', 'r'} and nontrivial_fmt:
                     break
+                # %s with None and width is not supported
+                if width and conversion == 's':
+                    break
                 # all dict substitutions must be named
                 if isinstance(node.right, ast.Dict) and not key:
                     break


Test patch:
diff --git a/tests/features/percent_format_test.py b/tests/features/percent_format_test.py
index 57ca3c6..0d620ce 100644
--- a/tests/features/percent_format_test.py
+++ b/tests/features/percent_format_test.py
@@ -158,6 +158,7 @@ def test_simplify_conversion_flag(s, expected):
         '"%4%" % ()',
         # no equivalent in format specifier
         '"%.2r" % (1.25)', '"%.2a" % (1.25)',
+        pytest.param('"%8s" % (None,)', id='unsafe width-string conversion'),
         # non-string mod
         'i % 3',
         # dict format but not keyed arguments
@@ -208,8 +209,8 @@ def test_percent_format_noop_if_bug_16806():
         ('"%s" % ("%s" % ("nested",),)', '"{}".format("{}".format("nested"))'),
         ('"%s%% percent" % (15,)', '"{}% percent".format(15)'),
         ('"%3f" % (15,)', '"{:3f}".format(15)'),
-        ('"%-5s" % ("hi",)', '"{:<5}".format("hi")'),
-        ('"%9s" % (5,)', '"{:>9}".format(5)'),
+        ('"%-5f" % (5,)', '"{:<5f}".format(5)'),
+        ('"%9f" % (5,)', '"{:9f}".format(5)'),
         ('"brace {} %s" % (1,)', '"brace {{}} {}".format(1)'),
         (
             '"%s" % (\n'