Skip to content

Commit 62f43a2

Browse files
authored
Merge pull request #429 from MarcoGorelli/universal_newlines_to_text
upgrade subprocess.run(universal_newlines=True) to subprocess.run(text=True) in --py37-plus
2 parents 9979122 + f0e9373 commit 62f43a2

File tree

4 files changed

+151
-0
lines changed

4 files changed

+151
-0
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,18 @@ _note_: `pyupgrade` is intentionally timid and will not create an f-string
462462
if it would make the expression longer or if the substitution parameters are
463463
anything but simple names or dotted names (as this can decrease readability).
464464

465+
466+
### `subprocess.run`: replace `universal_newlines` with `text`
467+
468+
Availability:
469+
- `--py37-plus` is passed on the commandline.
470+
471+
```diff
472+
-output = subprocess.run(['foo'], universal_newlines=True)
473+
+output = subprocess.run(['foo'], text=True)
474+
```
475+
476+
465477
### remove parentheses from `@functools.lru_cache()`
466478

467479
Availability:

pyupgrade/_data.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class State(NamedTuple):
4949
'select',
5050
'six',
5151
'socket',
52+
'subprocess',
5253
'sys',
5354
'typing',
5455
))
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import ast
2+
import functools
3+
from typing import Iterable
4+
from typing import List
5+
from typing import Tuple
6+
7+
from tokenize_rt import Offset
8+
from tokenize_rt import Token
9+
from tokenize_rt import tokens_to_src
10+
11+
from pyupgrade._ast_helpers import ast_to_offset
12+
from pyupgrade._ast_helpers import is_name_attr
13+
from pyupgrade._data import register
14+
from pyupgrade._data import State
15+
from pyupgrade._data import TokenFunc
16+
from pyupgrade._token_helpers import find_open_paren
17+
from pyupgrade._token_helpers import parse_call_args
18+
19+
20+
def _replace_universal_newlines_with_text(
21+
i: int,
22+
tokens: List[Token],
23+
*,
24+
arg_idx: int,
25+
) -> None:
26+
j = find_open_paren(tokens, i)
27+
func_args, _ = parse_call_args(tokens, j)
28+
src = tokens_to_src(tokens[slice(*func_args[arg_idx])])
29+
new_src = src.replace('universal_newlines', 'text', 1)
30+
tokens[slice(*func_args[arg_idx])] = [Token('SRC', new_src)]
31+
32+
33+
@register(ast.Call)
34+
def visit_Call(
35+
state: State,
36+
node: ast.Call,
37+
parent: ast.AST,
38+
) -> Iterable[Tuple[Offset, TokenFunc]]:
39+
if (
40+
state.settings.min_version >= (3, 7) and
41+
is_name_attr(
42+
node.func,
43+
state.from_imports,
44+
'subprocess',
45+
('run',),
46+
)
47+
):
48+
kwarg_idx = next(
49+
(
50+
n
51+
for n, keyword in enumerate(node.keywords)
52+
if keyword.arg == 'universal_newlines'
53+
),
54+
None,
55+
)
56+
if kwarg_idx is not None:
57+
func = functools.partial(
58+
_replace_universal_newlines_with_text,
59+
arg_idx=len(node.args) + kwarg_idx,
60+
)
61+
yield ast_to_offset(node), func
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import pytest
2+
3+
from pyupgrade._data import Settings
4+
from pyupgrade._main import _fix_plugins
5+
6+
7+
@pytest.mark.parametrize(
8+
('s', 'version'),
9+
(
10+
pytest.param(
11+
'import subprocess\n'
12+
'subprocess.run(["foo"], universal_newlines=True)\n',
13+
(3,),
14+
id='not Python3.7+',
15+
),
16+
pytest.param(
17+
'from foo import run\n'
18+
'run(["foo"], universal_newlines=True)\n',
19+
(3, 7),
20+
id='run imported, but not from subprocess',
21+
),
22+
pytest.param(
23+
'from subprocess import run\n'
24+
'run(["foo"], shell=True)\n',
25+
(3, 7),
26+
id='universal_newlines not used',
27+
),
28+
),
29+
)
30+
def test_fix_universal_newlines_to_text_noop(s, version):
31+
assert _fix_plugins(s, settings=Settings(min_version=version)) == s
32+
33+
34+
@pytest.mark.parametrize(
35+
('s', 'expected'),
36+
(
37+
pytest.param(
38+
'import subprocess\n'
39+
'subprocess.run(["foo"], universal_newlines=True)\n',
40+
41+
'import subprocess\n'
42+
'subprocess.run(["foo"], text=True)\n',
43+
44+
id='subprocess.run attribute',
45+
),
46+
pytest.param(
47+
'from subprocess import run\n'
48+
'run(["foo"], universal_newlines=True)\n',
49+
50+
'from subprocess import run\n'
51+
'run(["foo"], text=True)\n',
52+
53+
id='run imported from subprocess',
54+
),
55+
pytest.param(
56+
'from subprocess import run\n'
57+
'run(["foo"], universal_newlines=universal_newlines)\n',
58+
59+
'from subprocess import run\n'
60+
'run(["foo"], text=universal_newlines)\n',
61+
62+
id='universal_newlines appears as value',
63+
),
64+
pytest.param(
65+
'from subprocess import run\n'
66+
'run(["foo"], *foo, universal_newlines=universal_newlines)\n',
67+
68+
'from subprocess import run\n'
69+
'run(["foo"], *foo, text=universal_newlines)\n',
70+
71+
id='with starargs',
72+
),
73+
),
74+
)
75+
def test_fix_universal_newlines_to_text(s, expected):
76+
ret = _fix_plugins(s, settings=Settings(min_version=(3, 7)))
77+
assert ret == expected

0 commit comments

Comments
 (0)