Skip to content

Latest commit

 

History

History
886 lines (586 loc) · 26.1 KB

library.rst

File metadata and controls

886 lines (586 loc) · 26.1 KB

Using Rope As A Library

If you need other features, send a feature request. Have a look at contributing.rst.

This section will help you get started as soon as possible.

The first thing you should do is make a project:

import rope.base.project


myproject = rope.base.project.Project('/path/to/myproject')

It's good to know that:

  • A project is a folder in the file-system.
  • It can contain anything.
  • Rope searches for python modules and packages inside a project when needed.
  • Refactorings only change files and folders inside the project that has been passed to them.
  • Out of project modules that are imported from a module inside a project are handled but never changed by refactorings.
  • Rope makes a rope folder inside projects. By default the name of this folder is .ropeproject, but that can be changed using the constructor's ropefolder parameter. Passing None prevents rope from making this folder.
  • Rope uses the .ropeproject folder for things like saving object information and loading project configurations.
  • Project preferences can be configured by passing options to the constructor or in .ropeproject/config.py. See the default config.py, rope.base.default_config module, for more information.
  • All configurations that are available in the config.py file can be specified as keyword parameters to the Project constructor. These parameters override the ones in the config.py file.
  • Each project has a set of ignored resource patterns. You can use it to tell rope to ignore files and folders matching certain patterns.
  • The .ropeproject folder can be safely copied in other clones of a project if you don't want to lose your objectdb and history.

The rope.base.libutils module provides tools that make using rope as a library easier. We'll talk more about this module later.

