import unittest import tempfile import shutil import os import mock import sys from elpy import refactor from textwrap import dedent class RefactorTestCase(unittest.TestCase): def setUp(self): self.project_root = tempfile.mkdtemp(prefix="test-refactor-root") self.addCleanup(shutil.rmtree, self.project_root, ignore_errors=True) def create_file(self, name, contents=""): filename = os.path.join(self.project_root, name) contents = dedent(contents) offset = contents.find("_|_") if offset > -1: contents = contents[:offset] + contents[offset + 3:] with open(filename, "w") as f: f.write(contents) return filename, offset def assertSourceEqual(self, first, second, msg=None): """Fail if the two objects are unequal, ignoring indentation.""" self.assertEqual(dedent(first), dedent(second), msg=msg) class TestGetRefactorOptions(RefactorTestCase): def test_should_only_return_importsmodule_if_not_on_symbol(self): filename, offset = self.create_file("foo.py", """\ import foo _|_""") ref = refactor.Refactor(self.project_root, filename) options = ref.get_refactor_options(offset) self.assertTrue(all(opt['category'] in ('Imports', 'Module') for opt in options)) filename, offset = self.create_file("foo.py", """\ _|_ import foo""") ref = refactor.Refactor(self.project_root, filename) options = ref.get_refactor_options(offset) self.assertTrue(all(opt['category'] in ('Imports', 'Module') for opt in options)) def test_should_return_all_if_on_symbol(self): filename, offset = self.create_file("foo.py", "import _|_foo") ref = refactor.Refactor(self.project_root, filename) options = ref.get_refactor_options(offset) self.assertTrue(all(opt['category'] in ('Imports', 'Method', 'Module', 'Symbol') for opt in options)) def test_should_return_only_region_if_endoffset(self): filename, offset = self.create_file("foo.py", "import foo") ref = refactor.Refactor(self.project_root, filename) options = ref.get_refactor_options(offset, 5) self.assertTrue(all(opt['category'] == 'Region' for opt in options)) @unittest.skipIf(not refactor.ROPE_AVAILABLE, "Requires Rope") def test_should_treat_from_import_special(self): filename, offset = self.create_file("foo.py", """\ import foo _|_""") ref = refactor.Refactor(self.project_root, filename) options = ref.get_refactor_options(offset) self.assertFalse(any(opt['name'] == "refactor_froms_to_imports" for opt in options)) filename, offset = self.create_file("foo.py", "imp_|_ort foo") ref = refactor.Refactor(self.project_root, filename) options = ref.get_refactor_options(offset) self.assertTrue(any(opt['name'] == "refactor_froms_to_imports" for opt in options)) class TestGetChanges(RefactorTestCase): def test_should_fail_if_method_is_not_refactoring(self): filename, offset = self.create_file("foo.py") ref = refactor.Refactor(self.project_root, filename) self.assertRaises(ValueError, ref.get_changes, "bad_name") def test_should_return_method_results(self): filename, offset = self.create_file("foo.py") ref = refactor.Refactor(self.project_root, filename) with mock.patch.object(ref, 'refactor_extract_method') as test: test.return_value = "Meep!" self.assertEqual(ref.get_changes("refactor_extract_method", 1, 2), "Meep!") test.assert_called_with(1, 2) @unittest.skipIf(not refactor.ROPE_AVAILABLE, "Requires Rope") class TestIsOnSymbol(RefactorTestCase): def test_should_find_symbol(self): filename, offset = self.create_file("test.py", "__B_|_AR = 100") r = refactor.Refactor(self.project_root, filename) self.assertTrue(r._is_on_symbol(offset)) # Issue #111 def test_should_find_symbol_with_underscores(self): filename, offset = self.create_file("test.py", "_|___BAR = 100") r = refactor.Refactor(self.project_root, filename) self.assertTrue(r._is_on_symbol(offset)) def test_should_not_find_weird_places(self): filename, offset = self.create_file("test.py", "hello = _|_ 1 + 1") r = refactor.Refactor(self.project_root, filename) self.assertFalse(r._is_on_symbol(offset)) @unittest.skipIf(not refactor.ROPE_AVAILABLE, "Requires Rope") class TestFromsToImports(RefactorTestCase): def test_should_refactor(self): filename, offset = self.create_file( "foo.py", """\ _|_from datetime import datetime d = datetime(2013, 4, 7) """) ref = refactor.Refactor(self.project_root, filename) (change,) = ref.get_changes("refactor_froms_to_imports", offset) self.assertEqual(change['action'], 'change') self.assertEqual(change['file'], filename) self.assertSourceEqual(change['contents'], """\ import datetime d = datetime.datetime(2013, 4, 7) """) @unittest.skipIf(not refactor.ROPE_AVAILABLE, "Requires Rope") class TestOrganizeImports(RefactorTestCase): def test_should_refactor(self): filename, offset = self.create_file( "foo.py", """\ import unittest, base64 import datetime, json obj = json.dumps(23) unittest.TestCase() """) ref = refactor.Refactor(self.project_root, filename) (change,) = ref.get_changes("refactor_organize_imports") self.assertEqual(change['action'], 'change') self.assertEqual(change['file'], filename) self.assertSourceEqual(change['contents'], """\ import json import unittest obj = json.dumps(23) unittest.TestCase() """) @unittest.skipIf(not refactor.ROPE_AVAILABLE, "Requires Rope") class TestModuleToPackage(RefactorTestCase): def test_should_refactor(self): filename, offset = self.create_file( "foo.py", "_|_import os\n") ref = refactor.Refactor(self.project_root, filename) changes = ref.refactor_module_to_package() a, b, c = changes # Not sure why the a change is there. It's a CHANGE that # changes nothing... self.assertEqual(a['diff'], '') self.assertEqual(b['action'], 'create') self.assertEqual(b['type'], 'directory') self.assertEqual(b['path'], os.path.join(self.project_root, "foo")) self.assertEqual(c['action'], 'move') self.assertEqual(c['type'], 'file') self.assertEqual(c['source'], os.path.join(self.project_root, "foo.py")) self.assertEqual(c['destination'], os.path.join(self.project_root, "foo/__init__.py")) @unittest.skipIf(not refactor.ROPE_AVAILABLE, "Requires Rope") class TestRenameAtPoint(RefactorTestCase): def test_should_refactor(self): filename, offset = self.create_file( "foo.py", """\ class Foo(object): def _|_foo(self): return 5 def bar(self): return self.foo() """) file2, offset2 = self.create_file( "bar.py", """\ import foo x = foo.Foo() x.foo()""") ref = refactor.Refactor(self.project_root, filename) first, second = ref.refactor_rename_at_point(offset, "frob", in_hierarchy=False, docs=False) if first['file'] == filename: a, b = first, second else: a, b = second, first self.assertEqual(a['action'], 'change') self.assertEqual(a['file'], filename) self.assertSourceEqual(a['contents'], """\ class Foo(object): def frob(self): return 5 def bar(self): return self.frob() """) self.assertEqual(b['action'], 'change') self.assertEqual(b['file'], file2) self.assertSourceEqual(b['contents'], """\ import foo x = foo.Foo() x.frob()""") def test_should_refactor_in_hierarchy(self): filename, offset = self.create_file( "foo.py", """\ class Foo(object): def _|_foo(self): return 5 def bar(self): return self.foo() class Bar(Foo): def foo(self): return 42 class Baz(object): def foo(self): return 42 """) file2, offset2 = self.create_file( "bar.py", """\ import foo x, y, z = foo.Foo(), foo.Bar(), foo.Baz() x.foo() y.foo() z.foo()""") ref = refactor.Refactor(self.project_root, filename) first, second = ref.refactor_rename_at_point(offset, "frob", in_hierarchy=True, docs=False) if first['file'] == filename: a, b = first, second else: a, b = second, first self.assertEqual(a['action'], 'change') self.assertEqual(a['file'], filename) self.assertSourceEqual(a['contents'], """\ class Foo(object): def frob(self): return 5 def bar(self): return self.frob() class Bar(Foo): def frob(self): return 42 class Baz(object): def foo(self): return 42 """) self.assertEqual(b['action'], 'change') self.assertEqual(b['file'], file2) self.assertSourceEqual(b['contents'], """\ import foo x, y, z = foo.Foo(), foo.Bar(), foo.Baz() x.frob() y.frob() z.foo()""") def test_should_refactor_in_docstrings(self): filename, offset = self.create_file( "foo.py", """\ class Foo(object): "Frobnicate the foo" def _|_foo(self): return 5 print("I'm an unrelated foo") """) ref = refactor.Refactor(self.project_root, filename) (change,) = ref.refactor_rename_at_point(offset, "frob", in_hierarchy=False, docs=True) self.assertEqual(change['action'], 'change') self.assertEqual(change['file'], filename) self.assertSourceEqual(change['contents'], """\ class Foo(object): "Frobnicate the frob" def frob(self): return 5 print("I'm an unrelated foo") """) @unittest.skipIf(not refactor.ROPE_AVAILABLE, "Requires Rope") class TestRenameCurrentModule(RefactorTestCase): def test_should_refactor(self): filename, offset = self.create_file( "foo.py", "_|_import os\n") file2, offset = self.create_file( "bar.py", """\ _|_import foo foo.os """) dest = os.path.join(self.project_root, "frob.py") ref = refactor.Refactor(self.project_root, filename) a, b = ref.refactor_rename_current_module("frob") self.assertEqual(a['action'], 'change') self.assertEqual(a['file'], file2) self.assertEqual(a['contents'], "import frob\n" "frob.os\n") self.assertEqual(b['action'], 'move') self.assertEqual(b['type'], 'file') self.assertEqual(b['source'], filename) self.assertEqual(b['destination'], dest) @unittest.skipIf(not refactor.ROPE_AVAILABLE, "Requires Rope") class TestMoveModule(RefactorTestCase): def test_should_refactor(self): filename, offset = self.create_file( "foo.py", "_|_import os\n") file2, offset = self.create_file( "bar.py", """\ _|_import foo foo.os """) dest = os.path.join(self.project_root, "frob") os.mkdir(dest) with open(os.path.join(dest, "__init__.py"), "w") as f: f.write("") ref = refactor.Refactor(self.project_root, filename) a, b = ref.refactor_move_module(dest) self.assertEqual(a['action'], 'change') self.assertEqual(a['file'], file2) self.assertSourceEqual(a['contents'], """\ import frob.foo frob.foo.os """) self.assertEqual(b['action'], 'move') self.assertEqual(b['type'], 'file') self.assertEqual(b['source'], filename) self.assertEqual(b['destination'], os.path.join(dest, "foo.py")) @unittest.skipIf(not refactor.ROPE_AVAILABLE, "Requires Rope") class TestCreateInline(RefactorTestCase): def setUp(self): super(TestCreateInline, self).setUp() self.filename, self.offset = self.create_file( "foo.py", """\ def add(a, b): return a + b x = _|_add(2, 3) y = add(17, 4) """) def test_should_refactor_single_occurrenc(self): ref = refactor.Refactor(self.project_root, self.filename) (change,) = ref.refactor_create_inline(self.offset, True) self.assertEqual(change['action'], 'change') self.assertEqual(change['file'], self.filename) self.assertSourceEqual(change['contents'], """\ def add(a, b): return a + b x = 2 + 3 y = add(17, 4) """) def test_should_refactor_all_occurrencs(self): ref = refactor.Refactor(self.project_root, self.filename) (change,) = ref.refactor_create_inline(self.offset, False) self.assertEqual(change['action'], 'change') self.assertEqual(change['file'], self.filename) self.assertSourceEqual(change['contents'], """\ x = 2 + 3 y = 17 + 4 """) @unittest.skipIf(not refactor.ROPE_AVAILABLE, "Requires Rope") class TestExtractMethod(RefactorTestCase): def setUp(self): super(TestExtractMethod, self).setUp() self.filename, self.offset = self.create_file( "foo.py", """\ class Foo(object): def spaghetti(self, a, b): _|_x = a + 5 y = b + 23 return y """) @unittest.skipIf(sys.version_info >= (3, 5), "Python 3.5 not supported") def test_should_refactor_local(self): ref = refactor.Refactor(self.project_root, self.filename) (change,) = ref.refactor_extract_method(self.offset, 104, "calc", False) self.assertEqual(change['action'], 'change') self.assertEqual(change['file'], self.filename) expected = """\ class Foo(object): def spaghetti(self, a, b): return self.calc(a, b) def calc(self, a, b): x = a + 5 y = b + 23 return y """ expected2 = expected.replace("return self.calc(a, b)", "return self.calc(b, a)") expected2 = expected2.replace("def calc(self, a, b)", "def calc(self, b, a)") # This is silly, but it's what we got. if change['contents'] == dedent(expected2): self.assertSourceEqual(change['contents'], expected2) else: self.assertSourceEqual(change['contents'], expected) @unittest.skipIf(sys.version_info >= (3, 5), "Python 3.5 not supported") def test_should_refactor_global(self): ref = refactor.Refactor(self.project_root, self.filename) (change,) = ref.refactor_extract_method(self.offset, 104, "calc", True) self.assertEqual(change['action'], 'change') self.assertEqual(change['file'], self.filename) expected = """\ class Foo(object): def spaghetti(self, a, b): return calc(a, b) def calc(a, b): x = a + 5 y = b + 23 return y """ expected2 = expected.replace("return calc(a, b)", "return calc(b, a)") expected2 = expected2.replace("def calc(a, b)", "def calc(b, a)") if change['contents'] == dedent(expected2): self.assertSourceEqual(change['contents'], expected2) else: self.assertSourceEqual(change['contents'], expected) @unittest.skipIf(not refactor.ROPE_AVAILABLE, "Requires Rope") class TestUseFunction(RefactorTestCase): def test_should_refactor(self): filename, offset = self.create_file( "foo.py", """\ def _|_add_and_multiply(a, b, c): temp = a + b return temp * c f = 1 + 2 g = f * 3 """) ref = refactor.Refactor(self.project_root, filename) (change,) = ref.refactor_use_function(offset) self.assertEqual(change['action'], 'change') self.assertEqual(change['file'], filename) self.assertSourceEqual(change['contents'], """\ def add_and_multiply(a, b, c): temp = a + b return temp * c g = add_and_multiply(1, 2, 3) """)