Skip to content

Commit

Permalink
from __future__ import lines must always come first
Browse files Browse the repository at this point in the history
  • Loading branch information
boxed committed Oct 22, 2024
1 parent 68159da commit e9012a9
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 5 deletions.
36 changes: 31 additions & 5 deletions mutmut/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -508,15 +508,37 @@ def yield_mutants_for_class_body(node, no_mutate_lines):
yield 'filler', child_node.get_code(), None, None


def is_from_future_import_node(c):
if c.type == 'simple_stmt':
if c.children:
c2 = c.children[0]
if c2.type == 'import_from' and c2.children[1].type == 'name' and c2.children[1].value == '__future__':
return True
return False


def yield_future_imports(node):
for c in node.children:
if is_from_future_import_node(c):
yield 'filler', c.get_code(), None, None


def yield_mutants_for_module(node, no_mutate_lines):
assert node.type == 'file_input'

# First yield `from __future__`, then the rest
yield from yield_future_imports(node)

yield 'trampoline_impl', trampoline_impl, None, None
yield 'filler', '\n', None, None
assert node.type == 'file_input'
for child_node in node.children:
if child_node.type == 'funcdef':
yield from yield_mutants_for_function(child_node, no_mutate_lines=no_mutate_lines)
elif child_node.type == 'classdef':
yield from yield_mutants_for_class(child_node, no_mutate_lines=no_mutate_lines)
elif is_from_future_import_node(child_node):
# Don't yield `from __future__` after trampoline
pass
else:
yield 'filler', child_node.get_code(), None, None

Expand Down Expand Up @@ -636,10 +658,7 @@ def execute_pytest(self, params, **kwargs):
raise BadTestExecutionCommandsException(params)
return exit_code


def run_stats(self, *, tests):
import pytest

class StatsCollector:
def pytest_runtest_teardown(self, item, nextitem):
unused(nextitem)
Expand Down Expand Up @@ -1083,7 +1102,14 @@ def run(mutant_names, *, max_children):
time = datetime.now() - start
print(f' done in {round(time.total_seconds()*1000)}ms', )

sys.path.insert(0, os.path.abspath('mutants'))
src_path = (Path('mutants') / 'src')
source_path = (Path('mutants') / 'source')
if src_path.exists():
sys.path.insert(0, str(src_path.absolute()))
elif source_path.exists():
sys.path.insert(0, str(source_path.absolute))
else:
sys.path.insert(0, os.path.abspath('mutants'))

# TODO: config/option for runner
# runner = HammettRunner()
Expand Down
21 changes: 21 additions & 0 deletions tests/test_mutation.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,14 @@ def mutants_for_source(source):
return r


def full_mutated_source(source):
no_mutate_lines = pragma_no_mutate_lines(source)
r = []
for type_, x, name_and_hash, mutant_name in yield_mutants_for_module(parse(source, error_recovery=False), no_mutate_lines):
r.append(x)
return '\n'.join(r).strip()


def test_function_with_annotation():
source = "def capitalize(s : str):\n return s[0].upper() + s[1:] if s else s\n".strip()
mutants = mutants_for_source(source)
Expand Down Expand Up @@ -337,3 +345,16 @@ def member(self):
- return 3
+ return 4
'''.strip()


def test_from_future_still_first():
source = """
from __future__ import annotations
from collections.abc import Iterable
def foo():
return 1
""".strip()
mutated_source = full_mutated_source(source)
assert mutated_source.split('\n')[0] == 'from __future__ import annotations'
assert mutated_source.count('from __future__') == 1

0 comments on commit e9012a9

Please sign in to comment.