In rope, files and folders in a project are accessed through rope.base.resources.Resource objects. It has two subclasses File and Folder. What we care about is that refactorings and Changes (we'll talk about them later) use resources.

There are two options for creating a Resource for a path in a project. The first approach uses the `Project.get_resource()`_ method.

from rope.base import project


myresource = myproject.get_resource('/path/to/resource')

However, it's preferable to use the libutils.path_to_resource() function, because it's more flexible and offers a unified way to create resources. It takes a project and path as parameters with an optional type. The type parameter, with values file or folder, can create a resource for an object that doesn't exist yet.

from rope.base import libutils


myresource = libutils.path_to_resource(myproject, '/path/to/resource')

Consider we have a resource. How can we know anything about it? The answer is to use its path and real_path attributes. Resource.real_path is the absolute path of the resource in the file-system. The Resource.path attribute contains the address of a resource relative to the project's root.

As a short example of performing refactorings, we'll show how to extract a variable from a file. First we need the Resource object that points to a file in a project:

resource = libutils.path_to_resource(myproject, '/path/to/my/module.py')

Now we can make our Refactoring class:

from rope.refactor.extract import ExtractVariable


extractor = ExtractVariable(myproject, resource, start, end)

Where start and end are the offsets of the region to extract in resource. Be careful when calculating the offsets. DOS line-endings and multi-byte characters are considered to be one character. This is actually easier for IDEs, since most GUI libraries handle those cases for you.

Next, the IDE ask the user to configure refactoring options, like specifying the name of the extracted variable.

After that, we can calculate the changes:

changes = extractor.get_changes('extracted_variable')

Each refactoring returns a rope.base.change.Change object that holds the changes it made. Calculating those changes can be time consuming. See the rope.base.taskhandle.TaskHandle section for measuring its progress or interrupting it.

As mentioned in the last section each refactoring returns a rope.base.change.Change object. Now how can we know what it contains and how to perform it?

Previewing:

You can use changes.get_description() to get a preview. It is useful when you don't care much about the format. Otherwise you can use the changes object directly. See the documentation in rope.base.change module.

Performing:

The easiest way for performing the refactoring is to use the Project.do() method:

myproject.do(changes)

If you want to perform the changes yourself, you have two options. Note that the main reason for performing the changes manually is handling version control systems that are not supported by rope.

  1. The first approach is to use rope.base.fscommands (see Writing A FileSystemCommands). The changes can be performed as before using Project.do().
  2. The second approach is to perform the changes manually based on the returned changes object (again see the documentation in rope.base.change module). If this approach is used you cannot undo the refactoring using project.history.undo().

Updating Open Buffers In IDEs:

Usually editors need to reload the files changed by rope. You can use Change.get_changed_resources() to get the list of resources that need to be reloaded.

When using rope as a library, you probably change the files in it in parallel (for example in IDEs). To force rope to invalidate cached information about resources that have been removed or changed outside rope, you should call the Project.validate() method. You can pass a resource to this method. For example:

myproject.validate()

This validates all files and directories in the project. Call this function every time you want use rope (i.e., before performing refactorings).

One of the greatest strengths of rope is its static object analysis (SOA). You can perform SOA on a module using the PyCore.analyze_module() method. However, performing SOA on a module is not cheap. The best time for performing SOA is when saving files. And it should be performed only on changed scopes.

But because rope is not notified about the changes the IDE performs, you need to tell rope about the change. You can do so by using rope.base.libutils.report_change(). Whenever you want to change a module you can do something like:

# Do the writing.
old_contents = resource.read()
resource.write(new_content)

# Inform rope about the change.
libutils.report_change(myproject, path, old_contents)

Project.close() closes a project's open resources. Always call this function when you don't need a project anymore:

myproject.close()

The rope.base.libutils module contains functions that make life easier for building refactoring tools. In some cases, the functions offer a unified way to access or create objects. You're encouraged to use rope.base.libutils functions whenever possible, because the APIs here may not be as volatile as class methods.

Perform static object analysis on all Python files in the project. Note, this may be a very time consuming task.

libutils.analyze_modules(myproject)

Returns a rope.base.pyobjects.PyModule object for the code string. An optional resource argument can be specified for the resource this code is associated with. If force_errors` is ``True, then rope.base.exceptions.ModuleSyntaxError is raised when the code has syntax errors. Otherwise, syntax errors are silently ignored. Note that force_errors overrides the ignore_syntax_errors project configuration flag.

pymodule = libutils.get_string_module(myproject, source)

Get the rope.base.pyscopes.GlobalScope object for the code string. This is the outermost scope of the code encompassing the whole module.

scope = libutils.get_string_scope(myproject, source)

Returns True if the resource is a Python file.

libutils.is_python_file(myproject, resource)

Retrieves the dotted path string to the module that contains that given resource.

# If resource is 'path/to/resource.py' relative to the project's root
# directory, this returns the string: 'path.to.resource'.
module_name = libutils.modname(resource)

Retrieve the path relative to the project's root directory.

# Get the path relative to the project's root directory.
relpath = libutils.relative(myproject.address, path)

Get the resource --- a file or folder --- at the given path. An optional type argument can be used if the resource doesn't yet exist. The values for type are the strings 'file' or 'folder'.

# Resource for an existing file.
myfile = libutils.path_to_resource(myproject, '/path/to/file.py')

# Resource for a non-existing folder.
new_folder = libutils.path_to_resource(myproject, '/path/to/folder', type='folder')

Report to all a project's rope.base.resourceobserver.ResourceObservers that the contents of the file at path were changed. The new contents of the file are retrieved by reading the file. This function also reanalyzes the module to collect information about function calls that may have been changed.

libutils.report_change(myproject, '/path/to/file.py', old_contents)

You can create a project by:

project = Project(root_address)

Where the root_address is the root folder of your project.

A project has some useful attributes. Project.address is the address of the root folder of a project. Project.root is a Folder object that points to that folder.

Used to commit changes returned by refactorings:

project.do(changes)

A rope.base.history.History object. You can use its undo and redo methods for undoing or redoing changes. Note that you can use this only if you have committed your changes using rope.

When using rope as a library, you will probably change the files in that project in parallel (for example in IDEs). To force rope to invalidate cached information about resources that have been removed or changed outside rope, you should call Project.validate. You should pass a resource to this method. For example:

project.validate(project.root)

This validates all files and directories in the project.

Closes a project's open resources. Always call this function when you don't need a project anymore. Currently it closes the files used for storing object information and project history. Because some parts of these files are in memory for efficiency, not closing a project might put them in an inconsistent state.

The rope.base.fscommands module implements basic file system operations that rope needs to perform. The main reason for the existence of this module is supporting version control systems. Have a look at FileSystemCommands and SubversionCommands in the same module. If you need other version control systems you can write a new class that provides this interface. rope.base.project.Project accepts an fscommands argument. You can use this argument to force rope to use your new class.

Since version 0.5, rope makes a .ropeproject folder in the project by default for saving project configurations and data. The name of this folder is passed to the constructor if you want to change that. You can force rope not to make such a folder by passing None.

If such a folder exists, rope loads the config.py file in that folder. It might also use it for storing object information and history.

Provides useful methods for managing python modules and packages. Each project has a PyCore that can be accessed using the Project.pycore attribute.

PyCore.run_module() runs a resource. When running, it collects type information to do dynamic object inference. For this reason modules run much slower.

Also Pycore.analyze_module() collects object information for a module. The collected information can be used to enhance rope's static object inference.

A TaskHandle can be used for stopping and monitoring the progress of time consuming tasks, like some refactorings. The Project.do() and Refactoring.get_changes() methods for most refactorings take a keyword parameter called task_handle. You can pass a TaskHandle object to them. A TaskHandle can be used for interrupting or observing a task.

Always pass task_handle as keyword argument. It will always be the last argument, and new arguments of the refactoring are added before it.

A task might consist of a few JobSets. Each JobSet performs a few jobs. For instance calculating the changes for renaming a method in a class hierarchy has two job sets: one to find the classes for constructing the class hierarchy and another to change the occurrences.

The TaskHandle.current_jobset() returns the most recent JobSet or None if none has been started. You can use the methods of JobSet for obtaining information about the current job. So you might want to do something like:

import rope.base.taskhandle


handle = rope.base.taskhandle.TaskHandle("Test Task")

def update_progress():
    jobset = handle.current_jobsets()
    if jobset:
        text = ''
        # getting current job set name
        if jobset.get_name() is not None:
            text += jobset.get_name()
        # getting active job name
        if jobset.get_active_job_name() is not None:
            text += ' : ' + jobset.get_active_job_name()
        # adding done percent
        percent = jobset.get_percent_done()
        if percent is not None:
            text += ' ... %s percent done' % percent
        print text

handle.add_observer(update_progress)

changes = renamer.get_changes('new_name', task_handle=handle)

Also you can use something like this for stopping the task:

def stop():
    handle.stop()

After calling stop(), the thread that is executing the task will be interrupted by a rope.base.exceptions.InterruptedTaskError exception.

Have a look at rope.refactor package and its sub-modules. For example for performing a move refactoring you can create a Move object like this:

mover = Move(project, resource, offset)

Where resource and offset is the location to perform the refactoring.

Then you can commit the changes by it using the get_changes() method:

project.do(mover.get_changes(destination))

Where the destination module/package is the destination resource for move refactoring. Other refactorings classes have a similar interface.

Here is the list of refactorings rope provides. (Note that this list might be out of date.) For more information about these refactoring see pydocs in their modules and the unit-tests in the ropetest/refactor/ folder.

  • rope.refactor.rename: Rename something in the project. See the example below.
  • rope.refactor.move: Move a python element in the project.
  • rope.refactor.restructure: Restructure code. See the example below.
  • rope.refactor.extract: Extract methods/variables.
  • rope.refactor.inline: Inline occurrences of a method/variable/parameter.
  • rope.refactor.usefunction: Try to use a function wherever possible.
  • rope.refactor.method_object: Transform a function or a method to a method object.
  • rope.refactor.change_signature: Change the signature of a function/method.
  • rope.refactor.introduce_factory: Introduce a factory for a class and changes all constructors to use it.
  • rope.refactor.introduce_parameter: Introduce a parameter in a function.
  • rope.refactor.encapsulate_field: Generate a getter/setter for a field and changes its occurrences to use them.
  • rope.refactor.localtofield: Change a local variable to field.
  • rope.refactor.topackage: Transform a module to a package with the same name.
  • rope.refactor.importutils: Perform actions like organize imports.

Some refactorings, restructure and find occurrences accept an argument called resources. If it is a list of Files, all other resources in the project are ignored and the refactoring only analyzes them. If it is None all python modules in the project will be analyzed. Using this parameter, IDEs can let the user limit the files on which a refactoring should be applied.

Using rename refactoring:

# Creating a project
>>> from rope.base.project import Project
>>> project = Project('.')

# Working with files to create a module
>>> mod1 = project.root.create_file('mod1.py')
>>> mod1.write('a_var = 10\n')

# Alternatively you can use `generate` module.
# Creating modules and packages using `generate` module
>>> from rope.contrib import generate
>>> pkg = generate.create_package(project, 'pkg')
>>> mod2 = generate.create_module(project, 'mod2', pkg)
>>> mod2.write('import mod1\nprint mod1.a_var\n')

# We can use `Project.find_module` for finding modules, too
>>> assert mod2 == project.find_module('pkg.mod2')

# Performing rename refactoring on `mod1.a_var`
>>> from rope.refactor.rename import Rename
>>> changes = Rename(project, mod1, 1).get_changes('new_var')
>>> project.do(changes)
>>> mod1.read()
u'new_var = 10\n'
>>> mod2.read()
u'import mod1\nprint mod1.new_var\n'

# Undoing rename refactoring
>>> project.history.undo()
...
>>> mod1.read()
u'a_var = 10\n'
>>> mod2.read()
u'import mod1\nprint mod1.a_var\n'

# Cleaning up
>>> pkg.remove()
>>> mod1.remove()
>>> project.close()

The example for replacing occurrences of our pow function to ** operator (see the restructuring section of overview.rst):

# Setting up the project
>>> from rope.base.project import Project
>>> project = Project('.')

>>> mod1 = project.root.create_file('mod1.py')
>>> mod1.write('def pow(x, y):\n    result = 1\n'
...            '    for i in range(y):\n        result *= x\n'
...            '    return result\n')
>>> mod2 = project.root.create_file('mod2.py')
>>> mod2.write('import mod1\nprint(mod1.pow(2, 3))\n')

>>> from rope.refactor import restructure

>>> pattern = '${pow_func}(${param1}, ${param2})'
>>> goal = '${param1} ** ${param2}'
>>> args = {'pow_func': 'name=mod1.pow'}

>>> restructuring = restructure.Restructure(project, pattern, goal, args)

>>> project.do(restructuring.get_changes())
>>> mod2.read()
u'import mod1\nprint(2 ** 3)\n'

# Cleaning up
>>> mod1.remove()
>>> mod2.remove()
>>> project.close()

See code documentation and test suites for more information.

The get_changes() method of refactoring classes return a rope.base.change.Change object. You perform these changes by calling Project.do(). But as explained above some IDEs need to perform the changes themselves.

Every change to the file-system in rope is commited using an object that provides a rope.base.fscommands.FileSystemCommands interface. As explained above in rope.base.fscommands section, rope uses this interface to handle different VCSs.

You can implement your own fscommands object:

class MyFileSystemCommands(object):

  def create_file(self, path):
      """Create a new file"""
      # ...

  def create_folder(self, path):
      """Create a new folder"""
      # ...

  def move(self, path, new_location):
      """Move resource at `path` to `new_location`"""
      # ...

  def remove(self, path):
      """Remove resource"""
      # ...

  def write(self, path, data):
      """Write `data` to file at `path`"""
      # ...

And you can create a project like this:

my_fscommands = MyFileSystemCommands()
project = rope.base.project.Project('~/myproject',
                                    fscommands=my_fscommands)

The rope.contrib package contains modules that use rope base parts and provide useful features. rope.contrib.codeassist module can be used in IDEs:

from rope.ide import codeassist


# Get the proposals; you might want to pass a Resource
proposals = codeassist.code_assist(project, source_code, offset)

# Sorting proposals; for changing the order see pydoc
proposals = codeassist.sorted_proposals(proposals)

# Where to insert the completions
starting_offset = codeassist.starting_offset(source_code, offset)

# Applying a proposal
proposal = proposals[x]
replacement = proposal.name

new_source_code = (source_code[:starting_offset] +
                   replacement + source_code[offset:])

maxfixes parameter of code_assist decides how many syntax errors to fix. The default value is one. For instance:

def f():
    g(my^

myvariable = None

def g(p):
    invalid syntax ...

will report myvariable, only if maxfixes is greater than 1.

later_locals, if True, forces rope to propose names that are defined later in current scope. It is True by default. For instance:

def f():
    my^
    myvariable = None

will not report myvariable, if later_locals is False.

See pydocs and source code for more information (other functions in this module might be interesting, too; like get_doc, get_definition_location).

findit module provides find_occurrences() for finding occurrences of a name. Also the find_implementations() function finds the places in which a method is overridden.

This module can be used to find the modules that provide a name. IDEs can use this module to auto-import names. AutoImport.get_modules() returns the list of modules with the given global name. AutoImport.import_assist() tries to find the modules that have a global name that starts with the given prefix.

rope.refactor.multiproject can be used to perform a refactoring across multiple projects.

Usually refactorings have a main project. That is the project that contains the definition of the changing python name. Other projects depend on the main one, and the uses of the changed name in them should be updated.

Each refactoring changes only one project (the project passed to its constructor). But we can use the MultiProjectRefactoring proxy to perform a refactoring on other projects, too.

First we need to create a multi-project refactoring constructor. As an example consider that we want to perform a rename refactoring:

from rope.refactor import multiproject, rename


CrossRename = multiproject.MultiProjectRefactoring(rename.Rename,
                                                   projects)

Here projects is the list of dependant projects. It does not include the main project. The first argument is the refactoring class (such as Rename) or factory function (like create_move).

Next we can construct the refactoring:

renamer = CrossRename(project, resource, offset)

We create the rename refactoring as we do for normal refactoings. Note that project is the main project.

As mentioned above, other projects use the main project. Rope automatically adds the main project to the python path of other projects.

Finally we can calculate the changes. But instead of calling get_changes() (which returns main project changes only), we can call get_all_changes() with the same arguments. It returns a list of (project, changes) tuples. You can perform them manually by calling project.do(changes) for each tuple, or use multiproject.perform():

project_and_changes = renamer.get_all_changes('newname')

multiproject.perform(project_and_changes)