Skip to content

Commit e9012a9

Browse files
committed
from __future__ import lines must always come first
1 parent 68159da commit e9012a9

File tree

2 files changed

+52
-5
lines changed

2 files changed

+52
-5
lines changed

mutmut/__main__.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -508,15 +508,37 @@ def yield_mutants_for_class_body(node, no_mutate_lines):
508508
yield 'filler', child_node.get_code(), None, None
509509

510510

511+
def is_from_future_import_node(c):
512+
if c.type == 'simple_stmt':
513+
if c.children:
514+
c2 = c.children[0]
515+
if c2.type == 'import_from' and c2.children[1].type == 'name' and c2.children[1].value == '__future__':
516+
return True
517+
return False
518+
519+
520+
def yield_future_imports(node):
521+
for c in node.children:
522+
if is_from_future_import_node(c):
523+
yield 'filler', c.get_code(), None, None
524+
525+
511526
def yield_mutants_for_module(node, no_mutate_lines):
527+
assert node.type == 'file_input'
528+
529+
# First yield `from __future__`, then the rest
530+
yield from yield_future_imports(node)
531+
512532
yield 'trampoline_impl', trampoline_impl, None, None
513533
yield 'filler', '\n', None, None
514-
assert node.type == 'file_input'
515534
for child_node in node.children:
516535
if child_node.type == 'funcdef':
517536
yield from yield_mutants_for_function(child_node, no_mutate_lines=no_mutate_lines)
518537
elif child_node.type == 'classdef':
519538
yield from yield_mutants_for_class(child_node, no_mutate_lines=no_mutate_lines)
539+
elif is_from_future_import_node(child_node):
540+
# Don't yield `from __future__` after trampoline
541+
pass
520542
else:
521543
yield 'filler', child_node.get_code(), None, None
522544

@@ -636,10 +658,7 @@ def execute_pytest(self, params, **kwargs):
636658
raise BadTestExecutionCommandsException(params)
637659
return exit_code
638660

639-
640661
def run_stats(self, *, tests):
641-
import pytest
642-
643662
class StatsCollector:
644663
def pytest_runtest_teardown(self, item, nextitem):
645664
unused(nextitem)
@@ -1083,7 +1102,14 @@ def run(mutant_names, *, max_children):
10831102
time = datetime.now() - start
10841103
print(f' done in {round(time.total_seconds()*1000)}ms', )
10851104

1086-
sys.path.insert(0, os.path.abspath('mutants'))
1105+
src_path = (Path('mutants') / 'src')
1106+
source_path = (Path('mutants') / 'source')
1107+
if src_path.exists():
1108+
sys.path.insert(0, str(src_path.absolute()))
1109+
elif source_path.exists():
1110+
sys.path.insert(0, str(source_path.absolute))
1111+
else:
1112+
sys.path.insert(0, os.path.abspath('mutants'))
10871113

10881114
# TODO: config/option for runner
10891115
# runner = HammettRunner()

tests/test_mutation.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,14 @@ def mutants_for_source(source):
182182
return r
183183

184184

185+
def full_mutated_source(source):
186+
no_mutate_lines = pragma_no_mutate_lines(source)
187+
r = []
188+
for type_, x, name_and_hash, mutant_name in yield_mutants_for_module(parse(source, error_recovery=False), no_mutate_lines):
189+
r.append(x)
190+
return '\n'.join(r).strip()
191+
192+
185193
def test_function_with_annotation():
186194
source = "def capitalize(s : str):\n return s[0].upper() + s[1:] if s else s\n".strip()
187195
mutants = mutants_for_source(source)
@@ -337,3 +345,16 @@ def member(self):
337345
- return 3
338346
+ return 4
339347
'''.strip()
348+
349+
350+
def test_from_future_still_first():
351+
source = """
352+
from __future__ import annotations
353+
from collections.abc import Iterable
354+
355+
def foo():
356+
return 1
357+
""".strip()
358+
mutated_source = full_mutated_source(source)
359+
assert mutated_source.split('\n')[0] == 'from __future__ import annotations'
360+
assert mutated_source.count('from __future__') == 1

0 commit comments

Comments
 (0)