Skip to content

Commit 80a63c0

Browse files
committed
fix replacement of entire indented import
1 parent 8eb196d commit 80a63c0

File tree

3 files changed

+39
-5
lines changed

3 files changed

+39
-5
lines changed

pyupgrade/_plugins/imports.py

+17-3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from pyupgrade._data import TokenFunc
1818
from pyupgrade._token_helpers import find_end
1919
from pyupgrade._token_helpers import find_token
20+
from pyupgrade._token_helpers import has_space_before
2021
from pyupgrade._token_helpers import indented_amount
2122

2223
# GENERATED VIA generate-imports
@@ -259,13 +260,19 @@ def _remove_import(i: int, tokens: list[Token]) -> None:
259260

260261

261262
class FromImport(NamedTuple):
263+
start: int
262264
mod_start: int
263265
mod_end: int
264266
names: tuple[int, ...]
265267
end: int
266268

267269
@classmethod
268270
def parse(cls, i: int, tokens: list[Token]) -> FromImport:
271+
if has_space_before(i, tokens):
272+
start = i - 1
273+
else:
274+
start = i
275+
269276
j = i + 1
270277
# XXX: does not handle explicit relative imports
271278
while tokens[j].name != 'NAME':
@@ -290,7 +297,10 @@ def parse(cls, i: int, tokens: list[Token]) -> FromImport:
290297
if tokens[names[i]].src == 'as':
291298
del names[i:i + 2]
292299

293-
return cls(mod_start, mod_end + 1, tuple(names), end)
300+
return cls(start, mod_start, mod_end + 1, tuple(names), end)
301+
302+
def remove_self(self, tokens: list[Token]) -> None:
303+
del tokens[self.start:self.end]
294304

295305
def replace_modname(self, tokens: list[Token], modname: str) -> None:
296306
tokens[self.mod_start:self.mod_end] = [Token('CODE', modname)]
@@ -365,7 +375,7 @@ def _replace_from_mixed(
365375

366376
# all names rewritten -- delete import
367377
if len(parsed.names) == len(removal_idxs):
368-
del tokens[i:parsed.end]
378+
parsed.remove_self(tokens)
369379
else:
370380
parsed.remove_parts(tokens, removal_idxs)
371381

@@ -443,6 +453,10 @@ def _replace_import(
443453
except ValueError:
444454
return
445455

456+
if has_space_before(i, tokens):
457+
start = i - 1
458+
else:
459+
start = i
446460
end = find_end(tokens, i)
447461

448462
parts = []
@@ -478,7 +492,7 @@ def _replace_import(
478492
tokens[end:end] = [Token('CODE', ''.join(new_imports))]
479493

480494
if len(to_from) == len(parts):
481-
del tokens[i:end]
495+
del tokens[start:end]
482496
else:
483497
for idx, _, _, _ in reversed(to_from):
484498
if idx == 0: # look forward until next name and del

pyupgrade/_token_helpers.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -517,11 +517,15 @@ def replace_list_comp_brackets(i: int, tokens: list[Token]) -> None:
517517
tokens[start] = Token('OP', '(')
518518

519519

520+
def has_space_before(i: int, tokens: list[Token]) -> bool:
521+
return i >= 1 and tokens[i - 1].name in {UNIMPORTANT_WS, 'INDENT'}
522+
523+
520524
def indented_amount(i: int, tokens: list[Token]) -> str:
521525
if i == 0:
522526
return ''
523-
elif i >= 2 and tokens[i - 1].name in {UNIMPORTANT_WS, 'INDENT'}:
524-
if tokens[i - 2].name in {'NL', 'NEWLINE', 'DEDENT'}:
527+
elif has_space_before(i, tokens):
528+
if i >= 2 and tokens[i - 2].name in {'NL', 'NEWLINE', 'DEDENT'}:
525529
return tokens[i - 1].src
526530
else: # inline import
527531
raise ValueError('not at beginning of line')

tests/features/import_replaces_test.py

+16
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,14 @@ def test_mock_noop_keep_mock():
134134
'from collections.abc import Mapping as mapping\n',
135135
id='new import with aliased name',
136136
),
137+
pytest.param(
138+
'if True:\n'
139+
' from xml.etree import cElementTree as ET\n',
140+
(3,),
141+
'if True:\n'
142+
' from xml.etree import ElementTree as ET\n',
143+
id='indented and full import replaced',
144+
),
137145
pytest.param(
138146
'if True:\n'
139147
' from collections import Mapping, Counter\n',
@@ -152,6 +160,14 @@ def test_mock_noop_keep_mock():
152160
' import queue\n',
153161
id='indented import-import being added',
154162
),
163+
pytest.param(
164+
'if True:\n'
165+
' import mock\n',
166+
(3,),
167+
'if True:\n'
168+
' from unittest import mock\n',
169+
id='indented import-import rewritten',
170+
),
155171
pytest.param(
156172
'if True:\n'
157173
' if True:\n'

0 commit comments

Comments
 (0)