@@ -22,21 +22,21 @@ def create_move(project, resource, offset=None): |
22 | 22 | return MoveModule(project, resource) |
23 | 23 | this_pymodule = project.get_pymodule(resource) |
24 | 24 | pyname = evaluate.eval_location(this_pymodule, offset) |
25 | | - if pyname is None: |
26 | | - raise exceptions.RefactoringError( |
27 | | - 'Move only works on classes, functions, modules and methods.') |
28 | | - pyobject = pyname.get_object() |
29 | | - if isinstance(pyobject, pyobjects.PyModule) or \ |
30 | | - isinstance(pyobject, pyobjects.PyPackage): |
31 | | - return MoveModule(project, pyobject.get_resource()) |
32 | | - if isinstance(pyobject, pyobjects.PyFunction) and \ |
33 | | - isinstance(pyobject.parent, pyobjects.PyClass): |
34 | | - return MoveMethod(project, resource, offset) |
35 | | - if isinstance(pyobject, pyobjects.PyDefinedObject) and \ |
36 | | - isinstance(pyobject.parent, pyobjects.PyModule): |
37 | | - return MoveGlobal(project, resource, offset) |
| 25 | + if pyname is not None: |
| 26 | + pyobject = pyname.get_object() |
| 27 | + if isinstance(pyobject, pyobjects.PyModule) or \ |
| 28 | + isinstance(pyobject, pyobjects.PyPackage): |
| 29 | + return MoveModule(project, pyobject.get_resource()) |
| 30 | + if isinstance(pyobject, pyobjects.PyFunction) and \ |
| 31 | + isinstance(pyobject.parent, pyobjects.PyClass): |
| 32 | + return MoveMethod(project, resource, offset) |
| 33 | + if isinstance(pyobject, pyobjects.PyDefinedObject) and \ |
| 34 | + isinstance(pyobject.parent, pyobjects.PyModule) or \ |
| 35 | + isinstance(pyname, pynames.AssignedName): |
| 36 | + return MoveGlobal(project, resource, offset) |
38 | 37 | raise exceptions.RefactoringError( |
39 | | - 'Move only works on global classes/functions, modules and methods.') |
| 38 | + 'Move only works on global classes/functions/variables, modules and ' |
| 39 | + 'methods.') |
40 | 40 |
|
41 | 41 |
|
42 | 42 | class MoveMethod(object): |
@@ -203,42 +203,63 @@ def __init__(self, project, resource, offset): |
203 | 203 | self.project = project |
204 | 204 | this_pymodule = self.project.get_pymodule(resource) |
205 | 205 | self.old_pyname = evaluate.eval_location(this_pymodule, offset) |
| 206 | + if self.old_pyname is None: |
| 207 | + raise exceptions.RefactoringError( |
| 208 | + 'Move refactoring should be performed on a ' |
| 209 | + 'class/function/variable.') |
| 210 | + if self._is_variable(self.old_pyname): |
| 211 | + self.old_name = worder.get_name_at(resource, offset) |
| 212 | + pymodule = this_pymodule |
| 213 | + else: |
| 214 | + self.old_name = self.old_pyname.get_object().get_name() |
| 215 | + pymodule = self.old_pyname.get_object().get_module() |
206 | 216 | self._check_exceptional_conditions() |
207 | | - self.old_name = self.old_pyname.get_object().get_name() |
208 | | - pymodule = self.old_pyname.get_object().get_module() |
209 | 217 | self.source = pymodule.get_resource() |
210 | 218 | self.tools = _MoveTools(self.project, self.source, |
211 | 219 | self.old_pyname, self.old_name) |
212 | 220 | self.import_tools = self.tools.import_tools |
213 | 221 |
|
214 | 222 | def _import_filter(self, stmt): |
215 | 223 | module_name = libutils.modname(self.source) |
| 224 | + |
216 | 225 | if isinstance(stmt.import_info, importutils.NormalImport): |
| 226 | + # Affect any statement that imports the source module |
217 | 227 | return any(module_name == name |
218 | 228 | for name, alias in stmt.import_info.names_and_aliases) |
219 | 229 | elif isinstance(stmt.import_info, importutils.FromImport): |
| 230 | + # Affect statements importing from the source package |
220 | 231 | if '.' in module_name: |
221 | | - package_name = '.'.join(module_name.split('.')[:-1]) |
222 | | - if stmt.import_info.module_name == package_name: |
| 232 | + package_name, basename = module_name.rsplit('.', 1) |
| 233 | + if (stmt.import_info.module_name == package_name and |
| 234 | + any(basename == name |
| 235 | + for name, alias in stmt.import_info.names_and_aliases)): |
223 | 236 | return True |
224 | 237 | return stmt.import_info.module_name == module_name |
225 | 238 | return False |
226 | 239 |
|
227 | 240 | def _check_exceptional_conditions(self): |
228 | | - if self.old_pyname is None or \ |
229 | | - not isinstance(self.old_pyname.get_object(), |
230 | | - pyobjects.PyDefinedObject): |
231 | | - raise exceptions.RefactoringError( |
232 | | - 'Move refactoring should be performed on a class/function.') |
233 | | - moving_pyobject = self.old_pyname.get_object() |
234 | | - if not self._is_global(moving_pyobject): |
235 | | - raise exceptions.RefactoringError( |
236 | | - 'Move refactoring should be performed ' + |
237 | | - 'on a global class/function.') |
| 241 | + if self._is_variable(self.old_pyname): |
| 242 | + pymodule = self.old_pyname.get_definition_location()[0] |
| 243 | + try: |
| 244 | + pymodule.get_scope().get_name(self.old_name) |
| 245 | + except exceptions.NameNotFoundError: |
| 246 | + self._raise_refactoring_error() |
| 247 | + elif not (isinstance(self.old_pyname.get_object(), |
| 248 | + pyobjects.PyDefinedObject) and |
| 249 | + self._is_global(self.old_pyname.get_object())): |
| 250 | + self._raise_refactoring_error() |
| 251 | + |
| 252 | + def _raise_refactoring_error(self): |
| 253 | + raise exceptions.RefactoringError( |
| 254 | + 'Move refactoring should be performed on a global class, function ' |
| 255 | + 'or variable.') |
238 | 256 |
|
239 | 257 | def _is_global(self, pyobject): |
240 | 258 | return pyobject.get_scope().parent == pyobject.get_module().get_scope() |
241 | 259 |
|
| 260 | + def _is_variable(self, pyname): |
| 261 | + return isinstance(pyname, pynames.AssignedName) |
| 262 | + |
242 | 263 | def get_changes(self, dest, resources=None, |
243 | 264 | task_handle=taskhandle.NullTaskHandle()): |
244 | 265 | if resources is None: |
@@ -367,9 +388,23 @@ def _get_moving_element(self): |
367 | 388 | def _get_moving_region(self): |
368 | 389 | pymodule = self.project.get_pymodule(self.source) |
369 | 390 | lines = pymodule.lines |
370 | | - scope = self.old_pyname.get_object().get_scope() |
371 | | - start = lines.get_line_start(scope.get_start()) |
372 | | - end_line = scope.get_end() |
| 391 | + if self._is_variable(self.old_pyname): |
| 392 | + logical_lines = pymodule.logical_lines |
| 393 | + lineno = logical_lines.logical_line_in( |
| 394 | + self.old_pyname.get_definition_location()[1])[0] |
| 395 | + start = lines.get_line_start(lineno) |
| 396 | + end_line = logical_lines.logical_line_in(lineno)[1] |
| 397 | + else: |
| 398 | + scope = self.old_pyname.get_object().get_scope() |
| 399 | + start = lines.get_line_start(scope.get_start()) |
| 400 | + end_line = scope.get_end() |
| 401 | + |
| 402 | + # Include comment lines before the definition |
| 403 | + start_line = lines.get_line_number(start) |
| 404 | + while start_line > 1 and lines.get_line(start_line - 1).startswith('#'): |
| 405 | + start_line -= 1 |
| 406 | + start = lines.get_line_start(start_line) |
| 407 | + |
373 | 408 | while end_line < lines.length() and \ |
374 | 409 | lines.get_line(end_line + 1).strip() == '': |
375 | 410 | end_line += 1 |
@@ -650,6 +685,17 @@ def _add_imports_to_module(import_tools, pymodule, new_imports): |
650 | 685 | def moving_code_with_imports(project, resource, source): |
651 | 686 | import_tools = importutils.ImportTools(project) |
652 | 687 | pymodule = libutils.get_string_module(project, source, resource) |
| 688 | + |
| 689 | + # Strip comment prefix, if any. These need to stay before the moving |
| 690 | + # section, but imports would be added between them. |
| 691 | + lines = codeanalyze.SourceLinesAdapter(source) |
| 692 | + start = 1 |
| 693 | + while start < lines.length() and lines.get_line(start).startswith('#'): |
| 694 | + start += 1 |
| 695 | + moving_prefix = source[:lines.get_line_start(start)] |
| 696 | + pymodule = libutils.get_string_module( |
| 697 | + project, source[lines.get_line_start(start):], resource) |
| 698 | + |
653 | 699 | origin = project.get_pymodule(resource) |
654 | 700 |
|
655 | 701 | imports = [] |
@@ -680,7 +726,9 @@ def moving_code_with_imports(project, resource, source): |
680 | 726 | lines = codeanalyze.SourceLinesAdapter(source) |
681 | 727 | while start < lines.length() and not lines.get_line(start).strip(): |
682 | 728 | start += 1 |
683 | | - moving = source[lines.get_line_start(start):] |
| 729 | + |
| 730 | + # Reinsert the prefix which was removed at the beginning |
| 731 | + moving = moving_prefix + source[lines.get_line_start(start):] |
684 | 732 | return moving, imports |
685 | 733 |
|
686 | 734 |
|
|