Skip to content

Commit 21d3187

Browse files
committed
Define functions extracted from a class body before the class
Extract Method on code in a class body inserted the new function after the class, but class bodies run at definition time, so the class raised NameError. Insert the function before the (outermost) class instead, and collect class-body variables so they are passed as arguments. Fixes #825
1 parent e1f2cc2 commit 21d3187

3 files changed

Lines changed: 186 additions & 3 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- #623, #819, #863 Support MatchOr, MatchSequence, MatchStar (@jheld, @lieryan)
99
- #870 Add default implementation for is_dir() (@lieryan)
1010
- #872 Fix unicode handling in patchedast (@lieryan)
11+
- #825 Define functions extracted from a class body before the class (@ethanstoner)
1112

1213
# Release 1.14.0
1314

‎rope/refactor/extract.py‎

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,27 @@ def global_(self):
206206

207207
@property
208208
def method(self):
209-
return self.scope.parent is not None and self.scope.parent.get_kind() == "Class"
209+
return (
210+
self.scope.get_kind() == "Function"
211+
and self.scope.parent is not None
212+
and self.scope.parent.get_kind() == "Class"
213+
)
214+
215+
@property
216+
def class_to_define_before(self):
217+
"""The class the extracted function must be defined before, if any
218+
219+
Class bodies run when the class is defined, so a function extracted
220+
from a class body has to be defined before the (outermost) class.
221+
"""
222+
if self.scope.get_kind() != "Class":
223+
return None
224+
scope = self.scope
225+
while scope.parent.get_kind() == "Class":
226+
scope = scope.parent
227+
if self.make_global and scope.parent.get_kind() != "Module":
228+
return None
229+
return scope
210230

211231
@property
212232
def indents(self):
@@ -400,6 +420,8 @@ def __init__(self, info, matched_lines):
400420
def find_lineno(self):
401421
if self.info.variable and not self.info.make_global:
402422
return self._get_before_line()
423+
if self.info.class_to_define_before is not None:
424+
return self._get_before_class()
403425
if self.info.global_:
404426
toplevel = self._find_toplevel(self.info.scope)
405427
ast = self.info.pymodule.get_ast()
@@ -420,6 +442,10 @@ def _find_toplevel(self, scope):
420442
def find_indents(self):
421443
if self.info.variable and not self.info.make_global:
422444
return sourceutils.get_indents(self.info.lines, self._get_before_line())
445+
if self.info.class_to_define_before is not None:
446+
return sourceutils.get_indents(
447+
self.info.lines, self.info.class_to_define_before.get_start()
448+
)
423449
else:
424450
if self.info.global_ or self.info.make_global:
425451
return 0
@@ -432,6 +458,10 @@ def _get_before_line(self):
432458
def _get_after_scope(self):
433459
return self.info.scope.get_end() + 1
434460

461+
def _get_before_class(self):
462+
node = self.info.class_to_define_before.pyobject.get_ast()
463+
return min([node.lineno] + [d.lineno for d in node.decorator_list])
464+
435465

436466
class _ExceptionalConditionChecker:
437467
def __call__(self, info):
@@ -554,7 +584,7 @@ def _extracting_from_classmethod(self):
554584
return self.info.method and _get_function_kind(self.info.scope) == "classmethod"
555585

556586
def get_definition(self):
557-
if self.info.global_:
587+
if self.info.global_ or self.info.class_to_define_before is not None:
558588
return "\n%s\n" % self._get_function_definition()
559589
else:
560590
return "\n%s" % self._get_function_definition()
@@ -900,7 +930,12 @@ def _AugAssign(self, node):
900930
self.visit(node.target)
901931

902932
def _ClassDef(self, node):
903-
self._written_variable(node.name, node.lineno)
933+
if not self.is_global and self.host_function:
934+
self.host_function = False
935+
for child in node.body:
936+
self.visit(child)
937+
else:
938+
self._written_variable(node.name, node.lineno)
904939

905940
def _ListComp(self, node):
906941
self._comp_exp(node)

