PyDev Refactoring Beispiel

Im Folgenden soll ein kleines Beispiel für ein Refactoring in PyDev gegeben werden. Das Beispiel ist als externes Plugin realisiert. Da etliche Pakete aus den PyDev-plugins nicht exportiert werden, kann auf Teile der Funktionalität von PyDev nicht zugegriffen werden. Änderungen an PyDev selbst scheinen mir jedoch schon deshalb nicht einfach, weil es reichlich Fehlermeldungen nach dem Import der PyDev-Plugin-Projekte gibt (siehe Installation).

Das Insert-None-Refactoring

Ziel dieses Refactoring ist es, zu erkennen, ob der Cursor auf einem Namen steht und, wenn ja, in der Zeile davor ein neues Statement einzufügen, das diesem None zuweist. Diese Zuweisung ist nicht besonders nützlich, aber es handelt sich ja auch nur um ein Beispiel.

Das Beispiel ist nicht perfekt und es ist wahrscheinlich möglich, durch Anwendung des Refactorings fehlerhaften Code zu erzeugen, siehe #Probleme.

Um das Beispiel auszuprobieren, können das Eclipse-Projekt und das fertige Plugin heruntergeladen werden.

InsertNoneRefactoring

Diese Klasse übernimmt das eigentliche Refactoring. Wie mit dem Eclipse LTK üblich und wie in Eclipse Refactoring Framework beschrieben, werden zunächst checkInitialConditions(...) und checkFinalConditions(...) aufgerufen und danach in createChange die Änderungen berechnet.

Der wichtigste Teil ist checkInitialConditions(...). Hier wird mithilfe eines InsertNoneNameFinderVisitor der AST des Editors durchlaufen, um herauszufinden, ob der Cursor im Editor gerade auf einem Namen und dieser im passenden Kontext steht.

package feu.k01919.q7649347.pyrefactor.insertnone;
 
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.ltk.core.refactoring.Change;
import org.eclipse.ltk.core.refactoring.Refactoring;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;
import org.eclipse.ltk.core.refactoring.TextChange;
import org.eclipse.text.edits.InsertEdit;
import org.python.pydev.core.docutils.PySelection;
import org.python.pydev.editor.PyEdit;
import org.python.pydev.parser.jython.SimpleNode;
import org.python.pydev.parser.jython.ast.Name;
import org.python.pydev.parser.visitors.scope.GetNodeForExtractLocalVisitor;
import org.python.pydev.refactoring.core.base.PyDocumentChange;
import org.python.pydev.refactoring.core.base.PyTextFileChange;
import org.python.pydev.refactoring.core.base.RefactoringInfo;
 
/**
 * A refactoring assigning "None" to the name at the selection start.
 * @author Alexander Bürger <acfbuerger@googlemail.com>
 */
public class InsertNoneRefactoring extends Refactoring {
 
    private PyEdit editor;
    private RefactoringInfo info;
 
    private Name name;
 
    public InsertNoneRefactoring(PyEdit editor, RefactoringInfo info) {
        this.editor = editor;
        this.info = info;
    }
 
    @Override
    public RefactoringStatus checkInitialConditions(IProgressMonitor pm)
            throws CoreException, OperationCanceledException
    {
        RefactoringStatus status = new RefactoringStatus();
 
        // walk the complete AST to find out the name we sit on
        InsertNoneNameFinderVisitor nsv = new InsertNoneNameFinderVisitor(info.getDocument(), info.getUserSelection());
        try {
            // info.getModuleAdapter().getASTNode().accept(nsv); prohibited by access restriction
            editor.getAST().accept(nsv);
        } catch (Exception e) {
            status.addFatalError("AST Exception");
        }
        name = nsv.getName();
        if( name == null )
            status.addFatalError("No Name found at selection start.");
 
        return status;
    }
 
    @Override
    public RefactoringStatus checkFinalConditions(IProgressMonitor pm)
            throws CoreException, OperationCanceledException
    {
        // TODO check final conditions
        return new RefactoringStatus();
    }
 
