Klimi's new dotfiles with stow.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

381 lines
14 KiB

"""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()