‎ropetest/refactor/extracttest.py‎

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1758,6 +1758,153 @@ def new_func():
17581758
""")
17591759
self.assertEqual(expected, refactored)
17601760

1761+
def test_extract_method_in_class_body(self):
1762+
code = dedent("""\
1763+
class TSV:
1764+
delimiter = "\\t"
1765+
1766+
print(TSV.delimiter)
1767+
""")
1768+
start = code.index('"\\t"')
1769+
end = start + len('"\\t"')
1770+
refactored = self.do_extract_method(code, start, end, "extracted")
1771+
expected = dedent("""\
1772+
1773+
def extracted():
1774+
return "\\t"
1775+
1776+
class TSV:
1777+
delimiter = extracted()
1778+
1779+
print(TSV.delimiter)
1780+
""")
1781+
self.assertEqual(expected, refactored)
1782+
1783+
def test_extract_method_in_class_body_reading_class_variables(self):
1784+
code = dedent("""\
1785+
class A:
1786+
a = 1
1787+
b = a + 2
1788+
""")
1789+
start = code.index("a + 2")
1790+
end = start + len("a + 2")
1791+
refactored = self.do_extract_method(code, start, end, "extracted")
1792+
expected = dedent("""\
1793+
1794+
def extracted(a):
1795+
return a + 2
1796+
1797+
class A:
1798+
a = 1
1799+
b = extracted(a)
1800+
""")
1801+
self.assertEqual(expected, refactored)
1802+
1803+
def test_extract_method_in_decorated_class_body(self):
1804+
code = dedent("""\
1805+
@decorator
1806+
class A:
1807+
a = 1 + 2
1808+
""")
1809+
start = code.index("1 + 2")
1810+
end = start + len("1 + 2")
1811+
refactored = self.do_extract_method(code, start, end, "extracted")
1812+
expected = dedent("""\
1813+
1814+
def extracted():
1815+
return 1 + 2
1816+
1817+
@decorator
1818+
class A:
1819+
a = extracted()
1820+
""")
1821+
self.assertEqual(expected, refactored)
1822+
1823+
def test_extract_method_in_class_body_inside_function(self):
1824+
code = dedent("""\
1825+
def f():
1826+
class A:
1827+
a = 1 + 2
1828+
return A
1829+
""")
1830+
start = code.index("1 + 2")
1831+
end = start + len("1 + 2")
1832+
refactored = self.do_extract_method(code, start, end, "extracted")
1833+
expected = dedent("""\
1834+
def f():
1835+
1836+
def extracted():
1837+
return 1 + 2
1838+
1839+
class A:
1840+
a = extracted()
1841+
return A
1842+
""")
1843+
self.assertEqual(expected, refactored)
1844+
1845+
def test_extract_method_in_nested_class_body(self):
1846+
code = dedent("""\
1847+
class A:
1848+
class B:
1849+
a = 1 + 2
1850+
""")
1851+
start = code.index("1 + 2")
1852+
end = start + len("1 + 2")
1853+
refactored = self.do_extract_method(code, start, end, "extracted")
1854+
expected = dedent("""\
1855+
1856+
def extracted():
1857+
return 1 + 2
1858+
1859+
class A:
1860+
class B:
1861+
a = extracted()
1862+
""")
1863+
self.assertEqual(expected, refactored)
1864+
1865+
def test_global_extract_method_in_class_body(self):
1866+
code = dedent("""\
1867+
def f():
1868+
class A:
1869+
a = 1 + 2
1870+
return A
1871+
""")
1872+
start = code.index("1 + 2")
1873+
end = start + len("1 + 2")
1874+
refactored = self.do_extract_method(
1875+
code, start, end, "extracted", global_=True
1876+
)
1877+
expected = dedent("""\
1878+
def f():
1879+
class A:
1880+
a = extracted()
1881+
return A
1882+
1883+
def extracted():
1884+
return 1 + 2
1885+
""")
1886+
self.assertEqual(expected, refactored)
1887+
1888+
def test_global_extract_method_in_module_level_class_body(self):
1889+
code = dedent("""\
1890+
class A:
1891+
a = 1 + 2
1892+
""")
1893+
start = code.index("1 + 2")
1894+
end = start + len("1 + 2")
1895+
refactored = self.do_extract_method(
1896+
code, start, end, "extracted", global_=True
1897+
)
1898+
expected = dedent("""\
1899+
1900+
def extracted():
1901+
return 1 + 2
1902+
1903+
class A:
1904+
a = extracted()
1905+
""")
1906+
self.assertEqual(expected, refactored)
1907+
17611908
def test_where_to_search_when_extracting_global_names(self):
17621909
code = dedent("""\
17631910
def a():

0 commit comments

Comments
 (0)