|
|
- """Refactoring methods for elpy.
-
- This interfaces directly with rope, regardless of the backend used,
- because the other backends don't really offer refactoring choices.
- Once Jedi is similarly featureful as Rope we can try and offer both.
-
-
- # Too complex:
-
- - Restructure: Interesting, but too complex, and needs deep Rope
- knowledge to do well.
-
- - ChangeSignature: Slightly less complex interface, but still to
- complex, requiring a large effort for the benefit.
-
-
- # Too useless:
-
- I could not get these to work in any useful fashion. I might be doing
- something wrong.
-
- - ExtractVariable does not replace the code extracted with the
- variable, making it a glorified copy&paste function. Emacs can do
- better than this interface by itself.
-
- - EncapsulateField: Getter/setter methods are outdated, this should be
- using properties.
-
- - IntroduceFactory: Inserts a trivial method to the current class.
- Cute.
-
- - IntroduceParameter: Introduces a parameter correctly, but does not
- replace the old code with the parameter. So it just edits the
- argument list and adds a shiny default.
-
- - LocalToField: Seems to just add "self." in front of all occurrences
- of a variable in the local scope.
-
- - MethodObject: This turns the current method into a callable
- class/object. Not sure what that would be good for.
-
-
- # Can't even get to work:
-
- - ImportOrganizer expand_star_imports, handle_long_imports,
- relatives_to_absolutes: Seem not to do anything.
-
- - create_move: I was not able to figure out what it would like to see
- as its attrib argument.
-
- """
-
- import os
-
- from elpy.rpc import Fault
-
- try:
- from rope.base.exceptions import RefactoringError
- from rope.base.project import Project
- from rope.base.libutils import path_to_resource
- from rope.base import change as rope_change
- from rope.base import worder
- from rope.refactor.importutils import ImportOrganizer
- from rope.refactor.topackage import ModuleToPackage
- from rope.refactor.rename import Rename
- from rope.refactor.move import create_move
- from rope.refactor.inline import create_inline
- from rope.refactor.extract import ExtractMethod
- from rope.refactor.usefunction import UseFunction
- ROPE_AVAILABLE = True
- except ImportError:
- ROPE_AVAILABLE = False
-
-
- def options(description, **kwargs):
- """Decorator to set some options on a method."""
- def set_notes(function):
- function.refactor_notes = {'name': function.__name__,
- 'category': "Miscellaneous",
- 'description': description,
- 'doc': getattr(function, '__doc__',
- ''),
- 'args': []}
- function.refactor_notes.update(kwargs)
- return function
- return set_notes
-
-
- class Refactor(object):
- """The main refactoring interface.
-
- Once initialized, the first call should be to get_refactor_options
- to get a list of refactoring options at a given position. The
- returned value will also list any additional options required.
-
- Once you picked one, you can call get_changes to get the actual
- refactoring changes.
-
- """
- def __init__(self, project_root, filename):
- self.project_root = project_root
- if not ROPE_AVAILABLE:
- raise Fault('rope not installed, cannot refactor code.',
- code=400)
- if not os.path.exists(project_root):
- raise Fault(
- "cannot do refactoring without a local project root",
- code=400
- )
- self.project = Project(project_root, ropefolder=None)
- self.resource = path_to_resource(self.project, filename)
-
- def get_refactor_options(self, start, end=None):
- """Return a list of options for refactoring at the given position.
-
- If `end` is also given, refactoring on a region is assumed.
-
- Each option is a dictionary of key/value pairs. The value of
- the key 'name' is the one to be used for get_changes.
-
- The key 'args' contains a list of additional arguments
- required for get_changes.
-
- """
- result = []
- for symbol in dir(self):
- if not symbol.startswith("refactor_"):
- continue
- method = getattr(self, symbol)
- if not method.refactor_notes.get('available', True):
- continue
- category = method.refactor_notes['category']
- if end is not None and category != 'Region':
- continue
- if end is None and category == 'Region':
- continue
- is_on_symbol = self._is_on_symbol(start)
- if not is_on_symbol and category in ('Symbol', 'Method'):
- continue
- requires_import = method.refactor_notes.get('only_on_imports',
- False)
- if requires_import and not self._is_on_import_statement(start):
- continue
- result.append(method.refactor_notes)
- return result
-
- def _is_on_import_statement(self, offset):
- "Does this offset point to an import statement?"
- data = self.resource.read()
- bol = data.rfind("\n", 0, offset) + 1
- eol = data.find("\n", 0, bol)
- if eol == -1:
- eol = len(data)
- line = data[bol:eol]
- line = line.strip()
- if line.startswith("import ") or line.startswith("from "):
- return True
- else:
- return False
-
- def _is_on_symbol(self, offset):
- "Is this offset on a symbol?"
- if not ROPE_AVAILABLE:
- return False
- data = self.resource.read()
- if offset >= len(data):
- return False
- if data[offset] != '_' and not data[offset].isalnum():
- return False
- word = worder.get_name_at(self.resource, offset)
- if word:
- return True
- else:
- return False
-
- def get_changes(self, name, *args):
- """Return a list of changes for the named refactoring action.
-
- Changes are dictionaries describing a single action to be
- taken for the refactoring to be successful.
-
- A change has an action and possibly a type. In the description
- below, the action is before the slash and the type after it.
-
- change: Change file contents
- - file: The path to the file to change
- - contents: The new contents for the file
- - Diff: A unified diff showing the changes introduced
-
- create/file: Create a new file
- - file: The file to create
-
- create/directory: Create a new directory
- - path: The directory to create
-
- move/file: Rename a file
- - source: The path to the source file
- - destination: The path to the destination file name
-
- move/directory: Rename a directory
- - source: The path to the source directory
- - destination: The path to the destination directory name
-
- delete/file: Delete a file
- - file: The file to delete
-
- delete/directory: Delete a directory
- - path: The directory to delete
-
- """
- if not name.startswith("refactor_"):
- raise ValueError("Bad refactoring name {0}".format(name))
- method = getattr(self, name)
- if not method.refactor_notes.get('available', True):
- raise RuntimeError("Method not available")
- return method(*args)
-
- @options("Convert from x import y to import x.y as y", category="Imports",
- args=[("offset", "offset", None)],
- only_on_imports=True,
- available=ROPE_AVAILABLE)
- def refactor_froms_to_imports(self, offset):
- """Converting imports of the form "from ..." to "import ..."."""
- refactor = ImportOrganizer(self.project)
- changes = refactor.froms_to_imports(self.resource, offset)
- return translate_changes(changes)
-
- @options("Reorganize and clean up", category="Imports",
- available=ROPE_AVAILABLE)
- def refactor_organize_imports(self):
- """Clean up and organize imports."""
- refactor = ImportOrganizer(self.project)
- changes = refactor.organize_imports(self.resource)
- return translate_changes(changes)
-
- @options("Convert the current module into a package", category="Module",
- available=ROPE_AVAILABLE)
- def refactor_module_to_package(self):
- """Convert the current module into a package."""
- refactor = ModuleToPackage(self.project, self.resource)
- return self._get_changes(refactor)
-
- @options("Rename symbol at point", category="Symbol",
- args=[("offset", "offset", None),
- ("new_name", "string", "Rename to: "),
- ("in_hierarchy", "boolean",
- "Rename in super-/subclasses as well? "),
- ("docs", "boolean",
- "Replace occurences in docs and strings? ")
- ],
- available=ROPE_AVAILABLE)
- def refactor_rename_at_point(self, offset, new_name, in_hierarchy, docs):
- """Rename the symbol at point."""
- try:
- refactor = Rename(self.project, self.resource, offset)
- except RefactoringError as e:
- raise Fault(str(e), code=400)
- return self._get_changes(refactor, new_name,
- in_hierarchy=in_hierarchy, docs=docs)
-
- @options("Rename current module", category="Module",
- args=[("new_name", "string", "Rename to: ")],
- available=ROPE_AVAILABLE)
- def refactor_rename_current_module(self, new_name):
- """Rename the current module."""
- refactor = Rename(self.project, self.resource, None)
- return self._get_changes(refactor, new_name)
-
- @options("Move the current module to a different package",
- category="Module",
- args=[("new_name", "directory", "Destination package: ")],
- available=ROPE_AVAILABLE)
- def refactor_move_module(self, new_name):
- """Move the current module."""
- refactor = create_move(self.project, self.resource)
- resource = path_to_resource(self.project, new_name)
- return self._get_changes(refactor, resource)
-
- @options("Inline function call at point", category="Symbol",
- args=[("offset", "offset", None),
- ("only_this", "boolean", "Only this occurrence? ")],
- available=ROPE_AVAILABLE)
- def refactor_create_inline(self, offset, only_this):
- """Inline the function call at point."""
- refactor = create_inline(self.project, self.resource, offset)
- if only_this:
- return self._get_changes(refactor, remove=False, only_current=True)
- else:
- return self._get_changes(refactor, remove=True, only_current=False)
-
- @options("Extract current region as a method", category="Region",
- args=[("start", "start_offset", None),
- ("end", "end_offset", None),
- ("name", "string", "Method name: "),
- ("make_global", "boolean", "Create global method? ")],
- available=ROPE_AVAILABLE)
- def refactor_extract_method(self, start, end, name,
- make_global):
- """Extract region as a method."""
- refactor = ExtractMethod(self.project, self.resource, start, end)
- return self._get_changes(
- refactor, name, similar=True, global_=make_global
- )
-
- @options("Use the function at point wherever possible", category="Method",
- args=[("offset", "offset", None)],
- available=ROPE_AVAILABLE)
- def refactor_use_function(self, offset):
- """Use the function at point wherever possible."""
- try:
- refactor = UseFunction(self.project, self.resource, offset)
- except RefactoringError as e:
- raise Fault(
- 'Refactoring error: {}'.format(e),
- code=400
- )
- return self._get_changes(refactor)
-
- def _get_changes(self, refactor, *args, **kwargs):
- try:
- changes = refactor.get_changes(*args, **kwargs)
- except Exception as e:
- raise Fault("Error during refactoring: {}".format(e),
- code=400)
- return translate_changes(changes)
-
-
- def translate_changes(initial_change):
- """Translate rope.base.change.Change instances to dictionaries.
-
- See Refactor.get_changes for an explanation of the resulting
- dictionary.
-
- """
- agenda = [initial_change]
- result = []
- while agenda:
- change = agenda.pop(0)
- if isinstance(change, rope_change.ChangeSet):
- agenda.extend(change.changes)
- elif isinstance(change, rope_change.ChangeContents):
- result.append({'action': 'change',
- 'file': change.resource.real_path,
- 'contents': change.new_contents,
- 'diff': change.get_description()})
- elif isinstance(change, rope_change.CreateFile):
- result.append({'action': 'create',
- 'type': 'file',
- 'file': change.resource.real_path})
- elif isinstance(change, rope_change.CreateFolder):
- result.append({'action': 'create',
- 'type': 'directory',
- 'path': change.resource.real_path})
- elif isinstance(change, rope_change.MoveResource):
- result.append({'action': 'move',
- 'type': ('directory'
- if change.new_resource.is_folder()
- else 'file'),
- 'source': change.resource.real_path,
- 'destination': change.new_resource.real_path})
- elif isinstance(change, rope_change.RemoveResource):
- if change.resource.is_folder():
- result.append({'action': 'delete',
- 'type': 'directory',
- 'path': change.resource.real_path})
- else:
- result.append({'action': 'delete',
- 'type': 'file',
- 'file': change.resource.real_path})
- return result
-
-
- class FakeResource(object):
- """A fake resource in case Rope is absence."""
-
- def __init__(self, filename):
- self.real_path = filename
-
- def read(self):
- with open(self.real_path) as f:
- return f.read()
|