    @Override
    public Change createChange(IProgressMonitor pm) throws CoreException,
            OperationCanceledException
    {
        // figure out the offset in the document where we have to insert our new
        // statement; based on CreateLocalVariableEdit.calculateLineForLocal
        PySelection ps = new PySelection(info.getDocument(), info.getUserSelection());
        GetNodeForExtractLocalVisitor visitor = new GetNodeForExtractLocalVisitor(name.beginLine);
        try {
            editor.getAST().accept(visitor);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        SimpleNode lastNodeBeforePassedLine = visitor.getLastInContextBeforePassedLine();
        int insertLine;
        if (lastNodeBeforePassedLine != null) {
            insertLine = lastNodeBeforePassedLine.beginLine-1;
        } else {
            insertLine = ps.getStartLineIndex();
        }
        int insertOffset = ps.getLineOffset(insertLine);
 
        // use same indentation as the line where we want to insert our statement
        PySelection psi = new PySelection(info.getDocument(), insertLine, 0);
        String indent = psi.getIndentationFromLine();
 
        // this is based on AbstractFileChangeProcessor.createChange()
        TextChange tc;
        if (info.getSourceFile() != null) {
            tc = new PyTextFileChange(getName(), info.getSourceFile());
        } else {
            tc = new PyDocumentChange(getName(), info.getDocument());
        }
 
        // insert a line assigning None to the selected name
        tc.setEdit(new InsertEdit(insertOffset, indent + name.id + " = None" + info.getNewLineDelim()));
        return tc;
    }
 
    @Override
    public String getName() {
        return "InsertNone Refactoring";
    }
 
}

InsertNoneNameFinderVisitor

Dieser Visitor ist dazu gedacht, den AST eines PyEditors zu durchlaufen. Dabei wird untersucht, ob ein Name an der Stelle steht, an der die Auswahl im Editor beginnt — das kann auch die Cursorposition sein. Das Ende der Auswahl wird nicht beachtet.

Nicht immer ist es sinnvoll, in der Zeile vor dem Auftreten eines Namens eine Zuweisung zu diesem Namen durchzuführen. Zwei dieser Fälle (gibt es mehr?) werden hier gesondert behandelt:

  • Namen in global statements werden ignoriert, denn davor ist der Scope wahrscheinlich falsch und die Zuweisung wäre ein Programmfehler.
  • Namen der Argumente von Funktions-Definitionen werden ebenfalls ignoriert. Auch hier ist es höchstwahrscheinlich ein Fehler, eine Zuweisung einzufügen.
package feu.k01919.q7649347.pyrefactor.insertnone;
 
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextSelection;
import org.python.pydev.core.docutils.PySelection;
import org.python.pydev.parser.jython.SimpleNode;
import org.python.pydev.parser.jython.ast.FunctionDef;
import org.python.pydev.parser.jython.ast.Global;
import org.python.pydev.parser.jython.ast.Import;
import org.python.pydev.parser.jython.ast.ImportFrom;
import org.python.pydev.parser.jython.ast.Name;
import org.python.pydev.parser.jython.ast.VisitorBase;
import org.python.pydev.parser.jython.ast.stmtType;
 
/**
 * A visitor searching for a name at the selection start.
 * @author Alexander Bürger <acfbuerger@googlemail.com>
 */
public class InsertNoneNameFinderVisitor extends VisitorBase {
    private IDocument document;
    private ITextSelection selection;
    private Name name;
 
    public InsertNoneNameFinderVisitor(IDocument doc, ITextSelection s) {
        document = doc;
        selection = s;
    }
 
    public Object visitImport(Import node) throws Exception {
        // we are not interested in imports
        return null;
    }
 
    @Override
    public Object visitImportFrom(ImportFrom node) throws Exception {
        // we are not interested in imports
        return null;
    }
 
    @Override
    public Object visitGlobal(Global node) throws Exception {
        // we are not interested in global declarations
        return null;
    }
 
    @Override
    public Object visitFunctionDef(FunctionDef node) throws Exception {
        // for function definitions, we do not care about the arguments -- it
        // does not make sense to insert an assignment before a function definition
        if (node.body != null) {
            for (stmtType b: node.body) {
                if (b != null)
                    b.accept(this);
            }
        }
        return null;
    }
 
    @Override
    public Object visitName(Name node) throws Exception {
        // check if the start of the selection is within the name
        if( selection.getStartLine() == node.beginLine -1 ) {
            int selectionOffset = selection.getOffset();
            int begin = PySelection.getAbsoluteCursorOffset(document, node.beginLine - 1, node.beginColumn - 1);
            int end = begin + node.id.length();
            if (selectionOffset >= begin && selectionOffset <= end)
                return name = node;
        }
        return super.visitName(node);
    }
 
    /**
     * Retrieve the name we found.
     * @return the name, might be null
     */
    public Name getName() {
        return name;
    }
 
    @Override
    public void traverse(SimpleNode node) throws Exception {
        node.traverse(this);
    }
 
    @Override
    protected Object unhandled_node(SimpleNode node) throws Exception {
        return null;
    }
}

InsertNoneWizard

Diese Klasse implementiert den Dialog mit der Vorschau der Änderungen, die das Refactoring durchführen wird/würde. Da keine Eingaben des Benutzers zu verarbeiten sind, wird nur der Unterschied zwischen dem Original und dem geänderten Quelltext gezeigt.

package feu.k01919.q7649347.pyrefactor.insertnone;
 
import org.eclipse.ltk.ui.refactoring.RefactoringWizard;
 
/**
 * An almost empty wizard. It only whos the diff page.
 * @author Alexander Bürger <acfbuerger@googlemail.com>
 */
public class InsertNoneWizard extends RefactoringWizard {
 
    public InsertNoneWizard(InsertNoneRefactoring inr) {
        super(inr, DIALOG_BASED_USER_INTERFACE | CHECK_INITIAL_CONDITIONS_ON_OPEN |
                PREVIEW_EXPAND_FIRST_NODE | NO_BACK_BUTTON_ON_STATUS_DIALOG );
        setDefaultPageTitle(inr.getName());
    }
 
    @Override
    protected void addUserInputPages() {
        // no need for user input
    }
 
}

PyInsertNoneAction

Diese Klasse implementiert den ActionDelegate, der vom Benutzer aufgerufen werden kann.

package feu.k01919.q7649347.pyrefactor.actions;
 
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IEditorActionDelegate;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.python.pydev.core.MisconfigurationException;
import org.python.pydev.editor.PyEdit;
import org.python.pydev.refactoring.core.base.RefactoringInfo;
 
import feu.k01919.q7649347.pyrefactor.Activator;
import feu.k01919.q7649347.pyrefactor.insertnone.InsertNoneRefactoring;
import feu.k01919.q7649347.pyrefactor.insertnone.InsertNoneWizard;
 
/**
 * Editor action for Python refactoring example.
 * @see IEditorActionDelegate
 * @author Alexander Bürger <acfbuerger@googlemail.com>
 */
public class PyInsertNoneAction implements IEditorActionDelegate {
 
    private volatile PyEdit editor;
 
	/**
	 * @see IEditorActionDelegate#run
	 */
	public void run(IAction action) {
	    if( editor == null ) {
	        openError("Editor is not a PyDev PyEdit");
	        return;
	    }
 
        RefactoringInfo ri;
	    try {
	        ri = new RefactoringInfo(editor);
        } catch (MisconfigurationException e) {
            openError("PyDev Misconfiguration");
            return;
        }
 
        InsertNoneRefactoring inr = new InsertNoneRefactoring(editor, ri);
        InsertNoneWizard wizard = new InsertNoneWizard(inr);
        RefactoringWizardOpenOperation rwoo = new RefactoringWizardOpenOperation(wizard);
        try {
            rwoo.run(getShell(), inr.getName());
        } catch (InterruptedException e) {
        }
	}
 
    private Shell getShell() {
        IWorkbench workbench = PlatformUI.getWorkbench();
        IWorkbenchWindow window = workbench.getActiveWorkbenchWindow();
        return window.getShell();
    }
 
	private void openError(String message) {
        Status status = new Status(Status.ERROR, Activator.PLUGIN_ID, message);
        ErrorDialog.openError(getShell(), "PyInsertNone not possible.", message, status);
	}
 
    @Override
	public void selectionChanged(IAction action, ISelection selection) {
        // we do not care
	}
 
    @Override
    public void setActiveEditor(IAction action, IEditorPart editor) {
        if (editor instanceof PyEdit)
            this.editor = (PyEdit)editor;
        else
            this.editor = null;
    }
}

InsertNone in Aktion

Zur Demonstraion des InsertNoneRefactoring wird nun folgendes Python-Skript beispiel.py verwendet:

import sys
 
def meine_funktion(x, y):
    return x + 5*y
 
a = 5
vektor = [a,
          meine_funktion(a, -2),
          12]
 
if len(sys.argv)>1:
    print meine_funktion(float(sys.argv[1]), -1)
else:
    x = 17
    print x

Das Plugin wurde dabei in einer Debugging-Workbench gestartet.

meine_funktion

Hier soll vor Zeile 13 das Statement

    meine_funktion = None

eingefügt werden. Dazu wird der Cursor in Zeile 13 auf meine_funktion gesetzt und im Menü PyRefactor > Python Insert None aufgerufen. Es ergibt sich folgendes Bild:

PyDev Refactoring Beispiel 1.png

Vor dem print-Statement wird also eine Zuweisung eingefügt.

mehrzeilige Zuweisung

Hier soll vor Zeile 9 das Statement

a = None

eingefügt werden. Dazu wird der Cursor in Zeile 9 auf a gesetzt und im Menü PyRefactor > Python Insert None aufgerufen. Es ergibt sich folgendes Bild:

PyDev Refactoring Beispiel 2.png

Dank des GetNodeForExtractLocalVisitor wird die richtige Zeile vor der mehrzeiligen Zuweisung gefunden und die Zuweisung dort eingefügt.

Parameter

Hier soll geprüft werden, dass vor Zeile 4 kein Statement

x = None

eingefügt wird. Dazu wird der Cursor in Zeile 4 auf x gesetzt und im Menü PyRefactor > Python Insert None aufgerufen. Es ergibt sich folgendes Bild:

PyDev Refactoring Beispiel 3.png

Der InsertNoneNameFinderVisitor stellt fest, dass es sich um Parameter eines Funktions-Definition handelt und es gibt eine Fehlermeldung.

Probleme

Beispielsweise bei folgender Situation versagt der GetNodeForExtractLocalVisitor:

def fun(a,b):
    c = a; return 5*c+b

Wird hier der Ausdruck 5*ac+b selektiert und das PyDev-Refactoring Extract Local Variable aufgerufen, wird beispielsweise folgender Code erzeugt:

def fun(a,b):
    x = 5*c+b
    c = a; return x

Dieser Code ist fehlerhaft, denn c ist noch nicht definiert und kann nicht gelesen werden.

Zuletzt geändert am 15. Juli 2010 um 09:06