diff options
Diffstat (limited to 'python')
226 files changed, 7945 insertions, 2353 deletions
diff --git a/python/edu/build/pycharm_edu_build.gant b/python/edu/build/pycharm_edu_build.gant index 28d829e68b34..356df8c7d4c7 100644 --- a/python/edu/build/pycharm_edu_build.gant +++ b/python/edu/build/pycharm_edu_build.gant @@ -158,7 +158,6 @@ public layoutEducational(String classesPath, Set usedJars) { usedJars = collectUsedJars(modules(), approvedJars(), ["/ant/"], null) } - def appInfo = appInfoFile() def paths = new Paths(home) buildSearchableOptions("${projectBuilder.moduleOutput(findModule("platform-resources"))}/search", [], { projectBuilder.moduleRuntimeClasspath(findModule("main_pycharm_edu"), false).each { @@ -166,8 +165,9 @@ public layoutEducational(String classesPath, Set usedJars) { } }, "-Didea.platform.prefix=PyCharmEdu -Didea.no.jre.check=true") + def appInfo = appInfoFile() if (!dryRun) { - wireBuildDate("PE-${buildNumber}", appInfo) + wireBuildDate(buildNumber, appInfo) } Map args = [ @@ -223,7 +223,7 @@ private layoutPlugins(layouts) { } private String appInfoFile() { - return "$pythonEduHome/resources/idea/PyCharmEduApplicationInfo.xml" + return "$home/out/pycharmEDU/classes/production/python-educational/idea/PyCharmEduApplicationInfo.xml" } private layoutFull(Map args, String target, Set usedJars) { @@ -405,7 +405,7 @@ private layoutMac(Map _args, String target) { args.icns = "$pythonCommunityHome/resources/PyCharmCore.icns" args.bundleIdentifier = "com.jetbrains.pycharm" args.platform_prefix = "PyCharmEdu" - args.help_id = "PY" + args.help_id = "PE" args."idea.properties.path" = "${paths.distAll}/bin/idea.properties" args."idea.properties" = ["idea.no.jre.check": true, "ide.mac.useNativeClipboard": "false"]; layoutMacApp(target, ch, args) diff --git a/python/edu/build/upload_pythonInfo.xml b/python/edu/build/upload_pythonInfo.xml index f8d9477d1a3f..0633ad82f8c0 100644 --- a/python/edu/build/upload_pythonInfo.xml +++ b/python/edu/build/upload_pythonInfo.xml @@ -20,7 +20,7 @@ </fileset> <fileset dir="${home}/community/lib"> <include name="commons-net-3.1.jar"/> - <include name="jsch-0.1.50.jar"/> + <include name="jsch-0.1.51.jar"/> </fileset> </classpath> diff --git a/python/edu/course-creator/resources/META-INF/plugin.xml b/python/edu/course-creator/resources/META-INF/plugin.xml index 6a9a9ea90276..0e7f75c90051 100644 --- a/python/edu/course-creator/resources/META-INF/plugin.xml +++ b/python/edu/course-creator/resources/META-INF/plugin.xml @@ -26,6 +26,10 @@ <codeInsight.lineMarkerProvider language="Python" implementationClass="org.jetbrains.plugins.coursecreator.highlighting.CCTaskLineMarkerProvider"/> <treeStructureProvider implementation="org.jetbrains.plugins.coursecreator.projectView.CCTreeStructureProvider"/> + <codeInsight.lineMarkerProvider language="Python" implementationClass="org.jetbrains.plugins.coursecreator.RunTestsLineMarker"/> + <fileTypeFactory implementation="org.jetbrains.plugins.coursecreator.AnswerFileTypeFactory" /> + <refactoring.elementListenerProvider implementation="org.jetbrains.plugins.coursecreator.CCRefactoringElementListenerProvider"/> + <errorHandler implementation="com.intellij.diagnostic.ITNReporter"/> </extensions> <application-components> @@ -52,9 +56,19 @@ <action id="AddTaskWindow" class="org.jetbrains.plugins.coursecreator.actions.AddTaskWindow"> <add-to-group group-id="EditorPopupMenu" anchor="before" relative-to-action="CopyReference"/> </action> + <action id="ShowPreview" class="org.jetbrains.plugins.coursecreator.actions.CCShowPreview"> + <add-to-group group-id="ProjectViewPopupMenu" anchor="first"/> + </action> + <action id="RenameLesson" class="org.jetbrains.plugins.coursecreator.actions.CCRenameLesson"> + <add-to-group group-id="ProjectViewPopupMenu" anchor="before" relative-to-action="CutCopyPasteGroup"/> + </action> + <action id="RenameTask" class="org.jetbrains.plugins.coursecreator.actions.CCRenameTask"> + <add-to-group group-id="ProjectViewPopupMenu" anchor="before" relative-to-action="CutCopyPasteGroup"/> + </action> <action id="PackCourse" class="org.jetbrains.plugins.coursecreator.actions.CreateCourseArchive"> <add-to-group group-id="MainToolBar" anchor="last" /> </action> + <action id="CCRunTests" class="org.jetbrains.plugins.coursecreator.CCRunTests" text="Run tests" description="Run tests"/> </actions> </idea-plugin>
\ No newline at end of file diff --git a/python/edu/course-creator/resources/fileTemplates/internal/task.answer.ft b/python/edu/course-creator/resources/fileTemplates/internal/task.answer.ft new file mode 100644 index 000000000000..f0a4bcecdd24 --- /dev/null +++ b/python/edu/course-creator/resources/fileTemplates/internal/task.answer.ft @@ -0,0 +1 @@ +# TODO: type solution here diff --git a/python/edu/course-creator/resources/fileTemplates/internal/task.py.ft b/python/edu/course-creator/resources/fileTemplates/internal/task.py.ft deleted file mode 100644 index 0256e108786c..000000000000 --- a/python/edu/course-creator/resources/fileTemplates/internal/task.py.ft +++ /dev/null @@ -1 +0,0 @@ -# TODO: type solution here
\ No newline at end of file diff --git a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/AnswerFileType.java b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/AnswerFileType.java new file mode 100644 index 000000000000..3b8189aadffc --- /dev/null +++ b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/AnswerFileType.java @@ -0,0 +1,40 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.plugins.coursecreator; + +import com.jetbrains.python.PythonFileType; +import org.jetbrains.annotations.NotNull; + +public class AnswerFileType extends PythonFileType { + + @NotNull + @Override + public String getDefaultExtension() { + return "answer"; + } + + @NotNull + @Override + public String getDescription() { + return "Answer file"; + } + + @NotNull + @Override + public String getName() { + return "Answer"; + } +} diff --git a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/AnswerFileTypeFactory.java b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/AnswerFileTypeFactory.java new file mode 100644 index 000000000000..e4adcbc5b386 --- /dev/null +++ b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/AnswerFileTypeFactory.java @@ -0,0 +1,28 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.plugins.coursecreator; + + +import com.intellij.openapi.fileTypes.FileTypeConsumer; +import com.intellij.openapi.fileTypes.FileTypeFactory; +import org.jetbrains.annotations.NotNull; + +public class AnswerFileTypeFactory extends FileTypeFactory { + @Override + public void createFileTypes(@NotNull FileTypeConsumer fileTypeConsumer) { + fileTypeConsumer.consume(AnswerFileType.INSTANCE, "answer"); + } +} diff --git a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/CCEditorFactoryListener.java b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/CCEditorFactoryListener.java index 1eb8690aad22..acb0d43d0d12 100644 --- a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/CCEditorFactoryListener.java +++ b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/CCEditorFactoryListener.java @@ -35,6 +35,9 @@ public class CCEditorFactoryListener implements EditorFactoryListener { final Lesson lesson = course.getLesson(lessonDir.getName()); final Task task = lesson.getTask(taskDir.getName()); final TaskFile taskFile = task.getTaskFile(virtualFile.getName()); + if (taskFile == null) { + return; + } TaskFileModificationListener listener = new TaskFileModificationListener(taskFile); CCProjectService.addDocumentListener(editor.getDocument(), listener); editor.getDocument().addDocumentListener(listener); @@ -54,13 +57,10 @@ public class CCEditorFactoryListener implements EditorFactoryListener { editor.getSelectionModel().removeSelection(); } - private class TaskFileModificationListener extends StudyDocumentListener { - - private final TaskFile myTaskFile; + private static class TaskFileModificationListener extends StudyDocumentListener { public TaskFileModificationListener(TaskFile taskFile) { super(taskFile); - myTaskFile = taskFile; } @Override @@ -71,10 +71,5 @@ public class CCEditorFactoryListener implements EditorFactoryListener { taskWindow.setReplacementLength(taskWindow.getLength() + 1); } } - - @Override - protected boolean needModify() { - return myTaskFile.isTrackChanges(); - } } } diff --git a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/CCProjectComponent.java b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/CCProjectComponent.java index 34de943d2ad0..8b524dbffd4b 100644 --- a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/CCProjectComponent.java +++ b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/CCProjectComponent.java @@ -1,22 +1,33 @@ package org.jetbrains.plugins.coursecreator; +import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.components.ProjectComponent; +import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.EditorFactory; import com.intellij.openapi.editor.event.EditorFactoryEvent; import com.intellij.openapi.editor.impl.EditorFactoryImpl; -import com.intellij.openapi.fileEditor.FileEditor; -import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.fileEditor.*; import com.intellij.openapi.fileEditor.impl.text.PsiAwareTextEditorImpl; import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectManager; import com.intellij.openapi.startup.StartupManager; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.VirtualFileAdapter; +import com.intellij.openapi.vfs.VirtualFileEvent; +import com.intellij.openapi.vfs.VirtualFileManager; import org.jetbrains.annotations.NotNull; import org.jetbrains.plugins.coursecreator.format.Course; +import org.jetbrains.plugins.coursecreator.format.Lesson; +import org.jetbrains.plugins.coursecreator.format.Task; +import org.jetbrains.plugins.coursecreator.format.TaskFile; + +import java.io.IOException; public class CCProjectComponent implements ProjectComponent { + private static final Logger LOG = Logger.getInstance(CCProjectComponent.class.getName()); private final Project myProject; + private FileDeletedListener myListener; public CCProjectComponent(Project project) { myProject = project; @@ -37,15 +48,46 @@ public class CCProjectComponent implements ProjectComponent { StartupManager.getInstance(myProject).runWhenProjectIsInitialized(new Runnable() { @Override public void run() { - Course course = CCProjectService.getInstance(myProject).getCourse(); + final Course course = CCProjectService.getInstance(myProject).getCourse(); if (course != null) { - EditorFactory.getInstance().addEditorFactoryListener(new CCEditorFactoryListener(), myProject); + myProject.getMessageBus().connect(myProject).subscribe( + FileEditorManagerListener.FILE_EDITOR_MANAGER, new FileEditorManagerAdapter() { + @Override + public void selectionChanged(@NotNull FileEditorManagerEvent event) { + final VirtualFile oldFile = event.getOldFile(); + if (oldFile == null) { + return; + } + if (CCProjectService.getInstance(myProject).isTaskFile(oldFile)) { + FileEditorManager.getInstance(myProject).closeFile(oldFile); + ApplicationManager.getApplication().runWriteAction(new Runnable() { + @Override + public void run() { + try { + oldFile.delete(myProject); + } + catch (IOException e) { + LOG.error(e); + } + } + }); + } + } + }); + myListener = new FileDeletedListener(); + VirtualFileManager.getInstance().addVirtualFileListener(myListener); + final CCEditorFactoryListener editorFactoryListener = new CCEditorFactoryListener(); + EditorFactory.getInstance().addEditorFactoryListener(editorFactoryListener, myProject); VirtualFile[] files = FileEditorManager.getInstance(myProject).getOpenFiles(); for (VirtualFile file : files) { + if (CCProjectService.getInstance(myProject).isTaskFile(file)) { + FileEditorManager.getInstance(myProject).closeFile(file); + continue; + } FileEditor fileEditor = FileEditorManager.getInstance(myProject).getSelectedEditor(file); if (fileEditor instanceof PsiAwareTextEditorImpl) { Editor editor = ((PsiAwareTextEditorImpl)fileEditor).getEditor(); - new CCEditorFactoryListener().editorCreated(new EditorFactoryEvent(new EditorFactoryImpl(ProjectManager.getInstance()), editor )); + editorFactoryListener.editorCreated(new EditorFactoryEvent(new EditorFactoryImpl(ProjectManager.getInstance()), editor)); } } } @@ -54,5 +96,94 @@ public class CCProjectComponent implements ProjectComponent { } public void projectClosed() { + if (myListener != null) { + VirtualFileManager.getInstance().removeVirtualFileListener(myListener); + } + } + + private class FileDeletedListener extends VirtualFileAdapter { + + @Override + public void fileDeleted(@NotNull VirtualFileEvent event) { + if (myProject.isDisposed() || !myProject.isOpen()) { + return; + } + Course course = CCProjectService.getInstance(myProject).getCourse(); + if (course == null) { + return; + } + VirtualFile removedFile = event.getFile(); + if (removedFile.getName().contains(".answer")) { + deleteTaskFile(course, removedFile); + } + if (removedFile.getName().contains("task")) { + deleteTask(course, removedFile); + } + if (removedFile.getName().contains("lesson")) { + deleteLesson(course, removedFile); + } + } + + private void deleteLesson(Course course, VirtualFile file) { + VirtualFile courseDir = file.getParent(); + if (!courseDir.getName().equals(myProject.getName())) { + return; + } + Lesson lesson = course.getLesson(file.getName()); + if (lesson != null) { + course.getLessons().remove(lesson); + course.getLessonsMap().remove(file.getName()); + } + } + + private void deleteTask(Course course, VirtualFile removedFile) { + VirtualFile lessonDir = removedFile.getParent(); + if (lessonDir == null || !lessonDir.getName().contains("lesson")) { + return; + } + VirtualFile courseDir = lessonDir.getParent(); + if (!courseDir.getName().equals(myProject.getName())) { + return; + } + Lesson lesson = course.getLesson(lessonDir.getName()); + if (lesson == null) { + return; + } + Task task = lesson.getTask(removedFile.getName()); + if (task == null) { + return; + } + lesson.getTaskList().remove(task); + lesson.getTasksMap().remove(removedFile.getName()); + } + + private void deleteTaskFile(Course course, VirtualFile removedFile) { + VirtualFile taskDir = removedFile.getParent(); + if (taskDir == null || !taskDir.getName().contains("task")) { + return; + } + VirtualFile lessonDir = taskDir.getParent(); + if (lessonDir == null || !lessonDir.getName().contains("lesson")) { + return; + } + VirtualFile courseDir = lessonDir.getParent(); + if (!courseDir.getName().equals(myProject.getName())) { + return; + } + Lesson lesson = course.getLesson(lessonDir.getName()); + if (lesson == null) { + return; + } + Task task = lesson.getTask(taskDir.getName()); + if (task == null) { + return; + } + TaskFile taskFile = task.getTaskFile(removedFile.getName()); + if (taskFile == null) { + return; + } + String name = CCProjectService.getRealTaskFileName(removedFile.getName()); + task.getTaskFiles().remove(name); + } } } diff --git a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/CCProjectService.java b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/CCProjectService.java index 1e38bab865cb..c06925e9a453 100644 --- a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/CCProjectService.java +++ b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/CCProjectService.java @@ -32,6 +32,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.plugins.coursecreator.format.*; import java.io.File; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -70,6 +71,9 @@ public class CCProjectService implements PersistentStateComponent<Element> { @Override public void loadState(Element el) { myCourse = XmlSerializer.deserialize(el.getChild(COURSE_ELEMENT), Course.class); + if (myCourse != null) { + myCourse.init(); + } } public static CCProjectService getInstance(@NotNull Project project) { @@ -115,7 +119,7 @@ public class CCProjectService implements PersistentStateComponent<Element> { } List<TaskWindow> taskWindows = taskFile.getTaskWindows(); for (TaskWindow taskWindow : taskWindows) { - taskWindow.drawHighlighter(editor); + taskWindow.drawHighlighter(editor, false); } } @@ -131,8 +135,52 @@ public class CCProjectService implements PersistentStateComponent<Element> { myDocumentListeners.remove(document); } - public static boolean indexIsValid(int index, List<TaskWindow> collection) { + public static boolean indexIsValid(int index, Collection collection) { int size = collection.size(); return index >= 0 && index < size; } + + public boolean isTaskFile(VirtualFile file) { + if (myCourse == null || file == null) { + return false; + } + VirtualFile taskDir = file.getParent(); + if (taskDir != null) { + String taskDirName = taskDir.getName(); + if (taskDirName.contains("task")) { + VirtualFile lessonDir = taskDir.getParent(); + if (lessonDir != null) { + String lessonDirName = lessonDir.getName(); + int lessonIndex = getIndex(lessonDirName, "lesson"); + List<Lesson> lessons = myCourse.getLessons(); + if (!indexIsValid(lessonIndex, lessons)) { + return false; + } + Lesson lesson = lessons.get(lessonIndex); + int taskIndex = getIndex(taskDirName, "task"); + List<Task> tasks = lesson.getTaskList(); + if (!indexIsValid(taskIndex, tasks)) { + return false; + } + Task task = tasks.get(taskIndex); + return task.isTaskFile(file.getName()); + } + } + } + return false; + } + + public static int getIndex(@NotNull final String fullName, @NotNull final String logicalName) { + if (!fullName.contains(logicalName)) { + throw new IllegalArgumentException(); + } + return Integer.parseInt(fullName.substring(logicalName.length())) - 1; + } + public static String getRealTaskFileName(String name) { + if (!name.contains(".answer")) { + return null; + } + int nameEnd = name.indexOf(".answer"); + return name.substring(0, nameEnd) + ".py"; + } } diff --git a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/CCRefactoringElementListenerProvider.java b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/CCRefactoringElementListenerProvider.java new file mode 100644 index 000000000000..601e4fc7cb40 --- /dev/null +++ b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/CCRefactoringElementListenerProvider.java @@ -0,0 +1,96 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.plugins.coursecreator; + +import com.intellij.psi.PsiDirectory; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.refactoring.listeners.RefactoringElementAdapter; +import com.intellij.refactoring.listeners.RefactoringElementListener; +import com.intellij.refactoring.listeners.RefactoringElementListenerProvider; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.plugins.coursecreator.format.Course; +import org.jetbrains.plugins.coursecreator.format.Lesson; +import org.jetbrains.plugins.coursecreator.format.Task; +import org.jetbrains.plugins.coursecreator.format.TaskFile; + +import java.util.Map; + +public class CCRefactoringElementListenerProvider implements RefactoringElementListenerProvider { + @Nullable + @Override + public RefactoringElementListener getListener(PsiElement element) { + return new CCRenameListener(element); + } + + + static class CCRenameListener extends RefactoringElementAdapter { + + private String myElementName; + + public CCRenameListener(PsiElement element) { + if (element instanceof PsiFile) { + PsiFile psiFile = (PsiFile)element; + myElementName = psiFile.getName(); + } + } + + @Override + protected void elementRenamedOrMoved(@NotNull PsiElement newElement) { + if (newElement instanceof PsiFile && myElementName != null) { + PsiFile psiFile = (PsiFile)newElement; + if (myElementName.contains(".answer")) { + //this is task file + renameTaskFile(psiFile, myElementName); + } + } + } + + private static void renameTaskFile(PsiFile file, String oldName) { + PsiDirectory taskDir = file.getContainingDirectory(); + Course course = CCProjectService.getInstance(file.getProject()).getCourse(); + if (course == null) { + return; + } + if (taskDir == null || !taskDir.getName().contains("task")) { + return; + } + PsiDirectory lessonDir = taskDir.getParent(); + if (lessonDir == null || !lessonDir.getName().contains("lesson")) { + return; + } + Lesson lesson = course.getLesson(lessonDir.getName()); + if (lesson == null) { + return; + } + Task task = lesson.getTask(taskDir.getName()); + if (task == null) { + return; + } + Map<String, TaskFile> taskFiles = task.getTaskFiles(); + TaskFile taskFile = task.getTaskFile(oldName); + String realTaskFileName = CCProjectService.getRealTaskFileName(oldName); + taskFiles.remove(realTaskFileName); + taskFiles.put(CCProjectService.getRealTaskFileName(file.getName()), taskFile); + } + + @Override + public void undoElementMovedOrRenamed(@NotNull PsiElement newElement, @NotNull String oldQualifiedName) { + + } + } +} diff --git a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/CCRunTests.java b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/CCRunTests.java new file mode 100644 index 000000000000..d078972e3f56 --- /dev/null +++ b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/CCRunTests.java @@ -0,0 +1,284 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.plugins.coursecreator; + +import com.intellij.execution.*; +import com.intellij.execution.actions.ConfigurationContext; +import com.intellij.execution.configurations.ConfigurationFactory; +import com.intellij.execution.executors.DefaultRunExecutor; +import com.intellij.icons.AllIcons; +import com.intellij.ide.projectView.ProjectView; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.Presentation; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.fileEditor.FileDocumentManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.VirtualFileManager; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.util.containers.HashMap; +import com.jetbrains.python.run.PythonConfigurationType; +import com.jetbrains.python.run.PythonRunConfiguration; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.plugins.coursecreator.actions.CreateCourseArchive; +import org.jetbrains.plugins.coursecreator.format.*; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Map; + +public class CCRunTests extends AnAction { + private static final Logger LOG = Logger.getInstance(CCRunTests.class.getName()); + + public CCRunTests() { + getTemplatePresentation().setIcon(AllIcons.Actions.Lightning); + } + + @Override + public void update(@NotNull AnActionEvent e) { + Presentation presentation = e.getPresentation(); + final ConfigurationContext context = ConfigurationContext.getFromContext(e.getDataContext()); + Location location = context.getLocation(); + if (location == null) { + return; + } + PsiElement psiElement = location.getPsiElement(); + PsiFile psiFile = psiElement.getContainingFile(); + if (psiFile != null && psiFile.getName().contains(".answer")) { + presentation.setEnabled(true); + presentation.setText("Run tests from '" + psiFile.getName() + "'"); + } + else { + presentation.setEnabled(false); + } + } + + public void actionPerformed(@NotNull AnActionEvent e) { + final ConfigurationContext context = ConfigurationContext.getFromContext(e.getDataContext()); + run(context); + } + + public static void run(final @NotNull ConfigurationContext context) { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + @Override + public void run() { + final Project project = context.getProject(); + PsiElement location = context.getPsiLocation(); + final Course course = CCProjectService.getInstance(project).getCourse(); + if (course == null || location == null) { + return; + } + PsiFile psiFile = location.getContainingFile(); + final VirtualFile virtualFile = psiFile.getVirtualFile(); + final VirtualFile taskDir = virtualFile.getParent(); + if (taskDir == null) { + return; + } + final Task task = getTask(course, taskDir); + if (task == null) { + return; + } + for (final Map.Entry<String, TaskFile> entry : task.getTaskFiles().entrySet()) { + final String name = entry.getKey(); + createTestEnvironment(taskDir, name, entry.getValue(), project); + VirtualFile testFile = taskDir.findChild("tests.py"); + if (testFile == null) { + return; + } + executeTests(project, virtualFile, taskDir, testFile); + clearTestEnvironment(taskDir, project); + } + } + }); + } + + private static void createTestEnvironment(@NotNull final VirtualFile taskDir, final String fileName, @NotNull final TaskFile taskFile, + @NotNull final Project project) { + try { + String answerFileName = FileUtil.getNameWithoutExtension(fileName) + ".answer"; + final VirtualFile answerFile = taskDir.findChild(answerFileName); + if (answerFile == null) { + LOG.debug("could not find answer file " + answerFileName); + return; + } + ApplicationManager.getApplication().runWriteAction(new Runnable() { + @Override + public void run() { + final FileDocumentManager documentManager = FileDocumentManager.getInstance(); + documentManager.saveAllDocuments(); + } + }); + answerFile.copy(project, taskDir, fileName); + flushWindows(taskFile, answerFile); + createResourceFiles(answerFile, project); + } + catch (IOException e) { + LOG.error(e); + } + } + + private static void clearTestEnvironment(@NotNull final VirtualFile taskDir, @NotNull final Project project) { + try { + VirtualFile ideaDir = project.getBaseDir().findChild(".idea"); + if (ideaDir == null) { + LOG.debug("idea directory doesn't exist"); + return; + } + VirtualFile courseResourceDir = ideaDir.findChild("course"); + if (courseResourceDir == null) { + return; + } + courseResourceDir.delete(project); + VirtualFile[] taskDirChildren = taskDir.getChildren(); + for (VirtualFile file : taskDirChildren) { + if (file.getName().contains("_windows")) { + file.delete(project); + } + if (CCProjectService.getInstance(project).isTaskFile(file)) { + file.delete(project); + } + } + } + catch (IOException e) { + LOG.error(e); + } + } + + private static void executeTests(@NotNull final Project project, + @NotNull final VirtualFile virtualFile, + @NotNull final VirtualFile taskDir, + @NotNull final VirtualFile testFile) { + final ConfigurationFactory factory = PythonConfigurationType.getInstance().getConfigurationFactories()[0]; + final RunnerAndConfigurationSettings settings = + RunManager.getInstance(project).createRunConfiguration("test", factory); + + final PythonRunConfiguration configuration = (PythonRunConfiguration)settings.getConfiguration(); + configuration.setScriptName(testFile.getPath()); + VirtualFile userFile = taskDir.findChild(virtualFile.getNameWithoutExtension() + ".py"); + if (userFile == null) { + return; + } + VirtualFile ideaDir = project.getBaseDir().findChild(".idea"); + if (ideaDir == null) { + return; + } + VirtualFileManager.getInstance().refreshWithoutFileWatcher(true); + ProjectView.getInstance(project).refresh(); + VirtualFile courseDir = ideaDir.findChild("course"); + if (courseDir == null) { + return; + } + configuration.setScriptParameters(courseDir.getPath() + " " + userFile.getPath()); + Executor executor = DefaultRunExecutor.getRunExecutorInstance(); + ProgramRunnerUtil.executeConfiguration(project, settings, executor); + } + + @Nullable + private static Task getTask(@NotNull final Course course, @NotNull final VirtualFile taskDir) { + if (!taskDir.getName().contains("task")) { + return null; + } + VirtualFile lessonDir = taskDir.getParent(); + if (lessonDir == null || !lessonDir.getName().contains("lesson")) { + return null; + } + Lesson lesson = course.getLesson(lessonDir.getName()); + if (lesson == null) { + return null; + } + return lesson.getTask(taskDir.getName()); + } + + + //some tests could compare task files after user modifications with initial task files + private static void createResourceFiles(@NotNull final VirtualFile file, @NotNull final Project project) { + VirtualFile taskDir = file.getParent(); + int index = CCProjectService.getIndex(taskDir.getName(), "task"); + VirtualFile lessonDir = taskDir.getParent(); + int lessonIndex = CCProjectService.getIndex(lessonDir.getName(), "lesson"); + Course course = CCProjectService.getInstance(project).getCourse(); + if (course == null) { + return; + } + VirtualFile ideaDir = project.getBaseDir().findChild(".idea"); + assert ideaDir != null; + try { + VirtualFile taskResourceDir = ideaDir.createChildDirectory(project, "course").createChildDirectory(project, lessonDir.getName()) + .createChildDirectory(project, taskDir.getName()); + if (CCProjectService.indexIsValid(lessonIndex, course.getLessons())) { + Lesson lesson = course.getLessons().get(lessonIndex); + if (CCProjectService.indexIsValid(index, lesson.getTaskList())) { + Task task = lesson.getTaskList().get(index); + HashMap<TaskFile, TaskFile> taskFilesCopy = new HashMap<TaskFile, TaskFile>(); + for (Map.Entry<String, TaskFile> entry : task.getTaskFiles().entrySet()) { + CreateCourseArchive.createUserFile(project, taskFilesCopy, taskResourceDir, taskDir, entry); + CreateCourseArchive.resetTaskFiles(taskFilesCopy); + } + } + } + } + catch (IOException e) { + LOG.error(e); + } + } + + @SuppressWarnings("IOResourceOpenedButNotSafelyClosed") + public static VirtualFile flushWindows(TaskFile taskFile, VirtualFile file) { + VirtualFile taskDir = file.getParent(); + VirtualFile fileWindows = null; + final Document document = FileDocumentManager.getInstance().getDocument(file); + if (document == null) { + LOG.debug("Couldn't flush windows"); + return null; + } + if (taskDir != null) { + String name = file.getNameWithoutExtension() + "_windows"; + PrintWriter printWriter = null; + try { + fileWindows = taskDir.createChildData(taskFile, name); + printWriter = new PrintWriter(new FileOutputStream(fileWindows.getPath())); + for (TaskWindow taskWindow : taskFile.getTaskWindows()) { + int start = taskWindow.getRealStartOffset(document); + String windowDescription = document.getText(new TextRange(start, start + taskWindow.getReplacementLength())); + printWriter.println("#study_plugin_window = " + windowDescription); + } + ApplicationManager.getApplication().runWriteAction(new Runnable() { + @Override + public void run() { + FileDocumentManager.getInstance().saveDocument(document); + } + }); + } + catch (IOException e) { + LOG.error(e); + } + finally { + if (printWriter != null) { + printWriter.close(); + } + } + } + return fileWindows; + } +} diff --git a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/RunTestsLineMarker.java b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/RunTestsLineMarker.java new file mode 100644 index 000000000000..93e0a706f367 --- /dev/null +++ b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/RunTestsLineMarker.java @@ -0,0 +1,109 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.plugins.coursecreator; + +import com.intellij.codeHighlighting.Pass; +import com.intellij.codeInsight.daemon.GutterIconNavigationHandler; +import com.intellij.codeInsight.daemon.LineMarkerInfo; +import com.intellij.codeInsight.daemon.LineMarkerProvider; +import com.intellij.execution.actions.ConfigurationContext; +import com.intellij.icons.AllIcons; +import com.intellij.ide.DataManager; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.markup.GutterIconRenderer; +import com.intellij.psi.PsiComment; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiWhiteSpace; +import com.intellij.psi.util.PsiUtilBase; +import com.intellij.util.Function; +import com.jetbrains.python.psi.PyFile; +import com.jetbrains.python.psi.PyImportStatement; +import com.jetbrains.python.psi.PyStatement; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.awt.event.MouseEvent; +import java.util.Collection; +import java.util.List; + +public class RunTestsLineMarker implements LineMarkerProvider { + @Nullable + @Override + public LineMarkerInfo getLineMarkerInfo(@NotNull PsiElement element) { + return null; + } + + @Override + public void collectSlowLineMarkers(@NotNull List<PsiElement> elements, @NotNull Collection<LineMarkerInfo> result) { + for (PsiElement element : elements) { + if (isFirstCodeLine(element)) { + PsiFile psiFile = element.getContainingFile(); + if (psiFile == null || !psiFile.getName().contains(".answer")) { + continue; + } + result.add(new LineMarkerInfo<PsiElement>( + element, element.getTextRange(), AllIcons.Actions.Lightning, Pass.UPDATE_OVERRIDEN_MARKERS, + new Function<PsiElement, String>() { + @Override + public String fun(PsiElement e) { + return "Run tests from file '" + e.getContainingFile().getName() + "'"; + } + }, + new GutterIconNavigationHandler<PsiElement>() { + @Override + public void navigate(MouseEvent e, PsiElement elt) { + executeCurrentScript(elt); + } + }, + GutterIconRenderer.Alignment.RIGHT)); + } + } + } + + private static void executeCurrentScript(PsiElement elt) { + Editor editor = PsiUtilBase.findEditor(elt); + assert editor != null; + + final ConfigurationContext context = + ConfigurationContext.getFromContext(DataManager.getInstance().getDataContext(editor.getComponent())); + CCRunTests.run(context); + } + + private static boolean isFirstCodeLine(PsiElement element) { + return element instanceof PyStatement && + element.getParent() instanceof PyFile && + !isNothing(element) && + nothingBefore(element); + } + + private static boolean nothingBefore(PsiElement element) { + element = element.getPrevSibling(); + while (element != null) { + if (!isNothing(element)) { + return false; + } + element = element.getPrevSibling(); + } + + return true; + } + + private static boolean isNothing(PsiElement element) { + return (element instanceof PsiComment) || (element instanceof PyImportStatement) || (element instanceof PsiWhiteSpace); + } +} + diff --git a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/StudyDocumentListener.java b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/StudyDocumentListener.java index d803e0e8fd97..965bd31771a0 100644 --- a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/StudyDocumentListener.java +++ b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/StudyDocumentListener.java @@ -44,9 +44,6 @@ public abstract class StudyDocumentListener extends DocumentAdapter { @Override public void documentChanged(DocumentEvent e) { if (e instanceof DocumentEventImpl) { - if (!needModify()) { - return; - } DocumentEventImpl event = (DocumentEventImpl)e; Document document = e.getDocument(); int offset = e.getOffset(); @@ -65,7 +62,5 @@ public abstract class StudyDocumentListener extends DocumentAdapter { } protected abstract void updateTaskWindowLength(CharSequence fragment, TaskWindow taskWindow, int change); - - protected abstract boolean needModify(); } diff --git a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/actions/AddTaskWindow.java b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/actions/AddTaskWindow.java index ff88cea5fd42..0bc631dbd784 100644 --- a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/actions/AddTaskWindow.java +++ b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/actions/AddTaskWindow.java @@ -13,6 +13,7 @@ import com.intellij.openapi.ui.DialogWrapper; import com.intellij.psi.PsiDirectory; import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.PsiFile; +import org.jetbrains.annotations.NotNull; import org.jetbrains.plugins.coursecreator.CCProjectService; import org.jetbrains.plugins.coursecreator.format.*; import org.jetbrains.plugins.coursecreator.ui.CreateTaskWindowDialog; @@ -59,12 +60,12 @@ public class AddTaskWindow extends DumbAwareAction { } int index = taskFile.getTaskWindows().size() + 1; taskFile.addTaskWindow(taskWindow, index); - taskWindow.drawHighlighter(editor); + taskWindow.drawHighlighter(editor, false); DaemonCodeAnalyzerImpl.getInstance(project).restart(file); } @Override - public void update(AnActionEvent event) { + public void update(@NotNull AnActionEvent event) { final Presentation presentation = event.getPresentation(); final Project project = event.getData(CommonDataKeys.PROJECT); if (project == null) { diff --git a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/actions/CCRename.java b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/actions/CCRename.java new file mode 100644 index 000000000000..321a86a5ba35 --- /dev/null +++ b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/actions/CCRename.java @@ -0,0 +1,97 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.plugins.coursecreator.actions; + +import com.intellij.ide.IdeView; +import com.intellij.ide.projectView.ProjectView; +import com.intellij.ide.util.DirectoryChooserUtil; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.actionSystem.LangDataKeys; +import com.intellij.openapi.actionSystem.Presentation; +import com.intellij.openapi.project.DumbAwareAction; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiDirectory; +import com.intellij.psi.PsiFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.coursecreator.CCProjectService; +import org.jetbrains.plugins.coursecreator.format.Course; + +import javax.swing.*; + +public abstract class CCRename extends DumbAwareAction { + public CCRename(String text, String description, Icon icon) { + super(text, description, icon); + } + + @Override + public void update(@NotNull AnActionEvent event) { + final Presentation presentation = event.getPresentation(); + final Project project = event.getData(CommonDataKeys.PROJECT); + if (project == null) { + presentation.setVisible(false); + presentation.setEnabled(false); + return; + } + + final IdeView view = event.getData(LangDataKeys.IDE_VIEW); + if (view == null) { + presentation.setVisible(false); + presentation.setEnabled(false); + return; + } + + final PsiDirectory[] directories = view.getDirectories(); + if (directories.length == 0) { + presentation.setVisible(false); + presentation.setEnabled(false); + return; + } + final PsiFile file = CommonDataKeys.PSI_FILE.getData(event.getDataContext()); + final PsiDirectory directory = DirectoryChooserUtil.getOrChooseDirectory(view); + if (file != null ||directory == null || !directory.getName().contains(getFolderName())) { + presentation.setEnabled(false); + presentation.setVisible(false); + return; + } + presentation.setVisible(true); + presentation.setEnabled(true); + } + + public abstract String getFolderName(); + + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + final IdeView view = e.getData(LangDataKeys.IDE_VIEW); + final Project project = e.getData(CommonDataKeys.PROJECT); + + if (view == null || project == null) { + return; + } + final PsiDirectory directory = DirectoryChooserUtil.getOrChooseDirectory(view); + if (directory == null || !directory.getName().contains(getFolderName())) { + return; + } + Course course = CCProjectService.getInstance(project).getCourse(); + if (course == null) { + return; + } + if (!processRename(project, directory, course)) return; + ProjectView.getInstance(project).refresh(); + } + + public abstract boolean processRename(Project project, PsiDirectory directory, Course course); +} diff --git a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/actions/CCRenameLesson.java b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/actions/CCRenameLesson.java new file mode 100644 index 000000000000..3f580454ed6e --- /dev/null +++ b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/actions/CCRenameLesson.java @@ -0,0 +1,48 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.plugins.coursecreator.actions; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.Messages; +import com.intellij.psi.PsiDirectory; +import org.jetbrains.plugins.coursecreator.format.Course; +import org.jetbrains.plugins.coursecreator.format.Lesson; + +public class CCRenameLesson extends CCRename { + + public CCRenameLesson() { + super("Rename Lesson", "Rename Lesson", null); + } + + @Override + public String getFolderName() { + return "lesson"; + } + + @Override + public boolean processRename(Project project, PsiDirectory directory, Course course) { + Lesson lesson = course.getLesson(directory.getName()); + if (lesson == null) { + return false; + } + String newName = Messages.showInputDialog(project, "Enter new name", "Rename " + getFolderName(), null); + if (newName == null) { + return false; + } + lesson.setName(newName); + return true; + } +} diff --git a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/actions/CCRenameTask.java b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/actions/CCRenameTask.java new file mode 100644 index 000000000000..342621bf0d47 --- /dev/null +++ b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/actions/CCRenameTask.java @@ -0,0 +1,57 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.plugins.coursecreator.actions; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.Messages; +import com.intellij.psi.PsiDirectory; +import org.jetbrains.plugins.coursecreator.format.Course; +import org.jetbrains.plugins.coursecreator.format.Lesson; +import org.jetbrains.plugins.coursecreator.format.Task; + +public class CCRenameTask extends CCRename { + public CCRenameTask() { + super("Rename Task", "Rename Task", null); + } + + @Override + public String getFolderName() { + return "task"; + } + + @Override + public boolean processRename(Project project, PsiDirectory directory, Course course) { + PsiDirectory lessonDir = directory.getParent(); + if (lessonDir == null || !lessonDir.getName().contains("lesson")) { + return false; + } + Lesson lesson = course.getLesson(lessonDir.getName()); + if (lesson == null) { + return false; + } + Task task = lesson.getTask(directory.getName()); + if (task == null) { + return false; + } + String newName = Messages.showInputDialog(project, "Enter new name", "Rename " + getFolderName(), null); + if (newName == null) { + return false; + } + task.setName(newName); + return true; + + } +} diff --git a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/actions/CCShowPreview.java b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/actions/CCShowPreview.java new file mode 100644 index 000000000000..bccaacaf1957 --- /dev/null +++ b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/actions/CCShowPreview.java @@ -0,0 +1,104 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.plugins.coursecreator.actions; + +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.actionSystem.Presentation; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.project.DumbAwareAction; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiDirectory; +import com.intellij.psi.PsiFile; +import com.intellij.util.containers.hash.HashMap; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.coursecreator.CCProjectService; +import org.jetbrains.plugins.coursecreator.format.*; + +import java.util.Map; + +public class CCShowPreview extends DumbAwareAction { + public CCShowPreview() { + super("Show preview","Show preview", null); + } + + @Override + public void update(@NotNull AnActionEvent e) { + Presentation presentation = e.getPresentation(); + presentation.setEnabled(false); + presentation.setVisible(false); + final PsiFile file = CommonDataKeys.PSI_FILE.getData(e.getDataContext()); + if (file != null && file.getName().contains(".answer")) { + presentation.setEnabled(true); + presentation.setVisible(true); + } + } + + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + final Project project = e.getProject(); + if (project == null) { + return; + } + final PsiFile file = CommonDataKeys.PSI_FILE.getData(e.getDataContext()); + if (file == null || !file.getName().contains(".answer")) { + return; + } + final PsiDirectory taskDir = file.getContainingDirectory(); + if (taskDir == null) { + return; + } + PsiDirectory lessonDir = taskDir.getParentDirectory(); + if (lessonDir == null) { + return; + } + Course course = CCProjectService.getInstance(project).getCourse(); + if (course == null) { + return; + } + Lesson lesson = course.getLesson(lessonDir.getName()); + Task task = lesson.getTask(taskDir.getName()); + TaskFile taskFile = task.getTaskFile(file.getName()); + final Map<TaskFile, TaskFile> taskFilesCopy = new HashMap<TaskFile, TaskFile>(); + for (final Map.Entry<String, TaskFile> entry : task.getTaskFiles().entrySet()) { + if (entry.getValue() == taskFile) { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + @Override + public void run() { + CreateCourseArchive.createUserFile(project, taskFilesCopy, taskDir.getVirtualFile(), taskDir.getVirtualFile(), entry); + } + }); + } + } + String userFileName = FileUtil.getNameWithoutExtension(file.getName()) + ".py"; + VirtualFile userFile = taskDir.getVirtualFile().findChild(userFileName); + if (userFile != null) { + FileEditorManager.getInstance(project).openFile(userFile, true); + Editor editor = FileEditorManager.getInstance(project).getSelectedTextEditor(); + if (editor == null) { + return; + } + for (TaskWindow taskWindow : taskFile.getTaskWindows()) { + taskWindow.drawHighlighter(editor, true); + } + CreateCourseArchive.resetTaskFiles(taskFilesCopy); + } + } +}
\ No newline at end of file diff --git a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/actions/CreateCourseArchive.java b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/actions/CreateCourseArchive.java index 05428f4e82d1..8db49156c987 100644 --- a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/actions/CreateCourseArchive.java +++ b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/actions/CreateCourseArchive.java @@ -18,6 +18,7 @@ import com.intellij.openapi.ui.Messages; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFileManager; import com.intellij.util.io.ZipUtil; +import org.jetbrains.annotations.NotNull; import org.jetbrains.plugins.coursecreator.CCProjectService; import org.jetbrains.plugins.coursecreator.StudyDocumentListener; import org.jetbrains.plugins.coursecreator.format.*; @@ -29,8 +30,8 @@ import java.util.zip.ZipOutputStream; public class CreateCourseArchive extends DumbAwareAction { private static final Logger LOG = Logger.getInstance(CreateCourseArchive.class.getName()); - String myZipName; - String myLocationDir; + private String myZipName; + private String myLocationDir; public void setZipName(String zipName) { myZipName = zipName; @@ -60,46 +61,124 @@ public class CreateCourseArchive extends DumbAwareAction { } final VirtualFile baseDir = project.getBaseDir(); final Map<String, Lesson> lessons = course.getLessonsMap(); - //List<FileEditor> editorList = new ArrayList<FileEditor>(); - Map<VirtualFile, TaskFile> taskFiles = new HashMap<VirtualFile, TaskFile>(); + //map to store initial task file + final Map<TaskFile, TaskFile> taskFiles = new HashMap<TaskFile, TaskFile>(); for (Map.Entry<String, Lesson> lesson : lessons.entrySet()) { final VirtualFile lessonDir = baseDir.findChild(lesson.getKey()); if (lessonDir == null) continue; for (Map.Entry<String, Task> task : lesson.getValue().myTasksMap.entrySet()) { final VirtualFile taskDir = lessonDir.findChild(task.getKey()); if (taskDir == null) continue; - for (Map.Entry<String, TaskFile> entry : task.getValue().task_files.entrySet()) { - final VirtualFile file = taskDir.findChild(entry.getKey()); - if (file == null) continue; - final Document document = FileDocumentManager.getInstance().getDocument(file); - if (document == null) continue; - final TaskFile taskFile = entry.getValue(); - document.addDocumentListener(new InsertionListener(taskFile)); - taskFiles.put(file, taskFile); - taskFile.setTrackChanges(false); - Collections.sort(taskFile.getTaskWindows()); - for (int i = taskFile.getTaskWindows().size() - 1; i >=0 ; i--) { - final TaskWindow taskWindow = taskFile.getTaskWindows().get(i); - final String taskText = taskWindow.getTaskText(); - final int lineStartOffset = document.getLineStartOffset(taskWindow.line); - final int offset = lineStartOffset + taskWindow.start; - CommandProcessor.getInstance().executeCommand(project, new Runnable() { - @Override - public void run() { - ApplicationManager.getApplication().runWriteAction(new Runnable() { - @Override - public void run() { - document.replaceString(offset, offset + taskWindow.getReplacementLength(), taskText); - FileDocumentManager.getInstance().saveDocument(document); - } - }); - } - }, "x", "qwe"); - } + for (final Map.Entry<String, TaskFile> entry : task.getValue().task_files.entrySet()) { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + @Override + public void run() { + createUserFile(project, taskFiles, taskDir, taskDir, entry); + } + }); } } } generateJson(project); + packCourse(baseDir, lessons); + resetTaskFiles(taskFiles); + synchronize(project); + } + + public static void createUserFile(@NotNull final Project project, + @NotNull final Map<TaskFile, TaskFile> taskFilesCopy, + @NotNull final VirtualFile userFileDir, + @NotNull final VirtualFile answerFileDir, + @NotNull final Map.Entry<String, TaskFile> taskFiles) { + final String name = taskFiles.getKey(); + VirtualFile file = userFileDir.findChild(name); + if (file != null) { + try { + file.delete(project); + } + catch (IOException e) { + LOG.error(e); + } + } + try { + userFileDir.createChildData(project, name); + } + catch (IOException e) { + LOG.error(e); + } + + file = userFileDir.findChild(name); + assert file != null; + String answerFileName = file.getNameWithoutExtension() + ".answer"; + VirtualFile answerFile = answerFileDir.findChild(answerFileName); + if (answerFile == null) { + return; + } + final Document answerDocument = FileDocumentManager.getInstance().getDocument(answerFile); + if (answerDocument == null) { + return; + } + final Document document = FileDocumentManager.getInstance().getDocument(file); + if (document == null) return; + final TaskFile taskFile = taskFiles.getValue(); + TaskFile taskFileSaved = new TaskFile(); + taskFile.copy(taskFileSaved); + CommandProcessor.getInstance().executeCommand(project, new Runnable() { + @Override + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + @Override + public void run() { + document.replaceString(0, document.getTextLength(), answerDocument.getText()); + } + }); + } + }, "x", "qwe"); + InsertionListener listener = new InsertionListener(taskFile); + document.addDocumentListener(listener); + taskFilesCopy.put(taskFile, taskFileSaved); + Collections.sort(taskFile.getTaskWindows()); + for (int i = taskFile.getTaskWindows().size() - 1; i >= 0; i--) { + final TaskWindow taskWindow = taskFile.getTaskWindows().get(i); + replaceTaskWindow(project, document, taskWindow); + } + document.removeDocumentListener(listener); + } + + private static void replaceTaskWindow(@NotNull final Project project, + @NotNull final Document document, + @NotNull final TaskWindow taskWindow) { + final String taskText = taskWindow.getTaskText(); + final int lineStartOffset = document.getLineStartOffset(taskWindow.line); + final int offset = lineStartOffset + taskWindow.start; + CommandProcessor.getInstance().executeCommand(project, new Runnable() { + @Override + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + @Override + public void run() { + document.replaceString(offset, offset + taskWindow.getReplacementLength(), taskText); + FileDocumentManager.getInstance().saveDocument(document); + } + }); + } + }, "x", "qwe"); + } + + private static void synchronize(@NotNull final Project project) { + VirtualFileManager.getInstance().refreshWithoutFileWatcher(true); + ProjectView.getInstance(project).refresh(); + } + + public static void resetTaskFiles(@NotNull final Map<TaskFile, TaskFile> taskFiles) { + for (Map.Entry<TaskFile, TaskFile> entry : taskFiles.entrySet()) { + TaskFile realTaskFile = entry.getKey(); + TaskFile savedTaskFile = entry.getValue(); + realTaskFile.update(savedTaskFile); + } + } + + private void packCourse(@NotNull final VirtualFile baseDir, @NotNull final Map<String, Lesson> lessons) { try { File zipFile = new File(myLocationDir, myZipName + ".zip"); ZipOutputStream zos = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(zipFile))); @@ -108,7 +187,12 @@ public class CreateCourseArchive extends DumbAwareAction { final VirtualFile lessonDir = baseDir.findChild(entry.getKey()); if (lessonDir == null) continue; - ZipUtil.addFileOrDirRecursively(zos, null, new File(lessonDir.getPath()), lessonDir.getName(), null, null); + ZipUtil.addFileOrDirRecursively(zos, null, new File(lessonDir.getPath()), lessonDir.getName(), new FileFilter() { + @Override + public boolean accept(File pathname) { + return !pathname.getName().contains(".answer"); + } + }, null); } ZipUtil.addFileOrDirRecursively(zos, null, new File(baseDir.getPath(), "hints"), "hints", null, null); ZipUtil.addFileOrDirRecursively(zos, null, new File(baseDir.getPath(), "course.json"), "course.json", null, null); @@ -119,36 +203,9 @@ public class CreateCourseArchive extends DumbAwareAction { catch (IOException e1) { LOG.error(e1); } - - for (Map.Entry<VirtualFile, TaskFile> entry: taskFiles.entrySet()) { - TaskFile value = entry.getValue(); - final Document document = FileDocumentManager.getInstance().getDocument(entry.getKey()); - if (document == null) { - continue; - } - for (final TaskWindow taskWindow : value.getTaskWindows()){ - final int lineStartOffset = document.getLineStartOffset(taskWindow.line); - final int offset = lineStartOffset + taskWindow.start; - CommandProcessor.getInstance().executeCommand(project, new Runnable() { - @Override - public void run() { - ApplicationManager.getApplication().runWriteAction(new Runnable() { - @Override - public void run() { - document.replaceString(offset, offset + taskWindow.length, taskWindow.getPossibleAnswer()); - FileDocumentManager.getInstance().saveDocument(document); - } - }); - } - }, "x", "qwe"); - } - value.setTrackChanges(true); - } - VirtualFileManager.getInstance().refreshWithoutFileWatcher(true); - ProjectView.getInstance(project).refresh(); } - private void generateJson(Project project) { + private static void generateJson(@NotNull final Project project) { final CCProjectService service = CCProjectService.getInstance(project); final Course course = service.getCourse(); final Gson gson = new GsonBuilder().setPrettyPrinting().excludeFieldsWithoutExposeAnnotation().create(); @@ -179,7 +236,7 @@ public class CreateCourseArchive extends DumbAwareAction { } } - private class InsertionListener extends StudyDocumentListener { + private static class InsertionListener extends StudyDocumentListener { public InsertionListener(TaskFile taskFile) { super(taskFile); @@ -187,12 +244,7 @@ public class CreateCourseArchive extends DumbAwareAction { @Override protected void updateTaskWindowLength(CharSequence fragment, TaskWindow taskWindow, int change) { - //we don't need to update task window length - } - - @Override - protected boolean needModify() { - return true; + //we don't need to update task window length } } }
\ No newline at end of file diff --git a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/actions/CreateTask.java b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/actions/CreateTask.java index 0940135b97be..57a37b3f4194 100644 --- a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/actions/CreateTask.java +++ b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/actions/CreateTask.java @@ -18,6 +18,7 @@ import com.intellij.openapi.ui.Messages; import com.intellij.psi.PsiDirectory; import com.intellij.psi.PsiElement; import com.intellij.util.PlatformIcons; +import org.jetbrains.annotations.NotNull; import org.jetbrains.plugins.coursecreator.CCProjectService; import org.jetbrains.plugins.coursecreator.format.Course; import org.jetbrains.plugins.coursecreator.format.Lesson; @@ -29,7 +30,7 @@ public class CreateTask extends DumbAwareAction { } @Override - public void actionPerformed(AnActionEvent e) { + public void actionPerformed(final AnActionEvent e) { final IdeView view = e.getData(LangDataKeys.IDE_VIEW); final Project project = e.getData(CommonDataKeys.PROJECT); @@ -42,7 +43,7 @@ public class CreateTask extends DumbAwareAction { final CCProjectService service = CCProjectService.getInstance(project); final Course course = service.getCourse(); final Lesson lesson = course.getLesson(directory.getName()); - final int size = lesson.getTasklist().size(); + final int size = lesson.getTaskList().size(); final String taskName = Messages.showInputDialog("Name:", "Task Name", null, "task" + (size + 1), null); if (taskName == null) return; @@ -54,17 +55,16 @@ public class CreateTask extends DumbAwareAction { if (taskDirectory != null) { final FileTemplate template = FileTemplateManager.getInstance().getInternalTemplate("task.html"); final FileTemplate testsTemplate = FileTemplateManager.getInstance().getInternalTemplate("tests"); - final FileTemplate taskTemplate = FileTemplateManager.getInstance().getInternalTemplate("task.py"); + final FileTemplate taskTemplate = FileTemplateManager.getInstance().getInternalTemplate("task.answer"); try { final PsiElement taskFile = FileTemplateUtil.createFromTemplate(template, "task.html", null, taskDirectory); final PsiElement testsFile = FileTemplateUtil.createFromTemplate(testsTemplate, "tests.py", null, taskDirectory); - final PsiElement taskPyFile = FileTemplateUtil.createFromTemplate(taskTemplate, "file1" + ".py", null, taskDirectory); + final PsiElement taskPyFile = FileTemplateUtil.createFromTemplate(taskTemplate, "file1", null, taskDirectory); final Task task = new Task(taskName); - task.addTaskFile(taskPyFile.getContainingFile().getName(), size + 1); + task.addTaskFile("file1.py", size + 1); task.setIndex(size + 1); lesson.addTask(task, taskDirectory); - ApplicationManager.getApplication().invokeLater(new Runnable() { @Override public void run() { @@ -84,7 +84,7 @@ public class CreateTask extends DumbAwareAction { } @Override - public void update(AnActionEvent event) { + public void update(@NotNull AnActionEvent event) { final Presentation presentation = event.getPresentation(); final Project project = event.getData(CommonDataKeys.PROJECT); if (project == null) { diff --git a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/actions/CreateTaskFile.java b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/actions/CreateTaskFile.java index 5aafcebefd6e..368be275a5a4 100644 --- a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/actions/CreateTaskFile.java +++ b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/actions/CreateTaskFile.java @@ -55,10 +55,10 @@ public class CreateTaskFile extends DumbAwareAction { ApplicationManager.getApplication().runWriteAction(new Runnable() { @Override public void run() { - final FileTemplate taskTemplate = FileTemplateManager.getInstance().getInternalTemplate("task.py"); + final FileTemplate taskTemplate = FileTemplateManager.getInstance().getInternalTemplate("task.answer"); try { - final PsiElement taskPyFile = FileTemplateUtil.createFromTemplate(taskTemplate, taskFileName + ".py", null, taskDir); - task.addTaskFile(taskPyFile.getContainingFile().getName(), index); + final PsiElement taskPyFile = FileTemplateUtil.createFromTemplate(taskTemplate, taskFileName, null, taskDir); + task.addTaskFile(taskFileName + ".py", index); ApplicationManager.getApplication().invokeLater(new Runnable() { @Override public void run() { diff --git a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/format/Course.java b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/format/Course.java index eb62d59cd9b1..e124a6eb305d 100644 --- a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/format/Course.java +++ b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/format/Course.java @@ -4,10 +4,7 @@ import com.google.gson.annotations.Expose; import com.intellij.psi.PsiDirectory; import org.jetbrains.annotations.NotNull; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; public class Course { @Expose public List<Lesson> lessons = new ArrayList<Lesson>(); @@ -52,4 +49,13 @@ public class Course { public String getDescription() { return description; } + + public void init() { + lessons.clear(); + for (Lesson lesson: myLessonsMap.values()) { + lessons.add(lesson); + lesson.init(); + } + Collections.sort(lessons); + } } diff --git a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/format/Lesson.java b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/format/Lesson.java index 38720140caf1..bd91e8ec30af 100644 --- a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/format/Lesson.java +++ b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/format/Lesson.java @@ -4,12 +4,9 @@ import com.google.gson.annotations.Expose; import com.intellij.psi.PsiDirectory; import org.jetbrains.annotations.NotNull; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; -public class Lesson { +public class Lesson implements Comparable{ @Expose public String name; @Expose public List<Task> task_list = new ArrayList<Task>(); @@ -27,11 +24,15 @@ public class Lesson { task_list.add(task); } + public void setName(String name) { + this.name = name; + } + public Task getTask(@NotNull final String name) { return myTasksMap.get(name); } - public List<Task> getTasklist() { + public List<Task> getTaskList() { return task_list; } @@ -42,4 +43,22 @@ public class Lesson { public int getIndex() { return myIndex; } + + public Map<String, Task> getTasksMap() { + return myTasksMap; + } + + public void init() { + task_list.clear(); + for (Task task : myTasksMap.values()) { + task_list.add(task); + } + Collections.sort(task_list); + } + + @Override + public int compareTo(@NotNull Object o) { + Lesson lesson = (Lesson) o; + return myIndex - lesson.getIndex(); + } } diff --git a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/format/Task.java b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/format/Task.java index e6c085b5d6a1..886add86ceb4 100644 --- a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/format/Task.java +++ b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/format/Task.java @@ -2,11 +2,12 @@ package org.jetbrains.plugins.coursecreator.format; import com.google.gson.annotations.Expose; import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.coursecreator.CCProjectService; import java.util.HashMap; import java.util.Map; -public class Task { +public class Task implements Comparable{ @Expose public String name; @Expose public Map<String, TaskFile> task_files = new HashMap<String, TaskFile>(); public int myIndex; @@ -28,7 +29,8 @@ public class Task { } public TaskFile getTaskFile(@NotNull final String name) { - return task_files.get(name); + String fileName = CCProjectService.getRealTaskFileName(name); + return fileName != null ? task_files.get(fileName) : null; } public void setIndex(int index) { @@ -38,4 +40,18 @@ public class Task { public Map<String, TaskFile> getTaskFiles() { return task_files; } + + public boolean isTaskFile(String name) { + return task_files.get(name) != null; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public int compareTo(@NotNull Object o) { + Task task = (Task) o; + return myIndex - task.getIndex(); + } } diff --git a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/format/TaskFile.java b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/format/TaskFile.java index 85f0d91983f2..b88e375bc265 100644 --- a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/format/TaskFile.java +++ b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/format/TaskFile.java @@ -13,15 +13,6 @@ import java.util.List; public class TaskFile { @Expose public List<TaskWindow> task_windows = new ArrayList<TaskWindow>(); public int myIndex; - public boolean myTrackChanges = true; - - public boolean isTrackChanges() { - return myTrackChanges; - } - - public void setTrackChanges(boolean trackChanges) { - myTrackChanges = trackChanges; - } public TaskFile() {} @@ -108,4 +99,22 @@ public class TaskFile { } } } + + public void copy(@NotNull final TaskFile target) { + target.setIndex(myIndex); + for (TaskWindow taskWindow : task_windows) { + TaskWindow savedWindow = new TaskWindow(taskWindow.getLine(), taskWindow.getStart(), + taskWindow.getLength(), ""); + target.getTaskWindows().add(savedWindow); + savedWindow.setIndex(taskWindow.getIndex()); + } + } + + public void update(@NotNull final TaskFile source) { + for (TaskWindow taskWindow : source.getTaskWindows()) { + TaskWindow taskWindowUpdated = task_windows.get(taskWindow.getIndex() - 1); + taskWindowUpdated.setLine(taskWindow.getLine()); + taskWindowUpdated.setStart(taskWindow.getStart()); + } + } } diff --git a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/format/TaskWindow.java b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/format/TaskWindow.java index cb6418ec75d7..6b1be7ef3e7d 100644 --- a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/format/TaskWindow.java +++ b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/format/TaskWindow.java @@ -68,16 +68,17 @@ public class TaskWindow implements Comparable{ } } - public void drawHighlighter(@NotNull final Editor editor) { + public void drawHighlighter(@NotNull final Editor editor, boolean useLength) { int startOffset = editor.getDocument().getLineStartOffset(line) + start; - int endOffset = startOffset + myReplacementLength; + int highlighterLength = useLength ? length : myReplacementLength; + int endOffset = startOffset + highlighterLength; TextAttributes defaultTestAttributes = EditorColorsManager.getInstance().getGlobalScheme().getAttributes(EditorColors.LIVE_TEMPLATE_ATTRIBUTES); RangeHighlighter highlighter = editor.getMarkupModel().addRangeHighlighter(startOffset, endOffset, HighlighterLayer.LAST + 1, defaultTestAttributes, HighlighterTargetArea.EXACT_RANGE); highlighter.setGreedyToLeft(true); - highlighter.setGreedyToRight(true); + highlighter.setGreedyToRight(false); } public int getIndex() { @@ -123,10 +124,6 @@ public class TaskWindow implements Comparable{ return lineDiff; } - public String getPossibleAnswer() { - return possible_answer; - } - public int getLength() { return length; } diff --git a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/projectView/CCDirectoryNode.java b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/projectView/CCDirectoryNode.java index 1a7304123f0a..0969c54563c2 100644 --- a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/projectView/CCDirectoryNode.java +++ b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/projectView/CCDirectoryNode.java @@ -26,6 +26,8 @@ public class CCDirectoryNode extends PsiDirectoryNode { @Override protected void updateImpl(PresentationData data) { + //TODO:change presentable name for files with suffix _answer + String valueName = myValue.getName(); final Course course = CCProjectService.getInstance(myProject).getCourse(); if (course == null) return; diff --git a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/projectView/CCTreeStructureProvider.java b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/projectView/CCTreeStructureProvider.java index 69b78ec9a92b..b4fb50a96dc4 100644 --- a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/projectView/CCTreeStructureProvider.java +++ b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/projectView/CCTreeStructureProvider.java @@ -2,9 +2,11 @@ package org.jetbrains.plugins.coursecreator.projectView; import com.intellij.ide.projectView.TreeStructureProvider; import com.intellij.ide.projectView.ViewSettings; +import com.intellij.ide.projectView.impl.nodes.PsiFileNode; import com.intellij.ide.util.treeView.AbstractTreeNode; import com.intellij.openapi.project.DumbAware; import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiDirectory; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -27,11 +29,21 @@ public class CCTreeStructureProvider implements TreeStructureProvider, DumbAware Project project = node.getProject(); if (project != null) { if (node.getValue() instanceof PsiDirectory) { - PsiDirectory directory = (PsiDirectory) node.getValue(); + PsiDirectory directory = (PsiDirectory)node.getValue(); nodes.add(new CCDirectoryNode(project, directory, settings)); - } else { - nodes.add(node); + continue; } + if (node instanceof PsiFileNode) { + PsiFileNode fileNode = (PsiFileNode)node; + VirtualFile virtualFile = fileNode.getVirtualFile(); + if (virtualFile == null) { + continue; + } + if (CCProjectService.getInstance(project).isTaskFile(virtualFile)) { + continue; + } + } + nodes.add(node); } } return nodes; diff --git a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/ui/CreateCourseArchivePanel.form b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/ui/CreateCourseArchivePanel.form index 920dcb9494a7..096a85f8da59 100644 --- a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/ui/CreateCourseArchivePanel.form +++ b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/ui/CreateCourseArchivePanel.form @@ -27,7 +27,7 @@ <component id="160bb" class="javax.swing.JTextField" binding="myNameField" default-binding="true"> <constraints> <grid row="0" column="1" row-span="2" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false"> - <preferred-size width="150" height="-1"/> + <preferred-size width="300" height="-1"/> </grid> </constraints> <properties/> diff --git a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/ui/CreateTaskWindowDialog.java b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/ui/CreateTaskWindowDialog.java index c7e8f715672c..53a4a77b97f7 100644 --- a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/ui/CreateTaskWindowDialog.java +++ b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/ui/CreateTaskWindowDialog.java @@ -150,4 +150,10 @@ public class CreateTaskWindowDialog extends DialogWrapper { public void validateInput() { super.initValidation(); } + + @Nullable + @Override + public JComponent getPreferredFocusedComponent() { + return myPanel.getPreferredFocusedComponent(); + } } diff --git a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/ui/CreateTaskWindowPanel.java b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/ui/CreateTaskWindowPanel.java index 21a7eb063c63..a50840deaf2f 100644 --- a/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/ui/CreateTaskWindowPanel.java +++ b/python/edu/course-creator/src/org/jetbrains/plugins/coursecreator/ui/CreateTaskWindowPanel.java @@ -37,6 +37,7 @@ public class CreateTaskWindowPanel extends JPanel { } }); + myTaskWindowText.grabFocus(); myHintName.getDocument().addDocumentListener(new DocumentAdapter() { @Override protected void textChanged(DocumentEvent e) { @@ -93,4 +94,8 @@ public class CreateTaskWindowPanel extends JPanel { public void setGeneratedHintName(String generatedHintName) { myGeneratedHintName = generatedHintName; } + + public JComponent getPreferredFocusedComponent() { + return myTaskWindowText; + } } diff --git a/python/edu/learn-python/resources/META-INF/plugin.xml b/python/edu/learn-python/resources/META-INF/plugin.xml index 8e8bddcce5e5..b6ff0ce5d0d7 100644 --- a/python/edu/learn-python/resources/META-INF/plugin.xml +++ b/python/edu/learn-python/resources/META-INF/plugin.xml @@ -40,7 +40,7 @@ <actions> <action id="CheckAction" class="com.jetbrains.python.edu.actions.StudyCheckAction" text="check" - description="Runs tests for current tasks" icon="/icons/icon.jpg"> + description="Runs tests for current tasks"> </action> <action id="PrevWindowAction" class="com.jetbrains.python.edu.actions.StudyPrevWindowAction" text="PrevWindowAction" description="prev"> </action> @@ -60,10 +60,12 @@ <add-to-group group-id="MainToolBar" anchor="last"/> </action> - <action id="WelcomeScreen.LearnPython" class="com.jetbrains.python.edu.actions.StudyNewProject" icon="StudyIcons.EducationalProjectType"> + <action id="WelcomeScreen.PythonIntro" class="com.jetbrains.python.edu.actions.StudyIntroductionCourseAction" icon="StudyIcons.EducationalProjectType"> <add-to-group group-id="WelcomeScreen.QuickStart" anchor="first"/> </action> + <action id="ReloadCourseAction" class="com.jetbrains.python.edu.actions.StudyReloadCourseAction"/> + </actions> <extensions defaultExtensionNs="com.intellij"> @@ -74,6 +76,7 @@ <highlightErrorFilter implementation="com.jetbrains.python.edu.StudyHighlightErrorFilter"/> <applicationService serviceInterface="com.intellij.openapi.fileEditor.impl.EditorEmptyTextPainter" serviceImplementation="com.jetbrains.python.edu.StudyInstructionPainter" overrides="true"/> + <errorHandler implementation="com.intellij.diagnostic.ITNReporter"/> </extensions> <extensions defaultExtensionNs="Pythonid"> <visitorFilter language="Python" implementationClass="com.jetbrains.python.edu.highlighting.StudyVisitorFilter"/> diff --git a/python/edu/learn-python/resources/courses/introduction_course.zip b/python/edu/learn-python/resources/courses/introduction_course.zip Binary files differindex c39695e0c380..4fd2dab43830 100644 --- a/python/edu/learn-python/resources/courses/introduction_course.zip +++ b/python/edu/learn-python/resources/courses/introduction_course.zip diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/StudyDirectoryProjectGenerator.java b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyDirectoryProjectGenerator.java index 59bd8bc2ea7c..b558667734bc 100644 --- a/python/edu/learn-python/src/com/jetbrains/python/edu/StudyDirectoryProjectGenerator.java +++ b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyDirectoryProjectGenerator.java @@ -53,7 +53,7 @@ public class StudyDirectoryProjectGenerator extends PythonProjectGenerator imple @NotNull @Override public String getName() { - return "Learn Python"; + return "Educational"; } diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/StudyTestRunner.java b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyTestRunner.java index b0cd5ba89fed..1800deb0a0d9 100644 --- a/python/edu/learn-python/src/com/jetbrains/python/edu/StudyTestRunner.java +++ b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyTestRunner.java @@ -61,7 +61,9 @@ public class StudyTestRunner { try { while ((line = testOutputReader.readLine()) != null) { if (line.contains(TEST_FAILED)) { - return line.substring(TEST_FAILED.length(), line.length()); + String res = line.substring(TEST_FAILED.length(), line.length()); + StudyUtils.closeSilently(testOutputReader); + return res; } } } diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyIntroductionCourseAction.java b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyIntroductionCourseAction.java new file mode 100644 index 000000000000..ed1fff22878f --- /dev/null +++ b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyIntroductionCourseAction.java @@ -0,0 +1,75 @@ +/* + * Copyright 2000-2013 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jetbrains.python.edu.actions; + +import com.intellij.ide.impl.ProjectUtil; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.project.ProjectManager; +import com.intellij.openapi.projectRoots.Sdk; +import com.jetbrains.python.configuration.PyConfigurableInterpreterList; +import com.jetbrains.python.edu.StudyDirectoryProjectGenerator; +import com.jetbrains.python.edu.StudyUtils; +import com.jetbrains.python.edu.course.CourseInfo; +import com.jetbrains.python.newProject.actions.GenerateProjectCallback; +import com.jetbrains.python.newProject.actions.ProjectSpecificSettingsStep; +import icons.StudyIcons; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.util.List; +import java.util.Map; + +public class StudyIntroductionCourseAction extends AnAction { + + public StudyIntroductionCourseAction() { + super("Introduction to Python", "Introduction to Python", StudyIcons.EducationalProjectType); + } + + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + final File projectDir = new File(ProjectUtil.getBaseDir(), "PythonIntroduction"); + if (projectDir.exists()) { + ProjectUtil.openProject(projectDir.getPath(), null, false); + } + else { + final GenerateProjectCallback callback = new GenerateProjectCallback(null); + final StudyDirectoryProjectGenerator generator = new StudyDirectoryProjectGenerator(); + final Map<CourseInfo, File> courses = generator.getCourses(); + CourseInfo introCourse = null; + for (CourseInfo info : courses.keySet()) { + if ("Introduction to Python".equals(info.getName())) { + introCourse = info; + } + } + if (introCourse == null) { + introCourse = StudyUtils.getFirst(courses.keySet()); + } + generator.setSelectedCourse(introCourse); + final ProjectSpecificSettingsStep step = new ProjectSpecificSettingsStep(generator, callback, true); + + step.createPanel(); // initialize panel to set location + step.setLocation(projectDir.toString()); + + final Project project = ProjectManager.getInstance().getDefaultProject(); + final List<Sdk> sdks = PyConfigurableInterpreterList.getInstance(project).getAllPythonSdks(); + Sdk sdk = sdks.isEmpty() ? null : sdks.iterator().next(); + step.setSdk(sdk); + callback.consume(step); + } + } +} diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyNewProject.java b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyNewProject.java deleted file mode 100644 index 0b75c4bc01c4..000000000000 --- a/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyNewProject.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2000-2013 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.jetbrains.python.edu.actions; - -import com.jetbrains.python.edu.StudyDirectoryProjectGenerator; -import com.jetbrains.python.newProject.actions.GenerateProjectCallback; -import com.jetbrains.python.newProject.actions.ProjectSpecificAction; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -public class StudyNewProject extends ProjectSpecificAction { - - public StudyNewProject(@NotNull final String name, @Nullable final Runnable runnable) { - super(new GenerateProjectCallback(runnable), new StudyDirectoryProjectGenerator(), name, true); - } - - public StudyNewProject() { - this("Learn Python", null); - } - -} diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyRefreshTaskFileAction.java b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyRefreshTaskFileAction.java index a9448ddea0e3..a6c16d2553bf 100644 --- a/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyRefreshTaskFileAction.java +++ b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyRefreshTaskFileAction.java @@ -22,108 +22,139 @@ import com.jetbrains.python.edu.StudyTaskManager; import com.jetbrains.python.edu.StudyUtils; import com.jetbrains.python.edu.course.*; import com.jetbrains.python.edu.editor.StudyEditor; +import org.jetbrains.annotations.NotNull; import java.io.*; public class StudyRefreshTaskFileAction extends DumbAwareAction { private static final Logger LOG = Logger.getInstance(StudyRefreshTaskFileAction.class.getName()); - public void refresh(final Project project) { - ApplicationManager.getApplication().invokeLater(new Runnable() { + public static void refresh(final Project project) { + ApplicationManager.getApplication().invokeLater(new Runnable() { + @Override + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + @SuppressWarnings("IOResourceOpenedButNotSafelyClosed") @Override public void run() { - ApplicationManager.getApplication().runWriteAction(new Runnable() { - @SuppressWarnings("IOResourceOpenedButNotSafelyClosed") - @Override - public void run() { - final Editor editor = StudyEditor.getSelectedEditor(project); - assert editor != null; - final Document document = editor.getDocument(); - StudyDocumentListener listener = StudyEditor.getListener(document); - if (listener != null) { - document.removeDocumentListener(listener); - } - final int lineCount = document.getLineCount(); - if (lineCount != 0) { - CommandProcessor.getInstance().runUndoTransparentAction(new Runnable() { - @Override - public void run() { - document.deleteString(0, document.getLineEndOffset(lineCount - 1)); - } - }); - } - StudyTaskManager taskManager = StudyTaskManager.getInstance(project); - Course course = taskManager.getCourse(); - assert course != null; - File resourceFile = new File(course.getResourcePath()); - File resourceRoot = resourceFile.getParentFile(); - FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance(); - VirtualFile openedFile = fileDocumentManager.getFile(document); - assert openedFile != null; - final TaskFile selectedTaskFile = taskManager.getTaskFile(openedFile); - assert selectedTaskFile != null; - Task currentTask = selectedTaskFile.getTask(); - String lessonDir = Lesson.LESSON_DIR + String.valueOf(currentTask.getLesson().getIndex() + 1); - String taskDir = Task.TASK_DIR + String.valueOf(currentTask.getIndex() + 1); - File pattern = new File(new File(new File(resourceRoot, lessonDir), taskDir), openedFile.getName()); - BufferedReader reader = null; - try { - reader = new BufferedReader(new InputStreamReader(new FileInputStream(pattern))); - String line; - StringBuilder patternText = new StringBuilder(); - while ((line = reader.readLine()) != null) { - patternText.append(line); - patternText.append("\n"); - } - int patternLength = patternText.length(); - if (patternText.charAt(patternLength - 1) == '\n') { - patternText.delete(patternLength - 1, patternLength); - } - document.setText(patternText); - StudyStatus oldStatus = currentTask.getStatus(); - LessonInfo lessonInfo = currentTask.getLesson().getLessonInfo(); - lessonInfo.update(oldStatus, -1); - lessonInfo.update(StudyStatus.Unchecked, +1); - StudyUtils.updateStudyToolWindow(project); - for (TaskWindow taskWindow : selectedTaskFile.getTaskWindows()) { - taskWindow.reset(); - } - ProjectView.getInstance(project).refresh(); - if (listener != null) { - document.addDocumentListener(listener); - } - selectedTaskFile.drawAllWindows(editor); - ApplicationManager.getApplication().invokeLater(new Runnable() { - @Override - public void run() { - IdeFocusManager.getInstance(project).requestFocus(editor.getContentComponent(), true); - } - }); - selectedTaskFile.navigateToFirstTaskWindow(editor); - BalloonBuilder balloonBuilder = - JBPopupFactory.getInstance().createHtmlTextBalloonBuilder("You can now start again", MessageType.INFO, null); - final Balloon balloon = balloonBuilder.createBalloon(); - StudyEditor selectedStudyEditor = StudyEditor.getSelectedStudyEditor(project); - assert selectedStudyEditor != null; - balloon.showInCenterOf(selectedStudyEditor.getRefreshButton()); - Disposer.register(project, balloon); - } - catch (FileNotFoundException e1) { - LOG.error(e1); - } - catch (IOException e1) { - LOG.error(e1); - } - finally { - StudyUtils.closeSilently(reader); - } - } - }); + final Editor editor = StudyEditor.getSelectedEditor(project); + assert editor != null; + final Document document = editor.getDocument(); + refreshFile(editor, document, project); } }); + } + }); } - public void actionPerformed(AnActionEvent e) { + public static void refreshFile(@NotNull final Editor editor, @NotNull final Document document, @NotNull final Project project) { + StudyTaskManager taskManager = StudyTaskManager.getInstance(project); + Course course = taskManager.getCourse(); + assert course != null; + FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance(); + VirtualFile openedFile = fileDocumentManager.getFile(document); + assert openedFile != null; + final TaskFile selectedTaskFile = taskManager.getTaskFile(openedFile); + assert selectedTaskFile != null; + String openedFileName = openedFile.getName(); + Task currentTask = selectedTaskFile.getTask(); + resetTaskFile(document, project, course, selectedTaskFile, openedFileName, currentTask); + selectedTaskFile.drawAllWindows(editor); + ApplicationManager.getApplication().invokeLater(new Runnable() { + @Override + public void run() { + IdeFocusManager.getInstance(project).requestFocus(editor.getContentComponent(), true); + } + }); + selectedTaskFile.navigateToFirstTaskWindow(editor); + showBaloon(project); + } + + public static void resetTaskFile(Document document, Project project, Course course, TaskFile taskFile, String name, Task task) { + resetDocument(document, course, name, task); + updateLessonInfo(task); + StudyUtils.updateStudyToolWindow(project); + resetTaskWindows(taskFile); + ProjectView.getInstance(project).refresh(); + } + + private static void showBaloon(Project project) { + BalloonBuilder balloonBuilder = + JBPopupFactory.getInstance().createHtmlTextBalloonBuilder("You can now start again", MessageType.INFO, null); + final Balloon balloon = balloonBuilder.createBalloon(); + StudyEditor selectedStudyEditor = StudyEditor.getSelectedStudyEditor(project); + assert selectedStudyEditor != null; + balloon.showInCenterOf(selectedStudyEditor.getRefreshButton()); + Disposer.register(project, balloon); + } + + private static void resetTaskWindows(TaskFile selectedTaskFile) { + for (TaskWindow taskWindow : selectedTaskFile.getTaskWindows()) { + taskWindow.reset(); + } + } + + private static void updateLessonInfo(Task currentTask) { + StudyStatus oldStatus = currentTask.getStatus(); + LessonInfo lessonInfo = currentTask.getLesson().getLessonInfo(); + lessonInfo.update(oldStatus, -1); + lessonInfo.update(StudyStatus.Unchecked, +1); + } + + @SuppressWarnings("IOResourceOpenedButNotSafelyClosed") + private static void resetDocument(Document document, Course course, String fileName, Task task) { + BufferedReader reader = null; + StudyDocumentListener listener = StudyEditor.getListener(document); + if (listener != null) { + document.removeDocumentListener(listener); + } + clearDocument(document); + try { + String lessonDir = Lesson.LESSON_DIR + String.valueOf(task.getLesson().getIndex() + 1); + String taskDir = Task.TASK_DIR + String.valueOf(task.getIndex() + 1); + File resourceFile = new File(course.getResourcePath()); + File resourceRoot = resourceFile.getParentFile(); + File pattern = new File(new File(new File(resourceRoot, lessonDir), taskDir), fileName); + reader = new BufferedReader(new InputStreamReader(new FileInputStream(pattern))); + String line; + StringBuilder patternText = new StringBuilder(); + while ((line = reader.readLine()) != null) { + patternText.append(line); + patternText.append("\n"); + } + int patternLength = patternText.length(); + if (patternText.charAt(patternLength - 1) == '\n') { + patternText.delete(patternLength - 1, patternLength); + } + document.setText(patternText); + } + catch (FileNotFoundException e) { + LOG.error(e); + } + catch (IOException e) { + LOG.error(e); + } + finally { + StudyUtils.closeSilently(reader); + } + if (listener != null) { + document.addDocumentListener(listener); + } + } + + private static void clearDocument(final Document document) { + final int lineCount = document.getLineCount(); + if (lineCount != 0) { + CommandProcessor.getInstance().runUndoTransparentAction(new Runnable() { + @Override + public void run() { + document.deleteString(0, document.getLineEndOffset(lineCount - 1)); + } + }); + } + } + + public void actionPerformed(@NotNull AnActionEvent e) { refresh(e.getProject()); } } diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyReloadCourseAction.java b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyReloadCourseAction.java new file mode 100644 index 000000000000..beefaaa94f7e --- /dev/null +++ b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyReloadCourseAction.java @@ -0,0 +1,134 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jetbrains.python.edu.actions; + +import com.intellij.ide.projectView.ProjectView; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.Presentation; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.fileEditor.FileDocumentManager; +import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.project.DumbAwareAction; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.ui.tree.TreeUtil; +import com.jetbrains.python.edu.StudyTaskManager; +import com.jetbrains.python.edu.StudyUtils; +import com.jetbrains.python.edu.course.Course; +import com.jetbrains.python.edu.course.Lesson; +import com.jetbrains.python.edu.course.Task; +import com.jetbrains.python.edu.course.TaskFile; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import javax.swing.tree.TreePath; +import java.util.List; +import java.util.Map; + +public class StudyReloadCourseAction extends DumbAwareAction { + + public StudyReloadCourseAction() { + super("Reload Course", "Reload Course", null); + } + + @Override + public void update(@NotNull AnActionEvent e) { + Presentation presentation = e.getPresentation(); + Project project = e.getProject(); + if (project != null) { + Course course = StudyTaskManager.getInstance(project).getCourse(); + if (course != null) { + presentation.setVisible(true); + presentation.setEnabled(true); + } + } + presentation.setVisible(false); + presentation.setEnabled(false); + } + + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + Project project = e.getProject(); + if (project == null) { + return; + } + reloadCourse(project); + } + + public static void reloadCourse(@NotNull final Project project) { + ApplicationManager.getApplication().invokeLater(new Runnable() { + @Override + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + @Override + public void run() { + Course course = StudyTaskManager.getInstance(project).getCourse(); + if (course == null) { + return; + } + for (VirtualFile file : FileEditorManager.getInstance(project).getOpenFiles()) { + FileEditorManager.getInstance(project).closeFile(file); + } + JTree tree = ProjectView.getInstance(project).getCurrentProjectViewPane().getTree(); + TreePath path = TreeUtil.getFirstNodePath(tree); + tree.collapsePath(path); + List<Lesson> lessons = course.getLessons(); + for (Lesson lesson : lessons) { + List<Task> tasks = lesson.getTaskList(); + VirtualFile lessonDir = project.getBaseDir().findChild(Lesson.LESSON_DIR + (lesson.getIndex() + 1)); + if (lessonDir == null) { + continue; + } + for (Task task : tasks) { + VirtualFile taskDir = lessonDir.findChild(Task.TASK_DIR + (task.getIndex() + 1)); + if (taskDir == null) { + continue; + } + Map<String, TaskFile> taskFiles = task.getTaskFiles(); + for (Map.Entry<String, TaskFile> entry : taskFiles.entrySet()) { + String name = entry.getKey(); + TaskFile taskFile = entry.getValue(); + VirtualFile file = taskDir.findChild(name); + if (file == null) { + continue; + } + Document document = FileDocumentManager.getInstance().getDocument(file); + if (document == null) { + continue; + } + StudyRefreshTaskFileAction.resetTaskFile(document, project, course, taskFile, name, task); + } + } + } + Lesson firstLesson = StudyUtils.getFirst(lessons); + if (firstLesson == null) { + return; + } + Task firstTask = StudyUtils.getFirst(firstLesson.getTaskList()); + VirtualFile lessonDir = project.getBaseDir().findChild(Lesson.LESSON_DIR + (firstLesson.getIndex() + 1)); + if (lessonDir != null) { + VirtualFile taskDir = lessonDir.findChild(Task.TASK_DIR + (firstTask.getIndex() + 1)); + if (taskDir != null) { + ProjectView.getInstance(project).select(taskDir, taskDir, true); + } + } + } + }); + } + }); + } +} diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyTaskNavigationAction.java b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyTaskNavigationAction.java index 46c0981cb964..b98bbd098776 100644 --- a/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyTaskNavigationAction.java +++ b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyTaskNavigationAction.java @@ -1,5 +1,6 @@ package com.jetbrains.python.edu.actions; +import com.intellij.ide.projectView.ProjectView; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.project.DumbAwareAction; @@ -12,6 +13,7 @@ import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.wm.ToolWindow; import com.intellij.openapi.wm.ToolWindowId; import com.intellij.openapi.wm.ToolWindowManager; +import com.intellij.util.ui.tree.TreeUtil; import com.jetbrains.python.edu.StudyState; import com.jetbrains.python.edu.course.Lesson; import com.jetbrains.python.edu.course.Task; @@ -20,6 +22,7 @@ import com.jetbrains.python.edu.editor.StudyEditor; import org.jetbrains.annotations.NotNull; import javax.swing.*; +import javax.swing.tree.TreePath; import java.util.Map; @@ -74,7 +77,11 @@ abstract public class StudyTaskNavigationAction extends DumbAwareAction { } } } + JTree tree = ProjectView.getInstance(project).getCurrentProjectViewPane().getTree(); + TreePath path = TreeUtil.getFirstNodePath(tree); + tree.collapsePath(path); if (shouldBeActive != null) { + ProjectView.getInstance(project).select(shouldBeActive, shouldBeActive, false); FileEditorManager.getInstance(project).openFile(shouldBeActive, true); } ToolWindow runToolWindow = ToolWindowManager.getInstance(project).getToolWindow(ToolWindowId.RUN); diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/projectView/StudyDirectoryNode.java b/python/edu/learn-python/src/com/jetbrains/python/edu/projectView/StudyDirectoryNode.java index 2f80dba12695..d8faacd23946 100644 --- a/python/edu/learn-python/src/com/jetbrains/python/edu/projectView/StudyDirectoryNode.java +++ b/python/edu/learn-python/src/com/jetbrains/python/edu/projectView/StudyDirectoryNode.java @@ -1,8 +1,10 @@ package com.jetbrains.python.edu.projectView; import com.intellij.ide.projectView.PresentationData; +import com.intellij.ide.projectView.ProjectView; import com.intellij.ide.projectView.ViewSettings; import com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode; +import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiDirectory; @@ -17,6 +19,7 @@ import org.jetbrains.annotations.NotNull; import javax.swing.*; import java.awt.*; +import java.util.Set; public class StudyDirectoryNode extends PsiDirectoryNode { private final PsiDirectory myValue; @@ -110,4 +113,56 @@ public class StudyDirectoryNode extends PsiDirectoryNode { data.addText(additionalName, new SimpleTextAttributes(Font.PLAIN, color)); data.setIcon(icon); } + + @Override + public boolean canNavigate() { + return true; + } + + @Override + public boolean canNavigateToSource() { + return true; + } + + @Override + public void navigate(boolean requestFocus) { + if (myValue.getName().contains(Task.TASK_DIR)) { + TaskFile taskFile = null; + VirtualFile virtualFile = null; + for (PsiElement child : myValue.getChildren()) { + VirtualFile childFile = child.getContainingFile().getVirtualFile(); + taskFile = StudyTaskManager.getInstance(myProject).getTaskFile(childFile); + if (taskFile != null) { + virtualFile = childFile; + break; + } + } + if (taskFile != null) { + VirtualFile taskDir = virtualFile.getParent(); + Task task = taskFile.getTask(); + for (VirtualFile openFile : FileEditorManager.getInstance(myProject).getOpenFiles()) { + FileEditorManager.getInstance(myProject).closeFile(openFile); + } + VirtualFile child = null; + Set<String> fileNames = task.getTaskFiles().keySet(); + for (String name : fileNames) { + child = taskDir.findChild(name); + if (child != null) { + FileEditorManager.getInstance(myProject).openFile(child, true); + } + } + if (child != null) { + ProjectView.getInstance(myProject).select(child, child, false); + } + } + } + } + + @Override + public boolean expandOnDoubleClick() { + if (myValue.getName().contains(Task.TASK_DIR)) { + return false; + } + return super.expandOnDoubleClick(); + } } diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyToolWindowFactory.java b/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyToolWindowFactory.java index a553978c416a..0f8c5a53511a 100644 --- a/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyToolWindowFactory.java +++ b/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyToolWindowFactory.java @@ -9,6 +9,7 @@ import com.intellij.ui.content.Content; import com.intellij.ui.content.ContentFactory; import com.intellij.util.ui.UIUtil; import com.jetbrains.python.edu.StudyTaskManager; +import com.jetbrains.python.edu.actions.StudyReloadCourseAction; import com.jetbrains.python.edu.course.Course; import com.jetbrains.python.edu.course.Lesson; import com.jetbrains.python.edu.course.LessonInfo; @@ -17,6 +18,8 @@ import org.jetbrains.annotations.NotNull; import javax.swing.*; import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; import java.util.List; public class StudyToolWindowFactory implements ToolWindowFactory, DumbAware { @@ -41,7 +44,15 @@ public class StudyToolWindowFactory implements ToolWindowFactory, DumbAware { contentPanel.add(new JLabel(authorLabel)); contentPanel.add(Box.createRigidArea(new Dimension(0, 10))); contentPanel.add(new JLabel(description)); + contentPanel.add(Box.createRigidArea(new Dimension(0, 10))); + JButton reloadCourseButton = new JButton("reload course"); + reloadCourseButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + StudyReloadCourseAction.reloadCourse(project); + } + }); + contentPanel.add(reloadCourseButton); int taskNum = 0; int taskSolved = 0; int lessonsCompleted = 0; diff --git a/python/edu/resources/idea/PyCharmEduApplicationInfo.xml b/python/edu/resources/idea/PyCharmEduApplicationInfo.xml index eed232dcd3c7..939c84129455 100644 --- a/python/edu/resources/idea/PyCharmEduApplicationInfo.xml +++ b/python/edu/resources/idea/PyCharmEduApplicationInfo.xml @@ -19,5 +19,5 @@ <feedback eap-url="http://www.jetbrains.com/feedback/feedback.jsp?product=PyCharm&build=$BUILD&timezone=$TIMEZONE&eval=$EVAL" release-url="http://www.jetbrains.com/feedback/feedback.jsp?product=PyCharm&build=$BUILD&timezone=$TIMEZONE&eval=$EVAL"/> - <help file="pycharm-eduhelp.jar" root="pycharm"/> + <help file="pycharm-eduhelp.jar" root="pycharm-edu"/> </component> diff --git a/python/helpers/packaging_tool.py b/python/helpers/packaging_tool.py index 9101a16be1b3..c66cbcba2fe2 100644 --- a/python/helpers/packaging_tool.py +++ b/python/helpers/packaging_tool.py @@ -21,7 +21,7 @@ def exit(retcode): def usage(): - sys.stderr.write('Usage: packaging_tool.py <list|search|install|uninstall|pyvenv>\n') + sys.stderr.write('Usage: packaging_tool.py <list|install|uninstall|pyvenv>\n') sys.stderr.flush() exit(ERROR_WRONG_USAGE) @@ -58,13 +58,6 @@ def do_install(pkgs): error_no_pip() return pip.main(['install'] + pkgs) -def do_search(pkgs): - try: - import pip - except ImportError: - error_no_pip() - return pip.main(['search'] + pkgs) - def do_uninstall(pkgs): try: @@ -122,11 +115,6 @@ def main(): if len(sys.argv) != 2: usage() do_list() - elif cmd == 'search': - if len(sys.argv) < 2: - usage() - pkgs = sys.argv[2:] - do_search(pkgs) elif cmd == 'install': if len(sys.argv) < 2: usage() diff --git a/python/helpers/pycharm/_bdd_utils.py b/python/helpers/pycharm/_bdd_utils.py index eea1bebf1654..65d0f93102b9 100644 --- a/python/helpers/pycharm/_bdd_utils.py +++ b/python/helpers/pycharm/_bdd_utils.py @@ -3,7 +3,7 @@ Tools for running BDD frameworks in python. You probably need to extend BddRunner (see its doc). -You may also need "get_path_by_args" that gets folder (current or passed as first argument) +You may also need "get_what_to_run_by_env" that gets folder (current or passed as first argument) """ import os import time @@ -29,20 +29,33 @@ def fix_win_drive(feature_path): os.chdir(feature_disk) -def get_path_by_args(arguments): +def get_what_to_run_by_env(environment): """ - :type arguments list - :param arguments: arguments (sys.argv) - :return: tuple (base_dir, what_to_run) where dir is current or first argument from argv, checking it exists - :rtype tuple of str + :type environment dict + :param environment: os.environment (files and folders should be separated with | and passed to PY_STUFF_TO_RUN). + Scenarios optionally could be passed as SCENARIOS (names or order numbers, depends on runner) + :return: tuple (base_dir, scenarios[], what_to_run(list of feature files or folders))) where dir is current or first argument from env, checking it exists + :rtype tuple of (str, iterable) """ - what_to_run = arguments[1] if len(arguments) > 1 else "." - base_dir = what_to_run - assert os.path.exists(what_to_run), "{} does not exist".format(what_to_run) + if "PY_STUFF_TO_RUN" not in environment: + what_to_run = ["."] + else: + what_to_run = str(environment["PY_STUFF_TO_RUN"]).split("|") - if os.path.isfile(what_to_run): - base_dir = os.path.dirname(what_to_run) # User may point to the file directly - return base_dir, what_to_run + scenarios = [] + if "SCENARIOS" in environment: + scenarios = str(environment["SCENARIOS"]).split("|") + + if not what_to_run: + what_to_run = ["."] + + for path in what_to_run: + assert os.path.exists(path), "{} does not exist".format(path) + + base_dir = what_to_run[0] + if os.path.isfile(what_to_run[0]): + base_dir = os.path.dirname(what_to_run[0]) # User may point to the file directly + return base_dir, scenarios, what_to_run class BddRunner(object): diff --git a/python/helpers/pycharm/behave_runner.py b/python/helpers/pycharm/behave_runner.py index 2ec649ea7c1d..99acfdb9ffeb 100644 --- a/python/helpers/pycharm/behave_runner.py +++ b/python/helpers/pycharm/behave_runner.py @@ -1,13 +1,14 @@ # coding=utf-8 """ Behave BDD runner. -*FIRST* param now: folder to search "features" for. -Each "features" folder should have features and "steps" subdir. +See _bdd_utils#get_path_by_env for information how to pass list of features here. +Each feature could be file, folder with feature files or folder with "features" subfolder Other args are tag expressionsin format (--tags=.. --tags=..). See https://pythonhosted.org/behave/behave.html#tag-expression """ import functools +import glob import sys import os import traceback @@ -15,6 +16,7 @@ import traceback from behave.formatter.base import Formatter from behave.model import Step, ScenarioOutline, Feature, Scenario from behave.tag_expression import TagExpression +import re import _bdd_utils @@ -185,16 +187,19 @@ class _BehaveRunner(_bdd_utils.BddRunner): self.__real_runner.run() - def __filter_scenarios_by_tag(self, scenario): + def __filter_scenarios_by_args(self, scenario): """ - Filters out scenarios that should be skipped by tags + Filters out scenarios that should be skipped by tags or scenario names :param scenario scenario to check :return true if should pass """ assert isinstance(scenario, Scenario), scenario expected_tags = self.__config.tags + scenario_name_re = self.__config.name_re + if scenario_name_re and not scenario_name_re.match(scenario.name): + return False if not expected_tags: - return True # No tags are required + return True # No tags nor names are required return isinstance(expected_tags, TagExpression) and expected_tags.check(scenario.tags) @@ -213,7 +218,7 @@ class _BehaveRunner(_bdd_utils.BddRunner): scenarios.extend(scenario.scenarios) else: scenarios.append(scenario) - feature.scenarios = filter(self.__filter_scenarios_by_tag, scenarios) + feature.scenarios = filter(self.__filter_scenarios_by_args, scenarios) return features_to_run @@ -230,18 +235,27 @@ if __name__ == "__main__": command_args = list(filter(None, sys.argv[1:])) if command_args: _bdd_utils.fix_win_drive(command_args[0]) + (base_dir, scenario_names, what_to_run) = _bdd_utils.get_what_to_run_by_env(os.environ) + + for scenario_name in scenario_names: + command_args += ["-n", re.escape(scenario_name)] # TODO : rewite pythonic + my_config = configuration.Configuration(command_args=command_args) formatters.register_as(_Null, "com.intellij.python.null") my_config.format = ["com.intellij.python.null"] # To prevent output to stdout my_config.reporters = [] # To prevent summary to stdout my_config.stdout_capture = False # For test output my_config.stderr_capture = False # For test output - (base_dir, what_to_run) = _bdd_utils.get_path_by_args(sys.argv) - if not my_config.paths: # No path provided, trying to load dit manually - if os.path.isfile(what_to_run): # File is provided, load it - my_config.paths = [what_to_run] - else: # Dir is provided, find subdirs ro run - my_config.paths = _get_dirs_to_run(base_dir) + features = set() + for feature in what_to_run: + if os.path.isfile(feature) or glob.glob( + os.path.join(feature, "*.feature")): # File of folder with "features" provided, load it + features.add(feature) + elif os.path.isdir(feature): + features |= set(_get_dirs_to_run(feature)) # Find "features" subfolder + my_config.paths = list(features) + if what_to_run and not my_config.paths: + raise Exception("Nothing to run in {}".format(what_to_run)) _BehaveRunner(my_config, base_dir).run() diff --git a/python/helpers/pycharm/lettuce_runner.py b/python/helpers/pycharm/lettuce_runner.py index f0a4b5dbb873..2f64afc956d9 100644 --- a/python/helpers/pycharm/lettuce_runner.py +++ b/python/helpers/pycharm/lettuce_runner.py @@ -2,13 +2,14 @@ """ BDD lettuce framework runner TODO: Support other params (like tags) as well. -Supports only 1 param now: folder to search "features" for. +Supports only 2 params now: folder to search "features" for or file and "-s scenario_index" """ +import argparse +import os import _bdd_utils __author__ = 'Ilya.Kazakevich' from lettuce.exceptions import ReasonToFail -import sys import lettuce from lettuce import core @@ -18,31 +19,43 @@ class _LettuceRunner(_bdd_utils.BddRunner): Lettuce runner (BddRunner for lettuce) """ - def __init__(self, base_dir, what_to_run): + def __init__(self, base_dir, what_to_run, scenarios): """ + :param scenarios scenario numbers to run + :type scenarios list :param base_dir base directory to run tests in :type base_dir: str :param what_to_run folder or file to run :type what_to_run str + """ super(_LettuceRunner, self).__init__(base_dir) - self.__runner = lettuce.Runner(what_to_run) + self.__runner = lettuce.Runner(what_to_run, ",".join(scenarios)) def _get_features_to_run(self): super(_LettuceRunner, self)._get_features_to_run() - if self.__runner.single_feature: # We need to run one and only one feature - return [core.Feature.from_file(self.__runner.single_feature)] - - # Find all features in dir features = [] - for feature_file in self.__runner.loader.find_feature_files(): - feature = core.Feature.from_file(feature_file) - assert isinstance(feature, core.Feature), feature - # TODO: cut out due to https://github.com/gabrielfalcao/lettuce/issues/451 Fix when this issue fixed - feature.scenarios = filter(lambda s: not s.outlines, feature.scenarios) - if feature.scenarios: - features.append(feature) + if self.__runner.single_feature: # We need to run one and only one feature + features = [core.Feature.from_file(self.__runner.single_feature)] + else: + # Find all features in dir + for feature_file in self.__runner.loader.find_feature_files(): + feature = core.Feature.from_file(feature_file) + assert isinstance(feature, core.Feature), feature + # TODO: cut out due to https://github.com/gabrielfalcao/lettuce/issues/451 Fix when this issue fixed + feature.scenarios = filter(lambda s: not s.outlines, feature.scenarios) + if feature.scenarios: + features.append(feature) + + # Choose only selected scenarios + if self.__runner.scenarios: + for feature in features: + filtered_feature_scenarios = [] + for index in [i - 1 for i in self.__runner.scenarios]: # decrease index by 1 + if index < len(feature.scenarios): + filtered_feature_scenarios.append(feature.scenarios[index]) + feature.scenarios = filtered_feature_scenarios return features def _run_tests(self): @@ -108,6 +121,8 @@ class _LettuceRunner(_bdd_utils.BddRunner): if __name__ == "__main__": - (base_dir, what_to_run) = _bdd_utils.get_path_by_args(sys.argv) - _bdd_utils.fix_win_drive(what_to_run) - _LettuceRunner(base_dir, what_to_run).run()
\ No newline at end of file + (base_dir, scenarios, what_to_run) = _bdd_utils.get_what_to_run_by_env(os.environ) + if len(what_to_run) > 1: + raise Exception("Lettuce can't run more than one file now") + _bdd_utils.fix_win_drive(what_to_run[0]) + _LettuceRunner(base_dir, what_to_run[0], scenarios).run()
\ No newline at end of file diff --git a/python/helpers/pycharm_generator_utils/clr_tools.py b/python/helpers/pycharm_generator_utils/clr_tools.py index 4c273ae7d4d5..f4c3cfbf90f0 100644 --- a/python/helpers/pycharm_generator_utils/clr_tools.py +++ b/python/helpers/pycharm_generator_utils/clr_tools.py @@ -18,10 +18,14 @@ def get_namespace_by_name(object_name): first_part = parts[0] remain_part = parts[2] - while remain_part and type(_get_attr_by_name(imported_object, remain_part)) is type: # While we are in class + while remain_part and type(_get_attr_by_name(imported_object, remain_part)) is type: # While we are in class remain_part = remain_part.rpartition(".")[0] - return first_part + "." + remain_part if remain_part else first_part + if remain_part: + return first_part + "." + remain_part + else: + return first_part + def _import_first(object_name): """ @@ -33,13 +37,12 @@ def _import_first(object_name): """ while object_name: try: - return (__import__(object_name), object_name) + return (__import__(object_name, globals=[], locals=[], fromlist=[]), object_name) except ImportError: - object_name = object_name.rpartition(".")[0] # Remove rightest part + object_name = object_name.rpartition(".")[0] # Remove rightest part raise Exception("No module name found in name " + object_name) - def _get_attr_by_name(obj, name): """ Accepts chain of attributes in dot notation like "some.property.name" and gets them on object diff --git a/python/helpers/pydev/django_debug.py b/python/helpers/pydev/django_debug.py deleted file mode 100644 index 2b17864db47e..000000000000 --- a/python/helpers/pydev/django_debug.py +++ /dev/null @@ -1,124 +0,0 @@ -import inspect -from django_frame import DjangoTemplateFrame -from pydevd_comm import CMD_SET_BREAK -from pydevd_constants import DJANGO_SUSPEND, GetThreadId, DictContains -from pydevd_file_utils import NormFileToServer -from pydevd_breakpoints import LineBreakpoint -import pydevd_vars -import traceback - -class DjangoLineBreakpoint(LineBreakpoint): - - def __init__(self, file, line, condition, func_name, expression): - self.file = file - LineBreakpoint.__init__(self, line, condition, func_name, expression) - - def is_triggered(self, template_frame_file, template_frame_line): - return self.file == template_frame_file and self.line == template_frame_line - - def __str__(self): - return "DjangoLineBreakpoint: %s-%d" %(self.file, self.line) - - -def inherits(cls, *names): - if cls.__name__ in names: - return True - inherits_node = False - for base in inspect.getmro(cls): - if base.__name__ in names: - inherits_node = True - break - return inherits_node - - -def is_django_render_call(frame): - try: - name = frame.f_code.co_name - if name != 'render': - return False - - if not DictContains(frame.f_locals, 'self'): - return False - - cls = frame.f_locals['self'].__class__ - - inherits_node = inherits(cls, 'Node') - - if not inherits_node: - return False - - clsname = cls.__name__ - return clsname != 'TextNode' and clsname != 'NodeList' - except: - traceback.print_exc() - return False - - -def is_django_context_get_call(frame): - try: - if not DictContains(frame.f_locals, 'self'): - return False - - cls = frame.f_locals['self'].__class__ - - return inherits(cls, 'BaseContext') - except: - traceback.print_exc() - return False - - -def is_django_resolve_call(frame): - try: - name = frame.f_code.co_name - if name != '_resolve_lookup': - return False - - if not DictContains(frame.f_locals, 'self'): - return False - - cls = frame.f_locals['self'].__class__ - - clsname = cls.__name__ - return clsname == 'Variable' - except: - traceback.print_exc() - return False - - -def is_django_suspended(thread): - return thread.additionalInfo.suspend_type == DJANGO_SUSPEND - - -def suspend_django(py_db_frame, mainDebugger, thread, frame, cmd=CMD_SET_BREAK): - frame = DjangoTemplateFrame(frame) - - if frame.f_lineno is None: - return None - - #try: - # if thread.additionalInfo.filename == frame.f_code.co_filename and thread.additionalInfo.line == frame.f_lineno: - # return None # don't stay twice on the same line - #except AttributeError: - # pass - - pydevd_vars.addAdditionalFrameById(GetThreadId(thread), {id(frame): frame}) - - py_db_frame.setSuspend(thread, cmd) - thread.additionalInfo.suspend_type = DJANGO_SUSPEND - - thread.additionalInfo.filename = frame.f_code.co_filename - thread.additionalInfo.line = frame.f_lineno - - return frame - - -def find_django_render_frame(frame): - while frame is not None and not is_django_render_call(frame): - frame = frame.f_back - - return frame - - - - - diff --git a/python/helpers/pydev/django_frame.py b/python/helpers/pydev/django_frame.py deleted file mode 100644 index 4181572aed3c..000000000000 --- a/python/helpers/pydev/django_frame.py +++ /dev/null @@ -1,132 +0,0 @@ -from pydevd_file_utils import GetFileNameAndBaseFromFile -import pydev_log -import traceback -from pydevd_constants import DictContains - -def read_file(filename): - f = open(filename, "r") - try: - s = f.read() - finally: - f.close() - return s - - -def offset_to_line_number(text, offset): - curLine = 1 - curOffset = 0 - while curOffset < offset: - if curOffset == len(text): - return -1 - c = text[curOffset] - if c == '\n': - curLine += 1 - elif c == '\r': - curLine += 1 - if curOffset < len(text) and text[curOffset + 1] == '\n': - curOffset += 1 - - curOffset += 1 - - return curLine - - -def get_source(frame): - try: - node = frame.f_locals['self'] - if hasattr(node, 'source'): - return node.source - else: - pydev_log.error_once( - "WARNING: Template path is not available. Please set TEMPLATE_DEBUG=True " - "in your settings.py to make django template breakpoints working") - return None - - except: - pydev_log.debug(traceback.format_exc()) - return None - - -def get_template_file_name(frame): - try: - source = get_source(frame) - if source is None: - pydev_log.debug("Source is None\n") - return None - fname = source[0].name - - if fname == '<unknown source>': - pydev_log.debug("Source name is %s\n" % fname) - return None - else: - filename, base = GetFileNameAndBaseFromFile(fname) - return filename - except: - pydev_log.debug(traceback.format_exc()) - return None - - -def get_template_line(frame, template_frame_file): - source = get_source(frame) - try: - return offset_to_line_number(read_file(template_frame_file), source[1][0]) - except: - return None - - -class DjangoTemplateFrame: - def __init__( - self, - frame, - template_frame_file=None, - template_frame_line=None): - - if template_frame_file is None: - template_frame_file = get_template_file_name(frame) - - self.back_context = frame.f_locals['context'] - self.f_code = FCode('Django Template', template_frame_file) - - if template_frame_line is None: - template_frame_line = get_template_line(frame, template_frame_file) - self.f_lineno = template_frame_line - - self.f_back = frame - self.f_globals = {} - self.f_locals = self.collect_context() - self.f_trace = None - - def collect_context(self): - res = {} - try: - for d in self.back_context.dicts: - res.update(d) - except AttributeError: - pass - return res - - def changeVariable(self, name, value): - for d in self.back_context.dicts: - if DictContains(d, name): - d[name] = value - self.f_locals[name] = value - - -class FCode: - def __init__(self, name, filename): - self.co_name = name - self.co_filename = filename - - -def is_django_exception_break_context(frame): - try: - return frame.f_code.co_name in ['_resolve_lookup', 'find_template'] - except: - return False - - -def just_raised(trace): - if trace is None: - return False - return trace.tb_next is None - diff --git a/python/helpers/pydev/pydev_log.py b/python/helpers/pydev/pydev_log.py index 229784b76a91..b5e65b3102e6 100644 --- a/python/helpers/pydev/pydev_log.py +++ b/python/helpers/pydev/pydev_log.py @@ -2,6 +2,8 @@ import sys from pydevd_constants import DebugInfoHolder from pydevd_constants import DictContains +import traceback + WARN_ONCE_MAP = {} def stderr_write(message): @@ -18,11 +20,16 @@ def warn(message): if DebugInfoHolder.DEBUG_TRACE_LEVEL>1: stderr_write(message) + def info(message): stderr_write(message) -def error(message): + +def error(message, tb=False): stderr_write(message) + if tb: + traceback.print_exc() + def error_once(message): if not DictContains(WARN_ONCE_MAP, message): diff --git a/python/helpers/pydev/pydev_monkey_qt.py b/python/helpers/pydev/pydev_monkey_qt.py index 9c62686173dd..2675e9e55708 100644 --- a/python/helpers/pydev/pydev_monkey_qt.py +++ b/python/helpers/pydev/pydev_monkey_qt.py @@ -11,7 +11,7 @@ def set_trace_in_qt(): _patched_qt = False def patch_qt(): ''' - This method patches qt (PySide or PyQt4) so that we have hooks to set the tracing for QThread. + This method patches qt (PySide, PyQt4, PyQt5) so that we have hooks to set the tracing for QThread. ''' # Avoid patching more than once @@ -27,7 +27,10 @@ def patch_qt(): try: from PyQt4 import QtCore except: - return + try: + from PyQt5 import QtCore + except: + return _original_thread_init = QtCore.QThread.__init__ _original_runnable_init = QtCore.QRunnable.__init__ diff --git a/python/helpers/pydev/pydev_run_in_console.py b/python/helpers/pydev/pydev_run_in_console.py index 1b8e1d230175..731ead67115e 100644 --- a/python/helpers/pydev/pydev_run_in_console.py +++ b/python/helpers/pydev/pydev_run_in_console.py @@ -2,6 +2,7 @@ from pydevconsole import * import pydev_imports +from pydevd_utils import save_main_module def run_file(file, globals=None, locals=None): @@ -11,22 +12,8 @@ def run_file(file, globals=None, locals=None): file = new_target if globals is None: - # patch provided by: Scott Schlesier - when script is run, it does not - # use globals from pydevd: - # This will prevent the pydevd script from contaminating the namespace for the script to be debugged - - # pretend pydevd is not the main module, and - # convince the file to be debugged that it was loaded as main - sys.modules['pydevd'] = sys.modules['__main__'] - sys.modules['pydevd'].__name__ = 'pydevd' - - from imp import new_module - m = new_module('__main__') - sys.modules['__main__'] = m - if hasattr(sys.modules['pydevd'], '__loader__'): - setattr(m, '__loader__', getattr(sys.modules['pydevd'], '__loader__')) - - m.__file__ = file + m = save_main_module(file, 'pydev_run_in_console') + globals = m.__dict__ try: globals['__builtins__'] = __builtins__ diff --git a/python/helpers/pydev/pydevconsole.py b/python/helpers/pydev/pydevconsole.py index 8d4375f5a5aa..444aa2d1c48b 100644 --- a/python/helpers/pydev/pydevconsole.py +++ b/python/helpers/pydev/pydevconsole.py @@ -80,10 +80,18 @@ try: from pydev_imports import execfile __builtin__.execfile = execfile - except: pass +# Pull in runfile, the interface to UMD that wraps execfile +from pydev_umd import runfile, _set_globals_function +try: + import builtins + builtins.runfile = runfile +except: + import __builtin__ + __builtin__.runfile = runfile + #======================================================================================================================= # InterpreterInterface @@ -264,6 +272,9 @@ def start_server(host, port, interpreter): sys.stderr.write('Error starting server with host: %s, port: %s, client_port: %s\n' % (host, port, client_port)) raise + # Tell UMD the proper default namespace + _set_globals_function(interpreter.getNamespace) + server.register_function(interpreter.execLine) server.register_function(interpreter.execMultipleLines) server.register_function(interpreter.getCompletions) diff --git a/python/helpers/pydev/pydevd.py b/python/helpers/pydev/pydevd.py index 9d0da096c07d..8d68cea9876c 100644 --- a/python/helpers/pydev/pydevd.py +++ b/python/helpers/pydev/pydevd.py @@ -3,12 +3,15 @@ from __future__ import nested_scopes # Jython 2.1 support from pydevd_constants import * # @UnusedWildImport import pydev_monkey_qt +from pydevd_utils import save_main_module + pydev_monkey_qt.patch_qt() import traceback -from django_debug import DjangoLineBreakpoint -from pydevd_frame import add_exception_to_frame +from pydevd_plugin_utils import PluginManager + +from pydevd_frame_utils import add_exception_to_frame import pydev_imports from pydevd_breakpoints import * #@UnusedWildImport import fix_getpass @@ -110,13 +113,18 @@ DONT_TRACE = { 'linecache.py':1, 'threading.py':1, + # thirs party libs that we don't want to trace + 'pluginbase.py':1, + 'pkgutil_old.py':1, + 'uuid_old.py':1, + #things from pydev that we don't want to trace '_pydev_execfile.py':1, '_pydev_jython_execfile.py':1, '_pydev_threading':1, '_pydev_Queue':1, 'django_debug.py':1, - 'django_frame.py':1, + 'jinja2_debug.py':1, 'pydev_log.py':1, 'pydev_monkey.py':1 , 'pydevd.py':1 , @@ -301,17 +309,15 @@ class PyDB: self._cmd_queue = {} # the hash of Queues. Key is thread id, value is thread self.breakpoints = {} - self.django_breakpoints = {} self.file_to_id_to_line_breakpoint = {} - self.file_to_id_to_django_breakpoint = {} + self.file_to_id_to_plugin_breakpoint = {} # Note: breakpoints dict should not be mutated: a copy should be created # and later it should be assigned back (to prevent concurrency issues). self.break_on_uncaught_exceptions = {} self.break_on_caught_exceptions = {} - self.django_exception_break = {} self.readyToRun = False self._main_lock = _pydev_thread.allocate_lock() self._lock_running_thread_ids = _pydev_thread.allocate_lock() @@ -344,6 +350,8 @@ class PyDB: # This attribute holds the file-> lines which have an @IgnoreException. self.filename_to_lines_where_exceptions_are_ignored = {} + #working with plugins + self.plugin = PluginManager(self) def haveAliveThreads(self): for t in threadingEnumerate(): @@ -568,12 +576,16 @@ class PyDB: notify_on_terminate, notify_on_first_raise_only, ): - eb = ExceptionBreakpoint( - exception, - notify_always, - notify_on_terminate, - notify_on_first_raise_only, - ) + try: + eb = ExceptionBreakpoint( + exception, + notify_always, + notify_on_terminate, + notify_on_first_raise_only, + ) + except ImportError: + pydev_log.error("Error unable to add break on exception for: %s (exception could not be imported)\n" % (exception,)) + return None if eb.notify_on_terminate: cp = self.break_on_uncaught_exceptions.copy() @@ -839,15 +851,22 @@ class PyDB: if len(expression) <= 0 or expression is None or expression == "None": expression = None + supported_type = False if type == 'python-line': breakpoint = LineBreakpoint(line, condition, func_name, expression) breakpoints = self.breakpoints file_to_id_to_breakpoint = self.file_to_id_to_line_breakpoint - elif type == 'django-line': - breakpoint = DjangoLineBreakpoint(file, line, condition, func_name, expression) - breakpoints = self.django_breakpoints - file_to_id_to_breakpoint = self.file_to_id_to_django_breakpoint + supported_type = True else: + result = self.plugin.add_breakpoint('add_line_breakpoint', self, type, file, line, condition, expression, func_name) + if result is not None: + supported_type = True + breakpoint, breakpoints = result + file_to_id_to_breakpoint = self.file_to_id_to_plugin_breakpoint + else: + supported_type = False + + if not supported_type: raise NameError(type) if DebugInfoHolder.DEBUG_TRACE_BREAKPOINTS > 0: @@ -880,27 +899,31 @@ class PyDB: pydev_log.error('Error removing breakpoint. Expected breakpoint_id to be an int. Found: %s' % (breakpoint_id,)) else: + file_to_id_to_breakpoint = None if breakpoint_type == 'python-line': breakpoints = self.breakpoints file_to_id_to_breakpoint = self.file_to_id_to_line_breakpoint - elif breakpoint_type == 'django-line': - breakpoints = self.django_breakpoints - file_to_id_to_breakpoint = self.file_to_id_to_django_breakpoint else: - raise NameError(breakpoint_type) + result = self.plugin.get_breakpoints(self, breakpoint_type) + if result is not None: + file_to_id_to_breakpoint = self.file_to_id_to_plugin_breakpoint + breakpoints = result - try: - id_to_pybreakpoint = file_to_id_to_breakpoint.get(file, {}) - if DebugInfoHolder.DEBUG_TRACE_BREAKPOINTS > 0: - existing = id_to_pybreakpoint[breakpoint_id] - sys.stderr.write('Removed breakpoint:%s - line:%s - func_name:%s (id: %s)\n' % ( - file, existing.line, existing.func_name.encode('utf-8'), breakpoint_id)) + if file_to_id_to_breakpoint is None: + pydev_log.error('Error removing breakpoint. Cant handle breakpoint of type %s' % breakpoint_type) + else: + try: + id_to_pybreakpoint = file_to_id_to_breakpoint.get(file, {}) + if DebugInfoHolder.DEBUG_TRACE_BREAKPOINTS > 0: + existing = id_to_pybreakpoint[breakpoint_id] + sys.stderr.write('Removed breakpoint:%s - line:%s - func_name:%s (id: %s)\n' % ( + file, existing.line, existing.func_name.encode('utf-8'), breakpoint_id)) - del id_to_pybreakpoint[breakpoint_id] - self.consolidate_breakpoints(file, id_to_pybreakpoint, breakpoints) - except KeyError: - pydev_log.error("Error removing breakpoint: Breakpoint id not found: %s id: %s. Available ids: %s\n" % ( - file, breakpoint_id, DictKeys(id_to_pybreakpoint))) + del id_to_pybreakpoint[breakpoint_id] + self.consolidate_breakpoints(file, id_to_pybreakpoint, breakpoints) + except KeyError: + pydev_log.error("Error removing breakpoint: Breakpoint id not found: %s id: %s. Available ids: %s\n" % ( + file, breakpoint_id, DictKeys(id_to_pybreakpoint))) elif cmd_id == CMD_EVALUATE_EXPRESSION or cmd_id == CMD_EXEC_EXPRESSION: @@ -963,6 +986,8 @@ class PyDB: notify_on_terminate=break_on_uncaught, notify_on_first_raise_only=False, ) + if exception_breakpoint is None: + continue added.append(exception_breakpoint) self.update_after_exceptions_added(added) @@ -1013,28 +1038,58 @@ class PyDB: pass elif cmd_id == CMD_ADD_EXCEPTION_BREAK: - exception, notify_always, notify_on_terminate = text.split('\t', 2) - exception_breakpoint = self.add_break_on_exception( - exception, - notify_always=int(notify_always) > 0, - notify_on_terminate = int(notify_on_terminate) == 1, - notify_on_first_raise_only=int(notify_always) == 2 - ) - self.update_after_exceptions_added([exception_breakpoint]) + if text.find('\t') != -1: + exception, notify_always, notify_on_terminate = text.split('\t', 2) + else: + exception, notify_always, notify_on_terminate = text, 0, 0 + + if exception.find('-') != -1: + type, exception = exception.split('-') + else: + type = 'python' + + if type == 'python': + exception_breakpoint = self.add_break_on_exception( + exception, + notify_always=int(notify_always) > 0, + notify_on_terminate = int(notify_on_terminate) == 1, + notify_on_first_raise_only=int(notify_always) == 2 + ) + + if exception_breakpoint is not None: + self.update_after_exceptions_added([exception_breakpoint]) + else: + supported_type = self.plugin.add_breakpoint('add_exception_breakpoint', self, type, exception) + + if not supported_type: + raise NameError(type) + + elif cmd_id == CMD_REMOVE_EXCEPTION_BREAK: exception = text - try: - cp = self.break_on_uncaught_exceptions.copy() - DictPop(cp, exception, None) - self.break_on_uncaught_exceptions = cp + if exception.find('-') != -1: + type, exception = exception.split('-') + else: + type = 'python' - cp = self.break_on_caught_exceptions.copy() - DictPop(cp, exception, None) - self.break_on_caught_exceptions = cp - except: - pydev_log.debug("Error while removing exception %s"%sys.exc_info()[0]); - update_exception_hook(self) + if type == 'python': + try: + cp = self.break_on_uncaught_exceptions.copy() + DictPop(cp, exception, None) + self.break_on_uncaught_exceptions = cp + + cp = self.break_on_caught_exceptions.copy() + DictPop(cp, exception, None) + self.break_on_caught_exceptions = cp + except: + pydev_log.debug("Error while removing exception %s"%sys.exc_info()[0]) + update_exception_hook(self) + else: + supported_type = self.plugin.remove_exception_breakpoint(self, type, exception) + + if not supported_type: + raise NameError(type) elif cmd_id == CMD_LOAD_SOURCE: path = text @@ -1048,16 +1103,13 @@ class PyDB: elif cmd_id == CMD_ADD_DJANGO_EXCEPTION_BREAK: exception = text - self.django_exception_break[exception] = True - self.setTracingForUntracedContexts() + self.plugin.add_breakpoint('add_exception_breakpoint', self, 'django', exception) + elif cmd_id == CMD_REMOVE_DJANGO_EXCEPTION_BREAK: exception = text - try: - del self.django_exception_break[exception] - except : - pass + self.plugin.remove_exception_breakpoint(self, 'django', exception) elif cmd_id == CMD_EVALUATE_CONSOLE_EXPRESSION: # Command which takes care for the debug console communication @@ -1492,22 +1544,7 @@ class PyDB: file = new_target if globals is None: - # patch provided by: Scott Schlesier - when script is run, it does not - # use globals from pydevd: - # This will prevent the pydevd script from contaminating the namespace for the script to be debugged - - # pretend pydevd is not the main module, and - # convince the file to be debugged that it was loaded as main - sys.modules['pydevd'] = sys.modules['__main__'] - sys.modules['pydevd'].__name__ = 'pydevd' - - from imp import new_module - m = new_module('__main__') - sys.modules['__main__'] = m - if hasattr(sys.modules['pydevd'], '__loader__'): - setattr(m, '__loader__', getattr(sys.modules['pydevd'], '__loader__')) - - m.__file__ = file + m = save_main_module(file, 'pydevd') globals = m.__dict__ try: globals['__builtins__'] = __builtins__ @@ -1546,8 +1583,6 @@ class PyDB: pydev_imports.execfile(file, globals, locals) # execute the script - return globals - def exiting(self): sys.stdout.flush() sys.stderr.flush() @@ -2061,10 +2096,6 @@ if __name__ == '__main__': debugger = PyDB() - if setup['cmd-line']: - debugger.cmd_line = True - - if fix_app_engine_debug: sys.stderr.write("pydev debugger: google app engine integration enabled\n") curr_dir = os.path.dirname(__file__) diff --git a/python/helpers/pydev/pydevd_breakpoints.py b/python/helpers/pydev/pydevd_breakpoints.py index 1171157257e9..693823917a2f 100644 --- a/python/helpers/pydev/pydevd_breakpoints.py +++ b/python/helpers/pydev/pydevd_breakpoints.py @@ -40,8 +40,8 @@ class ExceptionBreakpoint: def __str__(self): return self.qname -class LineBreakpoint: +class LineBreakpoint(object): def __init__(self, line, condition, func_name, expression): self.line = line self.condition = condition diff --git a/python/helpers/pydev/pydevd_constants.py b/python/helpers/pydev/pydevd_constants.py index e878d3b48ead..5e7a7a926bfb 100644 --- a/python/helpers/pydev/pydevd_constants.py +++ b/python/helpers/pydev/pydevd_constants.py @@ -5,7 +5,6 @@ STATE_RUN = 1 STATE_SUSPEND = 2 PYTHON_SUSPEND = 1 -DJANGO_SUSPEND = 2 try: __setFalse = False diff --git a/python/helpers/pydev/pydevd_frame.py b/python/helpers/pydev/pydevd_frame.py index 5d1e78458391..922133ba1512 100644 --- a/python/helpers/pydev/pydevd_frame.py +++ b/python/helpers/pydev/pydevd_frame.py @@ -3,18 +3,15 @@ import os.path import re import traceback # @Reimport -from django_debug import find_django_render_frame -from django_debug import is_django_render_call, is_django_suspended, suspend_django, is_django_resolve_call, is_django_context_get_call -from django_frame import DjangoTemplateFrame -from django_frame import is_django_exception_break_context -from django_frame import just_raised, get_template_file_name, get_template_line import pydev_log from pydevd_breakpoints import get_exception_breakpoint, get_exception_name -from pydevd_comm import CMD_ADD_DJANGO_EXCEPTION_BREAK, \ - CMD_STEP_CAUGHT_EXCEPTION, CMD_STEP_RETURN, CMD_STEP_OVER, CMD_SET_BREAK, \ +from pydevd_comm import CMD_STEP_CAUGHT_EXCEPTION, CMD_STEP_RETURN, CMD_STEP_OVER, CMD_SET_BREAK, \ CMD_STEP_INTO, CMD_SMART_STEP_INTO, CMD_RUN_TO_LINE, CMD_SET_NEXT_STATEMENT from pydevd_constants import * # @UnusedWildImport from pydevd_file_utils import GetFilenameAndBase + +from pydevd_frame_utils import add_exception_to_frame, just_raised + try: from pydevd_signature import sendSignatureCallTrace except ImportError: @@ -55,17 +52,6 @@ class PyDBFrame: def doWaitSuspend(self, *args, **kwargs): self._args[0].doWaitSuspend(*args, **kwargs) - def _is_django_render_call(self, frame): - try: - return self._cached_is_django_render_call - except: - # Calculate lazily: note that a PyDBFrame always deals with the same - # frame over and over, so, we can cache this. - # -- although we can't cache things which change over time (such as - # the breakpoints for the file). - ret = self._cached_is_django_render_call = is_django_render_call(frame) - return ret - def trace_exception(self, frame, event, arg): if event == 'exception': flag, frame = self.should_stop_on_exception(frame, event, arg) @@ -97,22 +83,11 @@ class PyDBFrame: flag = False else: try: - if mainDebugger.django_exception_break and get_exception_name(exception) in [ - 'VariableDoesNotExist', 'TemplateDoesNotExist', 'TemplateSyntaxError'] \ - and just_raised(trace) and is_django_exception_break_context(frame): - - render_frame = find_django_render_frame(frame) - if render_frame: - suspend_frame = suspend_django( - self, mainDebugger, thread, render_frame, CMD_ADD_DJANGO_EXCEPTION_BREAK) - - if suspend_frame: - add_exception_to_frame(suspend_frame, (exception, value, trace)) - flag = True - thread.additionalInfo.message = 'VariableDoesNotExist' - suspend_frame.f_back = frame - frame = suspend_frame - except : + result = mainDebugger.plugin.exception_break(mainDebugger, self, frame, self._args, arg) + if result: + (flag, frame) = result + + except: flag = False return flag, frame @@ -253,7 +228,8 @@ class PyDBFrame: sendSignatureCallTrace(main_debugger, frame, filename) is_exception_event = event == 'exception' - has_exception_breakpoints = main_debugger.break_on_caught_exceptions or main_debugger.django_exception_break + has_exception_breakpoints = main_debugger.break_on_caught_exceptions \ + or main_debugger.plugin.has_exception_breaks(main_debugger) if is_exception_event: if has_exception_breakpoints: @@ -293,9 +269,8 @@ class PyDBFrame: can_skip = (step_cmd is None and stop_frame is None)\ or (step_cmd in (CMD_STEP_RETURN, CMD_STEP_OVER) and stop_frame is not frame) - check_stop_on_django_render_call = main_debugger.django_breakpoints and self._is_django_render_call(frame) - if check_stop_on_django_render_call: - can_skip = False + if can_skip: + can_skip = not main_debugger.plugin.can_not_skip(main_debugger, self, frame) # Let's check to see if we are in a function that has a breakpoint. If we don't have a breakpoint, # we will return nothing for the next trace @@ -334,29 +309,35 @@ class PyDBFrame: try: line = frame.f_lineno - - flag = False - if event == 'call' and info.pydev_state != STATE_SUSPEND and check_stop_on_django_render_call: - flag, frame = self.should_stop_on_django_breakpoint(frame, event, arg) - #return is not taken into account for breakpoint hit because we'd have a double-hit in this case #(one for the line and the other for the return). - if not flag and event != 'return' and info.pydev_state != STATE_SUSPEND and breakpoints_for_file is not None\ - and DictContains(breakpoints_for_file, line): - #ok, hit breakpoint, now, we have to discover if it is a conditional breakpoint - # lets do the conditional stuff here + stop_info = {} + breakpoint = None + exist_result = False + stop_info['stop'] = False + if not flag and event != 'return' and info.pydev_state != STATE_SUSPEND and breakpoints_for_file is not None \ + and DictContains(breakpoints_for_file, line): breakpoint = breakpoints_for_file[line] - - stop = True + new_frame = frame + stop_info['stop'] = True if step_cmd == CMD_STEP_OVER and stop_frame is frame and event in ('line', 'return'): - stop = False #we don't stop on breakpoint if we have to stop by step-over (it will be processed later) - else: + stop_info['stop'] = False #we don't stop on breakpoint if we have to stop by step-over (it will be processed later) + else: + result = main_debugger.plugin.get_breakpoint(main_debugger, self, frame, event, self._args) + if result: + exist_result = True + (flag, breakpoint, new_frame) = result + + if breakpoint: + #ok, hit breakpoint, now, we have to discover if it is a conditional breakpoint + # lets do the conditional stuff here + if stop_info['stop'] or exist_result: condition = breakpoint.condition if condition is not None: try: - val = eval(condition, frame.f_globals, frame.f_locals) + val = eval(condition, new_frame.f_globals, new_frame.f_locals) if not val: return self.trace_dispatch @@ -371,7 +352,7 @@ class PyDBFrame: if not main_debugger.suspend_on_breakpoint_exception: return self.trace_dispatch else: - stop = True + stop_info['stop'] = True try: additional_info = None try: @@ -395,18 +376,21 @@ class PyDBFrame: except: traceback.print_exc() - if breakpoint.expression is not None: - try: + if breakpoint.expression is not None: try: - val = eval(breakpoint.expression, frame.f_globals, frame.f_locals) - except: - val = sys.exc_info()[1] - finally: - if val is not None: - thread.additionalInfo.message = val - - if stop: - self.setSuspend(thread, CMD_SET_BREAK) + try: + val = eval(breakpoint.expression, new_frame.f_globals, new_frame.f_locals) + except: + val = sys.exc_info()[1] + finally: + if val is not None: + thread.additionalInfo.message = val + if stop_info['stop']: + self.setSuspend(thread, CMD_SET_BREAK) + elif flag: + result = main_debugger.plugin.suspend(main_debugger, thread, frame) + if result: + frame = result # if thread has a suspend flag, we suspend with a busy wait if info.pydev_state == STATE_SUSPEND: @@ -419,8 +403,6 @@ class PyDBFrame: #step handling. We stop when we hit the right frame try: - django_stop = False - should_skip = False if pydevd_dont_trace.should_trace_hook is not None: if not hasattr(self, 'should_skip'): @@ -432,34 +414,18 @@ class PyDBFrame: should_skip = self.should_skip if should_skip: - stop = False + stop_info['stop'] = False elif step_cmd == CMD_STEP_INTO: - stop = event in ('line', 'return') - - if is_django_suspended(thread): - #django_stop = event == 'call' and is_django_render_call(frame) - stop = stop and is_django_resolve_call(frame.f_back) and not is_django_context_get_call(frame) - if stop: - info.pydev_django_resolve_frame = 1 #we remember that we've go into python code from django rendering frame + stop_info['stop'] = event in ('line', 'return') + main_debugger.plugin.cmd_step_into(main_debugger, frame, event, self._args, stop_info) elif step_cmd == CMD_STEP_OVER: - if is_django_suspended(thread): - django_stop = event == 'call' and self._is_django_render_call(frame) - - stop = False - else: - if event == 'return' and info.pydev_django_resolve_frame is not None and is_django_resolve_call(frame.f_back): - #we return to Django suspend mode and should not stop before django rendering frame - stop_frame = info.pydev_step_stop = info.pydev_django_resolve_frame - info.pydev_django_resolve_frame = None - thread.additionalInfo.suspend_type = DJANGO_SUSPEND - - - stop = stop_frame is frame and event in ('line', 'return') + stop_info['stop'] = stop_frame is frame and event in ('line', 'return') + main_debugger.plugin.cmd_step_over(main_debugger, frame, event, self._args, stop_info) elif step_cmd == CMD_SMART_STEP_INTO: - stop = False + stop_info['stop'] = False if info.pydev_smart_step_stop is frame: info.pydev_func_name = None info.pydev_smart_step_stop = None @@ -472,13 +438,13 @@ class PyDBFrame: curr_func_name = '' if curr_func_name == info.pydev_func_name: - stop = True + stop_info['stop'] = True elif step_cmd == CMD_STEP_RETURN: - stop = event == 'return' and stop_frame is frame + stop_info['stop'] = event == 'return' and stop_frame is frame elif step_cmd == CMD_RUN_TO_LINE or step_cmd == CMD_SET_NEXT_STATEMENT: - stop = False + stop_info['stop'] = False if event == 'line' or event == 'exception': #Yes, we can only act on line events (weird hum?) @@ -493,50 +459,47 @@ class PyDBFrame: if curr_func_name == info.pydev_func_name: line = info.pydev_next_line if frame.f_lineno == line: - stop = True + stop_info['stop'] = True else: if frame.f_trace is None: frame.f_trace = self.trace_dispatch frame.f_lineno = line frame.f_trace = None - stop = True + stop_info['stop'] = True else: - stop = False - - if django_stop: - frame = suspend_django(self, main_debugger, thread, frame) - if frame: - self.doWaitSuspend(thread, frame, event, arg) - elif stop: - #event is always == line or return at this point - if event == 'line': - self.setSuspend(thread, step_cmd) - self.doWaitSuspend(thread, frame, event, arg) - else: #return event - back = frame.f_back - if back is not None: - #When we get to the pydevd run function, the debugging has actually finished for the main thread - #(note that it can still go on for other threads, but for this one, we just make it finish) - #So, just setting it to None should be OK - base = basename(back.f_code.co_filename) - if base == 'pydevd.py' and back.f_code.co_name == 'run': - back = None - - elif base == 'pydevd_traceproperty.py': - # We dont want to trace the return event of pydevd_traceproperty (custom property for debugging) - #if we're in a return, we want it to appear to the user in the previous frame! - return None + stop_info['stop'] = False - if back is not None: - #if we're in a return, we want it to appear to the user in the previous frame! + if True in DictIterValues(stop_info): + stopped_on_plugin = main_debugger.plugin.stop(main_debugger, frame, event, self._args, stop_info, arg, step_cmd) + if DictContains(stop_info, 'stop') and stop_info['stop'] and not stopped_on_plugin: + if event == 'line': self.setSuspend(thread, step_cmd) - self.doWaitSuspend(thread, back, event, arg) - else: - #in jython we may not have a back frame - info.pydev_step_stop = None - info.pydev_step_cmd = None - info.pydev_state = STATE_RUN + self.doWaitSuspend(thread, frame, event, arg) + else: #return event + back = frame.f_back + if back is not None: + #When we get to the pydevd run function, the debugging has actually finished for the main thread + #(note that it can still go on for other threads, but for this one, we just make it finish) + #So, just setting it to None should be OK + base = basename(back.f_code.co_filename) + if base == 'pydevd.py' and back.f_code.co_name == 'run': + back = None + + elif base == 'pydevd_traceproperty.py': + # We dont want to trace the return event of pydevd_traceproperty (custom property for debugging) + #if we're in a return, we want it to appear to the user in the previous frame! + return None + + if back is not None: + #if we're in a return, we want it to appear to the user in the previous frame! + self.setSuspend(thread, step_cmd) + self.doWaitSuspend(thread, back, event, arg) + else: + #in jython we may not have a back frame + info.pydev_step_stop = None + info.pydev_step_cmd = None + info.pydev_state = STATE_RUN except: @@ -562,61 +525,4 @@ class PyDBFrame: except ImportError: if hasattr(sys, 'exc_clear'): #jython does not have it sys.exc_clear() #don't keep the traceback - pass #ok, psyco not available - - def should_stop_on_django_breakpoint(self, frame, event, arg): - mainDebugger = self._args[0] - thread = self._args[3] - flag = False - template_frame_file = get_template_file_name(frame) - - #pydev_log.debug("Django is rendering a template: %s\n" % template_frame_file) - - django_breakpoints_for_file = mainDebugger.django_breakpoints.get(template_frame_file) - if django_breakpoints_for_file: - - #pydev_log.debug("Breakpoints for that file: %s\n" % django_breakpoints_for_file) - - template_frame_line = get_template_line(frame, template_frame_file) - - #pydev_log.debug("Tracing template line: %d\n" % template_frame_line) - - if DictContains(django_breakpoints_for_file, template_frame_line): - django_breakpoint = django_breakpoints_for_file[template_frame_line] - - if django_breakpoint.is_triggered(template_frame_file, template_frame_line): - - #pydev_log.debug("Breakpoint is triggered.\n") - - flag = True - new_frame = DjangoTemplateFrame( - frame, - template_frame_file=template_frame_file, - template_frame_line=template_frame_line, - ) - - if django_breakpoint.condition is not None: - try: - val = eval(django_breakpoint.condition, new_frame.f_globals, new_frame.f_locals) - if not val: - flag = False - pydev_log.debug("Condition '%s' is evaluated to %s. Not suspending.\n" % (django_breakpoint.condition, val)) - except: - pydev_log.info( - 'Error while evaluating condition \'%s\': %s\n' % (django_breakpoint.condition, sys.exc_info()[1])) - - if django_breakpoint.expression is not None: - try: - try: - val = eval(django_breakpoint.expression, new_frame.f_globals, new_frame.f_locals) - except: - val = sys.exc_info()[1] - finally: - if val is not None: - thread.additionalInfo.message = val - if flag: - frame = suspend_django(self, mainDebugger, thread, frame) - return flag, frame - -def add_exception_to_frame(frame, exception_info): - frame.f_locals['__exception__'] = exception_info
\ No newline at end of file + pass #ok, psyco not available
\ No newline at end of file diff --git a/python/helpers/pydev/pydevd_frame_utils.py b/python/helpers/pydev/pydevd_frame_utils.py index 23becca0570d..0c9e8446d19c 100644 --- a/python/helpers/pydev/pydevd_frame_utils.py +++ b/python/helpers/pydev/pydevd_frame_utils.py @@ -1,11 +1,11 @@ -class Frame: +class Frame(object): def __init__( self, f_back, f_fileno, f_code, f_locals, - f_globals={}, + f_globals=None, f_trace=None): self.f_back = f_back self.f_lineno = f_fileno @@ -14,8 +14,31 @@ class Frame: self.f_globals = f_globals self.f_trace = f_trace + if self.f_globals is None: + self.f_globals = {} -class FCode: + +class FCode(object): def __init__(self, name, filename): self.co_name = name - self.co_filename = filename
\ No newline at end of file + self.co_filename = filename + + +def add_exception_to_frame(frame, exception_info): + frame.f_locals['__exception__'] = exception_info + + +def just_raised(trace): + if trace is None: + return False + return trace.tb_next is None + + +def cached_call(obj, func, *args): + cached_name = '_cached_' + func.__name__ + if not hasattr(obj, cached_name): + setattr(obj, cached_name, func(*args)) + + return getattr(obj, cached_name) + + diff --git a/python/helpers/pydev/pydevd_plugin_utils.py b/python/helpers/pydev/pydevd_plugin_utils.py new file mode 100644 index 000000000000..5b106b8234db --- /dev/null +++ b/python/helpers/pydev/pydevd_plugin_utils.py @@ -0,0 +1,85 @@ +import os +import types + +import pydev_log +import pydevd_trace_api +from third_party.pluginbase import PluginBase + +def load_plugins(package): + plugin_base = PluginBase(package=package) + plugin_source = plugin_base.make_plugin_source(searchpath=[os.path.dirname(os.path.realpath(__file__)) + '/' + package], persist=True) + plugins = [] + for plugin in plugin_source.list_plugins(): + loaded_plugin = None + try: + loaded_plugin = plugin_source.load_plugin(plugin) + except: + pydev_log.error("Failed to load plugin %s" % plugin, True) + if loaded_plugin: + plugins.append(loaded_plugin) + + return plugins + + +def bind_func_to_method(func, obj, method_name): + foo = types.MethodType(func, obj) + + setattr(obj, method_name, foo) + return foo + + +class PluginManager(object): + def __init__(self, main_debugger): + self.plugins = load_plugins('pydevd_plugins') + self.active_plugins = [] + self.main_debugger = main_debugger + self.rebind_methods() + + def add_breakpoint(self, func_name, *args, **kwargs): + # add breakpoint for plugin and remember which plugin to use in tracing + for plugin in self.plugins: + if hasattr(plugin, func_name): + func = getattr(plugin, func_name) + result = func(self, *args, **kwargs) + if result: + self.activate(plugin) + + return result + return None + + def activate(self, plugin): + self.active_plugins.append(plugin) + self.rebind_methods() + + def rebind_methods(self): + if len(self.active_plugins) == 0: + self.bind_functions(pydevd_trace_api, getattr, pydevd_trace_api) + elif len(self.active_plugins) == 1: + self.bind_functions(pydevd_trace_api, getattr, self.active_plugins[0]) + else: + self.bind_functions(pydevd_trace_api, create_dispatch, self.active_plugins) + + def bind_functions(self, interface, function_factory, arg): + for name in dir(interface): + func = function_factory(arg, name) + if type(func) == types.FunctionType: + bind_func_to_method(func, self, name) + + +def create_dispatch(obj, name): + def dispatch(self, *args, **kwargs): + result = None + for p in self.active_plugins: + r = getattr(p, name)(self, *args, **kwargs) + if not result: + result = r + return result + return dispatch + + + + + + + + diff --git a/python/helpers/pydev/pydevd_plugins/__init__.py b/python/helpers/pydev/pydevd_plugins/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 --- /dev/null +++ b/python/helpers/pydev/pydevd_plugins/__init__.py diff --git a/python/helpers/pydev/pydevd_plugins/django_debug.py b/python/helpers/pydev/pydevd_plugins/django_debug.py new file mode 100644 index 000000000000..ac23d40bd381 --- /dev/null +++ b/python/helpers/pydev/pydevd_plugins/django_debug.py @@ -0,0 +1,357 @@ +from pydevd_comm import CMD_SET_BREAK, CMD_ADD_EXCEPTION_BREAK +import inspect +from pydevd_constants import STATE_SUSPEND, GetThreadId, DictContains +from pydevd_file_utils import NormFileToServer, GetFileNameAndBaseFromFile +from pydevd_breakpoints import LineBreakpoint, get_exception_name +import pydevd_vars +import traceback +import pydev_log +from pydevd_frame_utils import add_exception_to_frame, FCode, cached_call, just_raised + +DJANGO_SUSPEND = 2 + +class DjangoLineBreakpoint(LineBreakpoint): + def __init__(self, file, line, condition, func_name, expression): + self.file = file + LineBreakpoint.__init__(self, line, condition, func_name, expression) + + def is_triggered(self, template_frame_file, template_frame_line): + return self.file == template_frame_file and self.line == template_frame_line + + def __str__(self): + return "DjangoLineBreakpoint: %s-%d" %(self.file, self.line) + + +def add_line_breakpoint(plugin, pydb, type, file, line, condition, expression, func_name): + if type == 'django-line': + breakpoint = DjangoLineBreakpoint(file, line, condition, func_name, expression) + if not hasattr(pydb, 'django_breakpoints'): + pydb.django_breakpoints = {} + return breakpoint, pydb.django_breakpoints + return None + +def add_exception_breakpoint(plugin, pydb, type, exception): + if type == 'django': + if not hasattr(pydb, 'django_exception_break'): + pydb.django_exception_break = {} + pydb.django_exception_break[exception] = True + pydb.setTracingForUntracedContexts() + return True + return False + + +def remove_exception_breakpoint(plugin, pydb, type, exception): + if type == 'django': + try: + del pydb.django_exception_break[exception] + return True + except: + pass + return False + +def get_breakpoints(plugin, pydb, type): + if type == 'django-line': + return pydb.django_breakpoints + return None + +def _inherits(cls, *names): + if cls.__name__ in names: + return True + inherits_node = False + for base in inspect.getmro(cls): + if base.__name__ in names: + inherits_node = True + break + return inherits_node + + +def _is_django_render_call(frame): + try: + name = frame.f_code.co_name + if name != 'render': + return False + + if not DictContains(frame.f_locals, 'self'): + return False + + cls = frame.f_locals['self'].__class__ + + inherits_node = _inherits(cls, 'Node') + + if not inherits_node: + return False + + clsname = cls.__name__ + return clsname != 'TextNode' and clsname != 'NodeList' + except: + traceback.print_exc() + return False + + +def _is_django_context_get_call(frame): + try: + if not DictContains(frame.f_locals, 'self'): + return False + + cls = frame.f_locals['self'].__class__ + + return _inherits(cls, 'BaseContext') + except: + traceback.print_exc() + return False + + +def _is_django_resolve_call(frame): + try: + name = frame.f_code.co_name + if name != '_resolve_lookup': + return False + + if not DictContains(frame.f_locals, 'self'): + return False + + cls = frame.f_locals['self'].__class__ + + clsname = cls.__name__ + return clsname == 'Variable' + except: + traceback.print_exc() + return False + + +def _is_django_suspended(thread): + return thread.additionalInfo.suspend_type == DJANGO_SUSPEND + + +def suspend_django(mainDebugger, thread, frame, cmd=CMD_SET_BREAK): + frame = DjangoTemplateFrame(frame) + + if frame.f_lineno is None: + return None + + #try: + # if thread.additionalInfo.filename == frame.f_code.co_filename and thread.additionalInfo.line == frame.f_lineno: + # return None # don't stay twice on the same line + #except AttributeError: + # pass + + pydevd_vars.addAdditionalFrameById(GetThreadId(thread), {id(frame): frame}) + + mainDebugger.setSuspend(thread, cmd) + thread.additionalInfo.suspend_type = DJANGO_SUSPEND + + thread.additionalInfo.filename = frame.f_code.co_filename + thread.additionalInfo.line = frame.f_lineno + + return frame + + +def _find_django_render_frame(frame): + while frame is not None and not _is_django_render_call(frame): + frame = frame.f_back + + return frame + +#======================================================================================================================= +# Django Frame +#======================================================================================================================= + +def _read_file(filename): + f = open(filename, "r") + s = f.read() + f.close() + return s + + +def _offset_to_line_number(text, offset): + curLine = 1 + curOffset = 0 + while curOffset < offset: + if curOffset == len(text): + return -1 + c = text[curOffset] + if c == '\n': + curLine += 1 + elif c == '\r': + curLine += 1 + if curOffset < len(text) and text[curOffset + 1] == '\n': + curOffset += 1 + + curOffset += 1 + + return curLine + + +def _get_source(frame): + try: + node = frame.f_locals['self'] + if hasattr(node, 'source'): + return node.source + else: + pydev_log.error_once("WARNING: Template path is not available. Please set TEMPLATE_DEBUG=True in your settings.py to make " + " django template breakpoints working") + return None + + except: + pydev_log.debug(traceback.format_exc()) + return None + + +def _get_template_file_name(frame): + try: + source = _get_source(frame) + if source is None: + pydev_log.debug("Source is None\n") + return None + fname = source[0].name + + if fname == '<unknown source>': + pydev_log.debug("Source name is %s\n" % fname) + return None + else: + filename, base = GetFileNameAndBaseFromFile(fname) + return filename + except: + pydev_log.debug(traceback.format_exc()) + return None + + +def _get_template_line(frame): + source = _get_source(frame) + file_name = _get_template_file_name(frame) + try: + return _offset_to_line_number(_read_file(file_name), source[1][0]) + except: + return None + + +class DjangoTemplateFrame: + def __init__(self, frame): + file_name = _get_template_file_name(frame) + self.back_context = frame.f_locals['context'] + self.f_code = FCode('Django Template', file_name) + self.f_lineno = _get_template_line(frame) + self.f_back = frame + self.f_globals = {} + self.f_locals = self.collect_context(self.back_context) + self.f_trace = None + + def collect_context(self, context): + res = {} + try: + for d in context.dicts: + for k, v in d.items(): + res[k] = v + except AttributeError: + pass + return res + + def changeVariable(self, name, value): + for d in self.back_context.dicts: + for k, v in d.items(): + if k == name: + d[k] = value + +def _is_django_exception_break_context(frame): + try: + name = frame.f_code.co_name + except: + name = None + return name in ['_resolve_lookup', 'find_template'] + + +#======================================================================================================================= +# Django Step Commands +#======================================================================================================================= + +def can_not_skip(plugin, mainDebugger, pydb_frame, frame): + if hasattr(mainDebugger, 'django_breakpoints') and mainDebugger.django_breakpoints and cached_call(pydb_frame, _is_django_render_call, frame): + filename = _get_template_file_name(frame) + django_breakpoints_for_file = mainDebugger.django_breakpoints.get(filename) + if django_breakpoints_for_file: + return True + return False + +def has_exception_breaks(plugin, mainDebugger): + return hasattr(mainDebugger, 'django_exception_break') and mainDebugger.django_exception_break + + +def cmd_step_into(plugin, mainDebugger, frame, event, args, stop_info): + mainDebugger, filename, info, thread = args + if _is_django_suspended(thread): + #stop_info['django_stop'] = event == 'call' and cached_call(frame, is_django_render_call) + stop_info['stop'] = stop_info['stop'] and _is_django_resolve_call(frame.f_back) and not _is_django_context_get_call(frame) + if stop_info['stop']: + info.pydev_django_resolve_frame = 1 #we remember that we've go into python code from django rendering frame + + +def cmd_step_over(plugin, mainDebugger, frame, event, args, stop_info): + mainDebugger, filename, info, thread = args + if _is_django_suspended(thread): + stop_info['django_stop'] = event == 'call' and _is_django_render_call(frame) + stop_info['stop'] = False + return True + else: + if event == 'return' and info.pydev_django_resolve_frame is not None and _is_django_resolve_call(frame.f_back): + #we return to Django suspend mode and should not stop before django rendering frame + info.pydev_step_stop = info.pydev_django_resolve_frame + info.pydev_django_resolve_frame = None + thread.additionalInfo.suspend_type = DJANGO_SUSPEND + stop_info['stop'] = info.pydev_step_stop is frame and event in ('line', 'return') + + return False + + +def stop(plugin, mainDebugger, frame, event, args, stop_info, arg, step_cmd): + mainDebugger, filename, info, thread = args + if DictContains(stop_info, 'django_stop') and stop_info['django_stop']: + frame = suspend_django(mainDebugger, thread, frame, step_cmd) + if frame: + mainDebugger.doWaitSuspend(thread, frame, event, arg) + return True + return False + + +def get_breakpoint(plugin, mainDebugger, pydb_frame, frame, event, args): + mainDebugger, filename, info, thread = args + flag = False + django_breakpoint = None + new_frame = None + + if event == 'call' and info.pydev_state != STATE_SUSPEND and hasattr(mainDebugger, 'django_breakpoints') and \ + mainDebugger.django_breakpoints and cached_call(pydb_frame, _is_django_render_call, frame): + filename = _get_template_file_name(frame) + pydev_log.debug("Django is rendering a template: %s\n" % filename) + django_breakpoints_for_file = mainDebugger.django_breakpoints.get(filename) + if django_breakpoints_for_file: + pydev_log.debug("Breakpoints for that file: %s\n" % django_breakpoints_for_file) + template_line = _get_template_line(frame) + pydev_log.debug("Tracing template line: %d\n" % template_line) + + if DictContains(django_breakpoints_for_file, template_line): + django_breakpoint = django_breakpoints_for_file[template_line] + flag = True + new_frame = DjangoTemplateFrame(frame) + return flag, django_breakpoint, new_frame + + +def suspend(plugin, mainDebugger, thread, frame): + return suspend_django(mainDebugger, thread, frame) + +def exception_break(plugin, mainDebugger, pydb_frame, frame, args, arg): + mainDebugger, filename, info, thread = args + exception, value, trace = arg + if hasattr(mainDebugger, 'django_exception_break') and mainDebugger.django_exception_break and \ + get_exception_name(exception) in ['VariableDoesNotExist', 'TemplateDoesNotExist', 'TemplateSyntaxError'] and \ + just_raised(trace) and _is_django_exception_break_context(frame): + render_frame = _find_django_render_frame(frame) + if render_frame: + suspend_frame = suspend_django(mainDebugger, thread, render_frame, CMD_ADD_EXCEPTION_BREAK) + if suspend_frame: + add_exception_to_frame(suspend_frame, (exception, value, trace)) + flag = True + thread.additionalInfo.message = 'VariableDoesNotExist' + suspend_frame.f_back = frame + frame = suspend_frame + return (flag, frame) + return None
\ No newline at end of file diff --git a/python/helpers/pydev/pydevd_plugins/jinja2_debug.py b/python/helpers/pydev/pydevd_plugins/jinja2_debug.py new file mode 100644 index 000000000000..9968a81d0043 --- /dev/null +++ b/python/helpers/pydev/pydevd_plugins/jinja2_debug.py @@ -0,0 +1,341 @@ +import traceback +from pydevd_breakpoints import LineBreakpoint, get_exception_name +from pydevd_constants import GetThreadId, STATE_SUSPEND, DictContains +from pydevd_comm import CMD_SET_BREAK, CMD_STEP_OVER, CMD_ADD_EXCEPTION_BREAK +import pydevd_vars +from pydevd_file_utils import GetFileNameAndBaseFromFile +from pydevd_frame_utils import add_exception_to_frame, FCode, cached_call + +JINJA2_SUSPEND = 3 + +class Jinja2LineBreakpoint(LineBreakpoint): + + def __init__(self, file, line, condition, func_name, expression): + self.file = file + LineBreakpoint.__init__(self, line, condition, func_name, expression) + + def is_triggered(self, template_frame_file, template_frame_line): + return self.file == template_frame_file and self.line == template_frame_line + + def __str__(self): + return "Jinja2LineBreakpoint: %s-%d" %(self.file, self.line) + + +def add_line_breakpoint(plugin, pydb, type, file, line, condition, func_name, expression): + result = None + if type == 'jinja2-line': + breakpoint = Jinja2LineBreakpoint(file, line, condition, func_name, expression) + if not hasattr(pydb, 'jinja2_breakpoints'): + pydb.jinja2_breakpoints = {} + result = breakpoint, pydb.jinja2_breakpoints + return result + return result + +def add_exception_breakpoint(plugin, pydb, type, exception): + if type == 'jinja2': + if not hasattr(pydb, 'jinja2_exception_break'): + pydb.jinja2_exception_break = {} + pydb.jinja2_exception_break[exception] = True + pydb.setTracingForUntracedContexts() + return True + return False + +def remove_exception_breakpoint(plugin, pydb, type, exception): + if type == 'jinja2': + try: + del pydb.jinja2_exception_break[exception] + return True + except: + pass + return False + +def get_breakpoints(plugin, pydb, type): + if type == 'jinja2-line': + return pydb.jinja2_breakpoints + return None + + +def is_jinja2_render_call(frame): + try: + name = frame.f_code.co_name + if DictContains(frame.f_globals, "__jinja_template__") and name in ("root", "loop", "macro") or name.startswith("block_"): + return True + return False + except: + traceback.print_exc() + return False + + +def suspend_jinja2(pydb, thread, frame, cmd=CMD_SET_BREAK): + frame = Jinja2TemplateFrame(frame) + + if frame.f_lineno is None: + return None + + pydevd_vars.addAdditionalFrameById(GetThreadId(thread), {id(frame): frame}) + pydb.setSuspend(thread, cmd) + + thread.additionalInfo.suspend_type = JINJA2_SUSPEND + thread.additionalInfo.filename = frame.f_code.co_filename + thread.additionalInfo.line = frame.f_lineno + + return frame + +def is_jinja2_suspended(thread): + return thread.additionalInfo.suspend_type == JINJA2_SUSPEND + +def is_jinja2_context_call(frame): + return DictContains(frame.f_locals, "_Context__obj") + +def is_jinja2_internal_function(frame): + return DictContains(frame.f_locals, 'self') and frame.f_locals['self'].__class__.__name__ in \ + ('LoopContext', 'TemplateReference', 'Macro', 'BlockReference') + +def find_jinja2_render_frame(frame): + while frame is not None and not is_jinja2_render_call(frame): + frame = frame.f_back + + return frame + +def change_variable(plugin, pydb, frame, attr, expression): + if isinstance(frame, Jinja2TemplateFrame): + result = eval(expression, frame.f_globals, frame.f_locals) + frame.changeVariable(attr, result) + + +#======================================================================================================================= +# Jinja2 Frame +#======================================================================================================================= + +class Jinja2TemplateFrame: + + def __init__(self, frame): + file_name = get_jinja2_template_filename(frame) + self.back_context = None + if 'context' in frame.f_locals: + #sometimes we don't have 'context', e.g. in macros + self.back_context = frame.f_locals['context'] + self.f_code = FCode('template', file_name) + self.f_lineno = get_jinja2_template_line(frame) + self.f_back = find_render_function_frame(frame) + self.f_globals = {} + self.f_locals = self.collect_context(frame) + self.f_trace = None + + def collect_context(self, frame): + res = {} + if self.back_context is not None: + for k, v in self.back_context.items(): + res[k] = v + for k, v in frame.f_locals.items(): + if not k.startswith('l_'): + if not k in res: + #local variables should shadow globals from context + res[k] = v + elif v and not is_missing(v): + res[k[2:]] = v + return res + + def changeVariable(self, name, value): + for k, v in self.back_context.items(): + if k == name: + self.back_context.vars[k] = value + +def is_missing(item): + if item.__class__.__name__ == 'MissingType': + return True + return False + +def find_render_function_frame(frame): + #in order to hide internal rendering functions + old_frame = frame + try: + while not (DictContains(frame.f_locals, 'self') and frame.f_locals['self'].__class__.__name__ == 'Template' and \ + frame.f_code.co_name == 'render'): + frame = frame.f_back + if frame is None: + return old_frame + return frame + except: + return old_frame + +def get_jinja2_template_line(frame): + debug_info = None + if DictContains(frame.f_globals,'__jinja_template__'): + _debug_info = frame.f_globals['__jinja_template__']._debug_info + if _debug_info != '': + #sometimes template contains only plain text + debug_info = frame.f_globals['__jinja_template__'].debug_info + + if debug_info is None: + return None + + lineno = frame.f_lineno + + for pair in debug_info: + if pair[1] == lineno: + return pair[0] + + return None + +def get_jinja2_template_filename(frame): + if DictContains(frame.f_globals, '__jinja_template__'): + fname = frame.f_globals['__jinja_template__'].filename + filename, base = GetFileNameAndBaseFromFile(fname) + return filename + return None + + +#======================================================================================================================= +# Jinja2 Step Commands +#======================================================================================================================= + + +def has_exception_breaks(plugin, pydb): + return hasattr(pydb, 'jinja2_exception_break') and pydb.jinja2_exception_break + +def can_not_skip(plugin, pydb, pydb_frame, frame): + if hasattr(pydb, 'jinja2_breakpoints') and pydb.jinja2_breakpoints and cached_call(pydb_frame, is_jinja2_render_call, frame): + filename = get_jinja2_template_filename(frame) + jinja2_breakpoints_for_file = pydb.jinja2_breakpoints.get(filename) + if jinja2_breakpoints_for_file: + return True + return False + + +def cmd_step_into(plugin, pydb, frame, event, args, stop_info): + pydb, filename, info, thread = args + if not hasattr(info, 'pydev_call_from_jinja2'): + info.pydev_call_from_jinja2 = None + if not hasattr(info, 'pydev_call_inside_jinja2'): + info.pydev_call_inside_jinja2 = None + if is_jinja2_suspended(thread): + stop_info['jinja2_stop'] = event in ('call', 'line') and is_jinja2_render_call(frame) + stop_info['stop'] = False + if info.pydev_call_from_jinja2 is not None: + if is_jinja2_internal_function(frame): + #if internal Jinja2 function was called, we sould continue debugging inside template + info.pydev_call_from_jinja2 = None + else: + #we go into python code from Jinja2 rendering frame + stop_info['stop'] = True + + if event == 'call' and is_jinja2_context_call(frame.f_back): + #we called function from context, the next step will be in function + info.pydev_call_from_jinja2 = 1 + + if event == 'return' and is_jinja2_context_call(frame.f_back): + #we return from python code to Jinja2 rendering frame + info.pydev_step_stop = info.pydev_call_from_jinja2 + info.pydev_call_from_jinja2 = None + thread.additionalInfo.suspend_type = JINJA2_SUSPEND + stop_info['stop'] = False + + #print "info.pydev_call_from_jinja2", info.pydev_call_from_jinja2, "stop_info", stop_info, \ + # "thread.additionalInfo.suspend_type", thread.additionalInfo.suspend_type + #print "event", event, "farme.locals", frame.f_locals + + +def cmd_step_over(plugin, pydb, frame, event, args, stop_info): + pydb, filename, info, thread = args + if not hasattr(info, 'pydev_call_from_jinja2'): + info.pydev_call_from_jinja2 = None + if not hasattr(info, 'pydev_call_inside_jinja2'): + info.pydev_call_inside_jinja2 = None + if is_jinja2_suspended(thread): + stop_info['stop'] = False + + if info.pydev_call_inside_jinja2 is None: + if is_jinja2_render_call(frame): + if event == 'call': + info.pydev_call_inside_jinja2 = frame.f_back + if event in ('line', 'return'): + info.pydev_call_inside_jinja2 = frame + else: + if event == 'line': + if is_jinja2_render_call(frame) and info.pydev_call_inside_jinja2 is frame: + stop_info['jinja2_stop'] = True + if event == 'return': + if frame is info.pydev_call_inside_jinja2 and not DictContains(frame.f_back.f_locals,'event'): + info.pydev_call_inside_jinja2 = find_jinja2_render_frame(frame.f_back) + return True + else: + if event == 'return' and is_jinja2_context_call(frame.f_back): + #we return from python code to Jinja2 rendering frame + info.pydev_call_from_jinja2 = None + info.pydev_call_inside_jinja2 = find_jinja2_render_frame(frame) + thread.additionalInfo.suspend_type = JINJA2_SUSPEND + stop_info['stop'] = False + return True + #print "info.pydev_call_from_jinja2", info.pydev_call_from_jinja2, "stop", stop, "jinja_stop", jinja2_stop, \ + # "thread.additionalInfo.suspend_type", thread.additionalInfo.suspend_type + #print "event", event, "info.pydev_call_inside_jinja2", info.pydev_call_inside_jinja2 + #print "frame", frame, "frame.f_back", frame.f_back, "step_stop", info.pydev_step_stop + #print "is_context_call", is_jinja2_context_call(frame) + #print "render", is_jinja2_render_call(frame) + #print "-------------" + return False + + +def stop(plugin, pydb, frame, event, args, stop_info, arg, step_cmd): + pydb, filename, info, thread = args + if DictContains(stop_info, 'jinja2_stop') and stop_info['jinja2_stop']: + frame = suspend_jinja2(pydb, thread, frame, step_cmd) + if frame: + pydb.doWaitSuspend(thread, frame, event, arg) + return True + return False + + +def get_breakpoint(plugin, pydb, pydb_frame, frame, event, args): + pydb, filename, info, thread = args + new_frame = None + jinja2_breakpoint = None + flag = False + if event in ('line', 'call') and info.pydev_state != STATE_SUSPEND and hasattr(pydb, 'jinja2_breakpoints') and \ + pydb.jinja2_breakpoints and cached_call(pydb_frame, is_jinja2_render_call, frame): + filename = get_jinja2_template_filename(frame) + jinja2_breakpoints_for_file = pydb.jinja2_breakpoints.get(filename) + new_frame = Jinja2TemplateFrame(frame) + + if jinja2_breakpoints_for_file: + lineno = frame.f_lineno + template_lineno = get_jinja2_template_line(frame) + if template_lineno is not None and DictContains(jinja2_breakpoints_for_file, template_lineno): + jinja2_breakpoint = jinja2_breakpoints_for_file[template_lineno] + flag = True + new_frame = Jinja2TemplateFrame(frame) + + return flag, jinja2_breakpoint, new_frame + + +def suspend(plugin, pydb, thread, frame): + return suspend_jinja2(pydb, thread, frame) + + +def exception_break(plugin, pydb, pydb_frame, frame, args, arg): + pydb, filename, info, thread = args + exception, value, trace = arg + if hasattr(pydb, 'jinja2_exception_break') and pydb.jinja2_exception_break: + if get_exception_name(exception) in ('UndefinedError', 'TemplateNotFound', 'TemplatesNotFound'): + #errors in rendering + render_frame = find_jinja2_render_frame(frame) + if render_frame: + suspend_frame = suspend_jinja2(pydb, thread, render_frame, CMD_ADD_EXCEPTION_BREAK) + if suspend_frame: + add_exception_to_frame(suspend_frame, (exception, value, trace)) + flag = True + suspend_frame.f_back = frame + frame = suspend_frame + return flag, frame + elif get_exception_name(exception) in ('TemplateSyntaxError', 'TemplateAssertionError'): + #errors in compile time + name = frame.f_code.co_name + if name in ('template', 'top-level template code') or name.startswith('block '): + #Jinja2 translates exception info and creates fake frame on his own + pydb_frame.setSuspend(thread, CMD_ADD_EXCEPTION_BREAK) + add_exception_to_frame(frame, (exception, value, trace)) + thread.additionalInfo.suspend_type = JINJA2_SUSPEND + flag = True + return flag, frame + return None
\ No newline at end of file diff --git a/python/helpers/pydev/pydevd_resolver.py b/python/helpers/pydev/pydevd_resolver.py index ad49bd881ba0..444dead4cce7 100644 --- a/python/helpers/pydev/pydevd_resolver.py +++ b/python/helpers/pydev/pydevd_resolver.py @@ -409,6 +409,17 @@ class NdArrayResolver: return obj.dtype if attribute == 'size': return obj.size + if attribute.startswith('['): + container = NdArrayItemsContainer() + i = 0 + format_str = '%0' + str(int(len(str(len(obj))))) + 'd' + for item in obj: + setattr(container, format_str % i, item) + i += 1 + if i > MAX_ITEMS_TO_HANDLE: + setattr(container, TOO_LARGE_ATTR, TOO_LARGE_MSG) + break + return container return None def getDictionary(self, obj): @@ -427,9 +438,10 @@ class NdArrayResolver: ret['shape'] = obj.shape ret['dtype'] = obj.dtype ret['size'] = obj.size + ret['[0:%s]' % (len(obj))] = list(obj) return ret - +class NdArrayItemsContainer: pass #======================================================================================================================= # FrameResolver #======================================================================================================================= diff --git a/python/helpers/pydev/pydevd_trace_api.py b/python/helpers/pydev/pydevd_trace_api.py new file mode 100644 index 000000000000..5d2f30e98a3a --- /dev/null +++ b/python/helpers/pydev/pydevd_trace_api.py @@ -0,0 +1,35 @@ +def add_line_breakpoint(plugin, pydb, type, file, line, condition, expression, func_name): + return None + +def add_exception_breakpoint(plugin, pydb, type, exception): + return False + +def remove_exception_breakpoint(plugin, pydb, type, exception): + return False + +def get_breakpoints(plugin, pydb): + return None + +def can_not_skip(plugin, pydb, pydb_frame, frame): + return False + +def has_exception_breaks(plugin, pydb): + return False + +def cmd_step_into(plugin, pydb, frame, event, args, stop_info): + return False + +def cmd_step_over(plugin, pydb, frame, event, args, stop_info): + return False + +def stop(plugin, pydb, frame, event, args, stop_info, arg, step_cmd): + return False + +def get_breakpoint(plugin, pydb, pydb_frame, frame, event, args): + return None + +def suspend(plugin, pydb, thread, frame): + return None + +def exception_break(plugin, pydb, pydb_frame, frame, args, arg): + return None
\ No newline at end of file diff --git a/python/helpers/pydev/pydevd_utils.py b/python/helpers/pydev/pydevd_utils.py index 134b190881ac..753263ba4f31 100644 --- a/python/helpers/pydev/pydevd_utils.py +++ b/python/helpers/pydev/pydevd_utils.py @@ -6,7 +6,28 @@ except: from urllib.parse import quote import pydevd_constants -import pydev_log +import sys + + +def save_main_module(file, module_name): + # patch provided by: Scott Schlesier - when script is run, it does not + # use globals from pydevd: + # This will prevent the pydevd script from contaminating the namespace for the script to be debugged + # pretend pydevd is not the main module, and + # convince the file to be debugged that it was loaded as main + sys.modules[module_name] = sys.modules['__main__'] + sys.modules[module_name].__name__ = module_name + from imp import new_module + + m = new_module('__main__') + sys.modules['__main__'] = m + if hasattr(sys.modules[module_name], '__loader__'): + setattr(m, '__loader__', + getattr(sys.modules[module_name], '__loader__')) + m.__file__ = file + + return m + def to_number(x): if is_string(x): diff --git a/python/helpers/pydev/pydevd_vars.py b/python/helpers/pydev/pydevd_vars.py index 3baea5b61b99..e1aa436b8946 100644 --- a/python/helpers/pydev/pydevd_vars.py +++ b/python/helpers/pydev/pydevd_vars.py @@ -2,7 +2,6 @@ resolution/conversion to XML. """ import pickle -from django_frame import DjangoTemplateFrame from pydevd_constants import * #@UnusedWildImport from types import * #@UnusedWildImport @@ -360,10 +359,10 @@ def changeAttrExpression(thread_id, frame_id, attr, expression): try: expression = expression.replace('@LINE@', '\n') - if isinstance(frame, DjangoTemplateFrame): - result = eval(expression, frame.f_globals, frame.f_locals) - frame.changeVariable(attr, result) - return + # if isinstance(frame, DjangoTemplateFrame): # TODO: implemente for plugins + # result = eval(expression, frame.f_globals, frame.f_locals) + # frame.changeVariable(attr, result) + # return result if attr[:7] == "Globals": attr = attr[8:] @@ -374,7 +373,7 @@ def changeAttrExpression(thread_id, frame_id, attr, expression): if pydevd_save_locals.is_save_locals_available(): frame.f_locals[attr] = eval(expression, frame.f_globals, frame.f_locals) pydevd_save_locals.save_locals(frame) - return + return frame.f_locals[attr] #default way (only works for changing it in the topmost frame) result = eval(expression, frame.f_globals, frame.f_locals) diff --git a/python/helpers/pydev/test_debug.py b/python/helpers/pydev/test_debug.py index 2196ca6f9540..27da09b5593d 100644 --- a/python/helpers/pydev/test_debug.py +++ b/python/helpers/pydev/test_debug.py @@ -5,16 +5,18 @@ import os test_data_path = os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', '..', 'testData', 'debug')) + class PyDevTestCase(unittest.TestCase): def testZipFileExits(self): from pydevd_file_utils import exists - self.assertTrue(exists(test_data_path +'/zipped_lib.zip/zipped_module.py')) + self.assertTrue(exists(test_data_path + '/zipped_lib.zip/zipped_module.py')) self.assertFalse(exists(test_data_path + '/zipped_lib.zip/zipped_module2.py')) self.assertFalse(exists(test_data_path + '/zipped_lib2.zip/zipped_module.py')) def testEggFileExits(self): from pydevd_file_utils import exists + self.assertTrue(exists(test_data_path + '/pycharm-debug.egg/pydev/pydevd.py')) self.assertFalse(exists(test_data_path + '/pycharm-debug.egg/pydev/pydevd2.py')) diff --git a/python/helpers/pydev/tests/check_pydevconsole.py b/python/helpers/pydev/tests/check_pydevconsole.py new file mode 100644 index 000000000000..0ff1a8618a24 --- /dev/null +++ b/python/helpers/pydev/tests/check_pydevconsole.py @@ -0,0 +1,110 @@ +import sys +import os + +# Put pydevconsole in the path. +sys.argv[0] = os.path.dirname(sys.argv[0]) +sys.path.insert(1, os.path.join(os.path.dirname(sys.argv[0]))) + +print('Running tests with:', sys.executable) +print('PYTHONPATH:') +print('\n'.join(sorted(sys.path))) + +import threading +import unittest + +import pydevconsole +from pydev_imports import xmlrpclib, SimpleXMLRPCServer + +try: + raw_input + raw_input_name = 'raw_input' +except NameError: + raw_input_name = 'input' + +#======================================================================================================================= +# Test +#======================================================================================================================= +class Test(unittest.TestCase): + def startClientThread(self, client_port): + class ClientThread(threading.Thread): + def __init__(self, client_port): + threading.Thread.__init__(self) + self.client_port = client_port + + def run(self): + class HandleRequestInput: + def RequestInput(self): + return 'RequestInput: OK' + + handle_request_input = HandleRequestInput() + + import pydev_localhost + + print('Starting client with:', pydev_localhost.get_localhost(), self.client_port) + client_server = SimpleXMLRPCServer((pydev_localhost.get_localhost(), self.client_port), logRequests=False) + client_server.register_function(handle_request_input.RequestInput) + client_server.serve_forever() + + client_thread = ClientThread(client_port) + client_thread.setDaemon(True) + client_thread.start() + return client_thread + + + def getFreeAddresses(self): + import socket + + s = socket.socket() + s.bind(('', 0)) + port0 = s.getsockname()[1] + + s1 = socket.socket() + s1.bind(('', 0)) + port1 = s1.getsockname()[1] + s.close() + s1.close() + return port0, port1 + + + def testServer(self): + client_port, server_port = self.getFreeAddresses() + + class ServerThread(threading.Thread): + def __init__(self, client_port, server_port): + threading.Thread.__init__(self) + self.client_port = client_port + self.server_port = server_port + + def run(self): + import pydev_localhost + + print('Starting server with:', pydev_localhost.get_localhost(), self.server_port, self.client_port) + pydevconsole.StartServer(pydev_localhost.get_localhost(), self.server_port, self.client_port) + + server_thread = ServerThread(client_port, server_port) + server_thread.setDaemon(True) + server_thread.start() + + client_thread = self.startClientThread(client_port) #@UnusedVariable + + import time + + time.sleep(.3) #let's give it some time to start the threads + + import pydev_localhost + + server = xmlrpclib.Server('http://%s:%s' % (pydev_localhost.get_localhost(), server_port)) + server.addExec("import sys; print('Running with: %s %s' % (sys.executable or sys.platform, sys.version))") + server.addExec('class Foo:') + server.addExec(' pass') + server.addExec('') + server.addExec('foo = Foo()') + server.addExec('a = %s()' % raw_input_name) + server.addExec('print (a)') + +#======================================================================================================================= +# main +#======================================================================================================================= +if __name__ == '__main__': + unittest.main() + diff --git a/python/helpers/pydev/tests/test_pydev_ipython_010.py b/python/helpers/pydev/tests/test_pydev_ipython_010.py new file mode 100644 index 000000000000..1822763d4b29 --- /dev/null +++ b/python/helpers/pydev/tests/test_pydev_ipython_010.py @@ -0,0 +1,80 @@ +# TODO: This test no longer works (check if it should be fixed or removed altogether). + +#import unittest +#import sys +#import os +##make it as if we were executing from the directory above this one +#sys.argv[0] = os.path.dirname(sys.argv[0]) +##twice the dirname to get the previous level from this file. +#sys.path.insert(1, os.path.join(os.path.dirname(sys.argv[0]))) +# +#from pydev_localhost import get_localhost +# +# +#IS_JYTHON = sys.platform.find('java') != -1 +# +##======================================================================================================================= +## TestCase +##======================================================================================================================= +#class TestCase(unittest.TestCase): +# +# def setUp(self): +# unittest.TestCase.setUp(self) +# +# def tearDown(self): +# unittest.TestCase.tearDown(self) +# +# def testIPython(self): +# try: +# from pydev_ipython_console import PyDevFrontEnd +# except: +# if IS_JYTHON: +# return +# front_end = PyDevFrontEnd(get_localhost(), 0) +# +# front_end.input_buffer = 'if True:' +# self.assert_(not front_end._on_enter()) +# +# front_end.input_buffer = 'if True:\n' + \ +# front_end.continuation_prompt() + ' a = 10\n' +# self.assert_(not front_end._on_enter()) +# +# +# front_end.input_buffer = 'if True:\n' + \ +# front_end.continuation_prompt() + ' a = 10\n\n' +# self.assert_(front_end._on_enter()) +# +# +## front_end.input_buffer = ' print a' +## self.assert_(not front_end._on_enter()) +## front_end.input_buffer = '' +## self.assert_(front_end._on_enter()) +# +# +## front_end.input_buffer = 'a.' +## front_end.complete_current_input() +## front_end.input_buffer = 'if True:' +## front_end._on_enter() +# front_end.input_buffer = 'a = 30' +# front_end._on_enter() +# front_end.input_buffer = 'print a' +# front_end._on_enter() +# front_end.input_buffer = 'a?' +# front_end._on_enter() +# print front_end.complete('%') +# print front_end.complete('%e') +# print front_end.complete('cd c:/t') +# print front_end.complete('cd c:/temp/') +## front_end.input_buffer = 'print raw_input("press enter\\n")' +## front_end._on_enter() +## +# +##======================================================================================================================= +## main +##======================================================================================================================= +#if __name__ == '__main__': +# if sys.platform.find('java') == -1: +# #IPython not available for Jython +# unittest.main() +# else: +# print('not supported on Jython') diff --git a/python/helpers/pydev/tests_python/_debugger_case_qthread3.py b/python/helpers/pydev/tests_python/_debugger_case_qthread3.py index 22b0c91d7f13..9b326db7ccde 100644 --- a/python/helpers/pydev/tests_python/_debugger_case_qthread3.py +++ b/python/helpers/pydev/tests_python/_debugger_case_qthread3.py @@ -26,4 +26,5 @@ app = QtCore.QCoreApplication([]) runnable = Runnable() QtCore.QThreadPool.globalInstance().start(runnable) app.exec_() +QtCore.QThreadPool.globalInstance().waitForDone() print('TEST SUCEEDED!')
\ No newline at end of file diff --git a/python/helpers/pydev/third_party/pkgutil_old.py b/python/helpers/pydev/third_party/pkgutil_old.py new file mode 100644 index 000000000000..ce072ec9ef75 --- /dev/null +++ b/python/helpers/pydev/third_party/pkgutil_old.py @@ -0,0 +1,591 @@ +"""Utilities to support packages.""" + +# NOTE: This module must remain compatible with Python 2.3, as it is shared +# by setuptools for distribution with Python 2.3 and up. + +import os +import sys +import imp +import os.path +from types import ModuleType + +__all__ = [ + 'get_importer', 'iter_importers', 'get_loader', 'find_loader', + 'walk_packages', 'iter_modules', 'get_data', + 'ImpImporter', 'ImpLoader', 'read_code', 'extend_path', +] + +def read_code(stream): + # This helper is needed in order for the PEP 302 emulation to + # correctly handle compiled files + import marshal + + magic = stream.read(4) + if magic != imp.get_magic(): + return None + + stream.read(4) # Skip timestamp + return marshal.load(stream) + + +def simplegeneric(func): + """Make a trivial single-dispatch generic function""" + registry = {} + def wrapper(*args, **kw): + ob = args[0] + try: + cls = ob.__class__ + except AttributeError: + cls = type(ob) + try: + mro = cls.__mro__ + except AttributeError: + try: + class cls(cls, object): + pass + mro = cls.__mro__[1:] + except TypeError: + mro = object, # must be an ExtensionClass or some such :( + for t in mro: + if t in registry: + return registry[t](*args, **kw) + else: + return func(*args, **kw) + try: + wrapper.__name__ = func.__name__ + except (TypeError, AttributeError): + pass # Python 2.3 doesn't allow functions to be renamed + + def register(typ, func=None): + if func is None: + return lambda f: register(typ, f) + registry[typ] = func + return func + + wrapper.__dict__ = func.__dict__ + wrapper.__doc__ = func.__doc__ + wrapper.register = register + return wrapper + + +def walk_packages(path=None, prefix='', onerror=None): + """Yields (module_loader, name, ispkg) for all modules recursively + on path, or, if path is None, all accessible modules. + + 'path' should be either None or a list of paths to look for + modules in. + + 'prefix' is a string to output on the front of every module name + on output. + + Note that this function must import all *packages* (NOT all + modules!) on the given path, in order to access the __path__ + attribute to find submodules. + + 'onerror' is a function which gets called with one argument (the + name of the package which was being imported) if any exception + occurs while trying to import a package. If no onerror function is + supplied, ImportErrors are caught and ignored, while all other + exceptions are propagated, terminating the search. + + Examples: + + # list all modules python can access + walk_packages() + + # list all submodules of ctypes + walk_packages(ctypes.__path__, ctypes.__name__+'.') + """ + + def seen(p, m={}): + if p in m: + return True + m[p] = True + + for importer, name, ispkg in iter_modules(path, prefix): + yield importer, name, ispkg + + if ispkg: + try: + __import__(name) + except ImportError: + if onerror is not None: + onerror(name) + except Exception: + if onerror is not None: + onerror(name) + else: + raise + else: + path = getattr(sys.modules[name], '__path__', None) or [] + + # don't traverse path items we've seen before + path = [p for p in path if not seen(p)] + + for item in walk_packages(path, name+'.', onerror): + yield item + + +def iter_modules(path=None, prefix=''): + """Yields (module_loader, name, ispkg) for all submodules on path, + or, if path is None, all top-level modules on sys.path. + + 'path' should be either None or a list of paths to look for + modules in. + + 'prefix' is a string to output on the front of every module name + on output. + """ + + if path is None: + importers = iter_importers() + else: + importers = map(get_importer, path) + + yielded = {} + for i in importers: + for name, ispkg in iter_importer_modules(i, prefix): + if name not in yielded: + yielded[name] = 1 + yield i, name, ispkg + + +#@simplegeneric +def iter_importer_modules(importer, prefix=''): + if not hasattr(importer, 'iter_modules'): + return [] + return importer.iter_modules(prefix) + +iter_importer_modules = simplegeneric(iter_importer_modules) + + +class ImpImporter: + """PEP 302 Importer that wraps Python's "classic" import algorithm + + ImpImporter(dirname) produces a PEP 302 importer that searches that + directory. ImpImporter(None) produces a PEP 302 importer that searches + the current sys.path, plus any modules that are frozen or built-in. + + Note that ImpImporter does not currently support being used by placement + on sys.meta_path. + """ + + def __init__(self, path=None): + self.path = path + + def find_module(self, fullname, path=None): + # Note: we ignore 'path' argument since it is only used via meta_path + subname = fullname.split(".")[-1] + if subname != fullname and self.path is None: + return None + if self.path is None: + path = None + else: + path = [os.path.realpath(self.path)] + try: + file, filename, etc = imp.find_module(subname, path) + except ImportError: + return None + return ImpLoader(fullname, file, filename, etc) + + def iter_modules(self, prefix=''): + if self.path is None or not os.path.isdir(self.path): + return + + yielded = {} + import inspect + try: + filenames = os.listdir(self.path) + except OSError: + # ignore unreadable directories like import does + filenames = [] + filenames.sort() # handle packages before same-named modules + + for fn in filenames: + modname = inspect.getmodulename(fn) + if modname=='__init__' or modname in yielded: + continue + + path = os.path.join(self.path, fn) + ispkg = False + + if not modname and os.path.isdir(path) and '.' not in fn: + modname = fn + try: + dircontents = os.listdir(path) + except OSError: + # ignore unreadable directories like import does + dircontents = [] + for fn in dircontents: + subname = inspect.getmodulename(fn) + if subname=='__init__': + ispkg = True + break + else: + continue # not a package + + if modname and '.' not in modname: + yielded[modname] = 1 + yield prefix + modname, ispkg + + +class ImpLoader: + """PEP 302 Loader that wraps Python's "classic" import algorithm + """ + code = source = None + + def __init__(self, fullname, file, filename, etc): + self.file = file + self.filename = filename + self.fullname = fullname + self.etc = etc + + def load_module(self, fullname): + self._reopen() + try: + mod = imp.load_module(fullname, self.file, self.filename, self.etc) + finally: + if self.file: + self.file.close() + # Note: we don't set __loader__ because we want the module to look + # normal; i.e. this is just a wrapper for standard import machinery + return mod + + def get_data(self, pathname): + return open(pathname, "rb").read() + + def _reopen(self): + if self.file and self.file.closed: + mod_type = self.etc[2] + if mod_type==imp.PY_SOURCE: + self.file = open(self.filename, 'rU') + elif mod_type in (imp.PY_COMPILED, imp.C_EXTENSION): + self.file = open(self.filename, 'rb') + + def _fix_name(self, fullname): + if fullname is None: + fullname = self.fullname + elif fullname != self.fullname: + raise ImportError("Loader for module %s cannot handle " + "module %s" % (self.fullname, fullname)) + return fullname + + def is_package(self, fullname): + fullname = self._fix_name(fullname) + return self.etc[2]==imp.PKG_DIRECTORY + + def get_code(self, fullname=None): + fullname = self._fix_name(fullname) + if self.code is None: + mod_type = self.etc[2] + if mod_type==imp.PY_SOURCE: + source = self.get_source(fullname) + self.code = compile(source, self.filename, 'exec') + elif mod_type==imp.PY_COMPILED: + self._reopen() + try: + self.code = read_code(self.file) + finally: + self.file.close() + elif mod_type==imp.PKG_DIRECTORY: + self.code = self._get_delegate().get_code() + return self.code + + def get_source(self, fullname=None): + fullname = self._fix_name(fullname) + if self.source is None: + mod_type = self.etc[2] + if mod_type==imp.PY_SOURCE: + self._reopen() + try: + self.source = self.file.read() + finally: + self.file.close() + elif mod_type==imp.PY_COMPILED: + if os.path.exists(self.filename[:-1]): + f = open(self.filename[:-1], 'rU') + self.source = f.read() + f.close() + elif mod_type==imp.PKG_DIRECTORY: + self.source = self._get_delegate().get_source() + return self.source + + + def _get_delegate(self): + return ImpImporter(self.filename).find_module('__init__') + + def get_filename(self, fullname=None): + fullname = self._fix_name(fullname) + mod_type = self.etc[2] + if self.etc[2]==imp.PKG_DIRECTORY: + return self._get_delegate().get_filename() + elif self.etc[2] in (imp.PY_SOURCE, imp.PY_COMPILED, imp.C_EXTENSION): + return self.filename + return None + + +try: + import zipimport + from zipimport import zipimporter + + def iter_zipimport_modules(importer, prefix=''): + dirlist = zipimport._zip_directory_cache[importer.archive].keys() + dirlist.sort() + _prefix = importer.prefix + plen = len(_prefix) + yielded = {} + import inspect + for fn in dirlist: + if not fn.startswith(_prefix): + continue + + fn = fn[plen:].split(os.sep) + + if len(fn)==2 and fn[1].startswith('__init__.py'): + if fn[0] not in yielded: + yielded[fn[0]] = 1 + yield fn[0], True + + if len(fn)!=1: + continue + + modname = inspect.getmodulename(fn[0]) + if modname=='__init__': + continue + + if modname and '.' not in modname and modname not in yielded: + yielded[modname] = 1 + yield prefix + modname, False + + iter_importer_modules.register(zipimporter, iter_zipimport_modules) + +except ImportError: + pass + + +def get_importer(path_item): + """Retrieve a PEP 302 importer for the given path item + + The returned importer is cached in sys.path_importer_cache + if it was newly created by a path hook. + + If there is no importer, a wrapper around the basic import + machinery is returned. This wrapper is never inserted into + the importer cache (None is inserted instead). + + The cache (or part of it) can be cleared manually if a + rescan of sys.path_hooks is necessary. + """ + try: + importer = sys.path_importer_cache[path_item] + except KeyError: + for path_hook in sys.path_hooks: + try: + importer = path_hook(path_item) + break + except ImportError: + pass + else: + importer = None + sys.path_importer_cache.setdefault(path_item, importer) + + if importer is None: + try: + importer = ImpImporter(path_item) + except ImportError: + importer = None + return importer + + +def iter_importers(fullname=""): + """Yield PEP 302 importers for the given module name + + If fullname contains a '.', the importers will be for the package + containing fullname, otherwise they will be importers for sys.meta_path, + sys.path, and Python's "classic" import machinery, in that order. If + the named module is in a package, that package is imported as a side + effect of invoking this function. + + Non PEP 302 mechanisms (e.g. the Windows registry) used by the + standard import machinery to find files in alternative locations + are partially supported, but are searched AFTER sys.path. Normally, + these locations are searched BEFORE sys.path, preventing sys.path + entries from shadowing them. + + For this to cause a visible difference in behaviour, there must + be a module or package name that is accessible via both sys.path + and one of the non PEP 302 file system mechanisms. In this case, + the emulation will find the former version, while the builtin + import mechanism will find the latter. + + Items of the following types can be affected by this discrepancy: + imp.C_EXTENSION, imp.PY_SOURCE, imp.PY_COMPILED, imp.PKG_DIRECTORY + """ + if fullname.startswith('.'): + raise ImportError("Relative module names not supported") + if '.' in fullname: + # Get the containing package's __path__ + pkg = '.'.join(fullname.split('.')[:-1]) + if pkg not in sys.modules: + __import__(pkg) + path = getattr(sys.modules[pkg], '__path__', None) or [] + else: + for importer in sys.meta_path: + yield importer + path = sys.path + for item in path: + yield get_importer(item) + if '.' not in fullname: + yield ImpImporter() + +def get_loader(module_or_name): + """Get a PEP 302 "loader" object for module_or_name + + If the module or package is accessible via the normal import + mechanism, a wrapper around the relevant part of that machinery + is returned. Returns None if the module cannot be found or imported. + If the named module is not already imported, its containing package + (if any) is imported, in order to establish the package __path__. + + This function uses iter_importers(), and is thus subject to the same + limitations regarding platform-specific special import locations such + as the Windows registry. + """ + if module_or_name in sys.modules: + module_or_name = sys.modules[module_or_name] + if isinstance(module_or_name, ModuleType): + module = module_or_name + loader = getattr(module, '__loader__', None) + if loader is not None: + return loader + fullname = module.__name__ + else: + fullname = module_or_name + return find_loader(fullname) + +def find_loader(fullname): + """Find a PEP 302 "loader" object for fullname + + If fullname contains dots, path must be the containing package's __path__. + Returns None if the module cannot be found or imported. This function uses + iter_importers(), and is thus subject to the same limitations regarding + platform-specific special import locations such as the Windows registry. + """ + for importer in iter_importers(fullname): + loader = importer.find_module(fullname) + if loader is not None: + return loader + + return None + + +def extend_path(path, name): + """Extend a package's path. + + Intended use is to place the following code in a package's __init__.py: + + from pkgutil import extend_path + __path__ = extend_path(__path__, __name__) + + This will add to the package's __path__ all subdirectories of + directories on sys.path named after the package. This is useful + if one wants to distribute different parts of a single logical + package as multiple directories. + + It also looks for *.pkg files beginning where * matches the name + argument. This feature is similar to *.pth files (see site.py), + except that it doesn't special-case lines starting with 'import'. + A *.pkg file is trusted at face value: apart from checking for + duplicates, all entries found in a *.pkg file are added to the + path, regardless of whether they are exist the filesystem. (This + is a feature.) + + If the input path is not a list (as is the case for frozen + packages) it is returned unchanged. The input path is not + modified; an extended copy is returned. Items are only appended + to the copy at the end. + + It is assumed that sys.path is a sequence. Items of sys.path that + are not (unicode or 8-bit) strings referring to existing + directories are ignored. Unicode items of sys.path that cause + errors when used as filenames may cause this function to raise an + exception (in line with os.path.isdir() behavior). + """ + + if not isinstance(path, list): + # This could happen e.g. when this is called from inside a + # frozen package. Return the path unchanged in that case. + return path + + pname = os.path.join(*name.split('.')) # Reconstitute as relative path + # Just in case os.extsep != '.' + sname = os.extsep.join(name.split('.')) + sname_pkg = sname + os.extsep + "pkg" + init_py = "__init__" + os.extsep + "py" + + path = path[:] # Start with a copy of the existing path + + for dir in sys.path: + if not isinstance(dir, basestring) or not os.path.isdir(dir): + continue + subdir = os.path.join(dir, pname) + # XXX This may still add duplicate entries to path on + # case-insensitive filesystems + initfile = os.path.join(subdir, init_py) + if subdir not in path and os.path.isfile(initfile): + path.append(subdir) + # XXX Is this the right thing for subpackages like zope.app? + # It looks for a file named "zope.app.pkg" + pkgfile = os.path.join(dir, sname_pkg) + if os.path.isfile(pkgfile): + try: + f = open(pkgfile) + except IOError, msg: + sys.stderr.write("Can't open %s: %s\n" % + (pkgfile, msg)) + else: + for line in f: + line = line.rstrip('\n') + if not line or line.startswith('#'): + continue + path.append(line) # Don't check for existence! + f.close() + + return path + +def get_data(package, resource): + """Get a resource from a package. + + This is a wrapper round the PEP 302 loader get_data API. The package + argument should be the name of a package, in standard module format + (foo.bar). The resource argument should be in the form of a relative + filename, using '/' as the path separator. The parent directory name '..' + is not allowed, and nor is a rooted name (starting with a '/'). + + The function returns a binary string, which is the contents of the + specified resource. + + For packages located in the filesystem, which have already been imported, + this is the rough equivalent of + + d = os.path.dirname(sys.modules[package].__file__) + data = open(os.path.join(d, resource), 'rb').read() + + If the package cannot be located or loaded, or it uses a PEP 302 loader + which does not support get_data(), then None is returned. + """ + + loader = get_loader(package) + if loader is None or not hasattr(loader, 'get_data'): + return None + mod = sys.modules.get(package) or loader.load_module(package) + if mod is None or not hasattr(mod, '__file__'): + return None + + # Modify the resource name to be compatible with the loader.get_data + # signature - an os.path format "filename" starting with the dirname of + # the package's __file__ + parts = resource.split('/') + parts.insert(0, os.path.dirname(mod.__file__)) + resource_name = os.path.join(*parts) + return loader.get_data(resource_name) diff --git a/python/helpers/pydev/third_party/pluginbase.py b/python/helpers/pydev/third_party/pluginbase.py new file mode 100644 index 000000000000..0ad6404eee00 --- /dev/null +++ b/python/helpers/pydev/third_party/pluginbase.py @@ -0,0 +1,454 @@ +# -*- coding: utf-8 -*- +""" + pluginbase + ~~~~~~~~~~ + + Pluginbase is a module for Python that provides a system for building + plugin based applications. + + :copyright: (c) Copyright 2014 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import os +import sys + +from pydevd_constants import IS_PY24, IS_PY3K + + +if IS_PY24: + from third_party.uuid_old import uuid4 +else: + from uuid import uuid4 + +if IS_PY3K: + import pkgutil +else: + import pkgutil_old as pkgutil + +import errno +try: + from hashlib import md5 +except ImportError: + from md5 import md5 +import threading + +from types import ModuleType +from weakref import ref as weakref + + +PY2 = sys.version_info[0] == 2 +if PY2: + text_type = unicode + string_types = (unicode, str) + from cStringIO import StringIO as NativeBytesIO +else: + text_type = str + string_types = (str,) + from io import BytesIO as NativeBytesIO + + +_local = threading.local() + +_internalspace = ModuleType(__name__ + '._internalspace') +_internalspace.__path__ = [] +sys.modules[_internalspace.__name__] = _internalspace + + +def get_plugin_source(module=None, stacklevel=None): + """Returns the :class:`PluginSource` for the current module or the given + module. The module can be provided by name (in which case an import + will be attempted) or as a module object. + + If no plugin source can be discovered, the return value from this method + is `None`. + + This function can be very useful if additional data has been attached + to the plugin source. For instance this could allow plugins to get + access to a back reference to the application that created them. + + :param module: optionally the module to locate the plugin source of. + :param stacklevel: defines how many levels up the module should search + for before it discovers the plugin frame. The + default is 0. This can be useful for writing wrappers + around this function. + """ + if module is None: + frm = sys._getframe((stacklevel or 0) + 1) + name = frm.f_globals['__name__'] + glob = frm.f_globals + elif isinstance(module, string_types): + frm = sys._getframe(1) + name = module + glob = __import__(module, frm.f_globals, + frm.f_locals, ['__dict__']).__dict__ + else: + name = module.__name__ + glob = module.__dict__ + return _discover_space(name, glob) + + +def _discover_space(name, globals): + try: + return _local.space_stack[-1] + except (AttributeError, IndexError): + pass + + if '__pluginbase_state__' in globals: + return globals['__pluginbase_state__'].source + + mod_name = globals.get('__name__') + if mod_name is not None and \ + mod_name.startswith(_internalspace.__name__ + '.'): + end = mod_name.find('.', len(_internalspace.__name__) + 1) + space = sys.modules.get(mod_name[:end]) + if space is not None: + return space.__pluginbase_state__.source + + +def _shutdown_module(mod): + members = list(mod.__dict__.items()) + for key, value in members: + if key[:1] != '_': + setattr(mod, key, None) + for key, value in members: + setattr(mod, key, None) + + +def _to_bytes(s): + if isinstance(s, text_type): + return s.encode('utf-8') + return s + + +class _IntentionallyEmptyModule(ModuleType): + + def __getattr__(self, name): + try: + return ModuleType.__getattr__(self, name) + except AttributeError: + if name[:2] == '__': + raise + raise RuntimeError( + 'Attempted to import from a plugin base module (%s) without ' + 'having a plugin source activated. To solve this error ' + 'you have to move the import into a "with" block of the ' + 'associated plugin source.' % self.__name__) + + +class _PluginSourceModule(ModuleType): + + def __init__(self, source): + modname = '%s.%s' % (_internalspace.__name__, source.spaceid) + ModuleType.__init__(self, modname) + self.__pluginbase_state__ = PluginBaseState(source) + + @property + def __path__(self): + try: + ps = self.__pluginbase_state__.source + except AttributeError: + return [] + return ps.searchpath + ps.base.searchpath + + +def _setup_base_package(module_name): + try: + mod = __import__(module_name, None, None, ['__name__']) + except ImportError: + mod = None + if '.' in module_name: + parent_mod = __import__(module_name.rsplit('.', 1)[0], + None, None, ['__name__']) + else: + parent_mod = None + + if mod is None: + mod = _IntentionallyEmptyModule(module_name) + if parent_mod is not None: + setattr(parent_mod, module_name.rsplit('.', 1)[-1], mod) + sys.modules[module_name] = mod + + +class PluginBase(object): + """The plugin base acts as a control object around a dummy Python + package that acts as a container for plugins. Usually each + application creates exactly one base object for all plugins. + + :param package: the name of the package that acts as the plugin base. + Usually this module does not exist. Unless you know + what you are doing you should not create this module + on the file system. + :param searchpath: optionally a shared search path for modules that + will be used by all plugin sources registered. + """ + + def __init__(self, package, searchpath=None): + #: the name of the dummy package. + self.package = package + if searchpath is None: + searchpath = [] + #: the default search path shared by all plugins as list. + self.searchpath = searchpath + _setup_base_package(package) + + def make_plugin_source(self, *args, **kwargs): + """Creats a plugin source for this plugin base and returns it. + All parameters are forwarded to :class:`PluginSource`. + """ + return PluginSource(self, *args, **kwargs) + + +class PluginSource(object): + """The plugin source is what ultimately decides where plugins are + loaded from. Plugin bases can have multiple plugin sources which act + as isolation layer. While this is not a security system it generally + is not possible for plugins from different sources to accidentally + cross talk. + + Once a plugin source has been created it can be used in a ``with`` + statement to change the behavior of the ``import`` statement in the + block to define which source to load the plugins from:: + + plugin_source = plugin_base.make_plugin_source( + searchpath=['./path/to/plugins', './path/to/more/plugins']) + + with plugin_source: + from myapplication.plugins import my_plugin + + :param base: the base this plugin source belongs to. + :param identifier: optionally a stable identifier. If it's not defined + a random identifier is picked. It's useful to set this + to a stable value to have consistent tracebacks + between restarts and to support pickle. + :param searchpath: a list of paths where plugins are looked for. + :param persist: optionally this can be set to `True` and the plugins + will not be cleaned up when the plugin source gets + garbage collected. + """ + # Set these here to false by default so that a completely failing + # constructor does not fuck up the destructor. + persist = False + mod = None + + def __init__(self, base, identifier=None, searchpath=None, + persist=False): + #: indicates if this plugin source persists or not. + self.persist = persist + if identifier is None: + identifier = str(uuid4()) + #: the identifier for this source. + self.identifier = identifier + #: A reference to the plugin base that created this source. + self.base = base + #: a list of paths where plugins are searched in. + self.searchpath = searchpath + #: The internal module name of the plugin source as it appears + #: in the :mod:`pluginsource._internalspace`. + div = None + self.spaceid = '_sp' + md5( + _to_bytes(self.base.package) + _to_bytes('|') + + _to_bytes(self.identifier) + ).hexdigest() + #: a reference to the module on the internal + #: :mod:`pluginsource._internalspace`. + self.mod = _PluginSourceModule(self) + + if hasattr(_internalspace, self.spaceid): + raise RuntimeError('This plugin source already exists.') + sys.modules[self.mod.__name__] = self.mod + setattr(_internalspace, self.spaceid, self.mod) + + def __del__(self): + if not self.persist: + self.cleanup() + + def list_plugins(self): + """Returns a sorted list of all plugins that are available in this + plugin source. This can be useful to automatically discover plugins + that are available and is usually used together with + :meth:`load_plugin`. + """ + rv = [] + for _, modname, ispkg in pkgutil.iter_modules(self.mod.__path__): + rv.append(modname) + return sorted(rv) + + def load_plugin(self, name): + """This automatically loads a plugin by the given name from the + current source and returns the module. This is a convenient + alternative to the import statement and saves you from invoking + ``__import__`` or a similar function yourself. + + :param name: the name of the plugin to load. + """ + if '.' in name: + raise ImportError('Plugin names cannot contain dots.') + + #with self: + # return __import__(self.base.package + '.' + name, + # globals(), {}, ['__name__']) + + self.__assert_not_cleaned_up() + _local.__dict__.setdefault('space_stack', []).append(self) + try: + res = __import__(self.base.package + '.' + name, + globals(), {}, ['__name__']) + return res + finally: + try: + _local.space_stack.pop() + except (AttributeError, IndexError): + pass + + def open_resource(self, plugin, filename): + """This function locates a resource inside the plugin and returns + a byte stream to the contents of it. If the resource cannot be + loaded an :exc:`IOError` will be raised. Only plugins that are + real Python packages can contain resources. Plain old Python + modules do not allow this for obvious reasons. + + .. versionadded:: 0.3 + + :param plugin: the name of the plugin to open the resource of. + :param filename: the name of the file within the plugin to open. + """ + mod = self.load_plugin(plugin) + fn = getattr(mod, '__file__', None) + if fn is not None: + if fn.endswith(('.pyc', '.pyo')): + fn = fn[:-1] + if os.path.isfile(fn): + return open(os.path.join(os.path.dirname(fn), filename), 'rb') + buf = pkgutil.get_data(self.mod.__name__ + '.' + plugin, filename) + if buf is None: + raise IOError(errno.ENOEXITS, 'Could not find resource') + return NativeBytesIO(buf) + + def cleanup(self): + """Cleans up all loaded plugins manually. This is necessary to + call only if :attr:`persist` is enabled. Otherwise this happens + automatically when the source gets garbage collected. + """ + self.__cleanup() + + def __cleanup(self, _sys=sys, _shutdown_module=_shutdown_module): + # The default parameters are necessary because this can be fired + # from the destructor and so late when the interpreter shuts down + # that these functions and modules might be gone. + if self.mod is None: + return + modname = self.mod.__name__ + self.mod.__pluginbase_state__ = None + self.mod = None + try: + delattr(_internalspace, self.spaceid) + except AttributeError: + pass + prefix = modname + '.' + _sys.modules.pop(modname) + for key, value in list(_sys.modules.items()): + if not key.startswith(prefix): + continue + mod = _sys.modules.pop(key, None) + if mod is None: + continue + _shutdown_module(mod) + + def __assert_not_cleaned_up(self): + if self.mod is None: + raise RuntimeError('The plugin source was already cleaned up.') + + def __enter__(self): + self.__assert_not_cleaned_up() + _local.__dict__.setdefault('space_stack', []).append(self) + return self + + def __exit__(self, exc_type, exc_value, tb): + try: + _local.space_stack.pop() + except (AttributeError, IndexError): + pass + + def _rewrite_module_path(self, modname): + self.__assert_not_cleaned_up() + if modname == self.base.package: + return self.mod.__name__ + elif modname.startswith(self.base.package + '.'): + pieces = modname.split('.') + return self.mod.__name__ + '.' + '.'.join( + pieces[self.base.package.count('.') + 1:]) + + +class PluginBaseState(object): + __slots__ = ('_source',) + + def __init__(self, source): + if source.persist: + self._source = lambda: source + else: + self._source = weakref(source) + + @property + def source(self): + rv = self._source() + if rv is None: + raise AttributeError('Plugin source went away') + return rv + + +class _ImportHook(ModuleType): + + def __init__(self, name, system_import): + ModuleType.__init__(self, name) + self._system_import = system_import + self.enabled = True + + def enable(self): + """Enables the import hook which drives the plugin base system. + This is the default. + """ + self.enabled = True + + def disable(self): + """Disables the import hook and restores the default import system + behavior. This effectively breaks pluginbase but can be useful + for testing purposes. + """ + self.enabled = False + + def plugin_import(self, name, globals=None, locals=None, + fromlist=None, level=-2): + import_name = name + if self.enabled: + ref_globals = globals + if ref_globals is None: + ref_globals = sys._getframe(1).f_globals + space = _discover_space(name, ref_globals) + if space is not None: + actual_name = space._rewrite_module_path(name) + if actual_name is not None: + import_name = actual_name + if level == -2: + # fake impossible value; default value depends on version + if IS_PY24: + # the level parameter was added in version 2.5 + return self._system_import(import_name, globals, locals, fromlist) + elif IS_PY3K: + # default value for level parameter in python 3 + level = 0 + else: + # default value for level parameter in other versions + level = -1 + return self._system_import(import_name, globals, locals, + fromlist, level) + + +try: + import __builtin__ as builtins +except ImportError: + import builtins + +import_hook = _ImportHook(__name__ + '.import_hook', builtins.__import__) +builtins.__import__ = import_hook.plugin_import +sys.modules[import_hook.__name__] = import_hook +del builtins diff --git a/python/helpers/pydev/third_party/uuid_old.py b/python/helpers/pydev/third_party/uuid_old.py new file mode 100644 index 000000000000..ae3da25ca557 --- /dev/null +++ b/python/helpers/pydev/third_party/uuid_old.py @@ -0,0 +1,541 @@ +r"""UUID objects (universally unique identifiers) according to RFC 4122. + +This module provides immutable UUID objects (class UUID) and the functions +uuid1(), uuid3(), uuid4(), uuid5() for generating version 1, 3, 4, and 5 +UUIDs as specified in RFC 4122. + +If all you want is a unique ID, you should probably call uuid1() or uuid4(). +Note that uuid1() may compromise privacy since it creates a UUID containing +the computer's network address. uuid4() creates a random UUID. + +Typical usage: + + >>> import uuid + + # make a UUID based on the host ID and current time + >>> uuid.uuid1() + UUID('a8098c1a-f86e-11da-bd1a-00112444be1e') + + # make a UUID using an MD5 hash of a namespace UUID and a name + >>> uuid.uuid3(uuid.NAMESPACE_DNS, 'python.org') + UUID('6fa459ea-ee8a-3ca4-894e-db77e160355e') + + # make a random UUID + >>> uuid.uuid4() + UUID('16fd2706-8baf-433b-82eb-8c7fada847da') + + # make a UUID using a SHA-1 hash of a namespace UUID and a name + >>> uuid.uuid5(uuid.NAMESPACE_DNS, 'python.org') + UUID('886313e1-3b8a-5372-9b90-0c9aee199e5d') + + # make a UUID from a string of hex digits (braces and hyphens ignored) + >>> x = uuid.UUID('{00010203-0405-0607-0809-0a0b0c0d0e0f}') + + # convert a UUID to a string of hex digits in standard form + >>> str(x) + '00010203-0405-0607-0809-0a0b0c0d0e0f' + + # get the raw 16 bytes of the UUID + >>> x.bytes + '\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f' + + # make a UUID from a 16-byte string + >>> uuid.UUID(bytes=x.bytes) + UUID('00010203-0405-0607-0809-0a0b0c0d0e0f') +""" + +__author__ = 'Ka-Ping Yee <ping@zesty.ca>' + +RESERVED_NCS, RFC_4122, RESERVED_MICROSOFT, RESERVED_FUTURE = [ + 'reserved for NCS compatibility', 'specified in RFC 4122', + 'reserved for Microsoft compatibility', 'reserved for future definition'] + +class UUID(object): + """Instances of the UUID class represent UUIDs as specified in RFC 4122. + UUID objects are immutable, hashable, and usable as dictionary keys. + Converting a UUID to a string with str() yields something in the form + '12345678-1234-1234-1234-123456789abc'. The UUID constructor accepts + five possible forms: a similar string of hexadecimal digits, or a tuple + of six integer fields (with 32-bit, 16-bit, 16-bit, 8-bit, 8-bit, and + 48-bit values respectively) as an argument named 'fields', or a string + of 16 bytes (with all the integer fields in big-endian order) as an + argument named 'bytes', or a string of 16 bytes (with the first three + fields in little-endian order) as an argument named 'bytes_le', or a + single 128-bit integer as an argument named 'int'. + + UUIDs have these read-only attributes: + + bytes the UUID as a 16-byte string (containing the six + integer fields in big-endian byte order) + + bytes_le the UUID as a 16-byte string (with time_low, time_mid, + and time_hi_version in little-endian byte order) + + fields a tuple of the six integer fields of the UUID, + which are also available as six individual attributes + and two derived attributes: + + time_low the first 32 bits of the UUID + time_mid the next 16 bits of the UUID + time_hi_version the next 16 bits of the UUID + clock_seq_hi_variant the next 8 bits of the UUID + clock_seq_low the next 8 bits of the UUID + node the last 48 bits of the UUID + + time the 60-bit timestamp + clock_seq the 14-bit sequence number + + hex the UUID as a 32-character hexadecimal string + + int the UUID as a 128-bit integer + + urn the UUID as a URN as specified in RFC 4122 + + variant the UUID variant (one of the constants RESERVED_NCS, + RFC_4122, RESERVED_MICROSOFT, or RESERVED_FUTURE) + + version the UUID version number (1 through 5, meaningful only + when the variant is RFC_4122) + """ + + def __init__(self, hex=None, bytes=None, bytes_le=None, fields=None, + int=None, version=None): + r"""Create a UUID from either a string of 32 hexadecimal digits, + a string of 16 bytes as the 'bytes' argument, a string of 16 bytes + in little-endian order as the 'bytes_le' argument, a tuple of six + integers (32-bit time_low, 16-bit time_mid, 16-bit time_hi_version, + 8-bit clock_seq_hi_variant, 8-bit clock_seq_low, 48-bit node) as + the 'fields' argument, or a single 128-bit integer as the 'int' + argument. When a string of hex digits is given, curly braces, + hyphens, and a URN prefix are all optional. For example, these + expressions all yield the same UUID: + + UUID('{12345678-1234-5678-1234-567812345678}') + UUID('12345678123456781234567812345678') + UUID('urn:uuid:12345678-1234-5678-1234-567812345678') + UUID(bytes='\x12\x34\x56\x78'*4) + UUID(bytes_le='\x78\x56\x34\x12\x34\x12\x78\x56' + + '\x12\x34\x56\x78\x12\x34\x56\x78') + UUID(fields=(0x12345678, 0x1234, 0x5678, 0x12, 0x34, 0x567812345678)) + UUID(int=0x12345678123456781234567812345678) + + Exactly one of 'hex', 'bytes', 'bytes_le', 'fields', or 'int' must + be given. The 'version' argument is optional; if given, the resulting + UUID will have its variant and version set according to RFC 4122, + overriding the given 'hex', 'bytes', 'bytes_le', 'fields', or 'int'. + """ + + if [hex, bytes, bytes_le, fields, int].count(None) != 4: + raise TypeError('need one of hex, bytes, bytes_le, fields, or int') + if hex is not None: + hex = hex.replace('urn:', '').replace('uuid:', '') + hex = hex.strip('{}').replace('-', '') + if len(hex) != 32: + raise ValueError('badly formed hexadecimal UUID string') + int = long(hex, 16) + if bytes_le is not None: + if len(bytes_le) != 16: + raise ValueError('bytes_le is not a 16-char string') + bytes = (bytes_le[3] + bytes_le[2] + bytes_le[1] + bytes_le[0] + + bytes_le[5] + bytes_le[4] + bytes_le[7] + bytes_le[6] + + bytes_le[8:]) + if bytes is not None: + if len(bytes) != 16: + raise ValueError('bytes is not a 16-char string') + int = long(('%02x'*16) % tuple(map(ord, bytes)), 16) + if fields is not None: + if len(fields) != 6: + raise ValueError('fields is not a 6-tuple') + (time_low, time_mid, time_hi_version, + clock_seq_hi_variant, clock_seq_low, node) = fields + if not 0 <= time_low < 1<<32L: + raise ValueError('field 1 out of range (need a 32-bit value)') + if not 0 <= time_mid < 1<<16L: + raise ValueError('field 2 out of range (need a 16-bit value)') + if not 0 <= time_hi_version < 1<<16L: + raise ValueError('field 3 out of range (need a 16-bit value)') + if not 0 <= clock_seq_hi_variant < 1<<8L: + raise ValueError('field 4 out of range (need an 8-bit value)') + if not 0 <= clock_seq_low < 1<<8L: + raise ValueError('field 5 out of range (need an 8-bit value)') + if not 0 <= node < 1<<48L: + raise ValueError('field 6 out of range (need a 48-bit value)') + clock_seq = (clock_seq_hi_variant << 8L) | clock_seq_low + int = ((time_low << 96L) | (time_mid << 80L) | + (time_hi_version << 64L) | (clock_seq << 48L) | node) + if int is not None: + if not 0 <= int < 1<<128L: + raise ValueError('int is out of range (need a 128-bit value)') + if version is not None: + if not 1 <= version <= 5: + raise ValueError('illegal version number') + # Set the variant to RFC 4122. + int &= ~(0xc000 << 48L) + int |= 0x8000 << 48L + # Set the version number. + int &= ~(0xf000 << 64L) + int |= version << 76L + self.__dict__['int'] = int + + def __cmp__(self, other): + if isinstance(other, UUID): + return cmp(self.int, other.int) + return NotImplemented + + def __hash__(self): + return hash(self.int) + + def __int__(self): + return self.int + + def __repr__(self): + return 'UUID(%r)' % str(self) + + def __setattr__(self, name, value): + raise TypeError('UUID objects are immutable') + + def __str__(self): + hex = '%032x' % self.int + return '%s-%s-%s-%s-%s' % ( + hex[:8], hex[8:12], hex[12:16], hex[16:20], hex[20:]) + + def get_bytes(self): + bytes = '' + for shift in range(0, 128, 8): + bytes = chr((self.int >> shift) & 0xff) + bytes + return bytes + + bytes = property(get_bytes) + + def get_bytes_le(self): + bytes = self.bytes + return (bytes[3] + bytes[2] + bytes[1] + bytes[0] + + bytes[5] + bytes[4] + bytes[7] + bytes[6] + bytes[8:]) + + bytes_le = property(get_bytes_le) + + def get_fields(self): + return (self.time_low, self.time_mid, self.time_hi_version, + self.clock_seq_hi_variant, self.clock_seq_low, self.node) + + fields = property(get_fields) + + def get_time_low(self): + return self.int >> 96L + + time_low = property(get_time_low) + + def get_time_mid(self): + return (self.int >> 80L) & 0xffff + + time_mid = property(get_time_mid) + + def get_time_hi_version(self): + return (self.int >> 64L) & 0xffff + + time_hi_version = property(get_time_hi_version) + + def get_clock_seq_hi_variant(self): + return (self.int >> 56L) & 0xff + + clock_seq_hi_variant = property(get_clock_seq_hi_variant) + + def get_clock_seq_low(self): + return (self.int >> 48L) & 0xff + + clock_seq_low = property(get_clock_seq_low) + + def get_time(self): + return (((self.time_hi_version & 0x0fffL) << 48L) | + (self.time_mid << 32L) | self.time_low) + + time = property(get_time) + + def get_clock_seq(self): + return (((self.clock_seq_hi_variant & 0x3fL) << 8L) | + self.clock_seq_low) + + clock_seq = property(get_clock_seq) + + def get_node(self): + return self.int & 0xffffffffffff + + node = property(get_node) + + def get_hex(self): + return '%032x' % self.int + + hex = property(get_hex) + + def get_urn(self): + return 'urn:uuid:' + str(self) + + urn = property(get_urn) + + def get_variant(self): + if not self.int & (0x8000 << 48L): + return RESERVED_NCS + elif not self.int & (0x4000 << 48L): + return RFC_4122 + elif not self.int & (0x2000 << 48L): + return RESERVED_MICROSOFT + else: + return RESERVED_FUTURE + + variant = property(get_variant) + + def get_version(self): + # The version bits are only meaningful for RFC 4122 UUIDs. + if self.variant == RFC_4122: + return int((self.int >> 76L) & 0xf) + + version = property(get_version) + +def _find_mac(command, args, hw_identifiers, get_index): + import os + for dir in ['', '/sbin/', '/usr/sbin']: + executable = os.path.join(dir, command) + if not os.path.exists(executable): + continue + + try: + # LC_ALL to get English output, 2>/dev/null to + # prevent output on stderr + cmd = 'LC_ALL=C %s %s 2>/dev/null' % (executable, args) + pipe = os.popen(cmd) + except IOError: + continue + + for line in pipe: + words = line.lower().split() + for i in range(len(words)): + if words[i] in hw_identifiers: + return int(words[get_index(i)].replace(':', ''), 16) + return None + +def _ifconfig_getnode(): + """Get the hardware address on Unix by running ifconfig.""" + + # This works on Linux ('' or '-a'), Tru64 ('-av'), but not all Unixes. + for args in ('', '-a', '-av'): + mac = _find_mac('ifconfig', args, ['hwaddr', 'ether'], lambda i: i+1) + if mac: + return mac + + import socket + ip_addr = socket.gethostbyname(socket.gethostname()) + + # Try getting the MAC addr from arp based on our IP address (Solaris). + mac = _find_mac('arp', '-an', [ip_addr], lambda i: -1) + if mac: + return mac + + # This might work on HP-UX. + mac = _find_mac('lanscan', '-ai', ['lan0'], lambda i: 0) + if mac: + return mac + + return None + +def _ipconfig_getnode(): + """Get the hardware address on Windows by running ipconfig.exe.""" + import os, re + dirs = ['', r'c:\windows\system32', r'c:\winnt\system32'] + try: + import ctypes + buffer = ctypes.create_string_buffer(300) + ctypes.windll.kernel32.GetSystemDirectoryA(buffer, 300) + dirs.insert(0, buffer.value.decode('mbcs')) + except: + pass + for dir in dirs: + try: + pipe = os.popen(os.path.join(dir, 'ipconfig') + ' /all') + except IOError: + continue + for line in pipe: + value = line.split(':')[-1].strip().lower() + if re.match('([0-9a-f][0-9a-f]-){5}[0-9a-f][0-9a-f]', value): + return int(value.replace('-', ''), 16) + +def _netbios_getnode(): + """Get the hardware address on Windows using NetBIOS calls. + See http://support.microsoft.com/kb/118623 for details.""" + import win32wnet, netbios + ncb = netbios.NCB() + ncb.Command = netbios.NCBENUM + ncb.Buffer = adapters = netbios.LANA_ENUM() + adapters._pack() + if win32wnet.Netbios(ncb) != 0: + return + adapters._unpack() + for i in range(adapters.length): + ncb.Reset() + ncb.Command = netbios.NCBRESET + ncb.Lana_num = ord(adapters.lana[i]) + if win32wnet.Netbios(ncb) != 0: + continue + ncb.Reset() + ncb.Command = netbios.NCBASTAT + ncb.Lana_num = ord(adapters.lana[i]) + ncb.Callname = '*'.ljust(16) + ncb.Buffer = status = netbios.ADAPTER_STATUS() + if win32wnet.Netbios(ncb) != 0: + continue + status._unpack() + bytes = map(ord, status.adapter_address) + return ((bytes[0]<<40L) + (bytes[1]<<32L) + (bytes[2]<<24L) + + (bytes[3]<<16L) + (bytes[4]<<8L) + bytes[5]) + +# Thanks to Thomas Heller for ctypes and for his help with its use here. + +# If ctypes is available, use it to find system routines for UUID generation. +_uuid_generate_random = _uuid_generate_time = _UuidCreate = None +try: + import ctypes, ctypes.util + _buffer = ctypes.create_string_buffer(16) + + # The uuid_generate_* routines are provided by libuuid on at least + # Linux and FreeBSD, and provided by libc on Mac OS X. + for libname in ['uuid', 'c']: + try: + lib = ctypes.CDLL(ctypes.util.find_library(libname)) + except: + continue + if hasattr(lib, 'uuid_generate_random'): + _uuid_generate_random = lib.uuid_generate_random + if hasattr(lib, 'uuid_generate_time'): + _uuid_generate_time = lib.uuid_generate_time + + # On Windows prior to 2000, UuidCreate gives a UUID containing the + # hardware address. On Windows 2000 and later, UuidCreate makes a + # random UUID and UuidCreateSequential gives a UUID containing the + # hardware address. These routines are provided by the RPC runtime. + # NOTE: at least on Tim's WinXP Pro SP2 desktop box, while the last + # 6 bytes returned by UuidCreateSequential are fixed, they don't appear + # to bear any relationship to the MAC address of any network device + # on the box. + try: + lib = ctypes.windll.rpcrt4 + except: + lib = None + _UuidCreate = getattr(lib, 'UuidCreateSequential', + getattr(lib, 'UuidCreate', None)) +except: + pass + +def _unixdll_getnode(): + """Get the hardware address on Unix using ctypes.""" + _uuid_generate_time(_buffer) + return UUID(bytes=_buffer.raw).node + +def _windll_getnode(): + """Get the hardware address on Windows using ctypes.""" + if _UuidCreate(_buffer) == 0: + return UUID(bytes=_buffer.raw).node + +def _random_getnode(): + """Get a random node ID, with eighth bit set as suggested by RFC 4122.""" + import random + return random.randrange(0, 1<<48L) | 0x010000000000L + +_node = None + +def getnode(): + """Get the hardware address as a 48-bit positive integer. + + The first time this runs, it may launch a separate program, which could + be quite slow. If all attempts to obtain the hardware address fail, we + choose a random 48-bit number with its eighth bit set to 1 as recommended + in RFC 4122. + """ + + global _node + if _node is not None: + return _node + + import sys + if sys.platform == 'win32': + getters = [_windll_getnode, _netbios_getnode, _ipconfig_getnode] + else: + getters = [_unixdll_getnode, _ifconfig_getnode] + + for getter in getters + [_random_getnode]: + try: + _node = getter() + except: + continue + if _node is not None: + return _node + +_last_timestamp = None + +def uuid1(node=None, clock_seq=None): + """Generate a UUID from a host ID, sequence number, and the current time. + If 'node' is not given, getnode() is used to obtain the hardware + address. If 'clock_seq' is given, it is used as the sequence number; + otherwise a random 14-bit sequence number is chosen.""" + + # When the system provides a version-1 UUID generator, use it (but don't + # use UuidCreate here because its UUIDs don't conform to RFC 4122). + if _uuid_generate_time and node is clock_seq is None: + _uuid_generate_time(_buffer) + return UUID(bytes=_buffer.raw) + + global _last_timestamp + import time + nanoseconds = int(time.time() * 1e9) + # 0x01b21dd213814000 is the number of 100-ns intervals between the + # UUID epoch 1582-10-15 00:00:00 and the Unix epoch 1970-01-01 00:00:00. + timestamp = int(nanoseconds/100) + 0x01b21dd213814000L + if timestamp <= _last_timestamp: + timestamp = _last_timestamp + 1 + _last_timestamp = timestamp + if clock_seq is None: + import random + clock_seq = random.randrange(1<<14L) # instead of stable storage + time_low = timestamp & 0xffffffffL + time_mid = (timestamp >> 32L) & 0xffffL + time_hi_version = (timestamp >> 48L) & 0x0fffL + clock_seq_low = clock_seq & 0xffL + clock_seq_hi_variant = (clock_seq >> 8L) & 0x3fL + if node is None: + node = getnode() + return UUID(fields=(time_low, time_mid, time_hi_version, + clock_seq_hi_variant, clock_seq_low, node), version=1) + +def uuid3(namespace, name): + """Generate a UUID from the MD5 hash of a namespace UUID and a name.""" + import md5 + hash = md5.md5(namespace.bytes + name).digest() + return UUID(bytes=hash[:16], version=3) + +def uuid4(): + """Generate a random UUID.""" + + # When the system provides a version-4 UUID generator, use it. + if _uuid_generate_random: + _uuid_generate_random(_buffer) + return UUID(bytes=_buffer.raw) + + # Otherwise, get randomness from urandom or the 'random' module. + try: + import os + return UUID(bytes=os.urandom(16), version=4) + except: + import random + bytes = [chr(random.randrange(256)) for i in range(16)] + return UUID(bytes=bytes, version=4) + +def uuid5(namespace, name): + """Generate a UUID from the SHA-1 hash of a namespace UUID and a name.""" + import sha + hash = sha.sha(namespace.bytes + name).digest() + return UUID(bytes=hash[:16], version=5) + +# The following standard UUIDs are for use with uuid3() or uuid5(). + +NAMESPACE_DNS = UUID('6ba7b810-9dad-11d1-80b4-00c04fd430c8') +NAMESPACE_URL = UUID('6ba7b811-9dad-11d1-80b4-00c04fd430c8') +NAMESPACE_OID = UUID('6ba7b812-9dad-11d1-80b4-00c04fd430c8') +NAMESPACE_X500 = UUID('6ba7b814-9dad-11d1-80b4-00c04fd430c8') diff --git a/python/ide/src/com/jetbrains/python/configuration/PyActiveSdkConfigurable.java b/python/ide/src/com/jetbrains/python/configuration/PyActiveSdkConfigurable.java index fc165b8893f6..38195d1380b9 100644 --- a/python/ide/src/com/jetbrains/python/configuration/PyActiveSdkConfigurable.java +++ b/python/ide/src/com/jetbrains/python/configuration/PyActiveSdkConfigurable.java @@ -298,14 +298,14 @@ public class PyActiveSdkConfigurable implements UnnamedConfigurable { } final Sdk prevSdk = ProjectRootManager.getInstance(myProject).getProjectSdk(); - final Sdk selectedSdk = setSdk(newSdk); + setSdk(newSdk); // update string literals if different LanguageLevel was selected - if (prevSdk != null && selectedSdk != null) { - final PythonSdkFlavor flavor1 = PythonSdkFlavor.getFlavor(selectedSdk); + if (prevSdk != null && newSdk != null) { + final PythonSdkFlavor flavor1 = PythonSdkFlavor.getFlavor(newSdk); final PythonSdkFlavor flavor2 = PythonSdkFlavor.getFlavor(prevSdk); if (flavor1 != null && flavor2 != null) { - final LanguageLevel languageLevel1 = flavor1.getLanguageLevel(selectedSdk); + final LanguageLevel languageLevel1 = flavor1.getLanguageLevel(newSdk); final LanguageLevel languageLevel2 = flavor2.getLanguageLevel(prevSdk); if ((languageLevel1.isPy3K() && languageLevel2.isPy3K()) || (!languageLevel1.isPy3K()) && !languageLevel2.isPy3K()) { @@ -316,22 +316,20 @@ public class PyActiveSdkConfigurable implements UnnamedConfigurable { rehighlightStrings(myProject); } - private Sdk setSdk(Sdk item) { + private void setSdk(final Sdk item) { myAddedSdks.clear(); - final Sdk selectedSdk = myProjectSdksModel.findSdk(item); if (myModule == null) { final ProjectRootManager rootManager = ProjectRootManager.getInstance(myProject); ApplicationManager.getApplication().runWriteAction(new Runnable() { @Override public void run() { - rootManager.setProjectSdk(selectedSdk); + rootManager.setProjectSdk(item); } }); } else { - ModuleRootModificationUtil.setModuleSdk(myModule, selectedSdk); + ModuleRootModificationUtil.setModuleSdk(myModule, item); } - return selectedSdk; } public static void rehighlightStrings(final @NotNull Project project) { @@ -429,8 +427,6 @@ public class PyActiveSdkConfigurable implements UnnamedConfigurable { public void sdkAdded(Sdk sdk) { final Object item = myConfigurable.mySdkCombo.getSelectedItem(); - myConfigurable.resetSdkList(); - if (item instanceof PyDetectedSdk) { final String path = ((PyDetectedSdk)item).getHomePath(); if (path != null && path.equals(sdk.getHomePath())) diff --git a/python/ide/src/com/jetbrains/python/newProject/actions/AbstractProjectSettingsStep.java b/python/ide/src/com/jetbrains/python/newProject/actions/AbstractProjectSettingsStep.java index 1ad9cf7ececc..6514741fda45 100644 --- a/python/ide/src/com/jetbrains/python/newProject/actions/AbstractProjectSettingsStep.java +++ b/python/ide/src/com/jetbrains/python/newProject/actions/AbstractProjectSettingsStep.java @@ -33,15 +33,11 @@ import com.jetbrains.python.configuration.PyConfigurableInterpreterList; import com.jetbrains.python.configuration.VirtualEnvProjectFilter; import com.jetbrains.python.newProject.PyFrameworkProjectGenerator; import com.jetbrains.python.newProject.PythonProjectGenerator; -import com.jetbrains.python.packaging.PyExternalProcessException; -import com.jetbrains.python.packaging.PyPackage; import com.jetbrains.python.packaging.PyPackageManager; -import com.jetbrains.python.packaging.PyPackageManagerImpl; +import com.jetbrains.python.sdk.PySdkUtil; import com.jetbrains.python.sdk.PythonSdkType; -import com.jetbrains.python.sdk.flavors.JythonSdkFlavor; -import com.jetbrains.python.sdk.flavors.PyPySdkFlavor; -import com.jetbrains.python.sdk.flavors.PythonSdkFlavor; import icons.PythonIcons; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; @@ -79,22 +75,6 @@ abstract public class AbstractProjectSettingsStep extends AbstractActionWithPane myCallback = callback; myIsWelcomeScreen = isWelcomeScreen; myProjectDirectory = FileUtil.findSequentNonexistentFile(new File(ProjectUtil.getBaseDir()), "untitled", ""); - if (myProjectGenerator instanceof WebProjectTemplate) { - ((WebProjectTemplate)myProjectGenerator).getPeer().addSettingsStateListener(new WebProjectGenerator.SettingsStateListener() { - @Override - public void stateChanged(boolean validSettings) { - checkValid(); - } - }); - } - else if (myProjectGenerator instanceof PythonProjectGenerator) { - ((PythonProjectGenerator)myProjectGenerator).addSettingsStateListener(new PythonProjectGenerator.SettingsListener() { - @Override - public void stateChanged() { - checkValid(); - } - }); - } myCreateAction = new AnAction("Create", "Create Project", getIcon()) { @Override @@ -112,6 +92,7 @@ abstract public class AbstractProjectSettingsStep extends AbstractActionWithPane @Override public JPanel createPanel() { + initGeneratorListeners(); final JPanel basePanel = createBasePanel(); final JPanel mainPanel = new JPanel(new BorderLayout()); @@ -142,6 +123,25 @@ abstract public class AbstractProjectSettingsStep extends AbstractActionWithPane return mainPanel; } + private void initGeneratorListeners() { + if (myProjectGenerator instanceof WebProjectTemplate) { + ((WebProjectTemplate)myProjectGenerator).getPeer().addSettingsStateListener(new WebProjectGenerator.SettingsStateListener() { + @Override + public void stateChanged(boolean validSettings) { + checkValid(); + } + }); + } + else if (myProjectGenerator instanceof PythonProjectGenerator) { + ((PythonProjectGenerator)myProjectGenerator).addSettingsStateListener(new PythonProjectGenerator.SettingsListener() { + @Override + public void stateChanged() { + checkValid(); + } + }); + } + } + protected Icon getIcon() { return myProjectGenerator.getLogo(); } @@ -319,29 +319,13 @@ abstract public class AbstractProjectSettingsStep extends AbstractActionWithPane PyFrameworkProjectGenerator frameworkProjectGenerator = (PyFrameworkProjectGenerator)myProjectGenerator; String frameworkName = frameworkProjectGenerator.getFrameworkTitle(); if (sdk != null && !isFrameworkInstalled(sdk)) { - final PyPackageManagerImpl packageManager = (PyPackageManagerImpl)PyPackageManager.getInstance(sdk); - final boolean onlyWithCache = - PythonSdkFlavor.getFlavor(sdk) instanceof JythonSdkFlavor || PythonSdkFlavor.getFlavor(sdk) instanceof PyPySdkFlavor; String warningText = frameworkName + " will be installed on selected interpreter"; - try { - if (onlyWithCache && packageManager.cacheIsNotNull() || !onlyWithCache) { - final PyPackage pip = packageManager.findInstalledPackage("pip"); - myInstallFramework = true; - if (pip == null) { - warningText = "pip and " + warningText; - } - setWarningText(warningText); - } - } - catch (PyExternalProcessException ignored) { - myInstallFramework = true; - warningText = "pip and " + warningText; - setWarningText(warningText); - } - if (!myInstallFramework) { - setErrorText("No " + frameworkName + " support installed in selected interpreter"); - return false; + myInstallFramework = true; + final PyPackageManager packageManager = PyPackageManager.getInstance(sdk); + if (!packageManager.hasManagement(PySdkUtil.isRemote(sdk))) { + warningText = "Python packaging tools and " + warningText; } + setWarningText(warningText); } if (isPy3k && !((PyFrameworkProjectGenerator)myProjectGenerator).supportsPython3()) { setErrorText(frameworkName + " is not supported for the selected interpreter"); @@ -454,6 +438,10 @@ abstract public class AbstractProjectSettingsStep extends AbstractActionWithPane return myLocationField.getText(); } + public void setLocation(@NotNull final String location) { + myLocationField.setText(location); + } + public boolean installFramework() { return myInstallFramework; } diff --git a/python/ide/src/com/jetbrains/python/newProject/actions/GenerateProjectCallback.java b/python/ide/src/com/jetbrains/python/newProject/actions/GenerateProjectCallback.java index 2aa9a391512f..2e88b7f2767c 100644 --- a/python/ide/src/com/jetbrains/python/newProject/actions/GenerateProjectCallback.java +++ b/python/ide/src/com/jetbrains/python/newProject/actions/GenerateProjectCallback.java @@ -104,7 +104,7 @@ public class GenerateProjectCallback implements NullableConsumer<AbstractProject } @Nullable - private Project generateProject(@NotNull final Project project, @NotNull final AbstractProjectSettingsStep settings) { + private static Project generateProject(@NotNull final Project project, @NotNull final AbstractProjectSettingsStep settings) { final DirectoryProjectGenerator generator = settings.getProjectGenerator(); final File location = new File(settings.getProjectLocation()); if (!location.exists() && !location.mkdirs()) { diff --git a/python/openapi/src/com/jetbrains/python/documentation/PythonDocumentationQuickInfoProvider.java b/python/openapi/src/com/jetbrains/python/documentation/PythonDocumentationQuickInfoProvider.java new file mode 100644 index 000000000000..52dc4732133c --- /dev/null +++ b/python/openapi/src/com/jetbrains/python/documentation/PythonDocumentationQuickInfoProvider.java @@ -0,0 +1,25 @@ +package com.jetbrains.python.documentation; + +import com.intellij.openapi.extensions.ExtensionPointName; +import com.intellij.psi.PsiElement; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Allows you to inject quick info into python documentation provider + * + * @author Ilya.Kazakevich + */ +public interface PythonDocumentationQuickInfoProvider { + ExtensionPointName<PythonDocumentationQuickInfoProvider> EP_NAME = + ExtensionPointName.create("Pythonid.pythonDocumentationQuickInfoProvider"); + + /** + * Return quick info for <strong>original</strong> element. + * + * @param originalElement original element + * @return info (if exists) or null (if another provider should be checked) + */ + @Nullable + String getQuickInfo(@NotNull PsiElement originalElement); +} diff --git a/python/openapi/src/com/jetbrains/python/packaging/PyPackageManager.java b/python/openapi/src/com/jetbrains/python/packaging/PyPackageManager.java index 3b2080b00ba1..2a8003c46eaf 100644 --- a/python/openapi/src/com/jetbrains/python/packaging/PyPackageManager.java +++ b/python/openapi/src/com/jetbrains/python/packaging/PyPackageManager.java @@ -15,35 +15,45 @@ */ package com.jetbrains.python.packaging; -import com.intellij.openapi.project.Project; +import com.intellij.openapi.module.Module; import com.intellij.openapi.projectRoots.Sdk; +import com.intellij.openapi.util.Key; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.awt.*; +import java.util.List; +import java.util.Set; /** * @author yole */ public abstract class PyPackageManager { + public static final Key<Boolean> RUNNING_PACKAGING_TASKS = Key.create("PyPackageRequirementsInspection.RunningPackagingTasks"); + + public static final String PACKAGE_SETUPTOOLS = "setuptools"; + public static final String PACKAGE_PIP = "pip"; + public static final String PACKAGE_DISTRIBUTE = "distribute"; + + public static final String USE_USER_SITE = "--user"; + public static PyPackageManager getInstance(Sdk sdk) { return PyPackageManagers.getInstance().forSdk(sdk); } - /** - * Returns true if pip is installed for the specific interpreter; returns false if pip is not - * installed or if it is not currently known whether it's installed (e.g. for a remote interpreter). - * - * @return true if pip is known to be installed, false otherwise. - */ - public abstract boolean hasPip(); - public abstract void install(String requirementString) throws PyExternalProcessException; - public abstract void showInstallationError(Project project, String title, String description); - public abstract void showInstallationError(Component owner, String title, String description); + public abstract void installManagement() throws PyExternalProcessException; + public abstract boolean hasManagement(boolean cachedOnly); + public abstract void install(@NotNull String requirementString) throws PyExternalProcessException; + public abstract void install(@NotNull List<PyRequirement> requirements, @NotNull List<String> extraArgs) throws PyExternalProcessException; + public abstract void uninstall(@NotNull List<PyPackage> packages) throws PyExternalProcessException; public abstract void refresh(); + @NotNull + public abstract String createVirtualEnv(@NotNull String destinationDir, boolean useGlobalSite) throws PyExternalProcessException; @Nullable - public abstract PyPackage findInstalledPackage(String name) throws PyExternalProcessException; - - public abstract boolean findPackage(@NotNull final String name); - + public abstract List<PyPackage> getPackages(boolean cachedOnly) throws PyExternalProcessException; + @Nullable + public abstract PyPackage findPackage(@NotNull String name, boolean cachedOnly) throws PyExternalProcessException; + @Nullable + public abstract List<PyRequirement> getRequirements(@NotNull Module module); + @Nullable + public abstract Set<PyPackage> getDependents(@NotNull PyPackage pkg) throws PyExternalProcessException; } diff --git a/python/openapi/src/com/jetbrains/python/packaging/PyPackageManagers.java b/python/openapi/src/com/jetbrains/python/packaging/PyPackageManagers.java index 9f598748aa7b..2b6554040fe7 100644 --- a/python/openapi/src/com/jetbrains/python/packaging/PyPackageManagers.java +++ b/python/openapi/src/com/jetbrains/python/packaging/PyPackageManagers.java @@ -16,12 +16,8 @@ package com.jetbrains.python.packaging; import com.intellij.openapi.components.ServiceManager; -import com.intellij.openapi.module.Module; import com.intellij.openapi.projectRoots.Sdk; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.List; /** * @author yole @@ -35,22 +31,4 @@ public abstract class PyPackageManagers { @NotNull public abstract PyPackageManager forSdk(Sdk sdk); - - /** - * Returns the list of requirements from 'requirements.txt' or 'setup.py' files in the specified module. - * - * @param module the module to check for requirements - * @return the list of requirements, or null if the module contains neither requirements.txt nor setup.py. - */ - @Nullable - public abstract List<PyRequirement> getRequirements(Module module); - - /** - * Returns the list of requirements from 'requirements.txt' file in the specified module. - * - * @param module the module to check for requirements - * @return the list of requirements, or null if the module does not contain a requirements.txt - */ - @Nullable - public abstract List<PyRequirement> getRequirementsFromTxt(Module module); } diff --git a/python/openapi/src/com/jetbrains/python/templateLanguages/PyTemplatesUtil.java b/python/openapi/src/com/jetbrains/python/templateLanguages/PyTemplatesUtil.java index e660029fe384..c37c65ed4144 100644 --- a/python/openapi/src/com/jetbrains/python/templateLanguages/PyTemplatesUtil.java +++ b/python/openapi/src/com/jetbrains/python/templateLanguages/PyTemplatesUtil.java @@ -40,7 +40,7 @@ public class PyTemplatesUtil { if (templateBinding != null) { if (TemplatesService.ALL_TEMPLATE_BINDINGS.contains(templateBinding)) { try { - final PyPackage installedPackage = packageManager.findInstalledPackage(templateBinding); + final PyPackage installedPackage = packageManager.findPackage(templateBinding, false); if (installedPackage == null) return new ValidationResult(templateBinding + " will be installed on selected interpreter"); } @@ -50,7 +50,7 @@ public class PyTemplatesUtil { } if (language != null) { try { - final PyPackage installedPackage = packageManager.findInstalledPackage(language); + final PyPackage installedPackage = packageManager.findPackage(language, false); if (installedPackage == null) { return new ValidationResult(language + " will be installed on selected interpreter"); } diff --git a/python/psi-api/src/com/jetbrains/python/PyNames.java b/python/psi-api/src/com/jetbrains/python/PyNames.java index bd927c5d49bf..ec04e1b4cb61 100644 --- a/python/psi-api/src/com/jetbrains/python/PyNames.java +++ b/python/psi-api/src/com/jetbrains/python/PyNames.java @@ -164,6 +164,8 @@ public class PyNames { public static final String UNKNOWN_TYPE = "unknown"; + public static final String UNNAMED_ELEMENT = "<unnamed>"; + /** * Contains all known predefined names of "__foo__" form. */ diff --git a/python/pydevSrc/com/jetbrains/python/debugger/IPyDebugProcess.java b/python/pydevSrc/com/jetbrains/python/debugger/IPyDebugProcess.java index ab6ccfab0bc6..762225f18f12 100644 --- a/python/pydevSrc/com/jetbrains/python/debugger/IPyDebugProcess.java +++ b/python/pydevSrc/com/jetbrains/python/debugger/IPyDebugProcess.java @@ -1,6 +1,8 @@ package com.jetbrains.python.debugger; import com.intellij.execution.ui.ConsoleViewContentType; +import com.intellij.xdebugger.frame.XValueChildrenList; +import com.jetbrains.python.debugger.pydev.PyDebugCallback; import java.io.IOException; @@ -12,7 +14,7 @@ public interface IPyDebugProcess extends PyFrameAccessor { void threadSuspended(PyThreadInfo thread); - boolean isVariable(String name); + boolean canSaveToTemp(String name); void threadResumed(PyThreadInfo thread); @@ -25,4 +27,6 @@ public interface IPyDebugProcess extends PyFrameAccessor { void recordSignature(PySignature signature); void showConsole(PyThreadInfo thread); + + void loadReferrers(PyReferringObjectsValue var, PyDebugCallback<XValueChildrenList> callback); } diff --git a/python/pydevSrc/com/jetbrains/python/debugger/PyDebugValue.java b/python/pydevSrc/com/jetbrains/python/debugger/PyDebugValue.java index 69b64827c40f..ddec6797ea8d 100644 --- a/python/pydevSrc/com/jetbrains/python/debugger/PyDebugValue.java +++ b/python/pydevSrc/com/jetbrains/python/debugger/PyDebugValue.java @@ -5,6 +5,7 @@ import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.xdebugger.frame.XNamedValue; import com.intellij.xdebugger.frame.*; +import com.jetbrains.python.debugger.pydev.PyVariableLocator; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -21,14 +22,13 @@ public class PyDebugValue extends XNamedValue { private final String myValue; private final boolean myContainer; private final PyDebugValue myParent; + private String myId = null; private final PyFrameAccessor myFrameAccessor; - private final boolean myErrorOnEval; + private PyVariableLocator myVariableLocator; - public PyDebugValue(@NotNull final String name, final String type, final String value, final boolean container, boolean errorOnEval) { - this(name, type, value, container, errorOnEval, null, null); - } + private final boolean myErrorOnEval; public PyDebugValue(@NotNull final String name, final String type, final String value, final boolean container, boolean errorOnEval, final PyFrameAccessor frameAccessor) { @@ -97,7 +97,7 @@ public class PyDebugValue extends XNamedValue { myParent.buildExpression(result); if (("dict".equals(myParent.getType()) || "list".equals(myParent.getType()) || "tuple".equals(myParent.getType())) && !isLen(myName)) { - result.append('[').append(removeId(myName)).append(']'); + result.append('[').append(removeLeadingZeros(removeId(myName))).append(']'); } else if (("set".equals(myParent.getType())) && !isLen(myName)) { //set doesn't support indexing @@ -105,6 +105,9 @@ public class PyDebugValue extends XNamedValue { else if (isLen(myName)) { result.append('.').append(myName).append("()"); } + else if (("ndarray".equals(myParent.getType()) || "matrix".equals(myParent.getType())) && myName.startsWith("[")) { + result.append(removeLeadingZeros(myName)); + } else { result.append('.').append(myName); } @@ -119,6 +122,11 @@ public class PyDebugValue extends XNamedValue { return name; } + private static String removeLeadingZeros(@NotNull String name) { + //bugs.python.org/issue15254: "0" prefix for octal + return name.replaceFirst("^0+(?!$)", ""); + } + private static boolean isLen(String name) { return "__len__".equals(name); } @@ -179,4 +187,39 @@ public class PyDebugValue extends XNamedValue { public PyDebugValue setName(String newName) { return new PyDebugValue(newName, myType, myValue, myContainer, myErrorOnEval, myParent, myFrameAccessor); } + + @Nullable + @Override + public XReferrersProvider getReferrersProvider() { + if (myFrameAccessor.getReferrersLoader() != null) { + return new XReferrersProvider() { + @Override + public XValue getReferringObjectsValue() { + return new PyReferringObjectsValue(PyDebugValue.this); + } + }; + } else { + return null; + } + } + + public PyFrameAccessor getFrameAccessor() { + return myFrameAccessor; + } + + public PyVariableLocator getVariableLocator() { + return myVariableLocator; + } + + public void setVariableLocator(PyVariableLocator variableLocator) { + myVariableLocator = variableLocator; + } + + public String getId() { + return myId; + } + + public void setId(String id) { + myId = id; + } } diff --git a/python/pydevSrc/com/jetbrains/python/debugger/PyFrameAccessor.java b/python/pydevSrc/com/jetbrains/python/debugger/PyFrameAccessor.java index 530035e78a4b..01666456f420 100644 --- a/python/pydevSrc/com/jetbrains/python/debugger/PyFrameAccessor.java +++ b/python/pydevSrc/com/jetbrains/python/debugger/PyFrameAccessor.java @@ -17,4 +17,7 @@ public interface PyFrameAccessor { XValueChildrenList loadVariable(PyDebugValue var) throws PyDebuggerException; void changeVariable(PyDebugValue variable, String expression) throws PyDebuggerException; + + @Nullable + PyReferrersLoader getReferrersLoader(); } diff --git a/python/pydevSrc/com/jetbrains/python/debugger/PyReferrersLoader.java b/python/pydevSrc/com/jetbrains/python/debugger/PyReferrersLoader.java new file mode 100644 index 000000000000..edb2f641b192 --- /dev/null +++ b/python/pydevSrc/com/jetbrains/python/debugger/PyReferrersLoader.java @@ -0,0 +1,20 @@ +package com.jetbrains.python.debugger; + +import com.intellij.xdebugger.frame.XReferrersProvider; +import com.intellij.xdebugger.frame.XValueChildrenList; +import com.jetbrains.python.debugger.pydev.PyDebugCallback; + +/** + * @author traff + */ +public class PyReferrersLoader { + private final IPyDebugProcess myProcess; + + public PyReferrersLoader(IPyDebugProcess process) { + myProcess = process; + } + + public void loadReferrers(PyReferringObjectsValue value, PyDebugCallback<XValueChildrenList> callback) { + myProcess.loadReferrers(value, callback); + } +} diff --git a/python/pydevSrc/com/jetbrains/python/debugger/PyReferringObjectsValue.java b/python/pydevSrc/com/jetbrains/python/debugger/PyReferringObjectsValue.java new file mode 100644 index 000000000000..283653659d0d --- /dev/null +++ b/python/pydevSrc/com/jetbrains/python/debugger/PyReferringObjectsValue.java @@ -0,0 +1,71 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jetbrains.python.debugger; + +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.xdebugger.frame.XCompositeNode; +import com.intellij.xdebugger.frame.XValueChildrenList; +import com.jetbrains.python.debugger.pydev.PyDebugCallback; +import org.jetbrains.annotations.NotNull; + +public class PyReferringObjectsValue extends PyDebugValue { + private static final Logger LOG = Logger.getInstance(PyReferringObjectsValue.class); + + private final @NotNull PyReferrersLoader myReferrersLoader; + + public PyReferringObjectsValue(@NotNull String name, + String type, + String value, + boolean container, boolean errorOnEval, @NotNull PyFrameAccessor frameAccessor) { + super(name, type, value, container, errorOnEval, frameAccessor); + myReferrersLoader = frameAccessor.getReferrersLoader(); + } + + public PyReferringObjectsValue(PyDebugValue debugValue) { + this(debugValue.getName(), debugValue.getType(), debugValue.getValue(), debugValue.isContainer(), debugValue.isErrorOnEval(), debugValue.getFrameAccessor()); + } + + @Override + public boolean canNavigateToSource() { + return true; + } + + @Override + public void computeChildren(@NotNull final XCompositeNode node) { + if (node.isObsolete()) return; + + myReferrersLoader.loadReferrers(this, new PyDebugCallback<XValueChildrenList>() { + @Override + public void ok(XValueChildrenList value) { + if (!node.isObsolete()) { + node.addChildren(value, true); + } + } + + @Override + public void error(PyDebuggerException e) { + if (!node.isObsolete()) { + node.setErrorMessage("Unable to display children:" + e.getMessage()); + } + LOG.warn(e); + } + }); + } + + public boolean isField() { + return false; //TODO + } +} diff --git a/python/pydevSrc/com/jetbrains/python/debugger/PyThreadInfo.java b/python/pydevSrc/com/jetbrains/python/debugger/PyThreadInfo.java index c01be15384ab..7fffc636a9fb 100644 --- a/python/pydevSrc/com/jetbrains/python/debugger/PyThreadInfo.java +++ b/python/pydevSrc/com/jetbrains/python/debugger/PyThreadInfo.java @@ -80,7 +80,7 @@ public class PyThreadInfo { } public boolean isExceptionBreak() { - return myStopReason == AbstractCommand.ADD_EXCEPTION_BREAKPOINT || myStopReason == AbstractCommand.ADD_DJANGO_EXCEPTION_BREAKPOINT; + return myStopReason == AbstractCommand.ADD_EXCEPTION_BREAKPOINT; } @Override diff --git a/python/pydevSrc/com/jetbrains/python/debugger/PyTypeHandler.java b/python/pydevSrc/com/jetbrains/python/debugger/PyTypeHandler.java index 6db612488d4b..f882aae4d0e7 100644 --- a/python/pydevSrc/com/jetbrains/python/debugger/PyTypeHandler.java +++ b/python/pydevSrc/com/jetbrains/python/debugger/PyTypeHandler.java @@ -37,6 +37,9 @@ public class PyTypeHandler { FORMATTERS = new HashMap<String, Formatter>(); FORMATTERS.put("str", STR_FORMATTER); FORMATTERS.put("unicode", UNI_FORMATTER); + //numpy types + FORMATTERS.put("string_", STR_FORMATTER); + FORMATTERS.put("unicode_", UNI_FORMATTER); } private PyTypeHandler() { } diff --git a/python/pydevSrc/com/jetbrains/python/debugger/pydev/AbstractCommand.java b/python/pydevSrc/com/jetbrains/python/debugger/pydev/AbstractCommand.java index 15bb76b6c3d1..d13addaade04 100644 --- a/python/pydevSrc/com/jetbrains/python/debugger/pydev/AbstractCommand.java +++ b/python/pydevSrc/com/jetbrains/python/debugger/pydev/AbstractCommand.java @@ -28,11 +28,10 @@ public abstract class AbstractCommand<T> { public static final int ADD_EXCEPTION_BREAKPOINT = 122; public static final int REMOVE_EXCEPTION_BREAKPOINT = 123; public static final int LOAD_SOURCE = 124; - public static final int ADD_DJANGO_EXCEPTION_BREAKPOINT = 125; - public static final int REMOVE_DJANGO_EXCEPTION_BREAKPOINT = 126; public static final int SMART_STEP_INTO = 128; public static final int EXIT = 129; public static final int CALL_SIGNATURE_TRACE = 130; + public static final int CMD_RUN_CUSTOM_OPERATION = 135; public static final int SHOW_CONSOLE = 142; public static final int ERROR = 901; @@ -41,7 +40,7 @@ public abstract class AbstractCommand<T> { public static final String TAB_CHAR = "@_@TAB_CHAR@_@"; - @NotNull protected final RemoteDebugger myDebugger; + @NotNull private final RemoteDebugger myDebugger; private final int myCommandCode; private final ResponseProcessor<T> myResponseProcessor; @@ -107,7 +106,7 @@ public abstract class AbstractCommand<T> { } } - public void execute(final ProcessDebugger.DebugCallback<T> callback) { + public void execute(final PyDebugCallback<T> callback) { final int sequence = myDebugger.getNextSequence(); final ResponseProcessor<T> processor = getResponseProcessor(); @@ -186,6 +185,11 @@ public abstract class AbstractCommand<T> { return command == ERROR; } + @NotNull + public RemoteDebugger getDebugger() { + return myDebugger; + } + protected static class Payload { private final StringBuilder myBuilder = new StringBuilder(); private static final char SEPARATOR = '\t'; diff --git a/python/pydevSrc/com/jetbrains/python/debugger/pydev/ConsoleExecCommand.java b/python/pydevSrc/com/jetbrains/python/debugger/pydev/ConsoleExecCommand.java index 14d8c084a39d..c03890f088e8 100644 --- a/python/pydevSrc/com/jetbrains/python/debugger/pydev/ConsoleExecCommand.java +++ b/python/pydevSrc/com/jetbrains/python/debugger/pydev/ConsoleExecCommand.java @@ -28,7 +28,7 @@ public class ConsoleExecCommand extends AbstractFrameCommand<String> { return new ResponseProcessor<String>() { @Override protected String parseResponse(ProtocolFrame response) throws PyDebuggerException { - final PyDebugValue value = ProtocolParser.parseValue(response.getPayload(), myDebugger.getDebugProcess()); + final PyDebugValue value = ProtocolParser.parseValue(response.getPayload(), getDebugger().getDebugProcess()); return value.getValue(); } }; diff --git a/python/pydevSrc/com/jetbrains/python/debugger/pydev/GetReferrersCommand.java b/python/pydevSrc/com/jetbrains/python/debugger/pydev/GetReferrersCommand.java new file mode 100644 index 000000000000..3698266f66b7 --- /dev/null +++ b/python/pydevSrc/com/jetbrains/python/debugger/pydev/GetReferrersCommand.java @@ -0,0 +1,50 @@ +package com.jetbrains.python.debugger.pydev; + +import com.jetbrains.python.debugger.PyDebugValue; +import com.jetbrains.python.debugger.PyDebuggerException; +import com.jetbrains.python.debugger.PyReferringObjectsValue; + +import java.util.List; + +/** + * @author traff + */ +public class GetReferrersCommand extends RunCustomOperationCommand<List<PyDebugValue>> { + + public GetReferrersCommand(RemoteDebugger target, String threadId, String frameId, PyReferringObjectsValue value) { + super(target, createVariableLocator(threadId, frameId, value), "from pydevd_referrers import get_referrer_info", + "get_referrer_info"); + } + + @Override + protected ResponseProcessor<List<PyDebugValue>> createResponseProcessor() { + return new ResponseProcessor<List<PyDebugValue>>() { + @Override + protected List<PyDebugValue> parseResponse(ProtocolFrame response) throws PyDebuggerException { + return ProtocolParser.parseReferrers(decode(response.getPayload()), getDebugger().getDebugProcess()); + } + }; + } + + + private static PyVariableLocator createVariableLocator(final String threadId, final String frameId, final PyReferringObjectsValue var) { + return new PyVariableLocator() { + @Override + public String getThreadId() { + return threadId; + } + + + @Override + public String getPyDBLocation() { + if (var.getId() == null) { + return threadId + "\t" + frameId + "\tFRAME\t" + var.getName(); + } + //Ok, this only happens when we're dealing with references with no proper scope given and we need to get + //things by id (which is usually not ideal). In this case we keep the proper thread id and set the frame id + //as the id of the object to be searched later on based on the list of all alive objects. + return getThreadId() + "\t" + var.getId() + "\tBY_ID"; + } + }; + } +} diff --git a/python/pydevSrc/com/jetbrains/python/debugger/pydev/GetVariableCommand.java b/python/pydevSrc/com/jetbrains/python/debugger/pydev/GetVariableCommand.java index ef4bd12059d7..e5a218607ac8 100644 --- a/python/pydevSrc/com/jetbrains/python/debugger/pydev/GetVariableCommand.java +++ b/python/pydevSrc/com/jetbrains/python/debugger/pydev/GetVariableCommand.java @@ -5,6 +5,7 @@ import com.jetbrains.python.debugger.PyDebugValue; public class GetVariableCommand extends GetFrameCommand { + public static final String BY_ID = "BY_ID"; private final String myVariableName; private final PyDebugValue myParent; @@ -15,23 +16,40 @@ public class GetVariableCommand extends GetFrameCommand { } public static String composeName(final PyDebugValue var) { - final StringBuilder sb = new StringBuilder(var.getTempName()); + final StringBuilder sb = new StringBuilder(); PyDebugValue p = var; - while ((p = p.getParent()) != null) { - sb.insert(0, '\t').insert(0, p.getTempName()); + while (p != null) { + if (sb.length() > 0 ) { + sb.insert(0, '\t'); + } + if (p.getId() != null) { + sb.insert(0, BY_ID).insert(0, '\t').insert(0, p.getId()); + break; + } else { + sb.insert(0, p.getTempName()); + } + p = p.getParent(); } return sb.toString(); } @Override protected void buildPayload(Payload payload) { - super.buildPayload(payload); - payload.add(myVariableName); + if (myParent.getVariableLocator() != null) { + payload.add(myParent.getVariableLocator().getThreadId()).add(myParent.getVariableLocator().getPyDBLocation()); + } + else if (myVariableName.contains(BY_ID)) { + payload.add(getThreadId()).add(myVariableName); + } + else { + super.buildPayload(payload); + payload.add(myVariableName); + } } @Override protected PyDebugValue extend(final PyDebugValue value) { - return new PyDebugValue(value.getName(), value.getType(), value.getValue(), value.isContainer(), value.isErrorOnEval(), myParent, myDebugProcess); + return new PyDebugValue(value.getName(), value.getType(), value.getValue(), value.isContainer(), value.isErrorOnEval(), myParent, + myDebugProcess); } - } diff --git a/python/pydevSrc/com/jetbrains/python/debugger/pydev/MultiProcessDebugger.java b/python/pydevSrc/com/jetbrains/python/debugger/pydev/MultiProcessDebugger.java index d210d7ae03a8..d54bac8de85f 100644 --- a/python/pydevSrc/com/jetbrains/python/debugger/pydev/MultiProcessDebugger.java +++ b/python/pydevSrc/com/jetbrains/python/debugger/pydev/MultiProcessDebugger.java @@ -8,10 +8,7 @@ import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.xdebugger.frame.XValueChildrenList; import com.jetbrains.python.console.pydev.PydevCompletionVariant; -import com.jetbrains.python.debugger.IPyDebugProcess; -import com.jetbrains.python.debugger.PyDebugValue; -import com.jetbrains.python.debugger.PyDebuggerException; -import com.jetbrains.python.debugger.PyThreadInfo; +import com.jetbrains.python.debugger.*; import org.jetbrains.annotations.NotNull; import java.io.IOException; @@ -168,7 +165,7 @@ public class MultiProcessDebugger implements ProcessDebugger { } @Override - public void consoleExec(String threadId, String frameId, String expression, DebugCallback<String> callback) { + public void consoleExec(String threadId, String frameId, String expression, PyDebugCallback<String> callback) { debugger(threadId).consoleExec(threadId, frameId, expression, callback); } @@ -182,6 +179,11 @@ public class MultiProcessDebugger implements ProcessDebugger { return debugger(threadId).loadVariable(threadId, frameId, var); } + @Override + public void loadReferrers(String threadId, String frameId, PyReferringObjectsValue var, PyDebugCallback<XValueChildrenList> callback) { + debugger(threadId).loadReferrers(threadId, frameId, var, callback); + } + @NotNull private ProcessDebugger debugger(@NotNull String threadId) { ProcessDebugger debugger = myThreadRegistry.getDebugger(threadId); diff --git a/python/pydevSrc/com/jetbrains/python/debugger/pydev/ProcessDebugger.java b/python/pydevSrc/com/jetbrains/python/debugger/pydev/ProcessDebugger.java index 2d864f0df2ca..0fa5a5c21931 100644 --- a/python/pydevSrc/com/jetbrains/python/debugger/pydev/ProcessDebugger.java +++ b/python/pydevSrc/com/jetbrains/python/debugger/pydev/ProcessDebugger.java @@ -4,6 +4,7 @@ import com.intellij.xdebugger.frame.XValueChildrenList; import com.jetbrains.python.console.pydev.PydevCompletionVariant; import com.jetbrains.python.debugger.PyDebugValue; import com.jetbrains.python.debugger.PyDebuggerException; +import com.jetbrains.python.debugger.PyReferringObjectsValue; import com.jetbrains.python.debugger.PyThreadInfo; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -26,15 +27,17 @@ public interface ProcessDebugger { String expression, boolean execute, boolean trimResult) - throws PyDebuggerException; + throws PyDebuggerException; - void consoleExec(String threadId, String frameId, String expression, DebugCallback<String> callback); + void consoleExec(String threadId, String frameId, String expression, PyDebugCallback<String> callback); XValueChildrenList loadFrame(String threadId, String frameId) throws PyDebuggerException; // todo: don't generate temp variables for qualified expressions - just split 'em XValueChildrenList loadVariable(String threadId, String frameId, PyDebugValue var) throws PyDebuggerException; + void loadReferrers(String threadId, String frameId, PyReferringObjectsValue var, PyDebugCallback<XValueChildrenList> callback); + PyDebugValue changeVariable(String threadId, String frameId, PyDebugValue var, String value) throws PyDebuggerException; @@ -50,7 +53,7 @@ public interface ProcessDebugger { void suspendThread(String threadId); /** - * Disconnects current debug process. Closes all resources. + * Disconnects current debug process. Closes all resources. */ void close(); @@ -84,12 +87,4 @@ public interface ProcessDebugger { void addExceptionBreakpoint(ExceptionBreakpointCommandFactory factory); void removeExceptionBreakpoint(ExceptionBreakpointCommandFactory factory); - - /** - * @author traff - */ - interface DebugCallback<T> { - void ok(T value); - void error(PyDebuggerException exception); - } } diff --git a/python/pydevSrc/com/jetbrains/python/debugger/pydev/ProtocolParser.java b/python/pydevSrc/com/jetbrains/python/debugger/pydev/ProtocolParser.java index 0801077fbea3..1822b17efaa3 100644 --- a/python/pydevSrc/com/jetbrains/python/debugger/pydev/ProtocolParser.java +++ b/python/pydevSrc/com/jetbrains/python/debugger/pydev/ProtocolParser.java @@ -122,6 +122,32 @@ public class ProtocolParser { } @NotNull + public static List<PyDebugValue> parseReferrers(final String text, final PyFrameAccessor frameAccessor) throws PyDebuggerException { + final List<PyDebugValue> values = new LinkedList<PyDebugValue>(); + + final XppReader reader = openReader(text, false); + + while (reader.hasMoreChildren()) { + reader.moveDown(); + if (reader.getNodeName().equals("var")) { + PyDebugValue value = parseValue(reader, frameAccessor); + value.setId(readString(reader, "id", null)); + values.add(value); + } + else if (reader.getNodeName().equals("for")) { + //TODO + } + else { + throw new PyDebuggerException("Expected <var> or <for>, found " + reader.getNodeName()); + } + reader.moveUp(); + } + + return values; + } + + + @NotNull public static List<PyDebugValue> parseValues(final String text, final PyFrameAccessor frameAccessor) throws PyDebuggerException { final List<PyDebugValue> values = new LinkedList<PyDebugValue>(); diff --git a/python/pydevSrc/com/jetbrains/python/debugger/pydev/PyDebugCallback.java b/python/pydevSrc/com/jetbrains/python/debugger/pydev/PyDebugCallback.java new file mode 100644 index 000000000000..c78f3e20bdc5 --- /dev/null +++ b/python/pydevSrc/com/jetbrains/python/debugger/pydev/PyDebugCallback.java @@ -0,0 +1,12 @@ +package com.jetbrains.python.debugger.pydev; + +import com.jetbrains.python.debugger.PyDebuggerException; + +/** + * @author traff + */ +public interface PyDebugCallback<T> { + void ok(T value); + + void error(PyDebuggerException exception); +} diff --git a/python/pydevSrc/com/jetbrains/python/debugger/pydev/PyVariableLocator.java b/python/pydevSrc/com/jetbrains/python/debugger/pydev/PyVariableLocator.java new file mode 100644 index 000000000000..3ba4e1cd96de --- /dev/null +++ b/python/pydevSrc/com/jetbrains/python/debugger/pydev/PyVariableLocator.java @@ -0,0 +1,17 @@ +package com.jetbrains.python.debugger.pydev; + +/** + * IVariableLocator knows how to produce location information + * for CMD_GET_VARIABLE + * + * The location is specified as: + * + * thread_id, stack_frame, LOCAL|GLOBAL, attribute* + */ +public interface PyVariableLocator { + + public String getThreadId(); + + public String getPyDBLocation(); + +} diff --git a/python/pydevSrc/com/jetbrains/python/debugger/pydev/RemoteDebugger.java b/python/pydevSrc/com/jetbrains/python/debugger/pydev/RemoteDebugger.java index df33148edb10..4ce7bdf67434 100644 --- a/python/pydevSrc/com/jetbrains/python/debugger/pydev/RemoteDebugger.java +++ b/python/pydevSrc/com/jetbrains/python/debugger/pydev/RemoteDebugger.java @@ -133,7 +133,7 @@ public class RemoteDebugger implements ProcessDebugger { } @Override - public void consoleExec(String threadId, String frameId, String expression, DebugCallback<String> callback) { + public void consoleExec(String threadId, String frameId, String expression, PyDebugCallback<String> callback) { final ConsoleExecCommand command = new ConsoleExecCommand(this, threadId, frameId, expression); command.execute(callback); } @@ -154,6 +154,31 @@ public class RemoteDebugger implements ProcessDebugger { return command.getVariables(); } + + @Override + public void loadReferrers(final String threadId, + final String frameId, + final PyReferringObjectsValue var, + final PyDebugCallback<XValueChildrenList> callback) { + RunCustomOperationCommand cmd = new GetReferrersCommand(this, threadId, frameId, var); + + cmd.execute(new PyDebugCallback<List<PyDebugValue>>() { + @Override + public void ok(List<PyDebugValue> value) { + XValueChildrenList list = new XValueChildrenList(); + for (PyDebugValue v : value) { + list.add(v); + } + callback.ok(list); + } + + @Override + public void error(PyDebuggerException exception) { + callback.error(exception); + } + }); + } + @Override public PyDebugValue changeVariable(final String threadId, final String frameId, final PyDebugValue var, final String value) throws PyDebuggerException { @@ -191,7 +216,7 @@ public class RemoteDebugger implements ProcessDebugger { // todo: change variable in lists doesn't work - either fix in pydevd or format var name appropriately private void setTempVariable(final String threadId, final String frameId, final PyDebugValue var) { final PyDebugValue topVar = var.getTopParent(); - if (myDebugProcess.isVariable(topVar.getName())) { + if (!myDebugProcess.canSaveToTemp(topVar.getName())) { return; } if (myTempVars.contains(threadId, frameId, topVar.getTempName())) { @@ -343,7 +368,7 @@ public class RemoteDebugger implements ProcessDebugger { } } if (myDebuggerReader != null) { - myDebuggerReader.close(); + myDebuggerReader.stop(); } fireCloseEvent(); } @@ -428,7 +453,6 @@ public class RemoteDebugger implements ProcessDebugger { } private class DebuggerReader extends BaseOutputReader { - private boolean myClosing = false; private Reader myReader; private DebuggerReader(final Reader reader) throws IOException { @@ -442,7 +466,7 @@ public class RemoteDebugger implements ProcessDebugger { while (true) { boolean read = readAvailable(); - if (myClosing) { + if (isStopped) { break; } @@ -453,7 +477,7 @@ public class RemoteDebugger implements ProcessDebugger { fireCommunicationError(); } finally { - closeReader(myReader); + close(); fireExitEvent(); } } @@ -475,10 +499,10 @@ public class RemoteDebugger implements ProcessDebugger { else if (AbstractCommand.isCallSignatureTrace(frame.getCommand())) { recordCallSignature(ProtocolParser.parseCallSignature(frame.getPayload())); } - else if (AbstractCommand.isErrorEvent(frame.getCommand())) { - LOG.error("Error response from debugger: " + frame.getPayload()); - } else { + if (AbstractCommand.isErrorEvent(frame.getCommand())) { + LOG.error("Error response from debugger: " + frame.getPayload()); + } placeResponse(frame.getSequence(), frame); } } @@ -568,7 +592,13 @@ public class RemoteDebugger implements ProcessDebugger { } public void close() { - myClosing = true; + closeReader(myReader); + } + + @Override + public void stop() { + super.stop(); + close(); } @Override diff --git a/python/pydevSrc/com/jetbrains/python/debugger/pydev/RunCustomOperationCommand.java b/python/pydevSrc/com/jetbrains/python/debugger/pydev/RunCustomOperationCommand.java new file mode 100644 index 000000000000..7a3315b4acce --- /dev/null +++ b/python/pydevSrc/com/jetbrains/python/debugger/pydev/RunCustomOperationCommand.java @@ -0,0 +1,86 @@ +package com.jetbrains.python.debugger.pydev; + +import com.intellij.openapi.diagnostic.Logger; + +import java.io.File; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; + + +/** + * Run a custom bit of Python in the context of the specified debug target. + * <p> + * This command takes a variable or expression (expressed as an {@link PyVariableLocator#getPyDBLocation()} style + * location) and passes it to the function provided in the constructor. The constructor also takes either a code + * snippet that should define the function, or a file to execfile that should define the function. + * <p> + * Once created, the command should be posted to the target with {@link AbstractDebugTarget#postCommand(AbstractDebuggerCommand)}. + * Optionally, the function run on the target can return a string for further processing. In this case the command's + * {@link #setCompletionListener(ICommandResponseListener)} should be set and on completion, {@link #getResponsePayload()} + * can be used to obtain the returned value. + * <p> + * For an example, see {@link PrettyPrintCommandHandler} + */ +public class RunCustomOperationCommand<T> extends AbstractCommand<T> { + private static final Logger LOG = Logger.getInstance(RunCustomOperationCommand.class); + + private String myEncodedCodeOrFile; + private String myOperationFnName; + private PyVariableLocator myLocator; + private String myStyle; + + private RunCustomOperationCommand(RemoteDebugger target, PyVariableLocator locator, + String style, String codeOrFile, String operationFnName) { + super(target, CMD_RUN_CUSTOM_OPERATION); + + this.myLocator = locator; + this.myStyle = style; + this.myEncodedCodeOrFile = encode(codeOrFile); + this.myOperationFnName = operationFnName; + } + + /** + * Create a new command to run with the function defined in a string. + * + * @param target Debug Target to run on + * @param locator Location of variable or expression. + * @param operationSource Definition of the function to be run (this code is "exec"ed by the target) + * @param operationFnName Function to call, must be defined by operationSource + */ + public RunCustomOperationCommand(RemoteDebugger target, PyVariableLocator locator, + String operationSource, String operationFnName) { + this(target, locator, "EXEC", operationSource, operationFnName); + } + + + @Override + protected void buildPayload(Payload payload) { + payload.add(myLocator.getPyDBLocation() + "||" + myStyle).add(myEncodedCodeOrFile).add(myOperationFnName); + } + + @Override + public boolean isResponseExpected() { + return true; + } + + private static String encode(String in) { + try { + return URLEncoder.encode(in, "UTF-8"); + } catch (UnsupportedEncodingException e) { + LOG.error("Unreachable? UTF-8 is always supported.", e); + return ""; + } + } + + protected static String decode(String in) { + try { + return URLDecoder.decode(in, "UTF-8"); + } catch (UnsupportedEncodingException e) { + LOG.error("Unreachable? UTF-8 is always supported.", e); + return ""; + } + } + +} + diff --git a/python/python-rest/src/com/jetbrains/rest/RestPythonUtil.java b/python/python-rest/src/com/jetbrains/rest/RestPythonUtil.java index f32fce8e40b7..e6d379f03676 100644 --- a/python/python-rest/src/com/jetbrains/rest/RestPythonUtil.java +++ b/python/python-rest/src/com/jetbrains/rest/RestPythonUtil.java @@ -26,7 +26,6 @@ import com.intellij.openapi.projectRoots.Sdk; import com.jetbrains.python.packaging.PyExternalProcessException; import com.jetbrains.python.packaging.PyPackage; import com.jetbrains.python.packaging.PyPackageManager; -import com.jetbrains.python.packaging.PyPackageManagerImpl; import com.jetbrains.python.sdk.PythonSdkType; /** @@ -48,9 +47,9 @@ public class RestPythonUtil { if (module != null) { Sdk sdk = PythonSdkType.findPythonSdk(module); if (sdk != null) { - PyPackageManagerImpl manager = (PyPackageManagerImpl)PyPackageManager.getInstance(sdk); + PyPackageManager manager = PyPackageManager.getInstance(sdk); try { - final PyPackage sphinx = manager.findInstalledPackage("Sphinx"); + final PyPackage sphinx = manager.findPackage("Sphinx", false); presentation.setEnabled(sphinx != null); } catch (PyExternalProcessException ignored) { diff --git a/python/src/META-INF/pycharm-core.xml b/python/src/META-INF/pycharm-core.xml index a9f7824b5340..ac42944c9953 100644 --- a/python/src/META-INF/pycharm-core.xml +++ b/python/src/META-INF/pycharm-core.xml @@ -40,10 +40,10 @@ <projectAttachProcessor implementation="com.intellij.platform.ModuleAttachProcessor"/> - <projectConfigurable instance="com.jetbrains.python.configuration.PythonContentEntriesConfigurable"/> - <projectConfigurable instance="com.jetbrains.python.buildout.BuildoutModulesConfigurable"/> + <projectConfigurable groupId="project" instance="com.jetbrains.python.configuration.PythonContentEntriesConfigurable"/> + <projectConfigurable groupId="build" instance="com.jetbrains.python.buildout.BuildoutModulesConfigurable"/> <projectConfigurable groupId="project" instance="com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable"/> - <projectConfigurable instance="com.jetbrains.python.configuration.PyDependenciesConfigurable"/> + <projectConfigurable groupId="project" instance="com.jetbrains.python.configuration.PyDependenciesConfigurable"/> <directoryProjectConfigurator implementation="com.jetbrains.python.PythonSdkConfigurator" id="sdk" order="after PlatformProjectConfigurator"/> @@ -104,7 +104,7 @@ <add-to-group group-id="FileOpenGroup" anchor="after" relative-to-action="OpenFile"/> </action> - <action id="RerunFailedTests" class="com.intellij.execution.testframework.actions.AbstractRerunFailedTestsAction" + <action id="RerunFailedTests" class="com.intellij.execution.testframework.actions.RerunFailedTestsAction" icon="AllIcons.RunConfigurations.RerunFailedTests"/> <group id="WelcomeScreen.Platform.NewProject"> diff --git a/python/src/META-INF/python-core.xml b/python/src/META-INF/python-core.xml index 7adbc46c189c..5091d43386a1 100644 --- a/python/src/META-INF/python-core.xml +++ b/python/src/META-INF/python-core.xml @@ -137,6 +137,7 @@ <gotoTargetRendererProvider implementation="com.jetbrains.python.codeInsight.PyGotoTargetRendererProvider"/> <typeHierarchyProvider language="Python" implementationClass="com.jetbrains.python.hierarchy.PyTypeHierachyProvider"/> + <callHierarchyProvider language="Python" implementationClass="com.jetbrains.python.hierarchy.call.PyCallHierarchyProvider"/> <highlightUsagesHandlerFactory implementation="com.jetbrains.python.codeInsight.highlighting.PyHighlightExitPointsHandlerFactory"/> <joinLinesHandler implementation="com.jetbrains.python.editor.PyJoinLinesHandler"/> @@ -531,7 +532,8 @@ </extensions> - <extensionPoints> + <extensionPoints> + <extensionPoint qualifiedName="Pythonid.pythonDocumentationQuickInfoProvider" interface="com.jetbrains.python.documentation.PythonDocumentationQuickInfoProvider"/> <extensionPoint qualifiedName="Pythonid.importResolver" interface="com.jetbrains.python.psi.impl.PyImportResolver"/> <extensionPoint qualifiedName="Pythonid.magicLiteral" interface="com.jetbrains.python.magicLiteral.PyMagicLiteralExtensionPoint"/> <extensionPoint qualifiedName="Pythonid.unresolvedReferenceSkipper" interface="com.jetbrains.python.inspections.unresolvedReference.PyUnresolvedReferenceSkipperExtPoint"/> @@ -633,8 +635,6 @@ <separator/> <reference ref="AddToFavorites"/> <separator/> - <reference ref="RunContextPopupGroup"/> - <separator/> <reference ref="ReformatCode"/> <reference ref="OptimizeImports"/> <reference ref="$Delete"/> @@ -647,7 +647,26 @@ <reference ref="CompareFileWithEditor"/> </group> + <group id="PyCallHierarchyPopupMenu"> + <reference ref="EditSource"/> + <separator/> + <reference ref="FindUsages"/> + <reference ref="RefactoringMenu"/> + <separator/> + <reference ref="AddToFavorites"/> + <separator/> + <reference ref="ReformatCode"/> + <reference ref="OptimizeImports"/> + <separator/> + <reference ref="VersionControlsGroup"/> + + <separator/> + <reference ref="ExternalToolsGroup"/> + <separator/> + <reference ref="CompareTwoFiles"/> + <reference ref="CompareFileWithEditor"/> + </group> <action id="com.jetbrains.python.console.PyOpenDebugConsoleAction" class="com.jetbrains.python.console.PyOpenDebugConsoleAction" diff --git a/python/src/com/jetbrains/python/PyBundle.properties b/python/src/com/jetbrains/python/PyBundle.properties index 482daa88b66c..d82e9a6fde62 100644 --- a/python/src/com/jetbrains/python/PyBundle.properties +++ b/python/src/com/jetbrains/python/PyBundle.properties @@ -676,6 +676,9 @@ ANN.tuple.py3=tuple parameter unpacking is not supported in Python 3 ANN.star.import.at.top.only='import *' only allowed at module level +ANN.missing.closing.quote=Missing closing quote [{0}] +ANN.missing.closing.triple.quotes=Missing closing triple quotes + ANN.method.$0.removed.use.$1=Method ''{0}'' has been removed, use ''{1}'' instead ANN.method.$0.removed=Method ''{0}'' removed diff --git a/python/src/com/jetbrains/python/codeInsight/completion/PyKeywordCompletionContributor.java b/python/src/com/jetbrains/python/codeInsight/completion/PyKeywordCompletionContributor.java index 2ecf3b54a8d0..ab13a59ff0b8 100644 --- a/python/src/com/jetbrains/python/codeInsight/completion/PyKeywordCompletionContributor.java +++ b/python/src/com/jetbrains/python/codeInsight/completion/PyKeywordCompletionContributor.java @@ -278,7 +278,7 @@ public class PyKeywordCompletionContributor extends CompletionContributor { private static final PsiElementPattern.Capture<PsiElement> IN_IF_BODY = psiElement().inside(psiElement(PyStatementList.class).inside(psiElement(PyIfPart.class))); - private static final PsiElementPattern.Capture<PsiElement> IN_LOOP = + private static final PsiElementPattern.Capture<PsiElement> IN_LOOP = psiElement().inside(false, psiElement(PyLoopStatement.class), or(psiElement(PyFunction.class), psiElement(PyClass.class))); // not exactly a beauty @@ -645,6 +645,20 @@ public class PyKeywordCompletionContributor extends CompletionContributor { new PyKeywordCompletionProvider(PyNames.FROM)); } + private void addYieldExpression() { + extend(CompletionType.BASIC, + psiElement() + .withLanguage(PythonLanguage.getInstance()) + .andOr(psiElement() + .inside(false, psiElement(PyAssignmentStatement.class), psiElement(PyTargetExpression.class)) + .afterLeaf(psiElement().withElementType(PyTokenTypes.EQ)), + psiElement() + .inside(false, psiElement(PyAugAssignmentStatement.class), psiElement(PyTargetExpression.class)) + .afterLeaf(psiElement().withElementType(PyTokenTypes.AUG_ASSIGN_OPERATIONS)), + psiElement().inside(true, psiElement(PyParenthesizedExpression.class))), + new PyKeywordCompletionProvider(PyNames.YIELD)); + } + private void addYieldFrom() { extend(CompletionType.BASIC, psiElement() @@ -671,6 +685,7 @@ public class PyKeywordCompletionContributor extends CompletionContributor { //addExprIf(); addExprElse(); addRaiseFrom(); + addYieldExpression(); addYieldFrom(); addForToComprehensions(); addInToFor(); diff --git a/python/src/com/jetbrains/python/codeInsight/editorActions/moveUpDown/PyStatementMover.java b/python/src/com/jetbrains/python/codeInsight/editorActions/moveUpDown/PyStatementMover.java index 65cd5e1a79ce..89b6d9bc4a5d 100644 --- a/python/src/com/jetbrains/python/codeInsight/editorActions/moveUpDown/PyStatementMover.java +++ b/python/src/com/jetbrains/python/codeInsight/editorActions/moveUpDown/PyStatementMover.java @@ -108,7 +108,6 @@ public class PyStatementMover extends LineMover { if (moveOutsideFile(document, lineNumber)) return null; int lineEndOffset = document.getLineEndOffset(lineNumber); final int startOffset = document.getLineStartOffset(lineNumber); - lineEndOffset = startOffset != lineEndOffset ? lineEndOffset - 1 : lineEndOffset; final PyStatementList statementList = getStatementList(elementToMove); @@ -119,10 +118,6 @@ public class PyStatementMover extends LineMover { final int startLine = document.getLineNumber(start); final int endLine = document.getLineNumber(end); - if (elementToMove instanceof PsiComment && destination instanceof PsiComment) { - return new LineRange(lineNumber, lineNumber + 1); - } - if (elementToMove instanceof PyClass || elementToMove instanceof PyFunction) { PyElement scope = statementList == null ? (PyElement)elementToMove.getContainingFile() : statementList; if (destination != null) @@ -137,6 +132,11 @@ public class PyStatementMover extends LineMover { scopeRange = moveInto(elementToMove, file, editor, down, lineEndOffset); if (scopeRange != null) return scopeRange; + if (elementToMove instanceof PsiComment && ( PsiTreeUtil.isAncestor(destination, elementToMove, true)) || + destination instanceof PsiComment) { + return new LineRange(lineNumber, lineNumber + 1); + } + final PyElement scope = statementList == null ? (PyElement)elementToMove.getContainingFile() : statementList; if ((elementToMove instanceof PyClass) || (elementToMove instanceof PyFunction)) return new ScopeRange(scope, scope.getFirstChild(), !down, true); @@ -185,7 +185,6 @@ public class PyStatementMover extends LineMover { if (sibling != null) { final PyStatementList list = sibling.getStatementList(); - assert list != null; return new ScopeRange(list, down ? list.getFirstChild() : list.getLastChild(), !addBefore); } else { @@ -278,11 +277,23 @@ public class PyStatementMover extends LineMover { private static PsiElement getDestinationElement(@NotNull final PsiElement elementToMove, @NotNull final Document document, int lineEndOffset, boolean down) { - PsiElement destination = elementToMove.getContainingFile().findElementAt(lineEndOffset); - if (destination == null) return null; - if (destination instanceof PsiComment) return destination; + PsiElement destination = PyUtil.findPrevAtOffset(elementToMove.getContainingFile(), lineEndOffset, PsiWhiteSpace.class); PsiElement sibling = down ? PsiTreeUtil.getNextSiblingOfType(elementToMove, PyStatement.class) : - PsiTreeUtil.getPrevSiblingOfType(elementToMove, PyStatement.class); + PsiTreeUtil.getPrevSiblingOfType(elementToMove, PyStatement.class); + if (destination == null) { + if (elementToMove instanceof PyClass) { + destination = sibling; + } + else if (elementToMove instanceof PyFunction) { + if (!(sibling instanceof PyClass)) + destination = sibling; + else destination = null; + } + else { + return null; + } + } + if (destination instanceof PsiComment) return destination; if (elementToMove instanceof PyClass) { destination = sibling; } diff --git a/python/src/com/jetbrains/python/codeInsight/editorActions/smartEnter/fixers/PyWithFixer.java b/python/src/com/jetbrains/python/codeInsight/editorActions/smartEnter/fixers/PyWithFixer.java index ec236d9242f5..b2b71cb9e0d2 100644 --- a/python/src/com/jetbrains/python/codeInsight/editorActions/smartEnter/fixers/PyWithFixer.java +++ b/python/src/com/jetbrains/python/codeInsight/editorActions/smartEnter/fixers/PyWithFixer.java @@ -18,6 +18,7 @@ package com.jetbrains.python.codeInsight.editorActions.smartEnter.fixers; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.psi.PsiElement; +import com.intellij.util.ArrayUtil; import com.intellij.util.IncorrectOperationException; import com.jetbrains.python.PyTokenTypes; import com.jetbrains.python.codeInsight.editorActions.smartEnter.PySmartEnterProcessor; @@ -27,8 +28,6 @@ import com.jetbrains.python.psi.PyWithItem; import com.jetbrains.python.psi.PyWithStatement; import org.jetbrains.annotations.NotNull; -import static com.jetbrains.python.psi.PyUtil.sure; - /** * @author Mikhail Golubev */ @@ -42,11 +41,10 @@ public class PyWithFixer extends PyFixer<PyWithStatement> { final PsiElement colonToken = PyUtil.getFirstChildOfType(withStatement, PyTokenTypes.COLON); final PsiElement withToken = PyUtil.getFirstChildOfType(withStatement, PyTokenTypes.WITH_KEYWORD); final Document document = editor.getDocument(); - if (colonToken == null) { - int insertAt = sure(withToken).getTextRange().getEndOffset(); + if (colonToken == null && withToken != null) { + int insertAt = withToken.getTextRange().getEndOffset(); String textToInsert = ":"; - final PyWithItem[] withItems = withStatement.getWithItems(); - final PyWithItem lastItem = withItems.length != 0 ? withItems[withItems.length - 1] : null; + final PyWithItem lastItem = ArrayUtil.getLastElement(withStatement.getWithItems()); if (lastItem == null || lastItem.getExpression() == null) { textToInsert = " :"; processor.registerUnresolvedError(insertAt + 1); diff --git a/python/src/com/jetbrains/python/configuration/PyIntegratedToolsConfigurable.java b/python/src/com/jetbrains/python/configuration/PyIntegratedToolsConfigurable.java index fa869bfca359..e9585a5cd952 100644 --- a/python/src/com/jetbrains/python/configuration/PyIntegratedToolsConfigurable.java +++ b/python/src/com/jetbrains/python/configuration/PyIntegratedToolsConfigurable.java @@ -156,7 +156,7 @@ public class PyIntegratedToolsConfigurable implements SearchableConfigurable, No return new FacetConfigurationQuickFix() { @Override public void run(JComponent place) { - final PyPackageManagerImpl.UI ui = new PyPackageManagerImpl.UI(myProject, sdk, new PyPackageManagerImpl.UI.Listener() { + final PyPackageManagerUI ui = new PyPackageManagerUI(myProject, sdk, new PyPackageManagerUI.Listener() { @Override public void started() {} diff --git a/python/src/com/jetbrains/python/console/PydevConsoleCommunication.java b/python/src/com/jetbrains/python/console/PydevConsoleCommunication.java index 9c95b1cffda8..5bf06782d376 100644 --- a/python/src/com/jetbrains/python/console/PydevConsoleCommunication.java +++ b/python/src/com/jetbrains/python/console/PydevConsoleCommunication.java @@ -31,10 +31,7 @@ import com.intellij.util.Function; import com.intellij.xdebugger.frame.XValueChildrenList; import com.jetbrains.python.console.parsing.PythonConsoleData; import com.jetbrains.python.console.pydev.*; -import com.jetbrains.python.debugger.PyDebugValue; -import com.jetbrains.python.debugger.PyDebuggerException; -import com.jetbrains.python.debugger.PyFrameAccessor; -import com.jetbrains.python.debugger.PydevXmlUtils; +import com.jetbrains.python.debugger.*; import com.jetbrains.python.debugger.pydev.GetVariableCommand; import com.jetbrains.python.debugger.pydev.ProtocolParser; import org.apache.xmlrpc.WebServer; @@ -528,6 +525,11 @@ public class PydevConsoleCommunication extends AbstractConsoleCommunication impl } } + @Nullable + @Override + public PyReferrersLoader getReferrersLoader() { + return null; + } /** * Request that pydevconsole connect (with pydevd) to the specified port diff --git a/python/src/com/jetbrains/python/console/PythonDebugConsoleCommunication.java b/python/src/com/jetbrains/python/console/PythonDebugConsoleCommunication.java index da1ac53113ef..5f361b30228a 100644 --- a/python/src/com/jetbrains/python/console/PythonDebugConsoleCommunication.java +++ b/python/src/com/jetbrains/python/console/PythonDebugConsoleCommunication.java @@ -23,7 +23,7 @@ import com.jetbrains.python.console.pydev.InterpreterResponse; import com.jetbrains.python.console.pydev.PydevCompletionVariant; import com.jetbrains.python.debugger.PyDebugProcess; import com.jetbrains.python.debugger.PyDebuggerException; -import com.jetbrains.python.debugger.pydev.ProcessDebugger; +import com.jetbrains.python.debugger.pydev.PyDebugCallback; import org.jetbrains.annotations.NotNull; import java.util.List; @@ -63,8 +63,8 @@ public class PythonDebugConsoleCommunication extends AbstractConsoleCommunicatio return false; } - protected void exec(final ConsoleCodeFragment command, final ProcessDebugger.DebugCallback<Pair<String, Boolean>> callback) { - myDebugProcess.consoleExec(command.getText(), new ProcessDebugger.DebugCallback<String>() { + protected void exec(final ConsoleCodeFragment command, final PyDebugCallback<Pair<String, Boolean>> callback) { + myDebugProcess.consoleExec(command.getText(), new PyDebugCallback<String>() { @Override public void ok(String value) { callback.ok(parseExecResponseString(value)); @@ -79,7 +79,7 @@ public class PythonDebugConsoleCommunication extends AbstractConsoleCommunicatio public void execInterpreter(ConsoleCodeFragment code, final Function<InterpreterResponse, Object> callback) { myExpression.append(code.getText()); - exec(new ConsoleCodeFragment(myExpression.toString(), false), new ProcessDebugger.DebugCallback<Pair<String, Boolean>>() { + exec(new ConsoleCodeFragment(myExpression.toString(), false), new PyDebugCallback<Pair<String, Boolean>>() { @Override public void ok(Pair<String, Boolean> executed) { boolean more = executed.second; diff --git a/python/src/com/jetbrains/python/debugger/PyDebugProcess.java b/python/src/com/jetbrains/python/debugger/PyDebugProcess.java index cdea41298a4e..ea492e43cf2d 100644 --- a/python/src/com/jetbrains/python/debugger/PyDebugProcess.java +++ b/python/src/com/jetbrains/python/debugger/PyDebugProcess.java @@ -17,7 +17,6 @@ package com.jetbrains.python.debugger; import com.google.common.collect.Lists; import com.google.common.collect.Maps; -import com.intellij.execution.console.DuplexConsoleView; import com.intellij.execution.process.ProcessEvent; import com.intellij.execution.process.ProcessHandler; import com.intellij.execution.process.ProcessListener; @@ -88,6 +87,7 @@ public class PyDebugProcess extends XDebugProcess implements IPyDebugProcess, Pr private boolean myWaitingForConnection = false; private PyStackFrame myStackFrameBeforeResume; private PyStackFrame myConsoleContextFrame = null; + private PyReferrersLoader myReferrersProvider; public PyDebugProcess(final @NotNull XDebugSession session, @NotNull final ServerSocket serverSocket, @@ -489,7 +489,7 @@ public class PyDebugProcess extends XDebugProcess implements IPyDebugProcess, Pr return myDebugger.evaluate(frame.getThreadId(), frame.getFrameId(), expression, execute, trimResult); } - public void consoleExec(String command, ProcessDebugger.DebugCallback<String> callback) { + public void consoleExec(String command, PyDebugCallback<String> callback) { dropFrameCaches(); try { final PyStackFrame frame = currentFrame(); @@ -539,6 +539,17 @@ public class PyDebugProcess extends XDebugProcess implements IPyDebugProcess, Pr } @Override + public void loadReferrers(PyReferringObjectsValue var, PyDebugCallback<XValueChildrenList> callback) { + try { + final PyStackFrame frame = currentFrame(); + myDebugger.loadReferrers(frame.getThreadId(), frame.getFrameId(), var, callback); + } + catch (PyDebuggerException e) { + callback.error(e); + } + } + + @Override public void changeVariable(final PyDebugValue var, final String value) throws PyDebuggerException { final PyStackFrame frame = currentFrame(); PyDebugValue newValue = myDebugger.changeVariable(frame.getThreadId(), frame.getFrameId(), var, value); @@ -546,14 +557,23 @@ public class PyDebugProcess extends XDebugProcess implements IPyDebugProcess, Pr } @Nullable + @Override + public PyReferrersLoader getReferrersLoader() { + if (myReferrersProvider == null) { + myReferrersProvider = new PyReferrersLoader(this); + } + return myReferrersProvider; + } + + @Nullable public String loadSource(String path) { return myDebugger.loadSource(path); } @Override - public boolean isVariable(String name) { + public boolean canSaveToTemp(String name) { final Project project = getSession().getProject(); - return PyDebugSupportUtils.isVariable(project, name); + return PyDebugSupportUtils.canSaveToTemp(project, name); } private PyStackFrame currentFrame() throws PyDebuggerException { diff --git a/python/src/com/jetbrains/python/debugger/PyDebugSupportUtils.java b/python/src/com/jetbrains/python/debugger/PyDebugSupportUtils.java index d29259d559c6..d25596a13f2a 100644 --- a/python/src/com/jetbrains/python/debugger/PyDebugSupportUtils.java +++ b/python/src/com/jetbrains/python/debugger/PyDebugSupportUtils.java @@ -80,25 +80,29 @@ public class PyDebugSupportUtils { element instanceof PyNamedParameter; } - // is expression a variable reference + // is expression a variable reference and can be evaluated // todo: use patterns (?) - public static boolean isVariable(final Project project, final String expression) { + public static boolean canSaveToTemp(final Project project, final String expression) { return ApplicationManager.getApplication().runReadAction(new Computable<Boolean>() { public Boolean compute() { final PsiFile file = PyElementGenerator.getInstance(project).createDummyFile(LanguageLevel.getDefault(), expression); final PsiElement root = file.getFirstChild(); - return root instanceof PyExpressionStatement && - root.getFirstChild() instanceof PyReferenceExpression && - root.getFirstChild() == root.getLastChild() && - root.getFirstChild().getFirstChild() != null && - root.getFirstChild().getFirstChild().getNode().getElementType() == PyTokenTypes.IDENTIFIER && - root.getFirstChild().getFirstChild() == root.getFirstChild().getLastChild() && - root.getFirstChild().getFirstChild().getFirstChild() == null; + return !isVariable(root) && (root instanceof PyExpressionStatement); } }); } + private static Boolean isVariable(PsiElement root) { + return root instanceof PyExpressionStatement && + root.getFirstChild() instanceof PyReferenceExpression && + root.getFirstChild() == root.getLastChild() && + root.getFirstChild().getFirstChild() != null && + root.getFirstChild().getFirstChild().getNode().getElementType() == PyTokenTypes.IDENTIFIER && + root.getFirstChild().getFirstChild() == root.getFirstChild().getLastChild() && + root.getFirstChild().getFirstChild().getFirstChild() == null; + } + @Nullable private static String getLineText(@NotNull Document document, int line) { if (line > 0 && line < document.getLineCount()) { diff --git a/python/src/com/jetbrains/python/debugger/PyExceptionBreakpointProperties.java b/python/src/com/jetbrains/python/debugger/PyExceptionBreakpointProperties.java index d6bfd13f488c..93f9d89e386c 100644 --- a/python/src/com/jetbrains/python/debugger/PyExceptionBreakpointProperties.java +++ b/python/src/com/jetbrains/python/debugger/PyExceptionBreakpointProperties.java @@ -39,6 +39,7 @@ public class PyExceptionBreakpointProperties extends ExceptionBreakpointProperti public PyExceptionBreakpointProperties(@NotNull final String exception) { myException = exception; + myNotifyOnTerminate = true; } @Override @@ -78,6 +79,10 @@ public class PyExceptionBreakpointProperties extends ExceptionBreakpointProperti myNotifyOnlyOnFirst = notifyOnlyOnFirst; } + public String getException() { + return "python-" + myException; + } + @Override public ExceptionBreakpointCommand createAddCommand(RemoteDebugger debugger) { return ExceptionBreakpointCommand.addExceptionBreakpointCommand(debugger, getException(), diff --git a/python/src/com/jetbrains/python/documentation/PythonDocumentationProvider.java b/python/src/com/jetbrains/python/documentation/PythonDocumentationProvider.java index 913ed65f1e53..73a0cb604091 100644 --- a/python/src/com/jetbrains/python/documentation/PythonDocumentationProvider.java +++ b/python/src/com/jetbrains/python/documentation/PythonDocumentationProvider.java @@ -81,7 +81,16 @@ public class PythonDocumentationProvider extends AbstractDocumentationProvider i @NonNls private static final String EPYDOC_PREFIX = "@"; // provides ctrl+hover info - public String getQuickNavigateInfo(final PsiElement element, PsiElement originalElement) { + @Override + @Nullable + public String getQuickNavigateInfo(final PsiElement element, final PsiElement originalElement) { + for (final PythonDocumentationQuickInfoProvider point : PythonDocumentationQuickInfoProvider.EP_NAME.getExtensions()) { + String info = point.getQuickInfo(originalElement); + if (info != null) { + return info; + } + } + if (element instanceof PyFunction) { PyFunction func = (PyFunction)element; StringBuilder cat = new StringBuilder(); @@ -637,8 +646,9 @@ public class PythonDocumentationProvider extends AbstractDocumentationProvider i String raiseTarget = visitor.myRaiseTarget.getText(); if (visitor.myRaiseTarget instanceof PyCallExpression) { final PyExpression callee = ((PyCallExpression)visitor.myRaiseTarget).getCallee(); - if (callee != null) + if (callee != null) { raiseTarget = callee.getText(); + } } builder.append(" ").append(raiseTarget); } diff --git a/python/src/com/jetbrains/python/findUsages/PyFunctionFindUsagesHandler.java b/python/src/com/jetbrains/python/findUsages/PyFunctionFindUsagesHandler.java index 8c96916c9d80..fa7cf1b429c4 100644 --- a/python/src/com/jetbrains/python/findUsages/PyFunctionFindUsagesHandler.java +++ b/python/src/com/jetbrains/python/findUsages/PyFunctionFindUsagesHandler.java @@ -27,12 +27,12 @@ import java.util.List; public class PyFunctionFindUsagesHandler extends FindUsagesHandler { private final List<PsiElement> myAllElements; - protected PyFunctionFindUsagesHandler(@NotNull PsiElement psiElement) { + public PyFunctionFindUsagesHandler(@NotNull PsiElement psiElement) { super(psiElement); myAllElements = null; } - protected PyFunctionFindUsagesHandler(@NotNull PsiElement psiElement, List<PsiElement> allElements) { + public PyFunctionFindUsagesHandler(@NotNull PsiElement psiElement, List<PsiElement> allElements) { super(psiElement); myAllElements = allElements; } diff --git a/python/src/com/jetbrains/python/hierarchy/PyTypeHierarchyNodeDescriptor.java b/python/src/com/jetbrains/python/hierarchy/PyHierarchyNodeDescriptor.java index ea8c250b2e4f..c9720fb01af3 100644 --- a/python/src/com/jetbrains/python/hierarchy/PyTypeHierarchyNodeDescriptor.java +++ b/python/src/com/jetbrains/python/hierarchy/PyHierarchyNodeDescriptor.java @@ -19,15 +19,14 @@ import com.intellij.ide.IdeBundle; import com.intellij.ide.hierarchy.HierarchyNodeDescriptor; import com.intellij.ide.util.treeView.NodeDescriptor; import com.intellij.navigation.ItemPresentation; -import com.intellij.openapi.editor.markup.TextAttributes; import com.intellij.openapi.roots.ui.util.CompositeAppearance; import com.intellij.openapi.util.Comparing; import com.intellij.psi.NavigatablePsiElement; import com.intellij.psi.PsiElement; import com.jetbrains.python.psi.PyClass; +import com.jetbrains.python.psi.PyFunction; import org.jetbrains.annotations.NotNull; - -import java.awt.*; +import org.jetbrains.annotations.Nullable; /** * Created by IntelliJ IDEA. @@ -35,14 +34,14 @@ import java.awt.*; * Date: Jul 31, 2009 * Time: 6:26:37 PM */ -public class PyTypeHierarchyNodeDescriptor extends HierarchyNodeDescriptor { - - public PyTypeHierarchyNodeDescriptor(final NodeDescriptor parentDescriptor, @NotNull final PsiElement element, final boolean isBase) { +public class PyHierarchyNodeDescriptor extends HierarchyNodeDescriptor { + public PyHierarchyNodeDescriptor(final NodeDescriptor parentDescriptor, @NotNull final PsiElement element, final boolean isBase) { super(element.getProject(), parentDescriptor, element, isBase); } - public PyClass getClassElement() { - return (PyClass)myElement; + @Nullable + public PsiElement getPsiElement() { + return myElement; } public boolean isValid() { @@ -55,10 +54,6 @@ public class PyTypeHierarchyNodeDescriptor extends HierarchyNodeDescriptor { final CompositeAppearance oldText = myHighlightedText; myHighlightedText = new CompositeAppearance(); - TextAttributes classNameAttributes = null; - if (myColor != null) { - classNameAttributes = new TextAttributes(myColor, null, null, null, Font.PLAIN); - } NavigatablePsiElement element = (NavigatablePsiElement)myElement; if (element == null) { @@ -71,10 +66,14 @@ public class PyTypeHierarchyNodeDescriptor extends HierarchyNodeDescriptor { final ItemPresentation presentation = element.getPresentation(); if (presentation != null) { - final PyClass cl = getClassElement(); - myHighlightedText.getEnding().addText(cl.getName(), classNameAttributes); - myHighlightedText.getEnding() - .addText(" (" + cl.getContainingFile().getName() + ")", HierarchyNodeDescriptor.getPackageNameAttributes()); + if (element instanceof PyFunction) { + final PyClass cls = ((PyFunction)element).getContainingClass(); + if (cls != null) { + myHighlightedText.getEnding().addText(cls.getName() + "."); + } + } + myHighlightedText.getEnding().addText(presentation.getPresentableText()); + myHighlightedText.getEnding().addText(" " + presentation.getLocationString(), HierarchyNodeDescriptor.getPackageNameAttributes()); } myName = myHighlightedText.getText(); diff --git a/python/src/com/jetbrains/python/hierarchy/PyTypeHierarchyBrowser.java b/python/src/com/jetbrains/python/hierarchy/PyTypeHierarchyBrowser.java index 643440571618..817d462044fe 100644 --- a/python/src/com/jetbrains/python/hierarchy/PyTypeHierarchyBrowser.java +++ b/python/src/com/jetbrains/python/hierarchy/PyTypeHierarchyBrowser.java @@ -52,10 +52,10 @@ public class PyTypeHierarchyBrowser extends TypeHierarchyBrowserBase { @Nullable protected PsiElement getElementFromDescriptor(@NotNull HierarchyNodeDescriptor descriptor) { - if (!(descriptor instanceof PyTypeHierarchyNodeDescriptor)) { + if (!(descriptor instanceof PyHierarchyNodeDescriptor)) { return null; } - return ((PyTypeHierarchyNodeDescriptor)descriptor).getClassElement(); + return ((PyHierarchyNodeDescriptor)descriptor).getPsiElement(); } protected void createTrees(@NotNull Map<String, JTree> trees) { diff --git a/python/src/com/jetbrains/python/hierarchy/call/PyCallHierarchyBrowser.java b/python/src/com/jetbrains/python/hierarchy/call/PyCallHierarchyBrowser.java new file mode 100644 index 000000000000..c5224e617ec9 --- /dev/null +++ b/python/src/com/jetbrains/python/hierarchy/call/PyCallHierarchyBrowser.java @@ -0,0 +1,101 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jetbrains.python.hierarchy.call; + +import com.intellij.ide.hierarchy.CallHierarchyBrowserBase; +import com.intellij.ide.hierarchy.HierarchyNodeDescriptor; +import com.intellij.ide.hierarchy.HierarchyTreeStructure; +import com.intellij.ide.util.treeView.NodeDescriptor; +import com.intellij.openapi.actionSystem.*; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.psi.PsiElement; +import com.intellij.ui.PopupHandler; +import com.jetbrains.python.hierarchy.PyHierarchyNodeDescriptor; +import com.jetbrains.python.hierarchy.PyHierarchyUtils; +import com.jetbrains.python.psi.PyClass; +import com.jetbrains.python.psi.PyFile; +import com.jetbrains.python.psi.PyFunction; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.util.Comparator; +import java.util.Map; + +/** + * @author novokrest + */ +public class PyCallHierarchyBrowser extends CallHierarchyBrowserBase { + private static final Logger LOG = Logger.getInstance("#com.jetbrains.python.hierarchy.call.PyCallHierarchyBrowser"); + private static final String GROUP_PY_CALL_HIERARCHY_POPUP = "PyCallHierarchyPopupMenu"; + + public PyCallHierarchyBrowser(PsiElement function) { + super(function.getProject(), function); + } + + @Nullable + @Override + protected PsiElement getElementFromDescriptor(@NotNull HierarchyNodeDescriptor descriptor) { + if (descriptor instanceof PyHierarchyNodeDescriptor) { + PyHierarchyNodeDescriptor pyDescriptor = (PyHierarchyNodeDescriptor)descriptor; + return pyDescriptor.getPsiElement(); + } + return null; + } + + @Override + protected void createTrees(@NotNull Map<String, JTree> type2TreeMap) { + final ActionGroup group = (ActionGroup)ActionManager.getInstance().getAction(GROUP_PY_CALL_HIERARCHY_POPUP); + + final JTree callerTree = createHierarchyTree(group); + final JTree calleeTree = createHierarchyTree(group); + + type2TreeMap.put(CALLER_TYPE, callerTree); + type2TreeMap.put(CALLEE_TYPE, calleeTree); + } + + private JTree createHierarchyTree(ActionGroup group) { + final JTree tree = createTree(false); + PopupHandler.installPopupHandler(tree, group, ActionPlaces.CALL_HIERARCHY_VIEW_POPUP, ActionManager.getInstance()); + return tree; + } + + @Override + protected boolean isApplicableElement(@NotNull PsiElement element) { + return element instanceof PyFunction || element instanceof PyClass || element instanceof PyFile; + } + + @Nullable + @Override + protected HierarchyTreeStructure createHierarchyTreeStructure(@NotNull String typeName, @NotNull PsiElement psiElement) { + if (CALLER_TYPE.equals(typeName)) { + return new PyCallerFunctionTreeStructure(myProject, psiElement, getCurrentScopeType()); + } + else if (CALLEE_TYPE.equals(typeName)) { + return new PyCalleeFunctionTreeStructure(myProject, psiElement, getCurrentScopeType()); + } + else { + LOG.error("unexpected type: " + typeName); + return null; + } + } + + @Nullable + @Override + protected Comparator<NodeDescriptor> getComparator() { + return PyHierarchyUtils.getComparator(myProject); + } +} diff --git a/python/src/com/jetbrains/python/hierarchy/call/PyCallHierarchyProvider.java b/python/src/com/jetbrains/python/hierarchy/call/PyCallHierarchyProvider.java new file mode 100644 index 000000000000..67c0d6ddfa0b --- /dev/null +++ b/python/src/com/jetbrains/python/hierarchy/call/PyCallHierarchyProvider.java @@ -0,0 +1,76 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jetbrains.python.hierarchy.call; + +import com.intellij.codeInsight.TargetElementUtilBase; +import com.intellij.ide.hierarchy.CallHierarchyBrowserBase; +import com.intellij.ide.hierarchy.HierarchyBrowser; +import com.intellij.ide.hierarchy.HierarchyProvider; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiDocumentManager; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.util.PsiTreeUtil; +import com.jetbrains.python.psi.PyClass; +import com.jetbrains.python.psi.PyFile; +import com.jetbrains.python.psi.PyFunction; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author novokrest + */ +public class PyCallHierarchyProvider implements HierarchyProvider { + @Nullable + @Override + public PsiElement getTarget(@NotNull DataContext dataContext) { + Project project = CommonDataKeys.PROJECT.getData(dataContext); + if (project == null) return null; + + PsiElement element = CommonDataKeys.PSI_ELEMENT.getData(dataContext); + if (element == null) { + Editor editor = CommonDataKeys.EDITOR.getData(dataContext); + if (editor != null) { + PsiFile file = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument()); + if (file == null) return null; + + element = TargetElementUtilBase.findTargetElement(editor, TargetElementUtilBase.ELEMENT_NAME_ACCEPTED | + TargetElementUtilBase.REFERENCED_ELEMENT_ACCEPTED | + TargetElementUtilBase.LOOKUP_ITEM_ACCEPTED); + if (element instanceof PyFunction || element instanceof PyClass || element instanceof PyFile) { + return element; + } + + element = file.findElementAt(editor.getCaretModel().getOffset()); + } + } + return PsiTreeUtil.getNonStrictParentOfType(element, PyFunction.class, PyClass.class, PyFile.class); + } + + @NotNull + @Override + public HierarchyBrowser createHierarchyBrowser(PsiElement target) { + return new PyCallHierarchyBrowser(target); + } + + @Override + public void browserActivated(@NotNull HierarchyBrowser hierarchyBrowser) { + ((PyCallHierarchyBrowser)hierarchyBrowser).changeView(CallHierarchyBrowserBase.CALLER_TYPE); + } +} diff --git a/python/src/com/jetbrains/python/hierarchy/call/PyCallHierarchyTreeStructureBase.java b/python/src/com/jetbrains/python/hierarchy/call/PyCallHierarchyTreeStructureBase.java new file mode 100644 index 000000000000..c5d60138299c --- /dev/null +++ b/python/src/com/jetbrains/python/hierarchy/call/PyCallHierarchyTreeStructureBase.java @@ -0,0 +1,85 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jetbrains.python.hierarchy.call; + +import com.intellij.ide.hierarchy.HierarchyNodeDescriptor; +import com.intellij.ide.hierarchy.HierarchyTreeStructure; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiElement; +import com.intellij.util.ArrayUtil; +import com.jetbrains.python.hierarchy.PyHierarchyNodeDescriptor; +import com.jetbrains.python.psi.PyClass; +import com.jetbrains.python.psi.PyElement; +import com.jetbrains.python.psi.PyFile; +import com.jetbrains.python.psi.PyFunction; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +/** + * @author novokrest + */ +public abstract class PyCallHierarchyTreeStructureBase extends HierarchyTreeStructure { + private final String myScopeType; + + public PyCallHierarchyTreeStructureBase(Project project, PsiElement element, String currentScopeType) { + super(project, new PyHierarchyNodeDescriptor(null, element, true)); + myScopeType = currentScopeType; + } + + @NotNull + protected abstract List<PsiElement> getChildren(@NotNull PyElement element); + + @NotNull + @Override + protected Object[] buildChildren(@NotNull HierarchyNodeDescriptor descriptor) { + final List<PyHierarchyNodeDescriptor> descriptors = new ArrayList<PyHierarchyNodeDescriptor>(); + if (descriptor instanceof PyHierarchyNodeDescriptor) { + final PyHierarchyNodeDescriptor pyDescriptor = (PyHierarchyNodeDescriptor)descriptor; + final PsiElement element = pyDescriptor.getPsiElement(); + final boolean isCallable = element instanceof PyFunction || element instanceof PyClass || element instanceof PyFile; + HierarchyNodeDescriptor nodeDescriptor = getBaseDescriptor(); + if (!(element instanceof PyElement) || !isCallable || nodeDescriptor == null) { + return ArrayUtil.EMPTY_OBJECT_ARRAY; + } + + final List<PsiElement> children = getChildren((PyElement)element); + + final HashMap<PsiElement, PyHierarchyNodeDescriptor> callerToDescriptorMap = new HashMap<PsiElement, PyHierarchyNodeDescriptor>(); + PsiElement baseClass = element instanceof PyFunction ? ((PyFunction)element).getContainingClass() : null; + + for (PsiElement caller : children) { + if (isInScope(baseClass, caller, myScopeType)) { + PyHierarchyNodeDescriptor callerDescriptor = callerToDescriptorMap.get(caller); + if (callerDescriptor == null) { + callerDescriptor = new PyHierarchyNodeDescriptor(descriptor, caller, false); + callerToDescriptorMap.put(caller, callerDescriptor); + descriptors.add(callerDescriptor); + } + } + } + + } + return ArrayUtil.toObjectArray(descriptors); + } + + @Override + public boolean isAlwaysShowPlus() { + return true; + } +} diff --git a/python/src/com/jetbrains/python/hierarchy/call/PyCalleeFunctionTreeStructure.java b/python/src/com/jetbrains/python/hierarchy/call/PyCalleeFunctionTreeStructure.java new file mode 100644 index 000000000000..b0465083ef05 --- /dev/null +++ b/python/src/com/jetbrains/python/hierarchy/call/PyCalleeFunctionTreeStructure.java @@ -0,0 +1,42 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jetbrains.python.hierarchy.call; + +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiElement; +import com.jetbrains.python.psi.PyElement; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author novokrest + */ +public class PyCalleeFunctionTreeStructure extends PyCallHierarchyTreeStructureBase { + public PyCalleeFunctionTreeStructure(Project project, PsiElement element, String currentScopeType) { + super(project, element, currentScopeType); + } + + @NotNull + @Override + protected List<PsiElement> getChildren(@NotNull PyElement element) { + final List<PsiElement> callees = new ArrayList<PsiElement>(); + // TODO: Add callees from the dynamic call data manager + callees.addAll(PyStaticCallHierarchyUtil.getCallees(element)); + return callees; + } +} diff --git a/python/src/com/jetbrains/python/hierarchy/call/PyCallerFunctionTreeStructure.java b/python/src/com/jetbrains/python/hierarchy/call/PyCallerFunctionTreeStructure.java new file mode 100644 index 000000000000..d45e79ce8d95 --- /dev/null +++ b/python/src/com/jetbrains/python/hierarchy/call/PyCallerFunctionTreeStructure.java @@ -0,0 +1,42 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jetbrains.python.hierarchy.call; + +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiElement; +import com.jetbrains.python.psi.PyElement; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author novokrest + */ +public class PyCallerFunctionTreeStructure extends PyCallHierarchyTreeStructureBase { + public PyCallerFunctionTreeStructure(Project project, PsiElement element, String currentScopeType) { + super(project, element, currentScopeType); + } + + @NotNull + @Override + protected List<PsiElement> getChildren(@NotNull PyElement element) { + final List<PsiElement> callers = new ArrayList<PsiElement>(); + // TODO: Add callers from the dynamic call data manager + callers.addAll(PyStaticCallHierarchyUtil.getCallers(element)); + return callers; + } +} diff --git a/python/src/com/jetbrains/python/hierarchy/call/PyStaticCallHierarchyUtil.java b/python/src/com/jetbrains/python/hierarchy/call/PyStaticCallHierarchyUtil.java new file mode 100644 index 000000000000..5657dcb589e9 --- /dev/null +++ b/python/src/com/jetbrains/python/hierarchy/call/PyStaticCallHierarchyUtil.java @@ -0,0 +1,163 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jetbrains.python.hierarchy.call; + +import com.google.common.collect.Lists; +import com.intellij.find.findUsages.FindUsagesHandler; +import com.intellij.find.findUsages.FindUsagesOptions; +import com.intellij.psi.PsiElement; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.usageView.UsageInfo; +import com.intellij.util.ArrayUtil; +import com.intellij.util.CommonProcessors; +import com.jetbrains.python.PyNames; +import com.jetbrains.python.findUsages.PyClassFindUsagesHandler; +import com.jetbrains.python.findUsages.PyFunctionFindUsagesHandler; +import com.jetbrains.python.psi.*; +import com.jetbrains.python.psi.impl.PyBuiltinCache; +import com.jetbrains.python.psi.resolve.PyResolveContext; +import com.jetbrains.python.psi.search.PySuperMethodsSearch; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.List; + +/** + * @author novokrest + */ +public class PyStaticCallHierarchyUtil { + public static Collection<PsiElement> getCallees(@NotNull PyElement element) { + final List<PsiElement> callees = Lists.newArrayList(); + + final PyRecursiveElementVisitor visitor = new PyRecursiveElementVisitor() { + @Override + public void visitPyParameterList(PyParameterList node) { + } + + @Override + public void visitPyLambdaExpression(PyLambdaExpression node) { + } + + @Override + public void visitPyFunction(PyFunction innerFunction) { + for (PyParameter parameter : innerFunction.getParameterList().getParameters()) { + PsiElement defaultValue = parameter.getDefaultValue(); + if (defaultValue != null) { + defaultValue.accept(this); + } + } + } + + @Override + public void visitPyCallExpression(PyCallExpression callExpression) { + super.visitPyCallExpression(callExpression); + PsiElement calleeFunction = callExpression.resolveCalleeFunction(PyResolveContext.defaultContext()); + if (calleeFunction instanceof PyFunction) { + callees.add(calleeFunction); + } + } + }; + + visitor.visitElement(element); + + return callees; + } + + public static Collection<PsiElement> getCallers(@NotNull PyElement pyElement) { + final List<PsiElement> callers = Lists.newArrayList(); + final Collection<UsageInfo> usages = findUsages(pyElement); + + for (UsageInfo usage : usages) { + PsiElement element = usage.getElement(); + if (element == null) { + continue; + } + + element = element.getParent(); + while (element instanceof PyParenthesizedExpression) { + element = element.getParent(); + } + + if (element instanceof PyCallExpression) { + PsiElement caller = PsiTreeUtil.getParentOfType(element, PyParameterList.class, PyFunction.class); + if (caller instanceof PyFunction) { + callers.add(caller); + } + else if (caller instanceof PyParameterList) { + PsiElement innerFunction = PsiTreeUtil.getParentOfType(caller, PyFunction.class); + PsiElement outerFunction = PsiTreeUtil.getParentOfType(innerFunction, PyFunction.class); + if (innerFunction != null && outerFunction != null) { + callers.add(outerFunction); + } + } + } + } + + return callers; + } + + private static Collection<UsageInfo> findUsages(@NotNull final PsiElement element) { + final FindUsagesHandler handler = createFindUsageHandler(element); + if (handler == null) { + return Lists.newArrayList(); + } + final CommonProcessors.CollectProcessor<UsageInfo> processor = new CommonProcessors.CollectProcessor<UsageInfo>(); + final PsiElement[] psiElements = ArrayUtil.mergeArrays(handler.getPrimaryElements(), handler.getSecondaryElements()); + final FindUsagesOptions options = handler.getFindUsagesOptions(null); + for (PsiElement psiElement : psiElements) { + handler.processElementUsages(psiElement, processor, options); + } + return processor.getResults(); + } + + /** + * @see {@link com.jetbrains.python.findUsages.PyFindUsagesHandlerFactory#createFindUsagesHandler(com.intellij.psi.PsiElement, boolean) createFindUsagesHandler} + */ + @Nullable + private static FindUsagesHandler createFindUsageHandler(@NotNull final PsiElement element) { + if (element instanceof PyFunction) { + final Collection<PsiElement> superMethods = PySuperMethodsSearch.search((PyFunction)element, true).findAll(); + if (superMethods.size() > 0) { + final PsiElement next = superMethods.iterator().next(); + if (next instanceof PyFunction && !isInObject((PyFunction)next)) { + List<PsiElement> allMethods = Lists.newArrayList(); + allMethods.add(element); + allMethods.addAll(superMethods); + + return new PyFunctionFindUsagesHandler(element, allMethods); + } + } + return new PyFunctionFindUsagesHandler(element); + } + if (element instanceof PyClass) { + return new PyClassFindUsagesHandler((PyClass)element); + } + return null; + } + + /** + * @see {@link com.jetbrains.python.findUsages.PyFindUsagesHandlerFactory#isInObject(com.jetbrains.python.psi.PyFunction) isInObject} + */ + private static boolean isInObject(PyFunction fun) { + final PyClass containingClass = fun.getContainingClass(); + if (containingClass == null) { + return false; + } + return (PyNames.FAKE_OLD_BASE.equals(containingClass.getName()) || + (PyNames.OBJECT.equals(containingClass.getName()) && PyBuiltinCache.getInstance(fun).isBuiltin(containingClass))); + } +} diff --git a/python/src/com/jetbrains/python/hierarchy/treestructures/PySubTypesHierarchyTreeStructure.java b/python/src/com/jetbrains/python/hierarchy/treestructures/PySubTypesHierarchyTreeStructure.java index 7b43fe7329cf..f4287b28bd6c 100644 --- a/python/src/com/jetbrains/python/hierarchy/treestructures/PySubTypesHierarchyTreeStructure.java +++ b/python/src/com/jetbrains/python/hierarchy/treestructures/PySubTypesHierarchyTreeStructure.java @@ -18,9 +18,10 @@ package com.jetbrains.python.hierarchy.treestructures; import com.intellij.ide.hierarchy.HierarchyNodeDescriptor; import com.intellij.ide.hierarchy.HierarchyTreeStructure; import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiElement; import com.intellij.util.ArrayUtil; import com.intellij.util.Query; -import com.jetbrains.python.hierarchy.PyTypeHierarchyNodeDescriptor; +import com.jetbrains.python.hierarchy.PyHierarchyNodeDescriptor; import com.jetbrains.python.psi.PyClass; import com.jetbrains.python.psi.search.PyClassInheritorsSearch; import org.jetbrains.annotations.NotNull; @@ -40,17 +41,20 @@ public class PySubTypesHierarchyTreeStructure extends HierarchyTreeStructure { } public PySubTypesHierarchyTreeStructure(@NotNull final PyClass cl) { - super(cl.getProject(), new PyTypeHierarchyNodeDescriptor(null, cl, true)); + super(cl.getProject(), new PyHierarchyNodeDescriptor(null, cl, true)); } @NotNull protected Object[] buildChildren(@NotNull HierarchyNodeDescriptor descriptor) { - final PyClass classElement = ((PyTypeHierarchyNodeDescriptor)descriptor).getClassElement(); - Query<PyClass> subClasses = PyClassInheritorsSearch.search(classElement, false); + final List<PyHierarchyNodeDescriptor> res = new ArrayList<PyHierarchyNodeDescriptor>(); + final PsiElement element = ((PyHierarchyNodeDescriptor)descriptor).getPsiElement(); + if (element instanceof PyClass) { + final PyClass cls = (PyClass)element; + Query<PyClass> subClasses = PyClassInheritorsSearch.search(cls, false); + for (PyClass subClass : subClasses) { + res.add(new PyHierarchyNodeDescriptor(descriptor, subClass, false)); + } - List<PyTypeHierarchyNodeDescriptor> res = new ArrayList<PyTypeHierarchyNodeDescriptor>(); - for (PyClass cl : subClasses) { - res.add(new PyTypeHierarchyNodeDescriptor(descriptor, cl, false)); } return ArrayUtil.toObjectArray(res); diff --git a/python/src/com/jetbrains/python/hierarchy/treestructures/PySuperTypesHierarchyTreeStructure.java b/python/src/com/jetbrains/python/hierarchy/treestructures/PySuperTypesHierarchyTreeStructure.java index 38b4c6ee7f76..8087767c61d8 100644 --- a/python/src/com/jetbrains/python/hierarchy/treestructures/PySuperTypesHierarchyTreeStructure.java +++ b/python/src/com/jetbrains/python/hierarchy/treestructures/PySuperTypesHierarchyTreeStructure.java @@ -17,8 +17,8 @@ package com.jetbrains.python.hierarchy.treestructures; import com.intellij.ide.hierarchy.HierarchyNodeDescriptor; import com.intellij.ide.hierarchy.HierarchyTreeStructure; -import com.intellij.openapi.project.Project; -import com.jetbrains.python.hierarchy.PyTypeHierarchyNodeDescriptor; +import com.intellij.psi.PsiElement; +import com.jetbrains.python.hierarchy.PyHierarchyNodeDescriptor; import com.jetbrains.python.psi.PyClass; import org.jetbrains.annotations.NotNull; @@ -32,20 +32,23 @@ import java.util.List; * Time: 7:04:07 PM */ public class PySuperTypesHierarchyTreeStructure extends HierarchyTreeStructure { - protected PySuperTypesHierarchyTreeStructure(final Project project, final HierarchyNodeDescriptor baseDescriptor) { - super(project, baseDescriptor); - } - public PySuperTypesHierarchyTreeStructure(@NotNull final PyClass cl) { - super(cl.getProject(), new PyTypeHierarchyNodeDescriptor(null, cl, true)); + super(cl.getProject(), new PyHierarchyNodeDescriptor(null, cl, true)); } @NotNull protected Object[] buildChildren(@NotNull HierarchyNodeDescriptor descriptor) { - final PyClass[] superClasses = ((PyTypeHierarchyNodeDescriptor)descriptor).getClassElement().getSuperClasses(); - List<PyTypeHierarchyNodeDescriptor> res = new ArrayList<PyTypeHierarchyNodeDescriptor>(); - for (PyClass superClass : superClasses) { - res.add(new PyTypeHierarchyNodeDescriptor(descriptor, superClass, false)); + final List<PyHierarchyNodeDescriptor> res = new ArrayList<PyHierarchyNodeDescriptor>(); + if (descriptor instanceof PyHierarchyNodeDescriptor) { + final PyHierarchyNodeDescriptor pyDescriptor = (PyHierarchyNodeDescriptor)descriptor; + final PsiElement element = pyDescriptor.getPsiElement(); + if (element instanceof PyClass) { + final PyClass cls = (PyClass)element; + final PyClass[] superClasses = cls.getSuperClasses(); + for (PyClass superClass : superClasses) { + res.add(new PyHierarchyNodeDescriptor(descriptor, superClass, false)); + } + } } return res.toArray(); } diff --git a/python/src/com/jetbrains/python/hierarchy/treestructures/PyTypeHierarchyTreeStructure.java b/python/src/com/jetbrains/python/hierarchy/treestructures/PyTypeHierarchyTreeStructure.java index 6707ac9b7e14..0eb505f4857b 100644 --- a/python/src/com/jetbrains/python/hierarchy/treestructures/PyTypeHierarchyTreeStructure.java +++ b/python/src/com/jetbrains/python/hierarchy/treestructures/PyTypeHierarchyTreeStructure.java @@ -16,8 +16,7 @@ package com.jetbrains.python.hierarchy.treestructures; import com.intellij.ide.hierarchy.HierarchyNodeDescriptor; -import com.intellij.openapi.project.Project; -import com.jetbrains.python.hierarchy.PyTypeHierarchyNodeDescriptor; +import com.jetbrains.python.hierarchy.PyHierarchyNodeDescriptor; import com.jetbrains.python.psi.PyClass; import com.jetbrains.python.psi.PyUtil; import org.jetbrains.annotations.NotNull; @@ -28,30 +27,26 @@ import java.util.List; * @author Alexey.Ivanov */ public class PyTypeHierarchyTreeStructure extends PySubTypesHierarchyTreeStructure { - private static PyTypeHierarchyNodeDescriptor buildHierarchyElement(@NotNull final PyClass cl) { - PyTypeHierarchyNodeDescriptor descriptor = null; + public PyTypeHierarchyTreeStructure(@NotNull final PyClass cl) { + super(cl.getProject(), buildHierarchyElement(cl)); + setBaseElement(myBaseDescriptor); + } + + private static PyHierarchyNodeDescriptor buildHierarchyElement(@NotNull final PyClass cl) { + PyHierarchyNodeDescriptor descriptor = null; List<PyClass> superClasses = PyUtil.getAllSuperClasses(cl); for (int i = superClasses.size() - 1; i >= 0; --i) { final PyClass superClass = superClasses.get(i); - final PyTypeHierarchyNodeDescriptor newDescriptor = new PyTypeHierarchyNodeDescriptor(descriptor, superClass, false); + final PyHierarchyNodeDescriptor newDescriptor = new PyHierarchyNodeDescriptor(descriptor, superClass, false); if (descriptor != null) { - descriptor.setCachedChildren(new PyTypeHierarchyNodeDescriptor[]{newDescriptor}); + descriptor.setCachedChildren(new PyHierarchyNodeDescriptor[]{newDescriptor}); } descriptor = newDescriptor; } - final PyTypeHierarchyNodeDescriptor newDescriptor = new PyTypeHierarchyNodeDescriptor(descriptor, cl, true); + final PyHierarchyNodeDescriptor newDescriptor = new PyHierarchyNodeDescriptor(descriptor, cl, true); if (descriptor != null) { descriptor.setCachedChildren(new HierarchyNodeDescriptor[]{newDescriptor}); } return newDescriptor; } - - protected PyTypeHierarchyTreeStructure(final Project project, final HierarchyNodeDescriptor baseDescriptor) { - super(project, baseDescriptor); - } - - public PyTypeHierarchyTreeStructure(@NotNull final PyClass cl) { - super(cl.getProject(), buildHierarchyElement(cl)); - setBaseElement(myBaseDescriptor); - } } diff --git a/python/src/com/jetbrains/python/inspections/PyPackageRequirementsInspection.java b/python/src/com/jetbrains/python/inspections/PyPackageRequirementsInspection.java index ec3761154f28..0b73adc3fa4c 100644 --- a/python/src/com/jetbrains/python/inspections/PyPackageRequirementsInspection.java +++ b/python/src/com/jetbrains/python/inspections/PyPackageRequirementsInspection.java @@ -27,6 +27,7 @@ import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleUtilCore; import com.intellij.openapi.project.Project; import com.intellij.openapi.projectRoots.Sdk; +import com.intellij.openapi.ui.Messages; import com.intellij.openapi.util.JDOMExternalizableStringList; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; @@ -42,6 +43,7 @@ import com.jetbrains.python.packaging.*; import com.jetbrains.python.packaging.ui.PyChooseRequirementsDialog; import com.jetbrains.python.psi.*; import com.jetbrains.python.psi.impl.PyPsiUtils; +import com.jetbrains.python.sdk.PySdkUtil; import com.jetbrains.python.sdk.PythonSdkType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -111,9 +113,7 @@ public class PyPackageRequirementsInspection extends PyInspection { unsatisfiedNames.add(req.getName()); } final List<LocalQuickFix> quickFixes = new ArrayList<LocalQuickFix>(); - if (PyPackageManager.getInstance(sdk).hasPip()) { - quickFixes.add(new PyInstallRequirementsFix(null, module, sdk, unsatisfied)); - } + quickFixes.add(new PyInstallRequirementsFix(null, module, sdk, unsatisfied)); quickFixes.add(new IgnoreRequirementFix(unsatisfiedNames)); registerProblem(node, msg, ProblemHighlightType.GENERIC_ERROR_OR_WARNING, null, @@ -160,15 +160,16 @@ public class PyPackageRequirementsInspection extends PyInspection { return; } } - if (PyPackageManagerImpl.PACKAGE_SETUPTOOLS.equals(packageName)) { + if (PyPackageManager.PACKAGE_SETUPTOOLS.equals(packageName)) { return; } final Module module = ModuleUtilCore.findModuleForPsiElement(packageReferenceExpression); if (module != null) { - Collection<PyRequirement> requirements = PyPackageManagerImpl.getRequirements(module); - if (requirements != null) { - final Sdk sdk = PythonSdkType.findPythonSdk(module); - if (sdk != null) { + final Sdk sdk = PythonSdkType.findPythonSdk(module); + if (sdk != null) { + final PyPackageManager manager = PyPackageManager.getInstance(sdk); + Collection<PyRequirement> requirements = manager.getRequirements(module); + if (requirements != null) { requirements = getTransitiveRequirements(sdk, requirements, new HashSet<PyPackage>()); } if (requirements == null) return; @@ -191,9 +192,7 @@ public class PyPackageRequirementsInspection extends PyInspection { } } final List<LocalQuickFix> quickFixes = new ArrayList<LocalQuickFix>(); - if (sdk != null && PyPackageManager.getInstance(sdk).hasPip()) { - quickFixes.add(new AddToRequirementsFix(module, packageName, LanguageLevel.forElement(importedExpression))); - } + quickFixes.add(new AddToRequirementsFix(module, packageName, LanguageLevel.forElement(importedExpression))); quickFixes.add(new IgnoreRequirementFix(Collections.singleton(packageName))); registerProblem(packageReferenceExpression, String.format("Package '%s' is not listed in project requirements", packageName), ProblemHighlightType.WEAK_WARNING, null, @@ -211,7 +210,7 @@ public class PyPackageRequirementsInspection extends PyInspection { final Set<PyRequirement> results = new HashSet<PyRequirement>(requirements); final List<PyPackage> packages; try { - packages = ((PyPackageManagerImpl) PyPackageManager.getInstance(sdk)).getPackagesFast(); + packages = PyPackageManager.getInstance(sdk).getPackages(PySdkUtil.isRemote(sdk)); } catch (PyExternalProcessException e) { return null; @@ -242,12 +241,12 @@ public class PyPackageRequirementsInspection extends PyInspection { @Nullable private static List<PyRequirement> findUnsatisfiedRequirements(@NotNull Module module, @NotNull Sdk sdk, @NotNull Set<String> ignoredPackages) { - final PyPackageManagerImpl manager = (PyPackageManagerImpl)PyPackageManager.getInstance(sdk); - List<PyRequirement> requirements = PyPackageManagerImpl.getRequirements(module); + final PyPackageManager manager = PyPackageManager.getInstance(sdk); + List<PyRequirement> requirements = manager.getRequirements(module); if (requirements != null) { final List<PyPackage> packages; try { - packages = manager.getPackagesFast(); + packages = manager.getPackages(PySdkUtil.isRemote(sdk)); } catch (PyExternalProcessException e) { return null; @@ -265,11 +264,11 @@ public class PyPackageRequirementsInspection extends PyInspection { } private static void setRunningPackagingTasks(@NotNull Module module, boolean value) { - module.putUserData(PyPackageManagerImpl.RUNNING_PACKAGING_TASKS, value); + module.putUserData(PyPackageManager.RUNNING_PACKAGING_TASKS, value); } private static boolean isRunningPackagingTasks(@NotNull Module module) { - final Boolean value = module.getUserData(PyPackageManagerImpl.RUNNING_PACKAGING_TASKS); + final Boolean value = module.getUserData(PyPackageManager.RUNNING_PACKAGING_TASKS); return value != null && value; } @@ -302,6 +301,21 @@ public class PyPackageRequirementsInspection extends PyInspection { @Override public void applyFix(@NotNull final Project project, @NotNull ProblemDescriptor descriptor) { + boolean installManagement = false; + final PyPackageManager manager = PyPackageManager.getInstance(mySdk); + if (!manager.hasManagement(false)) { + final int result = Messages.showYesNoDialog(project, + "Python packaging tools are required for installing packages. Do you want to " + + "install 'pip' and 'setuptools' for your interpreter?", + "Install Python Packaging Tools", + Messages.getQuestionIcon()); + if (result == Messages.YES) { + installManagement = true; + } + else { + return; + } + } final List<PyRequirement> chosen; if (myUnsatisfied.size() > 1) { final PyChooseRequirementsDialog dialog = new PyChooseRequirementsDialog(project, myUnsatisfied); @@ -313,21 +327,48 @@ public class PyPackageRequirementsInspection extends PyInspection { if (chosen.isEmpty()) { return; } - final PyPackageManagerImpl.UI ui = new PyPackageManagerImpl.UI(project, mySdk, new PyPackageManagerImpl.UI.Listener() { - @Override - public void started() { - setRunningPackagingTasks(myModule, true); - } + if (installManagement) { + final PyPackageManagerUI ui = new PyPackageManagerUI(project, mySdk, new UIListener(myModule) { + @Override + public void finished(List<PyExternalProcessException> exceptions) { + super.finished(exceptions); + if (exceptions.isEmpty()) { + installRequirements(project, chosen); + } + } + }); + ui.installManagement(); + } + else { + installRequirements(project, chosen); + } + } - @Override - public void finished(List<PyExternalProcessException> exceptions) { - setRunningPackagingTasks(myModule, false); - } - }); - ui.install(chosen, Collections.<String>emptyList()); + private void installRequirements(Project project, List<PyRequirement> requirements) { + final PyPackageManagerUI ui = new PyPackageManagerUI(project, mySdk, new UIListener(myModule)); + ui.install(requirements, Collections.<String>emptyList()); + } + } + + private static class UIListener implements PyPackageManagerUI.Listener { + private final Module myModule; + + public UIListener(Module module) { + myModule = module; + } + + @Override + public void started() { + setRunningPackagingTasks(myModule, true); + } + + @Override + public void finished(List<PyExternalProcessException> exceptions) { + setRunningPackagingTasks(myModule, false); } } + private static class IgnoreRequirementFix implements LocalQuickFix { @NotNull private final Set<String> myPackageNames; diff --git a/python/src/com/jetbrains/python/inspections/quickfix/GenerateBinaryStubsFix.java b/python/src/com/jetbrains/python/inspections/quickfix/GenerateBinaryStubsFix.java index 7d087bd3eb08..17600d32befb 100644 --- a/python/src/com/jetbrains/python/inspections/quickfix/GenerateBinaryStubsFix.java +++ b/python/src/com/jetbrains/python/inspections/quickfix/GenerateBinaryStubsFix.java @@ -170,7 +170,7 @@ public class GenerateBinaryStubsFix implements LocalQuickFix { new String[]{ homePath, PythonHelpersLocator.getHelperPath("extra_syspath.py"), myQualifiedName}, - PythonSdkType.getVirtualEnvAdditionalEnv(homePath), 5000 + PythonSdkType.getVirtualEnvExtraEnv(homePath), 5000 ); if (runResult.getExitCode() == 0 && !runResult.isTimeout()) { final String extraPath = runResult.getStdout(); diff --git a/python/src/com/jetbrains/python/inspections/quickfix/PyDefaultArgumentQuickFix.java b/python/src/com/jetbrains/python/inspections/quickfix/PyDefaultArgumentQuickFix.java index db47f17f8ea8..6e0d5c465d0e 100644 --- a/python/src/com/jetbrains/python/inspections/quickfix/PyDefaultArgumentQuickFix.java +++ b/python/src/com/jetbrains/python/inspections/quickfix/PyDefaultArgumentQuickFix.java @@ -61,7 +61,7 @@ public class PyDefaultArgumentQuickFix implements LocalQuickFix { PyStatementList list = function.getStatementList(); PyParameterList paramList = function.getParameterList(); - final StringBuilder functionText = new StringBuilder("def foo("); + final StringBuilder functionText = new StringBuilder("def " + function.getName() + "("); int size = paramList.getParameters().length; for (int i = 0; i != size; ++i) { PyParameter p = paramList.getParameters()[i]; diff --git a/python/src/com/jetbrains/python/inspections/quickfix/StatementEffectFunctionCallQuickFix.java b/python/src/com/jetbrains/python/inspections/quickfix/StatementEffectFunctionCallQuickFix.java index 43f4f0394015..017bf1a1af29 100644 --- a/python/src/com/jetbrains/python/inspections/quickfix/StatementEffectFunctionCallQuickFix.java +++ b/python/src/com/jetbrains/python/inspections/quickfix/StatementEffectFunctionCallQuickFix.java @@ -88,7 +88,13 @@ public class StatementEffectFunctionCallQuickFix implements LocalQuickFix { if (next instanceof PyExpressionStatement) { final PyExpression expr = ((PyExpressionStatement)next).getExpression(); if (expr instanceof PyBinaryExpression) { - addInArguments(stringBuilder, (PyBinaryExpression)expr); + final PsiElement operator = ((PyBinaryExpression)expr).getPsiOperator(); + if (operator instanceof LeafPsiElement && ((LeafPsiElement)operator).getElementType() == PyTokenTypes.IN_KEYWORD) { + addInArguments(stringBuilder, (PyBinaryExpression)expr); + } + else { + stringBuilder.append(next.getText()); + } } else if (expr instanceof PyTupleExpression) { final PyExpression[] elements = ((PyTupleExpression)expr).getElements(); @@ -114,14 +120,11 @@ public class StatementEffectFunctionCallQuickFix implements LocalQuickFix { } private static void addInArguments(@NotNull final StringBuilder stringBuilder, @NotNull final PyBinaryExpression binaryExpression) { - final PsiElement operator = binaryExpression.getPsiOperator(); - if (operator instanceof LeafPsiElement && ((LeafPsiElement)operator).getElementType() == PyTokenTypes.IN_KEYWORD) { - stringBuilder.append(binaryExpression.getLeftExpression().getText()); - stringBuilder.append(", "); - final PyExpression rightExpression = binaryExpression.getRightExpression(); - if (rightExpression != null) - stringBuilder.append(rightExpression.getText()); - } + stringBuilder.append(binaryExpression.getLeftExpression().getText()); + stringBuilder.append(", "); + final PyExpression rightExpression = binaryExpression.getRightExpression(); + if (rightExpression != null) + stringBuilder.append(rightExpression.getText()); } private static void replacePrint(@NotNull final PsiElement expression) { diff --git a/python/src/com/jetbrains/python/inspections/unresolvedReference/PyUnresolvedReferencesInspection.java b/python/src/com/jetbrains/python/inspections/unresolvedReference/PyUnresolvedReferencesInspection.java index b7523caf90e7..8049546a6e92 100644 --- a/python/src/com/jetbrains/python/inspections/unresolvedReference/PyUnresolvedReferencesInspection.java +++ b/python/src/com/jetbrains/python/inspections/unresolvedReference/PyUnresolvedReferencesInspection.java @@ -48,7 +48,6 @@ import com.jetbrains.python.documentation.DocStringTypeReference; import com.jetbrains.python.inspections.*; import com.jetbrains.python.inspections.quickfix.*; import com.jetbrains.python.packaging.PyPIPackageUtil; -import com.jetbrains.python.packaging.PyPackageManager; import com.jetbrains.python.packaging.PyRequirement; import com.jetbrains.python.psi.*; import com.jetbrains.python.psi.impl.PyBuiltinCache; @@ -598,9 +597,7 @@ public class PyUnresolvedReferencesInspection extends PyInspection { if (PyPIPackageUtil.INSTANCE.isInPyPI(packageName)) { final List<PyRequirement> requirements = Collections.singletonList(new PyRequirement(packageName)); final String name = "Install package " + packageName; - if (PyPackageManager.getInstance(sdk).hasPip()) { - actions.add(new PyPackageRequirementsInspection.PyInstallRequirementsFix(name, module, sdk, requirements)); - } + actions.add(new PyPackageRequirementsInspection.PyInstallRequirementsFix(name, module, sdk, requirements)); } } } diff --git a/python/src/com/jetbrains/python/packaging/PyPackageManagerImpl.java b/python/src/com/jetbrains/python/packaging/PyPackageManagerImpl.java index 65d95eb62713..9b5f669168f7 100644 --- a/python/src/com/jetbrains/python/packaging/PyPackageManagerImpl.java +++ b/python/src/com/jetbrains/python/packaging/PyPackageManagerImpl.java @@ -15,31 +15,17 @@ */ package com.jetbrains.python.packaging; -import com.google.common.collect.Collections2; import com.google.common.collect.Lists; import com.intellij.execution.ExecutionException; import com.intellij.execution.process.ProcessOutput; import com.intellij.execution.util.ExecUtil; -import com.intellij.icons.AllIcons; -import com.intellij.notification.Notification; -import com.intellij.notification.NotificationListener; -import com.intellij.notification.NotificationType; import com.intellij.openapi.application.Application; import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.module.Module; -import com.intellij.openapi.progress.ProgressIndicator; -import com.intellij.openapi.progress.ProgressManager; -import com.intellij.openapi.progress.Task; -import com.intellij.openapi.project.Project; import com.intellij.openapi.projectRoots.Sdk; -import com.intellij.openapi.projectRoots.SdkAdditionalData; import com.intellij.openapi.projectRoots.impl.ProjectJdkImpl; import com.intellij.openapi.roots.OrderRootType; -import com.intellij.openapi.ui.Messages; -import com.intellij.openapi.util.Key; -import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.SystemInfo; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.text.StringUtil; @@ -49,46 +35,36 @@ import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFileManager; import com.intellij.openapi.vfs.newvfs.BulkFileListener; import com.intellij.openapi.vfs.newvfs.events.VFileEvent; -import com.intellij.remote.RemoteFile; -import com.intellij.remote.RemoteSdkAdditionalData; -import com.intellij.remote.RemoteSdkCredentials; -import com.intellij.remote.VagrantNotStartedException; import com.intellij.util.ArrayUtil; -import com.intellij.util.Function; -import com.intellij.util.PathMappingSettings; -import com.intellij.util.SystemProperties; import com.intellij.util.containers.HashSet; import com.intellij.util.messages.MessageBusConnection; import com.intellij.util.net.HttpConfigurable; -import com.intellij.webcore.packaging.PackagesNotificationPanel; import com.jetbrains.python.PythonHelpersLocator; import com.jetbrains.python.psi.LanguageLevel; import com.jetbrains.python.psi.PyExpression; import com.jetbrains.python.psi.PyListLiteralExpression; import com.jetbrains.python.psi.PyStringLiteralExpression; -import com.jetbrains.python.remote.PyRemoteSdkAdditionalDataBase; -import com.jetbrains.python.remote.PythonRemoteInterpreterManager; import com.jetbrains.python.sdk.PySdkUtil; import com.jetbrains.python.sdk.PythonSdkType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import javax.swing.event.HyperlinkEvent; -import java.awt.*; import java.io.File; import java.io.IOException; import java.util.*; -import java.util.List; /** * @author vlan */ -@SuppressWarnings({"UnusedDeclaration", "FieldAccessedSynchronizedAndUnsynchronized"}) public class PyPackageManagerImpl extends PyPackageManager { - private static final Logger LOG = Logger.getInstance(PyPackageManagerImpl.class); + // Bundled versions of package management tools + public static final String SETUPTOOLS_VERSION = "1.1.5"; + public static final String PIP_VERSION = "1.4.1"; + + public static final String SETUPTOOLS = PACKAGE_SETUPTOOLS + "-" + SETUPTOOLS_VERSION; + public static final String PIP = PACKAGE_PIP + "-" + PIP_VERSION; public static final int OK = 0; - public static final int ERROR_WRONG_USAGE = 1; public static final int ERROR_NO_PIP = 2; public static final int ERROR_NO_SETUPTOOLS = 3; public static final int ERROR_INVALID_SDK = -1; @@ -97,260 +73,24 @@ public class PyPackageManagerImpl extends PyPackageManager { public static final int ERROR_INVALID_OUTPUT = -4; public static final int ERROR_ACCESS_DENIED = -5; public static final int ERROR_EXECUTION = -6; - public static final int ERROR_INTERRUPTED = -7; - public static final int ERROR_VAGRANT_NOT_LAUNCHED = 101; - public static final int ERROR_REMOTE_ACCESS = 102; - - public static final String PACKAGE_PIP = "pip"; - public static final String PACKAGE_DISTRIBUTE = "distribute"; - public static final String PACKAGE_SETUPTOOLS = "setuptools"; - - public static final Key<Boolean> RUNNING_PACKAGING_TASKS = Key.create("PyPackageRequirementsInspection.RunningPackagingTasks"); + private static final Logger LOG = Logger.getInstance(PyPackageManagerImpl.class); private static final String PACKAGING_TOOL = "packaging_tool.py"; private static final String VIRTUALENV = "virtualenv.py"; private static final int TIMEOUT = 10 * 60 * 1000; private static final String BUILD_DIR_OPTION = "--build-dir"; - public static final String USE_USER_SITE = "--user"; public static final String INSTALL = "install"; public static final String UNINSTALL = "uninstall"; public static final String UNTAR = "untar"; - // Bundled versions of package management tools - public static final String SETUPTOOLS_VERSION = "1.1.5"; - public static final String PIP_VERSION = "1.4.1"; - - public static final String SETUPTOOLS = PACKAGE_SETUPTOOLS + "-" + SETUPTOOLS_VERSION; - public static final String PIP = PACKAGE_PIP + "-" + PIP_VERSION; - private static final String LAUNCH_VAGRANT = "launchVagrant"; - private List<PyPackage> myPackagesCache = null; private Map<String, Set<PyPackage>> myDependenciesCache = null; private PyExternalProcessException myExceptionCache = null; - private Sdk mySdk; - - public static class UI { - @Nullable private Listener myListener; - @NotNull private Project myProject; - @NotNull private Sdk mySdk; - - public interface Listener { - void started(); - - void finished(List<PyExternalProcessException> exceptions); - } - - public UI(@NotNull Project project, @NotNull Sdk sdk, @Nullable Listener listener) { - myProject = project; - mySdk = sdk; - myListener = listener; - } - - public void installManagement(@NotNull final String name) { - final String progressTitle; - final String successTitle; - progressTitle = "Installing package " + name; - successTitle = "Packages installed successfully"; - run(new MultiExternalRunnable() { - @Override - public List<PyExternalProcessException> run(@NotNull ProgressIndicator indicator) { - final List<PyExternalProcessException> exceptions = new ArrayList<PyExternalProcessException>(); - indicator.setText(String.format("Installing package '%s'...", name)); - final PyPackageManagerImpl manager = (PyPackageManagerImpl)PyPackageManagers.getInstance().forSdk(mySdk); - try { - manager.installManagement(name); - } - catch (PyExternalProcessException e) { - exceptions.add(e); - } - return exceptions; - } - }, progressTitle, successTitle, "Installed package " + name, - "Install package failed" - ); - } - - public void install(@NotNull final List<PyRequirement> requirements, @NotNull final List<String> extraArgs) { - final String progressTitle; - final String successTitle; - progressTitle = "Installing packages"; - successTitle = "Packages installed successfully"; - run(new MultiExternalRunnable() { - @Override - public List<PyExternalProcessException> run(@NotNull ProgressIndicator indicator) { - final int size = requirements.size(); - final List<PyExternalProcessException> exceptions = new ArrayList<PyExternalProcessException>(); - final PyPackageManagerImpl manager = (PyPackageManagerImpl)PyPackageManagers.getInstance().forSdk(mySdk); - for (int i = 0; i < size; i++) { - final PyRequirement requirement = requirements.get(i); - if (myListener != null) { - indicator.setText(String.format("Installing package '%s'...", requirement)); - indicator.setFraction((double)i / size); - } - try { - manager.install(list(requirement), extraArgs); - } - catch (PyExternalProcessException e) { - exceptions.add(e); - } - } - manager.refresh(); - return exceptions; - } - }, progressTitle, successTitle, "Installed packages: " + PyPackageUtil.requirementsToString(requirements), - "Install packages failed" - ); - } - - public void uninstall(@NotNull final List<PyPackage> packages) { - final String packagesString = StringUtil.join(packages, new Function<PyPackage, String>() { - @Override - public String fun(PyPackage pkg) { - return "'" + pkg.getName() + "'"; - } - }, ", "); - if (checkDependents(packages)) return; - - run(new MultiExternalRunnable() { - @Override - public List<PyExternalProcessException> run(@NotNull ProgressIndicator indicator) { - final PyPackageManagerImpl manager = (PyPackageManagerImpl)PyPackageManagers.getInstance().forSdk(mySdk); - try { - manager.uninstall(packages); - return list(); - } - catch (PyExternalProcessException e) { - return list(e); - } - finally { - manager.refresh(); - } - } - }, "Uninstalling packages", "Packages uninstalled successfully", "Uninstalled packages: " + packagesString, - "Uninstall packages failed" - ); - } - - private boolean checkDependents(@NotNull final List<PyPackage> packages) { - try { - final Map<String, Set<PyPackage>> dependentPackages = collectDependents(packages, mySdk); - final int[] warning = {0}; - if (!dependentPackages.isEmpty()) { - ApplicationManager.getApplication().invokeAndWait(new Runnable() { - @Override - public void run() { - if (dependentPackages.size() == 1) { - String message = "You are attempting to uninstall "; - List<String> dep = new ArrayList<String>(); - int size = 1; - for (Map.Entry<String, Set<PyPackage>> entry : dependentPackages.entrySet()) { - final Set<PyPackage> value = entry.getValue(); - size = value.size(); - dep.add(entry.getKey() + " package which is required for " + StringUtil.join(value, ", ")); - } - message += StringUtil.join(dep, "\n"); - message += size == 1 ? " package" : " packages"; - message += "\n\nDo you want to proceed?"; - warning[0] = Messages.showYesNoDialog(message, "Warning", - AllIcons.General.BalloonWarning); - } - else { - String message = "You are attempting to uninstall packages which are required for another packages.\n\n"; - List<String> dep = new ArrayList<String>(); - for (Map.Entry<String, Set<PyPackage>> entry : dependentPackages.entrySet()) { - dep.add(entry.getKey() + " -> " + StringUtil.join(entry.getValue(), ", ")); - } - message += StringUtil.join(dep, "\n"); - message += "\n\nDo you want to proceed?"; - warning[0] = Messages.showYesNoDialog(message, "Warning", - AllIcons.General.BalloonWarning); - } - } - }, ModalityState.current()); - } - if (warning[0] != Messages.YES) return true; - } - catch (PyExternalProcessException e) { - LOG.info("Error loading packages dependents: " + e.getMessage(), e); - } - return false; - } - - private interface MultiExternalRunnable { - List<PyExternalProcessException> run(@NotNull ProgressIndicator indicator); - } - - private void run(@NotNull final MultiExternalRunnable runnable, @NotNull final String progressTitle, - @NotNull final String successTitle, @NotNull final String successDescription, @NotNull final String failureTitle) { - ProgressManager.getInstance().run(new Task.Backgroundable(myProject, progressTitle, false) { - @Override - public void run(@NotNull ProgressIndicator indicator) { - indicator.setText(progressTitle + "..."); - final Ref<Notification> notificationRef = new Ref<Notification>(null); - final String PACKAGING_GROUP_ID = "Packaging"; - final Application application = ApplicationManager.getApplication(); - if (myListener != null) { - application.invokeLater(new Runnable() { - @Override - public void run() { - myListener.started(); - } - }); - } - - final List<PyExternalProcessException> exceptions = runnable.run(indicator); - if (exceptions.isEmpty()) { - notificationRef.set(new Notification(PACKAGING_GROUP_ID, successTitle, successDescription, NotificationType.INFORMATION)); - } - else { - final String progressLower = progressTitle.toLowerCase(); - final String firstLine = String.format("Error%s occurred when %s.", exceptions.size() > 1 ? "s" : "", progressLower); - - final String description = createDescription(exceptions, firstLine); - notificationRef.set(new Notification(PACKAGING_GROUP_ID, failureTitle, - firstLine + " <a href=\"xxx\">Details...</a>", - NotificationType.ERROR, - new NotificationListener() { - @Override - public void hyperlinkUpdate(@NotNull Notification notification, - @NotNull HyperlinkEvent event) { - assert myProject != null; - PackagesNotificationPanel.showError(myProject, failureTitle, description); - } - } - )); - } - application.invokeLater(new Runnable() { - @Override - public void run() { - if (myListener != null) { - myListener.finished(exceptions); - } - final Notification notification = notificationRef.get(); - if (notification != null) { - notification.notify(myProject); - } - } - }); - } - }); - } - - public static String createDescription(List<PyExternalProcessException> exceptions, String firstLine) { - final StringBuilder b = new StringBuilder(); - b.append(firstLine); - b.append("\n\n"); - for (PyExternalProcessException exception : exceptions) { - b.append(exception.toString()); - b.append("\n"); - } - return b.toString(); - } - } + protected Sdk mySdk; @Override public void refresh() { @@ -373,7 +113,23 @@ public class PyPackageManagerImpl extends PyPackageManager { }); } - private void installManagement(String name) throws PyExternalProcessException { + @Override + public void installManagement() throws PyExternalProcessException { + if (!hasPackage(PACKAGE_SETUPTOOLS, false) && !hasPackage(PACKAGE_DISTRIBUTE, false)) { + installManagement(SETUPTOOLS); + } + if (!hasPackage(PACKAGE_PIP, false)) { + installManagement(PIP); + } + } + + @Override + public boolean hasManagement(boolean cachedOnly) { + return (hasPackage(PACKAGE_SETUPTOOLS, cachedOnly) || hasPackage(PACKAGE_DISTRIBUTE, cachedOnly)) && + hasPackage(PACKAGE_PIP, cachedOnly); + } + + protected void installManagement(@NotNull String name) throws PyExternalProcessException { final String helperPath = getHelperPath(name); ArrayList<String> args = Lists.newArrayList(UNTAR, helperPath); @@ -390,7 +146,7 @@ public class PyPackageManagerImpl extends PyPackageManager { } final String fileName = dirName + name + File.separatorChar + "setup.py"; try { - output = getProcessOutput(fileName, Collections.<String>singletonList(INSTALL), true, dirName + name); + output = getProcessOutput(fileName, Collections.singletonList(INSTALL), true, dirName + name); final int retcode = output.getExitCode(); if (output.isTimeout()) { throw new PyExternalProcessException(ERROR_TIMEOUT, fileName, Lists.newArrayList(INSTALL), "Timed out"); @@ -406,17 +162,28 @@ public class PyPackageManagerImpl extends PyPackageManager { } finally { clearCaches(); - FileUtil.delete(new File(dirName)); //TODO: remove temp directory for remote interpreter + FileUtil.delete(new File(dirName)); + } + } + + private boolean hasPackage(@NotNull String name, boolean cachedOnly) { + try { + return findPackage(name, cachedOnly) != null; + } + catch (PyExternalProcessException ignored) { + return false; } } PyPackageManagerImpl(@NotNull Sdk sdk) { mySdk = sdk; + subscribeToLocalChanges(sdk); + } + + protected void subscribeToLocalChanges(Sdk sdk) { final Application app = ApplicationManager.getApplication(); final MessageBusConnection connection = app.getMessageBus().connect(); - if (!PySdkUtil.isRemote(sdk)) { - connection.subscribe(VirtualFileManager.VFS_CHANGES, new MySdkRootWatcher()); - } + connection.subscribe(VirtualFileManager.VFS_CHANGES, new MySdkRootWatcher()); } public Sdk getSdk() { @@ -424,27 +191,13 @@ public class PyPackageManagerImpl extends PyPackageManager { } @Override - public void install(String requirementString) throws PyExternalProcessException { - boolean hasSetuptools = false; - boolean hasPip = false; - try { - hasSetuptools = findInstalledPackage(SETUPTOOLS) != null; - } - catch (PyExternalProcessException ignored) { - } - try { - hasPip = findInstalledPackage(PIP) != null; - } - catch (PyExternalProcessException ignored) { - } - - if (!hasSetuptools) installManagement(SETUPTOOLS); - if (!hasPip) installManagement(PIP); + public void install(@NotNull String requirementString) throws PyExternalProcessException { + installManagement(); install(Collections.singletonList(PyRequirement.fromString(requirementString)), Collections.<String>emptyList()); } - public void install(@NotNull List<PyRequirement> requirements, @NotNull List<String> extraArgs) - throws PyExternalProcessException { + @Override + public void install(@NotNull List<PyRequirement> requirements, @NotNull List<String> extraArgs) throws PyExternalProcessException { final List<String> args = new ArrayList<String>(); args.add(INSTALL); final File buildDir; @@ -455,7 +208,7 @@ public class PyPackageManagerImpl extends PyPackageManager { throw new PyExternalProcessException(ERROR_ACCESS_DENIED, PACKAGING_TOOL, args, "Cannot create temporary build directory"); } if (!extraArgs.contains(BUILD_DIR_OPTION)) { - args.addAll(list(BUILD_DIR_OPTION, buildDir.getAbsolutePath())); + args.addAll(Arrays.asList(BUILD_DIR_OPTION, buildDir.getAbsolutePath())); } boolean useUserSite = extraArgs.contains(USE_USER_SITE); @@ -499,69 +252,22 @@ public class PyPackageManagerImpl extends PyPackageManager { } } - private static Map<String, Set<PyPackage>> collectDependents(@NotNull final List<PyPackage> packages, Sdk sdk) - throws PyExternalProcessException { - Map<String, Set<PyPackage>> dependentPackages = new HashMap<String, Set<PyPackage>>(); - for (PyPackage pkg : packages) { - final Set<PyPackage> dependents = - ((PyPackageManagerImpl)PyPackageManager.getInstance(sdk)).getDependents(pkg.getName()); - if (dependents != null && !dependents.isEmpty()) { - for (PyPackage dependent : dependents) { - if (!packages.contains(dependent)) { - dependentPackages.put(pkg.getName(), dependents); - } - } - } - } - return dependentPackages; - } - - public static String getUserSite() { - if (SystemInfo.isWindows) { - final String appdata = System.getenv("APPDATA"); - return appdata + File.separator + "Python"; - } - else { - final String userHome = SystemProperties.getUserHome(); - return userHome + File.separator + ".local"; - } - } - - - public boolean cacheIsNotNull() { - return myPackagesCache != null; - } - - /** - * Returns the list of packages for the SDK without initiating a remote connection. Returns null - * for a remote interpreter if the list of packages was not loaded. - * - * @return the list of packages or null - */ @Nullable - public synchronized List<PyPackage> getPackagesFast() throws PyExternalProcessException { - if (myPackagesCache != null) { - return myPackagesCache; - } - if (PySdkUtil.isRemote(mySdk)) { - return null; - } - return getPackages(); - } - - @NotNull - public synchronized List<PyPackage> getPackages() throws PyExternalProcessException { + public synchronized List<PyPackage> getPackages(boolean cachedOnly) throws PyExternalProcessException { if (myPackagesCache == null) { if (myExceptionCache != null) { throw myExceptionCache; } - + if (cachedOnly) { + return null; + } loadPackages(); } return myPackagesCache; } - public synchronized Set<PyPackage> getDependents(String pkg) throws PyExternalProcessException { + @Nullable + public synchronized Set<PyPackage> getDependents(@NotNull PyPackage pkg) throws PyExternalProcessException { if (myDependenciesCache == null) { if (myExceptionCache != null) { throw myExceptionCache; @@ -569,12 +275,12 @@ public class PyPackageManagerImpl extends PyPackageManager { loadPackages(); } - return myDependenciesCache.get(pkg); + return myDependenciesCache.get(pkg.getName()); } public synchronized void loadPackages() throws PyExternalProcessException { try { - final String output = runPythonHelper(PACKAGING_TOOL, list("list")); + final String output = runPythonHelper(PACKAGING_TOOL, Arrays.asList("list")); myPackagesCache = parsePackagingToolOutput(output); Collections.sort(myPackagesCache, new Comparator<PyPackage>() { @Override @@ -592,7 +298,7 @@ public class PyPackageManagerImpl extends PyPackageManager { } } - private void calculateDependents() { + private synchronized void calculateDependents() { myDependenciesCache = new HashMap<String, Set<PyPackage>>(); for (PyPackage p : myPackagesCache) { final List<PyRequirement> requirements = p.getRequirements(); @@ -608,47 +314,18 @@ public class PyPackageManagerImpl extends PyPackageManager { @Override @Nullable - public PyPackage findInstalledPackage(String name) throws PyExternalProcessException { - return findPackageByName(name, getPackages()); - } - - @Override - public boolean findPackage(@NotNull final String name) { - try { - final String output = runPythonHelper(PACKAGING_TOOL, list("search", name)); - return StringUtil.containsIgnoreCase(output, name + " "); - } - catch (PyExternalProcessException e) { - LOG.error(e.getMessage()); - return false; - } - } - - @Nullable - public PyPackage findPackageFast(String name) throws PyExternalProcessException { - final List<PyPackage> packages = getPackagesFast(); - return packages != null ? findPackageByName(name, packages) : null; - } - - @Nullable - private static PyPackage findPackageByName(String name, List<PyPackage> packages) { - for (PyPackage pkg : packages) { - if (name.equalsIgnoreCase(pkg.getName())) { - return pkg; + public PyPackage findPackage(@NotNull String name, boolean cachedOnly) throws PyExternalProcessException { + final List<PyPackage> packages = getPackages(cachedOnly); + if (packages != null) { + for (PyPackage pkg : packages) { + if (name.equalsIgnoreCase(pkg.getName())) { + return pkg; + } } } return null; } - public boolean hasPip() { - try { - return findPackageFast(PACKAGE_PIP) != null; - } - catch (PyExternalProcessException e) { - return false; - } - } - @NotNull public String createVirtualEnv(@NotNull String destinationDir, boolean useGlobalSite) throws PyExternalProcessException { final List<String> args = new ArrayList<String>(); @@ -674,30 +351,21 @@ public class PyPackageManagerImpl extends PyPackageManager { final String path = (binary != null) ? binary : binaryFallback; if (usePyVenv) { - // TODO: Still no 'packaging' and 'pysetup3' for Python 3.3rc1, see PEP 405 + // Still no 'packaging' and 'pysetup3' for Python 3.3rc1, see PEP 405 final VirtualFile binaryFile = LocalFileSystem.getInstance().refreshAndFindFileByPath(path); if (binaryFile != null) { final ProjectJdkImpl tmpSdk = new ProjectJdkImpl("", PythonSdkType.getInstance()); tmpSdk.setHomePath(path); - final PyPackageManagerImpl manager = new PyPackageManagerImpl(tmpSdk); - manager.installManagement(SETUPTOOLS); - manager.installManagement(PIP); + final PyPackageManager manager = PyPackageManager.getInstance(tmpSdk); + manager.installManagement(); } } return path; } - public static void deleteVirtualEnv(@NotNull String sdkHome) throws PyExternalProcessException { - final File root = PythonSdkType.getVirtualEnvRoot(sdkHome); - if (root != null) { - FileUtil.delete(root); - } - } - @Nullable - public static List<PyRequirement> getRequirements(@NotNull Module module) { - // TODO: Cache requirements, clear cache on requirements.txt or setup.py updates - List<PyRequirement> requirements = getRequirementsFromTxt(module); + public List<PyRequirement> getRequirements(@NotNull Module module) { + List<PyRequirement> requirements = PySdkUtil.getRequirementsFromTxt(module); if (requirements != null) { return requirements; } @@ -721,25 +389,12 @@ public class PyPackageManagerImpl extends PyPackageManager { return null; } - @Nullable - public static List<PyRequirement> getRequirementsFromTxt(Module module) { - final VirtualFile requirementsTxt = PyPackageUtil.findRequirementsTxt(module); - if (requirementsTxt != null) { - return PyRequirement.parse(requirementsTxt); - } - return null; - } - - private void clearCaches() { + protected synchronized void clearCaches() { myPackagesCache = null; myDependenciesCache = null; myExceptionCache = null; } - private static <T> List<T> list(T... xs) { - return Arrays.asList(xs); - } - @Nullable private static String getProxyString() { final HttpConfigurable settings = HttpConfigurable.getInstance(); @@ -792,164 +447,61 @@ public class PyPackageManagerImpl extends PyPackageManager { } @Nullable - private String getHelperPath(String helper) { - String helperPath; - final SdkAdditionalData sdkData = mySdk.getSdkAdditionalData(); - if (sdkData instanceof PyRemoteSdkAdditionalDataBase) { - PyRemoteSdkAdditionalDataBase remoteSdkData = (PyRemoteSdkAdditionalDataBase)mySdk.getSdkAdditionalData(); - - try { - final RemoteSdkCredentials remoteSdkCredentials = remoteSdkData.getRemoteSdkCredentials(false); - if (!StringUtil.isEmpty(remoteSdkCredentials.getHelpersPath())) { - helperPath = new RemoteFile(remoteSdkCredentials.getHelpersPath(), - helper).getPath(); - } - else { - helperPath = null; - } - } - catch (Exception e) { - helperPath = null; - LOG.error(e); - } - } - else { - helperPath = PythonHelpersLocator.getHelperPath(helper); - } - return helperPath; + protected String getHelperPath(String helper) { + return PythonHelpersLocator.getHelperPath(helper); } - private ProcessOutput getProcessOutput(@NotNull String helperPath, + protected ProcessOutput getProcessOutput(@NotNull String helperPath, @NotNull List<String> args, boolean askForSudo, - @Nullable String workingDir) - throws PyExternalProcessException { - final SdkAdditionalData sdkData = mySdk.getSdkAdditionalData(); + @Nullable String workingDir) throws PyExternalProcessException { final String homePath = mySdk.getHomePath(); if (homePath == null) { throw new PyExternalProcessException(ERROR_INVALID_SDK, helperPath, args, "Cannot find interpreter for SDK"); } - if (sdkData instanceof PyRemoteSdkAdditionalDataBase) { //remote interpreter - RemoteSdkCredentials remoteSdkCredentials; + if (workingDir == null) { + workingDir = new File(homePath).getParent(); + } + final List<String> cmdline = new ArrayList<String>(); + cmdline.add(homePath); + cmdline.add(helperPath); + cmdline.addAll(args); + LOG.info("Running packaging tool: " + StringUtil.join(cmdline, " ")); + + final boolean canCreate = FileUtil.ensureCanCreateFile(new File(homePath)); + if (!canCreate && !SystemInfo.isWindows && askForSudo) { //is system site interpreter --> we need sudo privileges try { - remoteSdkCredentials = ((RemoteSdkAdditionalData)sdkData).getRemoteSdkCredentials(false); - } - catch (InterruptedException e) { - LOG.error(e); - remoteSdkCredentials = null; - } - catch (final ExecutionException e) { - if (e.getCause() instanceof VagrantNotStartedException) { - throw new PyExternalProcessException(ERROR_VAGRANT_NOT_LAUNCHED, helperPath, args, "Vagrant instance is down. <a href=\"" + - LAUNCH_VAGRANT + - "\">Launch vagrant</a>") - .withHandler(LAUNCH_VAGRANT, new Runnable() { - @Override - public void run() { - final PythonRemoteInterpreterManager manager = PythonRemoteInterpreterManager.getInstance(); - if (manager != null) { - - try { - manager.runVagrant(((VagrantNotStartedException)e.getCause()).getVagrantFolder()); - clearCaches(); - } - catch (ExecutionException e1) { - throw new RuntimeException(e1); - } - } - } - }); - } - else { - throw new PyExternalProcessException(ERROR_REMOTE_ACCESS, helperPath, args, e.getMessage()); - } - } - final PythonRemoteInterpreterManager manager = PythonRemoteInterpreterManager.getInstance(); - if (manager != null && remoteSdkCredentials != null) { - final List<String> cmdline = new ArrayList<String>(); - cmdline.add(homePath); - cmdline.add(RemoteFile.detectSystemByPath(homePath).createRemoteFile(helperPath).getPath()); - cmdline.addAll(Collections2.transform(args, new com.google.common.base.Function<String, String>() { - @Override - public String apply(@Nullable String input) { - return quoteIfNeeded(input); + final ProcessOutput result = ExecUtil.sudoAndGetOutput(cmdline, + "Please enter your password to make changes in system packages: ", + workingDir); + String message = result.getStderr(); + if (result.getExitCode() != 0) { + final String stdout = result.getStdout(); + if (StringUtil.isEmptyOrSpaces(message)) { + message = stdout; } - })); - try { - if (askForSudo) { - askForSudo = !manager.ensureCanWrite(null, remoteSdkCredentials, remoteSdkCredentials.getInterpreterPath()); + if (StringUtil.isEmptyOrSpaces(message)) { + message = "Failed to perform action. Permission denied."; } - ProcessOutput processOutput; - do { - PathMappingSettings mappings = manager.setupMappings(null, (PyRemoteSdkAdditionalDataBase)sdkData, null); - processOutput = - manager.runRemoteProcess(null, remoteSdkCredentials, mappings, ArrayUtil.toStringArray(cmdline), workingDir, askForSudo); - if (askForSudo && processOutput.getStderr().contains("sudo: 3 incorrect password attempts")) { - continue; - } - break; - } - while (true); - return processOutput; + throw new PyExternalProcessException(result.getExitCode(), helperPath, args, message); } - catch (ExecutionException e) { - throw new PyExternalProcessException(ERROR_INVALID_SDK, helperPath, args, "Error running SDK: " + e.getMessage(), e); + if (SystemInfo.isMac && !StringUtil.isEmptyOrSpaces(message)) { + throw new PyExternalProcessException(result.getExitCode(), helperPath, args, message); } + return result; } - else { - throw new PyExternalProcessException(ERROR_INVALID_SDK, helperPath, args, - PythonRemoteInterpreterManager.WEB_DEPLOYMENT_PLUGIN_IS_DISABLED); + catch (ExecutionException e) { + throw new PyExternalProcessException(ERROR_EXECUTION, helperPath, args, e.getMessage()); + } + catch (IOException e) { + throw new PyExternalProcessException(ERROR_ACCESS_DENIED, helperPath, args, e.getMessage()); } } else { - if (workingDir == null) { - workingDir = new File(homePath).getParent(); - } - final List<String> cmdline = new ArrayList<String>(); - cmdline.add(homePath); - cmdline.add(helperPath); - cmdline.addAll(args); - LOG.info("Running packaging tool: " + StringUtil.join(cmdline, " ")); - - final boolean canCreate = FileUtil.ensureCanCreateFile(new File(homePath)); - if (!canCreate && !SystemInfo.isWindows && askForSudo) { //is system site interpreter --> we need sudo privileges - try { - final ProcessOutput result = ExecUtil.sudoAndGetOutput(cmdline, - "Please enter your password to make changes in system packages: ", - workingDir); - String message = result.getStderr(); - if (result.getExitCode() != 0) { - final String stdout = result.getStdout(); - if (StringUtil.isEmptyOrSpaces(message)) { - message = stdout; - } - if (StringUtil.isEmptyOrSpaces(message)) { - message = "Failed to perform action. Permission denied."; - } - throw new PyExternalProcessException(result.getExitCode(), helperPath, args, message); - } - if (SystemInfo.isMac && !StringUtil.isEmptyOrSpaces(message)) { - throw new PyExternalProcessException(result.getExitCode(), helperPath, args, message); - } - return result; - } - catch (ExecutionException e) { - throw new PyExternalProcessException(ERROR_EXECUTION, helperPath, args, e.getMessage()); - } - catch (IOException e) { - throw new PyExternalProcessException(ERROR_ACCESS_DENIED, helperPath, args, e.getMessage()); - } - } - else { - return PySdkUtil.getProcessOutput(workingDir, ArrayUtil.toStringArray(cmdline), TIMEOUT); - } + return PySdkUtil.getProcessOutput(workingDir, ArrayUtil.toStringArray(cmdline), TIMEOUT); } } - private static String quoteIfNeeded(String arg) { - return arg.replace("<", "\\<").replace(">", "\\>"); //TODO: move this logic to ParametersListUtil.encode - } - @NotNull private static List<PyPackage> parsePackagingToolOutput(@NotNull String s) throws PyExternalProcessException { final String[] lines = StringUtil.splitByLines(s); @@ -976,17 +528,6 @@ public class PyPackageManagerImpl extends PyPackageManager { return packages; } - - @Override - public void showInstallationError(Project project, String title, String description) { - PackagesNotificationPanel.showError(project, title, description); - } - - @Override - public void showInstallationError(Component owner, String title, String description) { - PackagesNotificationPanel.showError(owner, title, description); - } - private class MySdkRootWatcher extends BulkFileListener.Adapter { @Override public void after(@NotNull List<? extends VFileEvent> events) { diff --git a/python/src/com/jetbrains/python/packaging/PyPackageManagerUI.java b/python/src/com/jetbrains/python/packaging/PyPackageManagerUI.java new file mode 100644 index 000000000000..28dfa6c834a4 --- /dev/null +++ b/python/src/com/jetbrains/python/packaging/PyPackageManagerUI.java @@ -0,0 +1,367 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jetbrains.python.packaging; + +import com.intellij.icons.AllIcons; +import com.intellij.notification.Notification; +import com.intellij.notification.NotificationListener; +import com.intellij.notification.NotificationType; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ModalityState; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.progress.ProgressManager; +import com.intellij.openapi.progress.Task; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.projectRoots.Sdk; +import com.intellij.openapi.ui.Messages; +import com.intellij.openapi.util.Ref; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.util.Function; +import com.intellij.webcore.packaging.PackagesNotificationPanel; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.event.HyperlinkEvent; +import java.util.*; + +/** +* @author vlan +*/ +public class PyPackageManagerUI { + private static final Logger LOG = Logger.getInstance(PyPackageManagerUI.class); + @Nullable private Listener myListener; + @NotNull private Project myProject; + @NotNull private Sdk mySdk; + + public interface Listener { + void started(); + + void finished(List<PyExternalProcessException> exceptions); + } + + public PyPackageManagerUI(@NotNull Project project, @NotNull Sdk sdk, @Nullable Listener listener) { + myProject = project; + mySdk = sdk; + myListener = listener; + } + + public void installManagement() { + ProgressManager.getInstance().run(new InstallManagementTask(myProject, mySdk, myListener)); + } + + public void install(@NotNull final List<PyRequirement> requirements, @NotNull final List<String> extraArgs) { + ProgressManager.getInstance().run(new InstallTask(myProject, mySdk, requirements, extraArgs, myListener)); + } + + public void uninstall(@NotNull final List<PyPackage> packages) { + if (checkDependents(packages)) { + return; + } + ProgressManager.getInstance().run(new UninstallTask(myProject, mySdk, myListener, packages)); + } + + private boolean checkDependents(@NotNull final List<PyPackage> packages) { + try { + final Map<String, Set<PyPackage>> dependentPackages = collectDependents(packages, mySdk); + final int[] warning = {0}; + if (!dependentPackages.isEmpty()) { + ApplicationManager.getApplication().invokeAndWait(new Runnable() { + @Override + public void run() { + if (dependentPackages.size() == 1) { + String message = "You are attempting to uninstall "; + List<String> dep = new ArrayList<String>(); + int size = 1; + for (Map.Entry<String, Set<PyPackage>> entry : dependentPackages.entrySet()) { + final Set<PyPackage> value = entry.getValue(); + size = value.size(); + dep.add(entry.getKey() + " package which is required for " + StringUtil.join(value, ", ")); + } + message += StringUtil.join(dep, "\n"); + message += size == 1 ? " package" : " packages"; + message += "\n\nDo you want to proceed?"; + warning[0] = Messages.showYesNoDialog(message, "Warning", + AllIcons.General.BalloonWarning); + } + else { + String message = "You are attempting to uninstall packages which are required for another packages.\n\n"; + List<String> dep = new ArrayList<String>(); + for (Map.Entry<String, Set<PyPackage>> entry : dependentPackages.entrySet()) { + dep.add(entry.getKey() + " -> " + StringUtil.join(entry.getValue(), ", ")); + } + message += StringUtil.join(dep, "\n"); + message += "\n\nDo you want to proceed?"; + warning[0] = Messages.showYesNoDialog(message, "Warning", + AllIcons.General.BalloonWarning); + } + } + }, ModalityState.current()); + } + if (warning[0] != Messages.YES) return true; + } + catch (PyExternalProcessException e) { + LOG.info("Error loading packages dependents: " + e.getMessage(), e); + } + return false; + } + + private static Map<String, Set<PyPackage>> collectDependents(@NotNull final List<PyPackage> packages, Sdk sdk) + throws PyExternalProcessException { + Map<String, Set<PyPackage>> dependentPackages = new HashMap<String, Set<PyPackage>>(); + for (PyPackage pkg : packages) { + final Set<PyPackage> dependents = PyPackageManager.getInstance(sdk).getDependents(pkg); + if (dependents != null && !dependents.isEmpty()) { + for (PyPackage dependent : dependents) { + if (!packages.contains(dependent)) { + dependentPackages.put(pkg.getName(), dependents); + } + } + } + } + return dependentPackages; + } + + private abstract static class PackagingTask extends Task.Backgroundable { + private static final String PACKAGING_GROUP_ID = "Packaging"; + + @Nullable protected final Listener myListener; + + public PackagingTask(@Nullable Project project, @NotNull String title, @Nullable Listener listener) { + super(project, title); + myListener = listener; + } + + @Override + public void run(@NotNull ProgressIndicator indicator) { + taskStarted(indicator); + taskFinished(runTask(indicator)); + } + + @NotNull + protected abstract List<PyExternalProcessException> runTask(@NotNull ProgressIndicator indicator); + + @NotNull + protected abstract String getSuccessTitle(); + + @NotNull + protected abstract String getSuccessDescription(); + + @NotNull + protected abstract String getFailureTitle(); + + protected void taskStarted(@NotNull ProgressIndicator indicator) { + indicator.setText(getTitle() + "..."); + if (myListener != null) { + ApplicationManager.getApplication().invokeLater(new Runnable() { + @Override + public void run() { + myListener.started(); + } + }); + } + } + + protected void taskFinished(@NotNull final List<PyExternalProcessException> exceptions) { + final Ref<Notification> notificationRef = new Ref<Notification>(null); + if (exceptions.isEmpty()) { + notificationRef.set(new Notification(PACKAGING_GROUP_ID, getSuccessTitle(), getSuccessDescription(), + NotificationType.INFORMATION)); + } + else { + final String firstLine = getTitle() + ": error occurred."; + final String description = createDescription(exceptions, firstLine); + notificationRef.set(new Notification(PACKAGING_GROUP_ID, getFailureTitle(), + firstLine + " <a href=\"xxx\">Details...</a>", + NotificationType.ERROR, + new NotificationListener() { + @Override + public void hyperlinkUpdate(@NotNull Notification notification, + @NotNull HyperlinkEvent event) { + assert myProject != null; + PackagesNotificationPanel.showError(myProject, getFailureTitle(), description); + } + } + )); + } + ApplicationManager.getApplication().invokeLater(new Runnable() { + @Override + public void run() { + if (myListener != null) { + myListener.finished(exceptions); + } + final Notification notification = notificationRef.get(); + if (notification != null) { + notification.notify(myProject); + } + } + }); + } + } + + private static class InstallTask extends PackagingTask { + @NotNull protected final Sdk mySdk; + @NotNull private final List<PyRequirement> myRequirements; + @NotNull private final List<String> myExtraArgs; + + public InstallTask(@Nullable Project project, + @NotNull Sdk sdk, + @NotNull List<PyRequirement> requirements, + @NotNull List<String> extraArgs, + @Nullable Listener listener) { + super(project, "Installing packages", listener); + mySdk = sdk; + myRequirements = requirements; + myExtraArgs = extraArgs; + } + + @NotNull + @Override + protected List<PyExternalProcessException> runTask(@NotNull ProgressIndicator indicator) { + final List<PyExternalProcessException> exceptions = new ArrayList<PyExternalProcessException>(); + final int size = myRequirements.size(); + final PyPackageManager manager = PyPackageManagers.getInstance().forSdk(mySdk); + for (int i = 0; i < size; i++) { + final PyRequirement requirement = myRequirements.get(i); + indicator.setText(String.format("Installing package '%s'...", requirement)); + indicator.setFraction((double)i / size); + try { + manager.install(Arrays.asList(requirement), myExtraArgs); + } + catch (PyExternalProcessException e) { + exceptions.add(e); + } + } + manager.refresh(); + return exceptions; + } + + @NotNull + @Override + protected String getSuccessTitle() { + return "Packages installed successfully"; + } + + @NotNull + @Override + protected String getSuccessDescription() { + return "Installed packages: " + PyPackageUtil.requirementsToString(myRequirements); + } + + @NotNull + @Override + protected String getFailureTitle() { + return "Install packages failed"; + } + } + + private static class InstallManagementTask extends InstallTask { + + public InstallManagementTask(@Nullable Project project, + @NotNull Sdk sdk, + @Nullable Listener listener) { + super(project, sdk, Collections.<PyRequirement>emptyList(), Collections.<String>emptyList(), listener); + } + + @NotNull + @Override + protected List<PyExternalProcessException> runTask(@NotNull ProgressIndicator indicator) { + final List<PyExternalProcessException> exceptions = new ArrayList<PyExternalProcessException>(); + final PyPackageManager manager = PyPackageManagers.getInstance().forSdk(mySdk); + indicator.setText("Installing packaging tools..."); + indicator.setIndeterminate(true); + try { + manager.installManagement(); + } + catch (PyExternalProcessException e) { + exceptions.add(e); + } + manager.refresh(); + return exceptions; + } + + @NotNull + @Override + protected String getSuccessDescription() { + return "Installed Python packaging tools"; + } + } + + private static class UninstallTask extends PackagingTask { + @NotNull private final Sdk mySdk; + @NotNull private final List<PyPackage> myPackages; + + public UninstallTask(@Nullable Project project, + @NotNull Sdk sdk, + @Nullable Listener listener, + @NotNull List<PyPackage> packages) { + super(project, "Uninstalling packages", listener); + mySdk = sdk; + myPackages = packages; + } + + @NotNull + @Override + protected List<PyExternalProcessException> runTask(@NotNull ProgressIndicator indicator) { + final PyPackageManager manager = PyPackageManagers.getInstance().forSdk(mySdk); + try { + manager.uninstall(myPackages); + return Arrays.asList(); + } + catch (PyExternalProcessException e) { + return Arrays.asList(e); + } + finally { + manager.refresh(); + } + } + + @NotNull + @Override + protected String getSuccessTitle() { + return "Packages uninstalled successfully"; + } + + @NotNull + @Override + protected String getSuccessDescription() { + final String packagesString = StringUtil.join(myPackages, new Function<PyPackage, String>() { + @Override + public String fun(PyPackage pkg) { + return "'" + pkg.getName() + "'"; + } + }, ", "); + return "Uninstalled packages: " + packagesString; + } + + @NotNull + @Override + protected String getFailureTitle() { + return "Uninstall packages failed"; + } + } + + public static String createDescription(List<PyExternalProcessException> exceptions, String firstLine) { + final StringBuilder b = new StringBuilder(); + b.append(firstLine); + b.append("\n\n"); + for (PyExternalProcessException exception : exceptions) { + b.append(exception.toString()); + b.append("\n"); + } + return b.toString(); + } +} diff --git a/python/src/com/jetbrains/python/packaging/PyPackageManagersImpl.java b/python/src/com/jetbrains/python/packaging/PyPackageManagersImpl.java index 054e9e01d23f..e68cede3f799 100644 --- a/python/src/com/jetbrains/python/packaging/PyPackageManagersImpl.java +++ b/python/src/com/jetbrains/python/packaging/PyPackageManagersImpl.java @@ -15,13 +15,11 @@ */ package com.jetbrains.python.packaging; -import com.intellij.openapi.module.Module; import com.intellij.openapi.projectRoots.Sdk; +import com.jetbrains.python.sdk.PythonSdkType; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import java.util.HashMap; -import java.util.List; import java.util.Map; /** @@ -36,22 +34,14 @@ public class PyPackageManagersImpl extends PyPackageManagers { final String name = sdk.getName(); PyPackageManagerImpl manager = myInstances.get(name); if (manager == null) { - manager = new PyPackageManagerImpl(sdk); + if (PythonSdkType.isRemote(sdk)) { + manager = new PyRemotePackageManagerImpl(sdk); + } + else { + manager = new PyPackageManagerImpl(sdk); + } myInstances.put(name, manager); } return manager; } - - @Nullable - @Override - public List<PyRequirement> getRequirements(Module module) { - return PyPackageManagerImpl.getRequirements(module); - } - - - @Nullable - @Override - public List<PyRequirement> getRequirementsFromTxt(Module module) { - return PyPackageManagerImpl.getRequirementsFromTxt(module); - } } diff --git a/python/src/com/jetbrains/python/packaging/PyRemotePackageManagerImpl.java b/python/src/com/jetbrains/python/packaging/PyRemotePackageManagerImpl.java new file mode 100644 index 000000000000..6c6de77d9454 --- /dev/null +++ b/python/src/com/jetbrains/python/packaging/PyRemotePackageManagerImpl.java @@ -0,0 +1,177 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jetbrains.python.packaging; + +import com.google.common.base.Function; +import com.google.common.collect.Collections2; +import com.intellij.execution.ExecutionException; +import com.intellij.execution.process.ProcessOutput; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.projectRoots.Sdk; +import com.intellij.openapi.projectRoots.SdkAdditionalData; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.remote.RemoteFile; +import com.intellij.remote.RemoteSdkAdditionalData; +import com.intellij.remote.RemoteSdkCredentials; +import com.intellij.remote.VagrantNotStartedException; +import com.intellij.util.ArrayUtil; +import com.intellij.util.PathMappingSettings; +import com.jetbrains.python.remote.PyRemoteSdkAdditionalDataBase; +import com.jetbrains.python.remote.PythonRemoteInterpreterManager; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author vlan + */ +public class PyRemotePackageManagerImpl extends PyPackageManagerImpl { + private static final String LAUNCH_VAGRANT = "launchVagrant"; + public static final int ERROR_VAGRANT_NOT_LAUNCHED = 101; + public static final int ERROR_REMOTE_ACCESS = 102; + + private static final Logger LOG = Logger.getInstance(PyRemotePackageManagerImpl.class); + + PyRemotePackageManagerImpl(@NotNull Sdk sdk) { + super(sdk); + } + + @Nullable + @Override + protected String getHelperPath(String helper) { + final SdkAdditionalData sdkData = mySdk.getSdkAdditionalData(); + if (sdkData instanceof PyRemoteSdkAdditionalDataBase) { + final PyRemoteSdkAdditionalDataBase remoteSdkData = (PyRemoteSdkAdditionalDataBase)mySdk.getSdkAdditionalData(); + try { + final RemoteSdkCredentials remoteSdkCredentials = remoteSdkData.getRemoteSdkCredentials(false); + if (!StringUtil.isEmpty(remoteSdkCredentials.getHelpersPath())) { + return new RemoteFile(remoteSdkCredentials.getHelpersPath(), helper).getPath(); + } + else { + return null; + } + } + catch (Exception e) { + LOG.error(e); + } + } + return null; + } + + @Override + protected ProcessOutput getProcessOutput(@NotNull String helperPath, + @NotNull List<String> args, + boolean askForSudo, + @Nullable String workingDir) throws PyExternalProcessException { + final String homePath = mySdk.getHomePath(); + if (homePath == null) { + throw new PyExternalProcessException(ERROR_INVALID_SDK, helperPath, args, "Cannot find interpreter for SDK"); + } + final SdkAdditionalData sdkData = mySdk.getSdkAdditionalData(); + if (sdkData instanceof PyRemoteSdkAdditionalDataBase) { //remote interpreter + RemoteSdkCredentials remoteSdkCredentials; + try { + remoteSdkCredentials = ((RemoteSdkAdditionalData)sdkData).getRemoteSdkCredentials(false); + } + catch (InterruptedException e) { + LOG.error(e); + remoteSdkCredentials = null; + } + catch (final ExecutionException e) { + if (e.getCause() instanceof VagrantNotStartedException) { + throw new PyExternalProcessException(ERROR_VAGRANT_NOT_LAUNCHED, helperPath, args, "Vagrant instance is down. <a href=\"" + + LAUNCH_VAGRANT + + "\">Launch vagrant</a>") + .withHandler(LAUNCH_VAGRANT, new Runnable() { + @Override + public void run() { + final PythonRemoteInterpreterManager manager = PythonRemoteInterpreterManager.getInstance(); + if (manager != null) { + + try { + manager.runVagrant(((VagrantNotStartedException)e.getCause()).getVagrantFolder()); + clearCaches(); + } + catch (ExecutionException e1) { + throw new RuntimeException(e1); + } + } + } + }); + } + else { + throw new PyExternalProcessException(ERROR_REMOTE_ACCESS, helperPath, args, e.getMessage()); + } + } + final PythonRemoteInterpreterManager manager = PythonRemoteInterpreterManager.getInstance(); + if (manager != null && remoteSdkCredentials != null) { + final List<String> cmdline = new ArrayList<String>(); + cmdline.add(homePath); + cmdline.add(RemoteFile.detectSystemByPath(homePath).createRemoteFile(helperPath).getPath()); + cmdline.addAll(Collections2.transform(args, new Function<String, String>() { + @Override + public String apply(@Nullable String input) { + return quoteIfNeeded(input); + } + })); + try { + if (askForSudo) { + askForSudo = !manager.ensureCanWrite(null, remoteSdkCredentials, remoteSdkCredentials.getInterpreterPath()); + } + ProcessOutput processOutput; + do { + PathMappingSettings mappings = manager.setupMappings(null, (PyRemoteSdkAdditionalDataBase)sdkData, null); + processOutput = + manager.runRemoteProcess(null, remoteSdkCredentials, mappings, ArrayUtil.toStringArray(cmdline), workingDir, askForSudo); + if (askForSudo && processOutput.getStderr().contains("sudo: 3 incorrect password attempts")) { + continue; + } + break; + } + while (true); + return processOutput; + } + catch (ExecutionException e) { + throw new PyExternalProcessException(ERROR_INVALID_SDK, helperPath, args, "Error running SDK: " + e.getMessage(), e); + } + } + else { + throw new PyExternalProcessException(ERROR_INVALID_SDK, helperPath, args, + PythonRemoteInterpreterManager.WEB_DEPLOYMENT_PLUGIN_IS_DISABLED); + } + } + else { + throw new PyExternalProcessException(ERROR_INVALID_SDK, helperPath, args, "Invalid remote SDK"); + } + } + + @Override + protected void subscribeToLocalChanges(Sdk sdk) { + // Local VFS changes aren't needed + } + + @Override + protected void installManagement(@NotNull String name) throws PyExternalProcessException { + super.installManagement(name); + // TODO: remove temp directory for remote interpreter + } + + private static String quoteIfNeeded(String arg) { + return arg.replace("<", "\\<").replace(">", "\\>"); //TODO: move this logic to ParametersListUtil.encode + } +} diff --git a/python/src/com/jetbrains/python/packaging/ui/PyInstalledPackagesPanel.java b/python/src/com/jetbrains/python/packaging/ui/PyInstalledPackagesPanel.java index 348d2817ec75..094149336d5d 100644 --- a/python/src/com/jetbrains/python/packaging/ui/PyInstalledPackagesPanel.java +++ b/python/src/com/jetbrains/python/packaging/ui/PyInstalledPackagesPanel.java @@ -25,10 +25,8 @@ import com.intellij.util.Consumer; import com.intellij.webcore.packaging.InstalledPackage; import com.intellij.webcore.packaging.InstalledPackagesPanel; import com.intellij.webcore.packaging.PackagesNotificationPanel; -import com.jetbrains.python.packaging.PyExternalProcessException; -import com.jetbrains.python.packaging.PyPackage; -import com.jetbrains.python.packaging.PyPackageManager; -import com.jetbrains.python.packaging.PyPackageManagerImpl; +import com.jetbrains.python.packaging.*; +import com.jetbrains.python.sdk.PySdkUtil; import com.jetbrains.python.sdk.PythonSdkType; import com.jetbrains.python.sdk.flavors.IronPythonSdkFlavor; import com.jetbrains.python.sdk.flavors.PythonSdkFlavor; @@ -42,33 +40,13 @@ import java.util.Set; * @author yole */ public class PyInstalledPackagesPanel extends InstalledPackagesPanel { - public static final String INSTALL_SETUPTOOLS = "installSetuptools"; - public static final String INSTALL_PIP = "installPip"; + public static final String INSTALL_MANAGEMENT = "installManagement"; public static final String CREATE_VENV = "createVEnv"; - private boolean myHasSetuptools; - private boolean myHasPip = true; + private boolean myHasManagement = false; public PyInstalledPackagesPanel(Project project, PackagesNotificationPanel area) { super(project, area); - myNotificationArea.addLinkHandler(INSTALL_SETUPTOOLS, new Runnable() { - @Override - public void run() { - final Sdk sdk = getSelectedSdk(); - if (sdk != null) { - installManagementTool(sdk, PyPackageManagerImpl.SETUPTOOLS); - } - } - }); - myNotificationArea.addLinkHandler(INSTALL_PIP, new Runnable() { - @Override - public void run() { - final Sdk sdk = getSelectedSdk(); - if (sdk != null) { - installManagementTool(sdk, PyPackageManagerImpl.PIP); - } - } - }); } private Sdk getSelectedSdk() { @@ -85,19 +63,8 @@ public class PyInstalledPackagesPanel extends InstalledPackagesPanel { application.executeOnPooledThread(new Runnable() { @Override public void run() { - PyExternalProcessException exc = null; - try { - PyPackageManagerImpl packageManager = (PyPackageManagerImpl)PyPackageManager.getInstance(selectedSdk); - myHasSetuptools = packageManager.findInstalledPackage(PyPackageManagerImpl.PACKAGE_SETUPTOOLS) != null; - if (!myHasSetuptools) { - myHasSetuptools = packageManager.findInstalledPackage(PyPackageManagerImpl.PACKAGE_DISTRIBUTE) != null; - } - myHasPip = packageManager.findInstalledPackage(PyPackageManagerImpl.PACKAGE_PIP) != null; - } - catch (PyExternalProcessException e) { - exc = e; - } - final PyExternalProcessException externalProcessException = exc; + PyPackageManager packageManager = PyPackageManager.getInstance(selectedSdk); + myHasManagement = packageManager.hasManagement(false); application.invokeLater(new Runnable() { @Override public void run() { @@ -112,42 +79,24 @@ public class PyInstalledPackagesPanel extends InstalledPackagesPanel { myNotificationArea.hide(); if (!invalid) { String text = null; - if (externalProcessException != null) { - final int retCode = externalProcessException.getRetcode(); - if (retCode == PyPackageManagerImpl.ERROR_NO_PIP) { - myHasPip = false; - } - else if (retCode == PyPackageManagerImpl.ERROR_NO_SETUPTOOLS) { - myHasSetuptools = false; - } - else { - text = externalProcessException.getMessage(); - } - final boolean hasPackagingTools = myHasPip && myHasSetuptools; - allowCreateVirtualEnv &= !hasPackagingTools; - - if (externalProcessException.hasHandler()) { - final String key = externalProcessException.getHandler().first; - myNotificationArea.addLinkHandler(key, - new Runnable() { - @Override - public void run() { - externalProcessException.getHandler().second.run(); - myNotificationArea.removeLinkHandler(key); - updateNotifications(selectedSdk); + if (!myHasManagement) { + myNotificationArea.addLinkHandler(INSTALL_MANAGEMENT, + new Runnable() { + @Override + public void run() { + final Sdk sdk = getSelectedSdk(); + if (sdk != null) { + installManagementTools(sdk); } + myNotificationArea.removeLinkHandler(INSTALL_MANAGEMENT); + updateNotifications(selectedSdk); } - ); - } + } + ); } - if (text == null) { - if (!myHasSetuptools) { - text = "Python package management tools not found. <a href=\"" + INSTALL_SETUPTOOLS + "\">Install 'setuptools'</a>"; - } - else if (!myHasPip) { - text = "Python packaging tool 'pip' not found. <a href=\"" + INSTALL_PIP + "\">Install 'pip'</a>"; - } + if (!myHasManagement) { + text = "Python packaging tools not found. <a href=\"" + INSTALL_MANAGEMENT + "\">Install packaging tools</a>"; } if (text != null) { if (allowCreateVirtualEnv) { @@ -157,7 +106,7 @@ public class PyInstalledPackagesPanel extends InstalledPackagesPanel { } } - myInstallButton.setEnabled(!invalid && externalProcessException == null && myHasPip); + myInstallButton.setEnabled(!invalid && myHasManagement); } } }, ModalityState.any()); @@ -170,8 +119,8 @@ public class PyInstalledPackagesPanel extends InstalledPackagesPanel { return Sets.newHashSet("pip", "distutils", "setuptools"); } - private void installManagementTool(@NotNull final Sdk sdk, final String name) { - final PyPackageManagerImpl.UI ui = new PyPackageManagerImpl.UI(myProject, sdk, new PyPackageManagerImpl.UI.Listener() { + private void installManagementTools(@NotNull final Sdk sdk) { + final PyPackageManagerUI ui = new PyPackageManagerUI(myProject, sdk, new PyPackageManagerUI.Listener() { @Override public void started() { myPackagesTable.setPaintBusy(true); @@ -180,11 +129,11 @@ public class PyInstalledPackagesPanel extends InstalledPackagesPanel { @Override public void finished(List<PyExternalProcessException> exceptions) { myPackagesTable.setPaintBusy(false); - PyPackageManagerImpl packageManager = (PyPackageManagerImpl)PyPackageManager.getInstance(sdk); + PyPackageManager packageManager = PyPackageManager.getInstance(sdk); if (!exceptions.isEmpty()) { - final String firstLine = "Install package failed. "; - final String description = PyPackageManagerImpl.UI.createDescription(exceptions, firstLine); - packageManager.showInstallationError(myProject, "Failed to install " + name, description); + final String firstLine = "Install Python packaging tools failed. "; + final String description = PyPackageManagerUI.createDescription(exceptions, firstLine); + PackagesNotificationPanel.showError(myProject, "Failed to install Python packaging tools", description); } packageManager.refresh(); updatePackages(new PyPackageManagementService(myProject, sdk)); @@ -194,22 +143,22 @@ public class PyInstalledPackagesPanel extends InstalledPackagesPanel { updateNotifications(sdk); } }); - ui.installManagement(name); + ui.installManagement(); } @Override protected boolean canUninstallPackage(InstalledPackage pkg) { - if (!myHasPip) return false; + if (!myHasManagement) return false; if (PythonSdkType.isVirtualEnv(getSelectedSdk()) && pkg instanceof PyPackage) { final String location = ((PyPackage)pkg).getLocation(); - if (location != null && location.startsWith(PyPackageManagerImpl.getUserSite())) { + if (location != null && location.startsWith(PySdkUtil.getUserSite())) { return false; } } final String name = pkg.getName(); - if (PyPackageManagerImpl.PACKAGE_PIP.equals(name) || - PyPackageManagerImpl.PACKAGE_SETUPTOOLS.equals(name) || - PyPackageManagerImpl.PACKAGE_DISTRIBUTE.equals(name)) { + if (PyPackageManager.PACKAGE_PIP.equals(name) || + PyPackageManager.PACKAGE_SETUPTOOLS.equals(name) || + PyPackageManager.PACKAGE_DISTRIBUTE.equals(name)) { return false; } return true; @@ -217,6 +166,6 @@ public class PyInstalledPackagesPanel extends InstalledPackagesPanel { @Override protected boolean canUpgradePackage(InstalledPackage pyPackage) { - return myHasPip; + return myHasManagement; } } diff --git a/python/src/com/jetbrains/python/packaging/ui/PyPackageManagementService.java b/python/src/com/jetbrains/python/packaging/ui/PyPackageManagementService.java index ffd291a995b1..95330b361dfb 100644 --- a/python/src/com/jetbrains/python/packaging/ui/PyPackageManagementService.java +++ b/python/src/com/jetbrains/python/packaging/ui/PyPackageManagementService.java @@ -23,6 +23,7 @@ import com.intellij.webcore.packaging.InstalledPackage; import com.intellij.webcore.packaging.PackageManagementService; import com.intellij.webcore.packaging.RepoPackage; import com.jetbrains.python.packaging.*; +import com.jetbrains.python.sdk.PySdkUtil; import com.jetbrains.python.sdk.PythonSdkType; import org.apache.xmlrpc.AsyncCallback; import org.jetbrains.annotations.NonNls; @@ -112,7 +113,7 @@ public class PyPackageManagementService extends PackageManagementService { public String getInstallToUserText() { String userSiteText = "Install to user's site packages directory"; if (!PythonSdkType.isRemote(mySdk)) - userSiteText += " (" + PyPackageManagerImpl.getUserSite() + ")"; + userSiteText += " (" + PySdkUtil.getUserSite() + ")"; return userSiteText; } @@ -130,12 +131,12 @@ public class PyPackageManagementService extends PackageManagementService { public Collection<InstalledPackage> getInstalledPackages() throws IOException { List<PyPackage> packages; try { - packages = ((PyPackageManagerImpl)PyPackageManager.getInstance(mySdk)).getPackages(); + packages = PyPackageManager.getInstance(mySdk).getPackages(false); } catch (PyExternalProcessException e) { throw new IOException(e); } - return new ArrayList<InstalledPackage>(packages); + return packages != null ? new ArrayList<InstalledPackage>(packages) : new ArrayList<InstalledPackage>(); } @Override @@ -145,7 +146,7 @@ public class PyPackageManagementService extends PackageManagementService { final String repository = PyPIPackageUtil.PYPI_URL.equals(repoPackage.getRepoUrl()) ? null : repoPackage.getRepoUrl(); final List<String> extraArgs = new ArrayList<String>(); if (installToUser) { - extraArgs.add(PyPackageManagerImpl.USE_USER_SITE); + extraArgs.add(PyPackageManager.USE_USER_SITE); } if (extraOptions != null) { // TODO: Respect arguments quotation @@ -166,7 +167,7 @@ public class PyPackageManagementService extends PackageManagementService { req = new PyRequirement(packageName); } - final PyPackageManagerImpl.UI ui = new PyPackageManagerImpl.UI(myProject, mySdk, new PyPackageManagerImpl.UI.Listener() { + final PyPackageManagerUI ui = new PyPackageManagerUI(myProject, mySdk, new PyPackageManagerUI.Listener() { @Override public void started() { listener.operationStarted(packageName); @@ -183,7 +184,7 @@ public class PyPackageManagementService extends PackageManagementService { private String toErrorDescription(List<PyExternalProcessException> exceptions) { String errorDescription = null; if (exceptions != null && exceptions.size() > 0) { - errorDescription = PyPackageManagerImpl.UI.createDescription(exceptions, ""); + errorDescription = PyPackageManagerUI.createDescription(exceptions, ""); } return errorDescription; } @@ -191,7 +192,7 @@ public class PyPackageManagementService extends PackageManagementService { @Override public void uninstallPackages(List<InstalledPackage> installedPackages, final Listener listener) { final String packageName = installedPackages.size() == 1 ? installedPackages.get(0).getName() : null; - PyPackageManagerImpl.UI ui = new PyPackageManagerImpl.UI(myProject, mySdk, new PyPackageManagerImpl.UI.Listener() { + PyPackageManagerUI ui = new PyPackageManagerUI(myProject, mySdk, new PyPackageManagerUI.Listener() { @Override public void started() { listener.operationStarted(packageName); diff --git a/python/src/com/jetbrains/python/projectView/PyElementNode.java b/python/src/com/jetbrains/python/projectView/PyElementNode.java index 20c7b811b8a6..6ec347236773 100644 --- a/python/src/com/jetbrains/python/projectView/PyElementNode.java +++ b/python/src/com/jetbrains/python/projectView/PyElementNode.java @@ -19,11 +19,14 @@ import com.intellij.ide.projectView.PresentationData; import com.intellij.ide.projectView.ViewSettings; import com.intellij.ide.projectView.impl.nodes.BasePsiNode; import com.intellij.ide.util.treeView.AbstractTreeNode; +import com.intellij.navigation.ItemPresentation; import com.intellij.openapi.project.Project; +import com.jetbrains.python.PyNames; import com.jetbrains.python.psi.PyClass; import com.jetbrains.python.psi.PyElement; import com.jetbrains.python.psi.PyFunction; +import javax.swing.*; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -57,14 +60,23 @@ public class PyElementNode extends BasePsiNode<PyElement> { @Override protected void updateImpl(PresentationData data) { - PyElement value = getValue(); + final PyElement value = getValue(); final String name = value.getName(); - StringBuilder presentableText = new StringBuilder(name != null ? name : "<unnamed>"); - if (value instanceof PyFunction) { - presentableText.append(((PyFunction) value).getParameterList().getPresentableText(false)); + final ItemPresentation presentation = value.getPresentation(); + String presentableText = name != null ? name : PyNames.UNNAMED_ELEMENT; + Icon presentableIcon = value.getIcon(0); + if (presentation != null) { + final String text = presentation.getPresentableText(); + if (text != null) { + presentableText = text; + } + final Icon icon = presentation.getIcon(false); + if (icon != null) { + presentableIcon = icon; + } } - data.setPresentableText(presentableText.toString()); - data.setIcon(value.getIcon(0)); + data.setPresentableText(presentableText); + data.setIcon(presentableIcon); } @Override diff --git a/python/src/com/jetbrains/python/psi/PyUtil.java b/python/src/com/jetbrains/python/psi/PyUtil.java index cd4e582bb6f2..dc5cbf4917a5 100644 --- a/python/src/com/jetbrains/python/psi/PyUtil.java +++ b/python/src/com/jetbrains/python/psi/PyUtil.java @@ -1394,8 +1394,8 @@ public class PyUtil { @Nullable public static PsiElement findPrevAtOffset(PsiFile psiFile, int caretOffset, Class... toSkip) { - PsiElement element = psiFile.findElementAt(caretOffset); - if (element == null || caretOffset < 0) { + PsiElement element; + if (caretOffset < 0) { return null; } int lineStartOffset = 0; diff --git a/python/src/com/jetbrains/python/psi/impl/PyClassImpl.java b/python/src/com/jetbrains/python/psi/impl/PyClassImpl.java index bb2104fc0c77..611cbc84ad2e 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyClassImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyClassImpl.java @@ -17,6 +17,7 @@ package com.jetbrains.python.psi.impl; import com.intellij.codeInsight.completion.CompletionUtil; import com.intellij.lang.ASTNode; +import com.intellij.navigation.ItemPresentation; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.NotNullLazyValue; @@ -53,10 +54,13 @@ import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.util.*; +import static com.intellij.openapi.util.text.StringUtil.join; +import static com.intellij.openapi.util.text.StringUtil.notNullize; + /** * @author yole */ -public class PyClassImpl extends PyPresentableElementImpl<PyClassStub> implements PyClass { +public class PyClassImpl extends PyBaseElementImpl<PyClassStub> implements PyClass { public static final PyClass[] EMPTY_ARRAY = new PyClassImpl[0]; private List<PyTargetExpression> myInstanceAttributes; @@ -288,6 +292,32 @@ public class PyClassImpl extends PyPresentableElementImpl<PyClassStub> implement return result.toArray(new PyClass[result.size()]); } + @Override + public ItemPresentation getPresentation() { + return new PyElementPresentation(this) { + @Nullable + @Override + public String getPresentableText() { + if (!isValid()) { + return null; + } + final StringBuilder result = new StringBuilder(notNullize(getName(), PyNames.UNNAMED_ELEMENT)); + final PyExpression[] superClassExpressions = getSuperClassExpressions(); + if (superClassExpressions.length > 0) { + result.append("("); + result.append(join(Arrays.asList(superClassExpressions), new Function<PyExpression, String>() { + public String fun(PyExpression expr) { + String name = expr.getText(); + return notNullize(name, PyNames.UNNAMED_ELEMENT); + } + }, ", ")); + result.append(")"); + } + return result.toString(); + } + }; + } + @NotNull private static List<PyClassLikeType> mroMerge(@NotNull List<List<PyClassLikeType>> sequences) { List<PyClassLikeType> result = new LinkedList<PyClassLikeType>(); // need to insert to 0th position on linearize diff --git a/python/src/com/jetbrains/python/psi/impl/PyElementPresentation.java b/python/src/com/jetbrains/python/psi/impl/PyElementPresentation.java new file mode 100644 index 000000000000..7888071c7321 --- /dev/null +++ b/python/src/com/jetbrains/python/psi/impl/PyElementPresentation.java @@ -0,0 +1,76 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jetbrains.python.psi.impl; + +import com.intellij.navigation.ColoredItemPresentation; +import com.intellij.openapi.editor.colors.TextAttributesKey; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiFile; +import com.jetbrains.python.PyNames; +import com.jetbrains.python.psi.PyElement; +import com.jetbrains.python.psi.resolve.QualifiedNameFinder; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; + +/** +* @author vlan +*/ +public class PyElementPresentation implements ColoredItemPresentation { + @NotNull private final PyElement myElement; + + public PyElementPresentation(@NotNull PyElement element) { + myElement = element; + } + + @Nullable + @Override + public TextAttributesKey getTextAttributesKey() { + return null; + } + + @Nullable + @Override + public String getPresentableText() { + final String name = myElement.getName(); + return name != null ? name : PyNames.UNNAMED_ELEMENT; + } + + @Nullable + @Override + public String getLocationString() { + return "(" + getPackageForFile(myElement.getContainingFile()) + ")"; + } + + @Nullable + @Override + public Icon getIcon(boolean unused) { + return myElement.getIcon(0); + } + + public static String getPackageForFile(@NotNull PsiFile containingFile) { + final VirtualFile vFile = containingFile.getVirtualFile(); + + if (vFile != null) { + final String importableName = QualifiedNameFinder.findShortestImportableName(containingFile, vFile); + if (importableName != null) { + return importableName; + } + } + return ""; + } +} diff --git a/python/src/com/jetbrains/python/psi/impl/PyFunctionImpl.java b/python/src/com/jetbrains/python/psi/impl/PyFunctionImpl.java index 86f84abb7fb6..2c1842f3337f 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyFunctionImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyFunctionImpl.java @@ -16,6 +16,7 @@ package com.jetbrains.python.psi.impl; import com.intellij.lang.ASTNode; +import com.intellij.navigation.ItemPresentation; import com.intellij.openapi.extensions.Extensions; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.Ref; @@ -54,6 +55,7 @@ import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.util.*; +import static com.intellij.openapi.util.text.StringUtil.notNullize; import static com.jetbrains.python.psi.PyFunction.Modifier.CLASSMETHOD; import static com.jetbrains.python.psi.PyFunction.Modifier.STATICMETHOD; import static com.jetbrains.python.psi.impl.PyCallExpressionHelper.interpretAsModifierWrappingCall; @@ -61,7 +63,7 @@ import static com.jetbrains.python.psi.impl.PyCallExpressionHelper.interpretAsMo /** * Implements PyFunction. */ -public class PyFunctionImpl extends PyPresentableElementImpl<PyFunctionStub> implements PyFunction { +public class PyFunctionImpl extends PyBaseElementImpl<PyFunctionStub> implements PyFunction { public PyFunctionImpl(ASTNode astNode) { super(astNode); @@ -259,6 +261,17 @@ public class PyFunctionImpl extends PyPresentableElementImpl<PyFunctionStub> imp return type; } + @Override + public ItemPresentation getPresentation() { + return new PyElementPresentation(this) { + @Nullable + @Override + public String getPresentableText() { + return notNullize(getName(), PyNames.UNNAMED_ELEMENT) + getParameterList().getPresentableText(true); + } + }; + } + @Nullable private PyType replaceSelf(@Nullable PyType returnType, @Nullable PyExpression receiver, @NotNull TypeEvalContext context) { if (receiver != null) { @@ -292,32 +305,30 @@ public class PyFunctionImpl extends PyPresentableElementImpl<PyFunctionStub> imp final PyBuiltinCache cache = PyBuiltinCache.getInstance(this); final PyStatementList statements = getStatementList(); final Set<PyType> types = new LinkedHashSet<PyType>(); - if (statements != null) { - statements.accept(new PyRecursiveElementVisitor() { - @Override - public void visitPyYieldExpression(PyYieldExpression node) { - final PyType type = context.getType(node); - if (node.isDelegating() && type instanceof PyCollectionType) { - final PyCollectionType collectionType = (PyCollectionType)type; - types.add(collectionType.getElementType(context)); - } - else { - types.add(type); - } + statements.accept(new PyRecursiveElementVisitor() { + @Override + public void visitPyYieldExpression(PyYieldExpression node) { + final PyType type = context.getType(node); + if (node.isDelegating() && type instanceof PyCollectionType) { + final PyCollectionType collectionType = (PyCollectionType)type; + types.add(collectionType.getElementType(context)); } - - @Override - public void visitPyFunction(PyFunction node) { - // Ignore nested functions + else { + types.add(type); } - }); - final int n = types.size(); - if (n == 1) { - elementType = Ref.create(types.iterator().next()); } - else if (n > 0) { - elementType = Ref.create(PyUnionType.union(types)); + + @Override + public void visitPyFunction(PyFunction node) { + // Ignore nested functions } + }); + final int n = types.size(); + if (n == 1) { + elementType = Ref.create(types.iterator().next()); + } + else if (n > 0) { + elementType = Ref.create(PyUnionType.union(types)); } if (elementType != null) { final PyClass generator = cache.getClass(PyNames.FAKE_GENERATOR); @@ -335,14 +346,12 @@ public class PyFunctionImpl extends PyPresentableElementImpl<PyFunctionStub> imp public PyType getReturnStatementType(TypeEvalContext typeEvalContext) { ReturnVisitor visitor = new ReturnVisitor(this, typeEvalContext); final PyStatementList statements = getStatementList(); - if (statements != null) { - statements.accept(visitor); - if (isGeneratedStub() && !visitor.myHasReturns) { - if (PyNames.INIT.equals(getName())) { - return PyNoneType.INSTANCE; - } - return null; + statements.accept(visitor); + if (isGeneratedStub() && !visitor.myHasReturns) { + if (PyNames.INIT.equals(getName())) { + return PyNoneType.INSTANCE; } + return null; } return visitor.result(); } @@ -376,9 +385,6 @@ public class PyFunctionImpl extends PyPresentableElementImpl<PyFunctionStub> imp @Nullable public String extractDeprecationMessage() { PyStatementList statementList = getStatementList(); - if (statementList == null) { - return null; - } return extractDeprecationMessage(Arrays.asList(statementList.getStatements())); } @@ -430,7 +436,7 @@ public class PyFunctionImpl extends PyPresentableElementImpl<PyFunctionStub> imp @Nullable @Override public StructuredDocString getStructuredDocString() { - return CachedValuesManager.getManager(getProject()).getCachedValue(this, myCachedStructuredDocStringProvider); + return CachedValuesManager.getCachedValue(this, myCachedStructuredDocStringProvider); } private boolean isGeneratedStub() { @@ -527,15 +533,7 @@ public class PyFunctionImpl extends PyPresentableElementImpl<PyFunctionStub> imp public PyStringLiteralExpression getDocStringExpression() { final PyStatementList stmtList = getStatementList(); - return stmtList != null ? DocStringUtil.findDocStringExpression(stmtList) : null; - } - - protected String getElementLocation() { - final PyClass containingClass = getContainingClass(); - if (containingClass != null) { - return "(" + containingClass.getName() + " in " + getPackageForFile(getContainingFile()) + ")"; - } - return super.getElementLocation(); + return DocStringUtil.findDocStringExpression(stmtList); } @NotNull diff --git a/python/src/com/jetbrains/python/psi/impl/PyNamedParameterImpl.java b/python/src/com/jetbrains/python/psi/impl/PyNamedParameterImpl.java index cc9178c07f9e..64108441c347 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyNamedParameterImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyNamedParameterImpl.java @@ -16,6 +16,7 @@ package com.jetbrains.python.psi.impl; import com.intellij.lang.ASTNode; +import com.intellij.navigation.ItemPresentation; import com.intellij.openapi.extensions.Extensions; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; @@ -48,7 +49,7 @@ import java.util.Map; /** * @author yole */ -public class PyNamedParameterImpl extends PyPresentableElementImpl<PyNamedParameterStub> implements PyNamedParameter { +public class PyNamedParameterImpl extends PyBaseElementImpl<PyNamedParameterStub> implements PyNamedParameter { public PyNamedParameterImpl(ASTNode astNode) { super(astNode); } @@ -280,6 +281,11 @@ public class PyNamedParameterImpl extends PyPresentableElementImpl<PyNamedParame return null; } + @Override + public ItemPresentation getPresentation() { + return new PyElementPresentation(this); + } + private static void processLocalCalls(@NotNull PyFunction function, @NotNull Processor<PyCallExpression> processor) { final PsiFile file = function.getContainingFile(); final String name = function.getName(); diff --git a/python/src/com/jetbrains/python/psi/impl/PyPresentableElementImpl.java b/python/src/com/jetbrains/python/psi/impl/PyPresentableElementImpl.java deleted file mode 100644 index d04d04062651..000000000000 --- a/python/src/com/jetbrains/python/psi/impl/PyPresentableElementImpl.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2000-2013 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.jetbrains.python.psi.impl; - -import com.intellij.lang.ASTNode; -import com.intellij.navigation.ItemPresentation; -import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.psi.PsiFile; -import com.intellij.psi.PsiNamedElement; -import com.intellij.psi.stubs.IStubElementType; -import com.intellij.psi.stubs.StubElement; -import com.jetbrains.python.psi.resolve.QualifiedNameFinder; - -import javax.swing.*; - -/** - * @author yole - */ -public abstract class PyPresentableElementImpl<T extends StubElement> extends PyBaseElementImpl<T> implements PsiNamedElement { - public PyPresentableElementImpl(ASTNode astNode) { - super(astNode); - } - - protected PyPresentableElementImpl(final T stub, final IStubElementType nodeType) { - super(stub, nodeType); - } - - public ItemPresentation getPresentation() { - return new ItemPresentation() { - public String getPresentableText() { - final String name = getName(); - return name != null ? name : "<none>"; - } - - public String getLocationString() { - return getElementLocation(); - } - - public Icon getIcon(final boolean open) { - return PyPresentableElementImpl.this.getIcon(0); - } - }; - } - - protected String getElementLocation() { - return "(" + getPackageForFile(getContainingFile()) + ")"; - } - - public static String getPackageForFile(final PsiFile containingFile) { - final VirtualFile vFile = containingFile.getVirtualFile(); - - if (vFile != null) { - final String importableName = QualifiedNameFinder.findShortestImportableName(containingFile, vFile); - if (importableName != null) { - return importableName; - } - } - return ""; - } -} diff --git a/python/src/com/jetbrains/python/psi/impl/PySingleStarParameterImpl.java b/python/src/com/jetbrains/python/psi/impl/PySingleStarParameterImpl.java index 70a46fbe39f9..44a01759f403 100644 --- a/python/src/com/jetbrains/python/psi/impl/PySingleStarParameterImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PySingleStarParameterImpl.java @@ -16,21 +16,18 @@ package com.jetbrains.python.psi.impl; import com.intellij.lang.ASTNode; -import com.intellij.psi.PsiElement; -import com.intellij.util.IncorrectOperationException; +import com.intellij.navigation.ItemPresentation; import com.jetbrains.python.PyElementTypes; import com.jetbrains.python.psi.PyExpression; import com.jetbrains.python.psi.PyNamedParameter; import com.jetbrains.python.psi.PySingleStarParameter; import com.jetbrains.python.psi.PyTupleParameter; import com.jetbrains.python.psi.stubs.PySingleStarParameterStub; -import org.jetbrains.annotations.NonNls; -import org.jetbrains.annotations.NotNull; /** * @author yole */ -public class PySingleStarParameterImpl extends PyPresentableElementImpl<PySingleStarParameterStub> implements PySingleStarParameter { +public class PySingleStarParameterImpl extends PyBaseElementImpl<PySingleStarParameterStub> implements PySingleStarParameter { public PySingleStarParameterImpl(ASTNode astNode) { super(astNode); } @@ -39,22 +36,22 @@ public class PySingleStarParameterImpl extends PyPresentableElementImpl<PySingle super(stub, PyElementTypes.SINGLE_STAR_PARAMETER); } - public PsiElement setName(@NonNls @NotNull String name) throws IncorrectOperationException { - throw new UnsupportedOperationException(); - } - + @Override public PyNamedParameter getAsNamed() { return null; } + @Override public PyTupleParameter getAsTuple() { return null; } + @Override public PyExpression getDefaultValue() { return null; } + @Override public boolean hasDefaultValue() { return false; } @@ -63,4 +60,9 @@ public class PySingleStarParameterImpl extends PyPresentableElementImpl<PySingle public boolean isSelf() { return false; } + + @Override + public ItemPresentation getPresentation() { + return new PyElementPresentation(this); + } } diff --git a/python/src/com/jetbrains/python/psi/impl/PyStringLiteralExpressionImpl.java b/python/src/com/jetbrains/python/psi/impl/PyStringLiteralExpressionImpl.java index bf519737f0f4..084aeecc3df0 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyStringLiteralExpressionImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyStringLiteralExpressionImpl.java @@ -327,7 +327,7 @@ public class PyStringLiteralExpressionImpl extends PyElementImpl implements PySt @Nullable @Override public String getLocationString() { - return "(" + PyPresentableElementImpl.getPackageForFile(getContainingFile()) + ")"; + return "(" + PyElementPresentation.getPackageForFile(getContainingFile()) + ")"; } @Nullable diff --git a/python/src/com/jetbrains/python/psi/impl/PyTargetExpressionImpl.java b/python/src/com/jetbrains/python/psi/impl/PyTargetExpressionImpl.java index 3805c8df525e..d125190b77e9 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyTargetExpressionImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyTargetExpressionImpl.java @@ -16,6 +16,7 @@ package com.jetbrains.python.psi.impl; import com.intellij.lang.ASTNode; +import com.intellij.navigation.ItemPresentation; import com.intellij.openapi.extensions.Extensions; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.Ref; @@ -61,7 +62,7 @@ import java.util.List; /** * @author yole */ -public class PyTargetExpressionImpl extends PyPresentableElementImpl<PyTargetExpressionStub> implements PyTargetExpression { +public class PyTargetExpressionImpl extends PyBaseElementImpl<PyTargetExpressionStub> implements PyTargetExpression { QualifiedName myQualifiedName; public PyTargetExpressionImpl(ASTNode astNode) { @@ -659,12 +660,19 @@ public class PyTargetExpressionImpl extends PyPresentableElementImpl<PyTargetExp return null; } - protected String getElementLocation() { - final PyClass containingClass = getContainingClass(); - if (containingClass != null) { - return "(" + containingClass.getName() + " in " + getPackageForFile(getContainingFile()) + ")"; - } - return super.getElementLocation(); + @Override + public ItemPresentation getPresentation() { + return new PyElementPresentation(this) { + @Nullable + @Override + public String getLocationString() { + final PyClass containingClass = getContainingClass(); + if (containingClass != null) { + return "(" + containingClass.getName() + " in " + getPackageForFile(getContainingFile()) + ")"; + } + return super.getLocationString(); + } + }; } @Nullable diff --git a/python/src/com/jetbrains/python/psi/impl/PyTupleParameterImpl.java b/python/src/com/jetbrains/python/psi/impl/PyTupleParameterImpl.java index 09e097cb0e99..9935841c33bb 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyTupleParameterImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyTupleParameterImpl.java @@ -16,19 +16,17 @@ package com.jetbrains.python.psi.impl; import com.intellij.lang.ASTNode; -import com.intellij.psi.PsiElement; -import com.intellij.util.IncorrectOperationException; +import com.intellij.navigation.ItemPresentation; import com.jetbrains.python.PyElementTypes; import com.jetbrains.python.PythonDialectsTokenSetProvider; import com.jetbrains.python.psi.*; import com.jetbrains.python.psi.stubs.PyTupleParameterStub; -import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; /** * Represents a tuple parameter as stubbed element. */ -public class PyTupleParameterImpl extends PyPresentableElementImpl<PyTupleParameterStub> implements PyTupleParameter { +public class PyTupleParameterImpl extends PyBaseElementImpl<PyTupleParameterStub> implements PyTupleParameter { public PyTupleParameterImpl(ASTNode astNode) { super(astNode); @@ -62,10 +60,6 @@ public class PyTupleParameterImpl extends PyPresentableElementImpl<PyTupleParame return getDefaultValue() != null; } - public PsiElement setName(@NonNls @NotNull String name) throws IncorrectOperationException { - throw new IncorrectOperationException("Can't rename a tuple parameter to '" + name +"'"); - } - @Override protected void acceptPyVisitor(PyElementVisitor pyVisitor) { pyVisitor.visitPyTupleParameter(this); @@ -80,4 +74,9 @@ public class PyTupleParameterImpl extends PyPresentableElementImpl<PyTupleParame public boolean isSelf() { return false; } + + @Override + public ItemPresentation getPresentation() { + return new PyElementPresentation(this); + } } diff --git a/python/src/com/jetbrains/python/psi/impl/PythonLanguageLevelPusher.java b/python/src/com/jetbrains/python/psi/impl/PythonLanguageLevelPusher.java index 7e15cab6ffda..af767684643e 100644 --- a/python/src/com/jetbrains/python/psi/impl/PythonLanguageLevelPusher.java +++ b/python/src/com/jetbrains/python/psi/impl/PythonLanguageLevelPusher.java @@ -19,7 +19,9 @@ import com.intellij.facet.Facet; import com.intellij.facet.FacetManager; import com.intellij.openapi.application.Application; import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.fileTypes.FileType; import com.intellij.openapi.fileTypes.FileTypeManager; +import com.intellij.openapi.fileTypes.FileTypeRegistry; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleManager; import com.intellij.openapi.module.ModuleType; @@ -163,7 +165,8 @@ public class PythonLanguageLevelPusher implements FilePropertyPusher<LanguageLev oStream.close(); for (VirtualFile child : fileOrDir.getChildren()) { - if (!child.isDirectory() && PythonFileType.INSTANCE.equals(child.getFileType())) { + final FileType fileType = FileTypeRegistry.getInstance().getFileTypeByFileName(child.getName()); + if (!child.isDirectory() && PythonFileType.INSTANCE.equals(fileType)) { PushedFilePropertiesUpdater.getInstance(project).filePropertiesChanged(child); } } diff --git a/python/src/com/jetbrains/python/refactoring/changeSignature/PyChangeSignatureHandler.java b/python/src/com/jetbrains/python/refactoring/changeSignature/PyChangeSignatureHandler.java index 6895249a641f..e360c9e46685 100644 --- a/python/src/com/jetbrains/python/refactoring/changeSignature/PyChangeSignatureHandler.java +++ b/python/src/com/jetbrains/python/refactoring/changeSignature/PyChangeSignatureHandler.java @@ -126,7 +126,7 @@ public class PyChangeSignatureHandler implements ChangeSignatureHandler { private static boolean isNotUnderSourceRoot(@NotNull final Project project, @Nullable final PsiFile psiFile, - @NotNull final Editor editor) { + @Nullable final Editor editor) { if (psiFile == null) return true; final VirtualFile virtualFile = psiFile.getVirtualFile(); if (virtualFile != null) { diff --git a/python/src/com/jetbrains/python/refactoring/move/PyMoveClassOrFunctionDelegate.java b/python/src/com/jetbrains/python/refactoring/move/PyMoveClassOrFunctionDelegate.java index cf1b4d19dd4e..8306a4faec23 100644 --- a/python/src/com/jetbrains/python/refactoring/move/PyMoveClassOrFunctionDelegate.java +++ b/python/src/com/jetbrains/python/refactoring/move/PyMoveClassOrFunctionDelegate.java @@ -39,11 +39,10 @@ import org.jetbrains.annotations.Nullable; * @author vlan */ public class PyMoveClassOrFunctionDelegate extends MoveHandlerDelegate { - @Override public boolean canMove(PsiElement[] elements, @Nullable PsiElement targetContainer) { for (PsiElement element : elements) { - if (element instanceof PyClass || element instanceof PyFunction) continue; + if ((element instanceof PyClass || element instanceof PyFunction) && PyUtil.isTopLevel(element)) continue; return false; } return super.canMove(elements, targetContainer); diff --git a/python/src/com/jetbrains/python/sdk/CreateVirtualEnvDialog.java b/python/src/com/jetbrains/python/sdk/CreateVirtualEnvDialog.java index 39fc2c909cf6..1d0a4bc3ad54 100644 --- a/python/src/com/jetbrains/python/sdk/CreateVirtualEnvDialog.java +++ b/python/src/com/jetbrains/python/sdk/CreateVirtualEnvDialog.java @@ -48,9 +48,9 @@ import com.intellij.ui.components.JBLabel; import com.intellij.util.NullableConsumer; import com.intellij.util.PathUtil; import com.intellij.util.PlatformUtils; +import com.intellij.webcore.packaging.PackagesNotificationPanel; import com.jetbrains.python.packaging.PyExternalProcessException; import com.jetbrains.python.packaging.PyPackageManager; -import com.jetbrains.python.packaging.PyPackageManagerImpl; import com.jetbrains.python.packaging.PyPackageService; import com.jetbrains.python.sdk.flavors.VirtualEnvSdkFlavor; import com.jetbrains.python.ui.IdeaDialog; @@ -416,7 +416,7 @@ public class CreateVirtualEnvDialog extends IdeaDialog { String myPath; public void run(@NotNull final ProgressIndicator indicator) { - final PyPackageManagerImpl packageManager = (PyPackageManagerImpl)PyPackageManager.getInstance(basicSdk); + final PyPackageManager packageManager = PyPackageManager.getInstance(basicSdk); try { indicator.setText("Creating virtual environment for " + basicSdk.getName()); myPath = packageManager.createVirtualEnv(getDestination(), useGlobalSitePackages()); @@ -425,7 +425,7 @@ public class CreateVirtualEnvDialog extends IdeaDialog { ApplicationManager.getApplication().invokeLater(new Runnable() { @Override public void run() { - packageManager.showInstallationError(getOwner(), "Failed to Create Virtual Environment", e.toString()); + PackagesNotificationPanel.showError(getOwner(), "Failed to Create Virtual Environment", e.toString()); } }, ModalityState.any()); } diff --git a/python/src/com/jetbrains/python/sdk/PySdkUtil.java b/python/src/com/jetbrains/python/sdk/PySdkUtil.java index 8f2b62b1b414..97d5ee545cbb 100644 --- a/python/src/com/jetbrains/python/sdk/PySdkUtil.java +++ b/python/src/com/jetbrains/python/sdk/PySdkUtil.java @@ -15,10 +15,13 @@ */ package com.jetbrains.python.sdk; +import com.intellij.execution.ExecutionException; import com.intellij.execution.process.CapturingProcessHandler; import com.intellij.execution.process.ProcessOutput; +import com.intellij.execution.util.ExecUtil; import com.intellij.openapi.application.PathManager; import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.module.Module; import com.intellij.openapi.projectRoots.Sdk; import com.intellij.openapi.roots.OrderRootType; import com.intellij.openapi.util.SystemInfo; @@ -29,8 +32,10 @@ import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.remote.RemoteSdkAdditionalData; -import com.intellij.util.ArrayUtil; +import com.intellij.util.SystemProperties; import com.intellij.util.containers.HashMap; +import com.jetbrains.python.packaging.PyPackageUtil; +import com.jetbrains.python.packaging.PyRequirement; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -38,8 +43,7 @@ import org.jetbrains.annotations.Nullable; import java.io.File; import java.io.IOException; import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Collections; +import java.util.Arrays; import java.util.List; import java.util.Map; @@ -56,6 +60,7 @@ public class PySdkUtil { // Windows EOF marker, Ctrl+Z public static final int SUBSTITUTE = 26; + public static final String PATH_ENV_VARIABLE = "PATH"; private PySdkUtil() { // explicitly none @@ -87,57 +92,29 @@ public class PySdkUtil { return getProcessOutput(homePath, command, null, timeout); } - /** - * Executes a process and returns its stdout and stderr outputs as lists of lines. - * Waits for process for possibly limited duration. - * - * @param homePath process run directory - * @param command command to execute and its arguments - * @param addEnv items are prepended to same-named values of inherited process environment. - * @param timeout how many milliseconds to wait until the process terminates; non-positive means inifinity. - * @return a tuple of (stdout lines, stderr lines, exit_code), lines in them have line terminators stripped, or may be null. - */ @NotNull public static ProcessOutput getProcessOutput(String homePath, @NonNls String[] command, - @Nullable @NonNls String[] addEnv, + @Nullable @NonNls Map<String, String> extraEnv, final int timeout) { - return getProcessOutput(homePath, command, addEnv, timeout, null, true); + return getProcessOutput(homePath, command, extraEnv, timeout, null, true); } - /** - * Executes a process and returns its stdout and stderr outputs as lists of lines. - * Waits for process for possibly limited duration. - * - * @param homePath process run directory - * @param command command to execute and its arguments - * @param addEnv items are prepended to same-named values of inherited process environment. - * @param timeout how many milliseconds to wait until the process terminates; non-positive means infinity. - * @param stdin the data to write to the process standard input stream - * @param needEOFMarker - * @return a tuple of (stdout lines, stderr lines, exit_code), lines in them have line terminators stripped, or may be null. - */ @NotNull public static ProcessOutput getProcessOutput(String homePath, @NonNls String[] command, - @Nullable @NonNls String[] addEnv, + @Nullable @NonNls Map<String, String> extraEnv, final int timeout, @Nullable byte[] stdin, boolean needEOFMarker) { - final ProcessOutput failureOutput = new ProcessOutput(); if (homePath == null || !new File(homePath).exists()) { - return failureOutput; + return new ProcessOutput(); } + final Map<String, String> systemEnv = System.getenv(); + final Map<String, String> env = extraEnv != null ? mergeEnvVariables(systemEnv, extraEnv) : systemEnv; try { - List<String> commands = new ArrayList<String>(); - if (SystemInfo.isWindows && StringUtil.endsWithIgnoreCase(command[0], ".bat")) { - commands.add("cmd"); - commands.add("/c"); - } - Collections.addAll(commands, command); - String[] newEnv = buildAdditionalEnv(addEnv); - Process process = Runtime.getRuntime().exec(ArrayUtil.toStringArray(commands), newEnv, new File(homePath)); - CapturingProcessHandler processHandler = new CapturingProcessHandler(process); + final Process process = ExecUtil.exec(Arrays.asList(command), homePath, env); + final CapturingProcessHandler processHandler = new CapturingProcessHandler(process); if (stdin != null) { final OutputStream processInput = processHandler.getProcessInput(); assert processInput != null; @@ -152,72 +129,61 @@ public class PySdkUtil { } return processHandler.runProcess(timeout); } - catch (final IOException ex) { - LOG.warn(ex); - return new ProcessOutput() { - @Override - public String getStderr() { - String err = super.getStderr(); - if (!StringUtil.isEmpty(err)) { - err += "\n" + ex.getMessage(); - } - else { - err = ex.getMessage(); - } - return err; - } - }; + catch (ExecutionException e) { + return getOutputForException(e); + } + catch (IOException e) { + return getOutputForException(e); } } - private static String[] buildAdditionalEnv(String[] addEnv) { - String[] newEnv = null; - if (addEnv != null) { - Map<String, String> envMap = buildEnvMap(addEnv); - newEnv = new String[envMap.size()]; - int i = 0; - for (Map.Entry<String, String> entry : envMap.entrySet()) { - newEnv[i] = entry.getKey() + "=" + entry.getValue(); - i += 1; + private static ProcessOutput getOutputForException(final Exception e) { + LOG.warn(e); + return new ProcessOutput() { + @Override + public String getStderr() { + String err = super.getStderr(); + if (!StringUtil.isEmpty(err)) { + err += "\n" + e.getMessage(); + } + else { + err = e.getMessage(); + } + return err; } - } - return newEnv; + }; } - public static Map<String, String> buildEnvMap(String[] addEnv) { - Map<String, String> envMap = new HashMap<String, String>(System.getenv()); - // turn additional ent into map - Map<String, String> addMap = new HashMap<String, String>(); - for (String envItem : addEnv) { - int pos = envItem.indexOf('='); - if (pos > 0) { - String key = envItem.substring(0, pos); - String value = envItem.substring(pos + 1, envItem.length()); - addMap.put(key, value); - } - else { - LOG.warn(String.format("Invalid env value: '%s'", envItem)); - } - } - // fuse old and new - for (Map.Entry<String, String> entry : addMap.entrySet()) { - final String key = entry.getKey(); - final String value = entry.getValue(); - final String oldValue = envMap.get(key); - if (oldValue != null) { - envMap.put(key, value + oldValue); + @NotNull + public static Map<String, String> mergeEnvVariables(@NotNull Map<String, String> environment, + @NotNull Map<String, String> extraEnvironment) { + final Map<String, String> result = new HashMap<String, String>(environment); + for (Map.Entry<String, String> entry : extraEnvironment.entrySet()) { + if (PATH_ENV_VARIABLE.equals(entry.getKey()) && result.containsKey(PATH_ENV_VARIABLE)) { + result.put(PATH_ENV_VARIABLE, result.get(PATH_ENV_VARIABLE) + File.pathSeparator + entry.getValue()); } else { - envMap.put(key, value); + result.put(entry.getKey(), entry.getValue()); } } - return envMap; + return result; } public static boolean isRemote(@Nullable Sdk sdk) { return sdk != null && sdk.getSdkAdditionalData() instanceof RemoteSdkAdditionalData; } + public static String getUserSite() { + if (SystemInfo.isWindows) { + final String appdata = System.getenv("APPDATA"); + return appdata + File.separator + "Python"; + } + else { + final String userHome = SystemProperties.getUserHome(); + return userHome + File.separator + ".local"; + } + } + public static boolean isElementInSkeletons(@NotNull final PsiElement element) { final PsiFile file = element.getContainingFile(); if (file != null) { @@ -266,4 +232,13 @@ public class PySdkUtil { } return null; } + + @Nullable + public static List<PyRequirement> getRequirementsFromTxt(Module module) { + final VirtualFile requirementsTxt = PyPackageUtil.findRequirementsTxt(module); + if (requirementsTxt != null) { + return PyRequirement.parse(requirementsTxt); + } + return null; + } } diff --git a/python/src/com/jetbrains/python/sdk/PythonSdkDetailsStep.java b/python/src/com/jetbrains/python/sdk/PythonSdkDetailsStep.java index 923629ce4473..cc6a2c6b6121 100644 --- a/python/src/com/jetbrains/python/sdk/PythonSdkDetailsStep.java +++ b/python/src/com/jetbrains/python/sdk/PythonSdkDetailsStep.java @@ -46,7 +46,7 @@ import java.util.List; import java.util.Set; public class PythonSdkDetailsStep extends BaseListPopupStep<String> { - private DialogWrapper myMore; + @Nullable private DialogWrapper myMore; private final Project myProject; private final Component myOwnerComponent; private final Sdk[] myExistingSdks; @@ -120,6 +120,8 @@ public class PythonSdkDetailsStep extends BaseListPopupStep<String> { } private void optionSelected(final String selectedValue) { + if (!MORE.equals(selectedValue) && myMore != null) + Disposer.dispose(myMore.getDisposable()); if (LOCAL.equals(selectedValue)) { createLocalSdk(); } @@ -129,7 +131,7 @@ public class PythonSdkDetailsStep extends BaseListPopupStep<String> { else if (VIRTUALENV.equals(selectedValue)) { createVirtualEnvSdk(); } - else { + else if (myMore != null) { myMore.show(); } } diff --git a/python/src/com/jetbrains/python/sdk/PythonSdkType.java b/python/src/com/jetbrains/python/sdk/PythonSdkType.java index 58de09370d37..460c875491ce 100644 --- a/python/src/com/jetbrains/python/sdk/PythonSdkType.java +++ b/python/src/com/jetbrains/python/sdk/PythonSdkType.java @@ -15,6 +15,7 @@ */ package com.jetbrains.python.sdk; +import com.google.common.collect.ImmutableMap; import com.intellij.execution.ExecutionException; import com.intellij.execution.configurations.GeneralCommandLine; import com.intellij.execution.process.ProcessOutput; @@ -771,16 +772,12 @@ public class PythonSdkType extends SdkType { } @NotNull - public static List<String> getSysPathsFromScript(String bin_path) throws InvalidSdkException { + public static List<String> getSysPathsFromScript(@NotNull String binaryPath) throws InvalidSdkException { String scriptFile = PythonHelpersLocator.getHelperPath("syspath.py"); // to handle the situation when PYTHONPATH contains ., we need to run the syspath script in the // directory of the script itself - otherwise the dir in which we run the script (e.g. /usr/bin) will be added to SDK path - String[] add_environment = getVirtualEnvAdditionalEnv(bin_path); - final ProcessOutput run_result = PySdkUtil.getProcessOutput( - new File(scriptFile).getParent(), - new String[]{bin_path, scriptFile}, - add_environment, MINUTE - ); + final ProcessOutput run_result = PySdkUtil.getProcessOutput(new File(scriptFile).getParent(), new String[]{binaryPath, scriptFile}, + getVirtualEnvExtraEnv(binaryPath), MINUTE); if (!run_result.checkSuccess(LOG)) { throw new InvalidSdkException(String.format("Failed to determine Python's sys.path value:\nSTDOUT: %s\nSTDERR: %s", run_result.getStdout(), @@ -789,15 +786,16 @@ public class PythonSdkType extends SdkType { return run_result.getStdoutLines(); } - // Returns a piece of env good as additional env for getProcessOutput. + /** + * Returns a piece of env good as additional env for getProcessOutput. + */ @Nullable - public static String[] getVirtualEnvAdditionalEnv(String bin_path) { - File virtualenv_root = getVirtualEnvRoot(bin_path); - String[] add_environment = null; - if (virtualenv_root != null) { - add_environment = new String[]{"PATH=" + virtualenv_root + File.pathSeparator}; + public static Map<String, String> getVirtualEnvExtraEnv(@NotNull String binaryPath) { + final File root = getVirtualEnvRoot(binaryPath); + if (root != null) { + return ImmutableMap.of("PATH", root.toString()); } - return add_environment; + return null; } @Nullable diff --git a/python/src/com/jetbrains/python/sdk/PythonSdkUpdater.java b/python/src/com/jetbrains/python/sdk/PythonSdkUpdater.java index b4b968fdb108..fb77cdb26f8f 100644 --- a/python/src/com/jetbrains/python/sdk/PythonSdkUpdater.java +++ b/python/src/com/jetbrains/python/sdk/PythonSdkUpdater.java @@ -32,7 +32,6 @@ import com.intellij.openapi.projectRoots.SdkModificator; import com.intellij.openapi.projectRoots.SdkTypeId; import com.intellij.openapi.roots.OrderRootType; import com.intellij.openapi.startup.StartupActivity; -import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.io.FileUtilRt; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; @@ -149,7 +148,7 @@ public class PythonSdkUpdater implements StartupActivity { } public static void updateSdk(@Nullable Project project, @Nullable Component ownerComponent, @NotNull final Sdk sdk, String skeletonsPath) throws InvalidSdkException { - PySkeletonRefresher.refreshSkeletonsOfSdk(project, ownerComponent, skeletonsPath, new Ref<Boolean>(false), sdk); // NOTE: whole thing would need a rename + PySkeletonRefresher.refreshSkeletonsOfSdk(project, ownerComponent, skeletonsPath, sdk); // NOTE: whole thing would need a rename if (!PySdkUtil.isRemote(sdk)) { updateSysPath(sdk); } diff --git a/python/src/com/jetbrains/python/sdk/flavors/WinPythonSdkFlavor.java b/python/src/com/jetbrains/python/sdk/flavors/WinPythonSdkFlavor.java index 9d22af8c03bf..3a9ac951509d 100644 --- a/python/src/com/jetbrains/python/sdk/flavors/WinPythonSdkFlavor.java +++ b/python/src/com/jetbrains/python/sdk/flavors/WinPythonSdkFlavor.java @@ -37,6 +37,8 @@ public class WinPythonSdkFlavor extends CPythonSdkFlavor { "HKEY_LOCAL_MACHINE\\SOFTWARE\\Wow6432Node\\Python\\PythonCore", "python.exe", "HKEY_LOCAL_MACHINE\\SOFTWARE\\IronPython", "ipy.exe"); + private static Set<String> ourRegistryCache; + private WinPythonSdkFlavor() { } @@ -78,18 +80,25 @@ public class WinPythonSdkFlavor extends CPythonSdkFlavor { } public static void findInRegistry(Collection<String> candidates) { - for (Map.Entry<String, String> entry : ourRegistryMap.entrySet()) { - final String prefix = entry.getKey(); - final String exePath = entry.getValue(); - List<String> strings = WindowsRegistryUtil.readRegistryBranch(prefix); - for (String string : strings) { - final String path = - WindowsRegistryUtil.readRegistryDefault(prefix + "\\" + string + - "\\InstallPath"); - if (path != null) { - File f = new File(path, exePath); - if (f.exists()) { - candidates.add(FileUtil.toSystemDependentName(f.getPath())); + fillRegistryCache(); + candidates.addAll(ourRegistryCache); + } + + private static void fillRegistryCache() { + if (ourRegistryCache == null) { + ourRegistryCache = new HashSet<String>(); + for (Map.Entry<String, String> entry : ourRegistryMap.entrySet()) { + final String prefix = entry.getKey(); + final String exePath = entry.getValue(); + List<String> strings = WindowsRegistryUtil.readRegistryBranch(prefix); + for (String string : strings) { + final String path = WindowsRegistryUtil.readRegistryDefault(prefix + "\\" + string + + "\\InstallPath"); + if (path != null) { + File f = new File(path, exePath); + if (f.exists()) { + ourRegistryCache.add(FileUtil.toSystemDependentName(f.getPath())); + } } } } diff --git a/python/src/com/jetbrains/python/sdk/skeletons/PySkeletonGenerator.java b/python/src/com/jetbrains/python/sdk/skeletons/PySkeletonGenerator.java index 901472e6d2c3..7e099041e16a 100644 --- a/python/src/com/jetbrains/python/sdk/skeletons/PySkeletonGenerator.java +++ b/python/src/com/jetbrains/python/sdk/skeletons/PySkeletonGenerator.java @@ -15,6 +15,7 @@ */ package com.jetbrains.python.sdk.skeletons; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; import com.intellij.execution.process.ProcessOutput; import com.intellij.openapi.application.ex.ApplicationManagerEx; @@ -52,21 +53,12 @@ public class PySkeletonGenerator { ENV_PATH_PARAM.put(IronPythonSdkFlavor.class, "IRONPYTHONPATH"); // TODO: Make strategy and move to PythonSdkFlavor? } - protected static final Logger LOG = Logger.getInstance("#" + PySkeletonGenerator.class.getName()); - - protected static final int MINUTE = 60 * 1000; - protected static final String GENERATOR3 = "generator3.py"; - private static final String[] EMPTY_ENVS = new String[0]; private final String mySkeletonsPath; - /** - * Env variables to be added to skeleton generator - */ - @NotNull - private final String[] myEnvs; + @NotNull private final Map<String, String> myEnv; public void finishSkeletonsGeneration() { } @@ -85,7 +77,6 @@ public class PySkeletonGenerator { } } - /** * @param skeletonPath path where skeletons should be generated * @param pySdk SDK @@ -94,11 +85,11 @@ public class PySkeletonGenerator { public PySkeletonGenerator(String skeletonPath, @NotNull final Sdk pySdk, @Nullable final String currentFolder) { mySkeletonsPath = skeletonPath; final PythonSdkFlavor flavor = PythonSdkFlavor.getFlavor(pySdk); - if ((currentFolder != null) && (flavor != null) && ENV_PATH_PARAM.containsKey(flavor.getClass())) { - myEnvs = new String[]{String.format("%s=%s", ENV_PATH_PARAM.get(flavor.getClass()), currentFolder)}; + if (currentFolder != null && flavor != null && ENV_PATH_PARAM.containsKey(flavor.getClass())) { + myEnv = ImmutableMap.of(ENV_PATH_PARAM.get(flavor.getClass()), currentFolder); } else { - myEnvs = EMPTY_ENVS; + myEnv = Collections.emptyMap(); } } @@ -171,23 +162,19 @@ public class PySkeletonGenerator { if (modfilename != null) { commandLine.add(modfilename); } - final List<String> envs = new ArrayList<String>(Arrays.asList(myEnvs)); - final String[] virtualEnvAdditionalEnv = PythonSdkType.getVirtualEnvAdditionalEnv(binaryPath); - if (virtualEnvAdditionalEnv != null) { - envs.addAll(Arrays.asList(virtualEnvAdditionalEnv)); - } + final Map<String, String> extraEnv = PythonSdkType.getVirtualEnvExtraEnv(binaryPath); + final Map<String, String> env = extraEnv != null ? PySdkUtil.mergeEnvVariables(myEnv, extraEnv) : myEnv; - return getProcessOutput(parent_dir, ArrayUtil.toStringArray(commandLine), envs.toArray(new String[envs.size()]), - MINUTE * 10 - ); + return getProcessOutput(parent_dir, ArrayUtil.toStringArray(commandLine), env, MINUTE * 10); } - protected ProcessOutput getProcessOutput(String homePath, String[] commandLine, String[] env, int timeout) throws InvalidSdkException { + protected ProcessOutput getProcessOutput(String homePath, String[] commandLine, Map<String, String> extraEnv, + int timeout) throws InvalidSdkException { return PySdkUtil.getProcessOutput( homePath, commandLine, - env, + extraEnv, timeout ); } @@ -207,7 +194,7 @@ public class PySkeletonGenerator { "-d", mySkeletonsPath, // output dir "-b", // for builtins }, - PythonSdkType.getVirtualEnvAdditionalEnv(binaryPath), MINUTE * 5 + PythonSdkType.getVirtualEnvExtraEnv(binaryPath), MINUTE * 5 ); runResult.checkSuccess(LOG); LOG.info("Rebuilding builtin skeletons took " + (System.currentTimeMillis() - startTime) + " ms"); @@ -228,7 +215,7 @@ public class PySkeletonGenerator { final ProcessOutput process = getProcessOutput(parentDir, ArrayUtil.toStringArray(cmd), - PythonSdkType.getVirtualEnvAdditionalEnv(homePath), + PythonSdkType.getVirtualEnvExtraEnv(homePath), MINUTE * 4); // see PY-3898 LOG.info("Retrieving binary module list took " + (System.currentTimeMillis() - startTime) + " ms"); diff --git a/python/src/com/jetbrains/python/sdk/skeletons/PySkeletonRefresher.java b/python/src/com/jetbrains/python/sdk/skeletons/PySkeletonRefresher.java index 4e4a43102202..b1e8ca19acf8 100644 --- a/python/src/com/jetbrains/python/sdk/skeletons/PySkeletonRefresher.java +++ b/python/src/com/jetbrains/python/sdk/skeletons/PySkeletonRefresher.java @@ -19,9 +19,6 @@ import com.google.common.base.Joiner; import com.google.common.collect.Lists; import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer; import com.intellij.execution.ExecutionException; -import com.intellij.notification.Notification; -import com.intellij.notification.NotificationType; -import com.intellij.notification.Notifications; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.PathManager; import com.intellij.openapi.diagnostic.Logger; @@ -31,7 +28,6 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.projectRoots.Sdk; import com.intellij.openapi.roots.OrderRootType; import com.intellij.openapi.util.Pair; -import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.SystemInfo; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.text.StringUtil; @@ -48,7 +44,6 @@ import com.jetbrains.python.PyNames; import com.jetbrains.python.codeInsight.userSkeletons.PyUserSkeletonsUtil; import com.jetbrains.python.packaging.PyExternalProcessException; import com.jetbrains.python.packaging.PyPackageManager; -import com.jetbrains.python.packaging.PyPackageManagerImpl; import com.jetbrains.python.psi.resolve.PythonSdkPathCache; import com.jetbrains.python.remote.PythonRemoteInterpreterManager; import com.jetbrains.python.sdk.InvalidSdkException; @@ -105,10 +100,6 @@ public class PySkeletonRefresher { private PySkeletonGenerator mySkeletonsGenerator; - public static void refreshSkeletonsOfSdk(@NotNull Project project, @NotNull Sdk sdk) throws InvalidSdkException { - refreshSkeletonsOfSdk(project, null, PythonSdkType.findSkeletonsPath(sdk), new Ref<Boolean>(false), sdk); - } - public static synchronized boolean isGeneratingSkeletons() { return ourGeneratingCount > 0; } @@ -120,7 +111,6 @@ public class PySkeletonRefresher { public static void refreshSkeletonsOfSdk(@Nullable Project project, Component ownerComponent, String skeletonsPath, - @Nullable Ref<Boolean> migrationFlag, @NotNull Sdk sdk) throws InvalidSdkException { final Map<String, List<String>> errors = new TreeMap<String, List<String>>(); @@ -137,7 +127,7 @@ public class PySkeletonRefresher { changeGeneratingSkeletons(1); try { - List<String> sdkErrors = refresher.regenerateSkeletons(checker, migrationFlag); + List<String> sdkErrors = refresher.regenerateSkeletons(checker); if (sdkErrors.size() > 0) { String sdkName = sdk.getName(); List<String> knownErrors = errors.get(sdkName); @@ -286,8 +276,7 @@ public class PySkeletonRefresher { return mySkeletonsPath; } - public List<String> regenerateSkeletons(@Nullable SkeletonVersionChecker cachedChecker, - @Nullable Ref<Boolean> migrationFlag) throws InvalidSdkException { + public List<String> regenerateSkeletons(@Nullable SkeletonVersionChecker cachedChecker) throws InvalidSdkException { final List<String> errorList = new SmartList<String>(); final String homePath = mySdk.getHomePath(); final String skeletonsPath = getSkeletonsPath(); @@ -299,14 +288,13 @@ public class PySkeletonRefresher { final String readablePath = FileUtil.getLocationRelativeToUserHome(homePath); mySkeletonsGenerator.prepare(); - myBlacklist = loadBlacklist(); indicate(PyBundle.message("sdk.gen.querying.$0", readablePath)); // get generator version and binary libs list in one go - final PySkeletonGenerator.ListBinariesResult binaries = - mySkeletonsGenerator.listBinaries(mySdk, calculateExtraSysPath(mySdk, getSkeletonsPath())); + final String extraSysPath = calculateExtraSysPath(mySdk, getSkeletonsPath()); + final PySkeletonGenerator.ListBinariesResult binaries = mySkeletonsGenerator.listBinaries(mySdk, extraSysPath); myGeneratorVersion = binaries.generatorVersion; myPregeneratedSkeletons = findPregeneratedSkeletons(); @@ -325,77 +313,19 @@ public class PySkeletonRefresher { final SkeletonHeader oldHeader = readSkeletonHeader(builtinsFile); final boolean oldOrNonExisting = oldHeader == null || oldHeader.getVersion() == 0; - if (migrationFlag != null && !migrationFlag.get() && oldOrNonExisting) { - migrationFlag.set(true); - Notifications.Bus.notify( - new Notification( - PythonSdkType.SKELETONS_TOPIC, PyBundle.message("sdk.gen.notify.converting.old.skels"), - PyBundle.message("sdk.gen.notify.converting.text"), - NotificationType.INFORMATION - ) - ); - } - if (myPregeneratedSkeletons != null && oldOrNonExisting) { - indicate("Unpacking pregenerated skeletons..."); - try { - final VirtualFile jar = JarFileSystem.getInstance().getVirtualFileForJar(myPregeneratedSkeletons); - if (jar != null) { - ZipUtil.extract(new File(jar.getPath()), - new File(getSkeletonsPath()), null); - } - } - catch (IOException e) { - LOG.info("Error unpacking pregenerated skeletons", e); - } + unpackPreGeneratedSkeletons(); } if (oldOrNonExisting) { - final Sdk base = PythonSdkType.getInstance().getVirtualEnvBaseSdk(mySdk); - if (base != null) { - indicate("Copying base SDK skeletons for virtualenv..."); - final String baseSkeletonsPath = PythonSdkType.getSkeletonsPath(PathManager.getSystemPath(), base.getHomePath()); - final PySkeletonGenerator.ListBinariesResult baseBinaries = - mySkeletonsGenerator.listBinaries(base, calculateExtraSysPath(base, baseSkeletonsPath)); - for (Map.Entry<String, PyBinaryItem> entry : binaries.modules.entrySet()) { - final String module = entry.getKey(); - final PyBinaryItem binary = entry.getValue(); - final PyBinaryItem baseBinary = baseBinaries.modules.get(module); - final File fromFile = getSkeleton(module, baseSkeletonsPath); - if (baseBinaries.modules.containsKey(module) && - fromFile.exists() && - binary.length() == baseBinary.length()) { // Weak binary modules equality check - final File toFile = fromFile.isDirectory() ? - getPackageSkeleton(module, skeletonsPath) : - getModuleSkeleton(module, skeletonsPath); - try { - FileUtil.copy(fromFile, toFile); - } - catch (IOException e) { - LOG.info("Error copying base virtualenv SDK skeleton for " + module, e); - } - } - } - } + copyBaseSdkSkeletonsToVirtualEnv(skeletonsPath, binaries); } - final SkeletonHeader newHeader = readSkeletonHeader(builtinsFile); - final boolean mustUpdateBuiltins = myPregeneratedSkeletons == null && - (newHeader == null || newHeader.getVersion() < myVersionChecker.getBuiltinVersion()); - if (mustUpdateBuiltins) { - indicate(PyBundle.message("sdk.gen.updating.builtins.$0", readablePath)); - mySkeletonsGenerator.generateBuiltinSkeletons(mySdk); - if (myProject != null) { - PythonSdkPathCache.getInstance(myProject, mySdk).clearBuiltins(); - } - } + final boolean builtinsUpdated = updateSkeletonsForBuiltins(readablePath, builtinsFile); if (!binaries.modules.isEmpty()) { - indicate(PyBundle.message("sdk.gen.updating.$0", readablePath)); - - List<UpdateResult> updateErrors = updateOrCreateSkeletons(binaries.modules); //Skeletons regeneration - + final List<UpdateResult> updateErrors = updateOrCreateSkeletons(binaries.modules); if (updateErrors.size() > 0) { indicateMinor(BLACKLIST_FILE_NAME); for (UpdateResult error : updateErrors) { @@ -410,7 +340,6 @@ public class PySkeletonRefresher { } indicate(PyBundle.message("sdk.gen.reloading")); - mySkeletonsGenerator.refreshGeneratedSkeletons(); if (!oldOrNonExisting) { @@ -419,14 +348,15 @@ public class PySkeletonRefresher { } if (PySdkUtil.isRemote(mySdk)) { try { - ((PyPackageManagerImpl)PyPackageManager.getInstance(mySdk)).loadPackages(); + // Force loading packages + PyPackageManager.getInstance(mySdk).getPackages(false); } catch (PyExternalProcessException e) { // ignore - already logged } } - if ((mustUpdateBuiltins || PySdkUtil.isRemote(mySdk)) && myProject != null) { + if ((builtinsUpdated || PySdkUtil.isRemote(mySdk)) && myProject != null) { ApplicationManager.getApplication().invokeLater(new Runnable() { @Override public void run() { @@ -438,6 +368,64 @@ public class PySkeletonRefresher { return errorList; } + private boolean updateSkeletonsForBuiltins(String readablePath, File builtinsFile) throws InvalidSdkException { + final SkeletonHeader newHeader = readSkeletonHeader(builtinsFile); + final boolean mustUpdateBuiltins = myPregeneratedSkeletons == null && + (newHeader == null || newHeader.getVersion() < myVersionChecker.getBuiltinVersion()); + if (mustUpdateBuiltins) { + indicate(PyBundle.message("sdk.gen.updating.builtins.$0", readablePath)); + mySkeletonsGenerator.generateBuiltinSkeletons(mySdk); + if (myProject != null) { + PythonSdkPathCache.getInstance(myProject, mySdk).clearBuiltins(); + } + } + return mustUpdateBuiltins; + } + + private void copyBaseSdkSkeletonsToVirtualEnv(String skeletonsPath, PySkeletonGenerator.ListBinariesResult binaries) + throws InvalidSdkException { + final Sdk base = PythonSdkType.getInstance().getVirtualEnvBaseSdk(mySdk); + if (base != null) { + indicate("Copying base SDK skeletons for virtualenv..."); + final String baseSkeletonsPath = PythonSdkType.getSkeletonsPath(PathManager.getSystemPath(), base.getHomePath()); + final PySkeletonGenerator.ListBinariesResult baseBinaries = + mySkeletonsGenerator.listBinaries(base, calculateExtraSysPath(base, baseSkeletonsPath)); + for (Map.Entry<String, PyBinaryItem> entry : binaries.modules.entrySet()) { + final String module = entry.getKey(); + final PyBinaryItem binary = entry.getValue(); + final PyBinaryItem baseBinary = baseBinaries.modules.get(module); + final File fromFile = getSkeleton(module, baseSkeletonsPath); + if (baseBinaries.modules.containsKey(module) && + fromFile.exists() && + binary.length() == baseBinary.length()) { // Weak binary modules equality check + final File toFile = fromFile.isDirectory() ? + getPackageSkeleton(module, skeletonsPath) : + getModuleSkeleton(module, skeletonsPath); + try { + FileUtil.copy(fromFile, toFile); + } + catch (IOException e) { + LOG.info("Error copying base virtualenv SDK skeleton for " + module, e); + } + } + } + } + } + + private void unpackPreGeneratedSkeletons() throws InvalidSdkException { + indicate("Unpacking pregenerated skeletons..."); + try { + final VirtualFile jar = JarFileSystem.getInstance().getVirtualFileForJar(myPregeneratedSkeletons); + if (jar != null) { + ZipUtil.extract(new File(jar.getPath()), + new File(getSkeletonsPath()), null); + } + } + catch (IOException e) { + LOG.info("Error unpacking pregenerated skeletons", e); + } + } + @Nullable public static SkeletonHeader readSkeletonHeader(@NotNull File file) { try { @@ -835,7 +823,7 @@ public class PySkeletonRefresher { return null; } LOG.info("Pregenerated skeletons root is " + root); - final String versionString = mySdk.getVersionString(); + @NonNls final String versionString = mySdk.getVersionString(); if (versionString == null) { return null; } diff --git a/python/src/com/jetbrains/python/statistics/PyPackageUsagesCollector.java b/python/src/com/jetbrains/python/statistics/PyPackageUsagesCollector.java index f8b4adde5b33..8e029e042aa0 100644 --- a/python/src/com/jetbrains/python/statistics/PyPackageUsagesCollector.java +++ b/python/src/com/jetbrains/python/statistics/PyPackageUsagesCollector.java @@ -25,7 +25,7 @@ import com.intellij.openapi.module.ModuleManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.projectRoots.Sdk; import com.jetbrains.python.packaging.PyPIPackageUtil; -import com.jetbrains.python.packaging.PyPackageManagerImpl; +import com.jetbrains.python.packaging.PyPackageManager; import com.jetbrains.python.packaging.PyRequirement; import com.jetbrains.python.sdk.PythonSdkType; import org.jetbrains.annotations.NotNull; @@ -46,12 +46,12 @@ public class PyPackageUsagesCollector extends AbstractApplicationUsagesCollector public Set<UsageDescriptor> getProjectUsages(@NotNull Project project) throws CollectUsagesException { final Set<UsageDescriptor> result = new HashSet<UsageDescriptor>(); for(final Module m: ModuleManager.getInstance(project).getModules()) { - Sdk pythonSdk = PythonSdkType.findPythonSdk(m); + final Sdk pythonSdk = PythonSdkType.findPythonSdk(m); if (pythonSdk != null) { ApplicationManager.getApplication().runReadAction(new Runnable() { @Override public void run() { - List<PyRequirement> requirements = PyPackageManagerImpl.getRequirements(m); + List<PyRequirement> requirements = PyPackageManager.getInstance(pythonSdk).getRequirements(m); if (requirements != null) { Collection<String> packages = new HashSet<String>(PyPIPackageUtil.INSTANCE.getPackageNames()); for (PyRequirement requirement : requirements) { diff --git a/python/src/com/jetbrains/python/structureView/PyStructureViewElement.java b/python/src/com/jetbrains/python/structureView/PyStructureViewElement.java index 93c44f7f2a17..de9f7bf5a5ca 100644 --- a/python/src/com/jetbrains/python/structureView/PyStructureViewElement.java +++ b/python/src/com/jetbrains/python/structureView/PyStructureViewElement.java @@ -23,7 +23,6 @@ import com.intellij.openapi.editor.colors.TextAttributesKey; import com.intellij.psi.PsiElement; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.ui.LayeredIcon; -import com.intellij.util.Function; import com.jetbrains.python.PyNames; import com.jetbrains.python.psi.*; import icons.PythonIcons; @@ -33,9 +32,6 @@ import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.util.*; -import static com.intellij.openapi.util.text.StringUtil.join; -import static com.intellij.openapi.util.text.StringUtil.notNullize; - /** * Handles nodes in Structure View. * @author yole @@ -237,32 +233,15 @@ public class PyStructureViewElement implements StructureViewTreeElement { @NotNull @Override public ItemPresentation getPresentation() { + final ItemPresentation presentation = myElement.getPresentation(); return new ColoredItemPresentation() { + @Nullable + @Override public String getPresentableText() { - final String unnamed = "<unnamed>"; - if (myElement instanceof PyFunction) { - PyParameterList argList = ((PyFunction) myElement).getParameterList(); - StringBuilder result = new StringBuilder(notNullize(myElement.getName(), unnamed)); - result.append(argList.getPresentableText(true)); - return result.toString(); - } - else if (myElement instanceof PyClass && myElement.isValid()) { - PyClass c = (PyClass) myElement; - StringBuilder result = new StringBuilder(notNullize(c.getName(), unnamed)); - PyExpression[] superClassExpressions = c.getSuperClassExpressions(); - if (superClassExpressions.length > 0) { - result.append("("); - result.append(join(Arrays.asList(superClassExpressions), new Function<PyExpression, String>() { - public String fun(PyExpression expr) { - String name = expr.getText(); - return notNullize(name, unnamed); - } - }, ", ")); - result.append(")"); - } - return result.toString(); + if (myElement instanceof PyFile) { + return myElement.getName(); } - return notNullize(myElement.getName(), unnamed); + return presentation != null ? presentation.getPresentableText() : PyNames.UNNAMED_ELEMENT; } @Nullable diff --git a/python/src/com/jetbrains/python/testing/PyRerunFailedTestsAction.java b/python/src/com/jetbrains/python/testing/PyRerunFailedTestsAction.java index 0b922514e52e..01c8ac68540c 100644 --- a/python/src/com/jetbrains/python/testing/PyRerunFailedTestsAction.java +++ b/python/src/com/jetbrains/python/testing/PyRerunFailedTestsAction.java @@ -28,33 +28,29 @@ import com.intellij.execution.testframework.actions.AbstractRerunFailedTestsActi import com.intellij.openapi.module.Module; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.ComponentContainer; +import com.intellij.psi.PsiElement; +import com.intellij.psi.search.GlobalSearchScope; import com.jetbrains.python.run.AbstractPythonRunConfiguration; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; +import java.util.*; -/* - * User: ktisha - */ public class PyRerunFailedTestsAction extends AbstractRerunFailedTestsAction { - protected PyRerunFailedTestsAction(@NotNull ComponentContainer componentContainer) { super(componentContainer); } @Override @Nullable - public MyRunProfile getRunProfile() { + protected MyRunProfile getRunProfile(@NotNull ExecutionEnvironment environment) { final TestFrameworkRunningModel model = getModel(); - if (model == null) return null; - final AbstractPythonRunConfiguration configuration = (AbstractPythonRunConfiguration)model.getProperties().getConfiguration(); - return new MyTestRunProfile(configuration); + if (model == null) { + return null; + } + return new MyTestRunProfile((AbstractPythonRunConfiguration)model.getProperties().getConfiguration()); } - private class MyTestRunProfile extends MyRunProfile { public MyTestRunProfile(RunConfigurationBase configuration) { @@ -71,6 +67,19 @@ public class PyRerunFailedTestsAction extends AbstractRerunFailedTestsAction { @Override public RunProfileState getState(@NotNull Executor executor, @NotNull ExecutionEnvironment env) throws ExecutionException { final AbstractPythonRunConfiguration configuration = ((AbstractPythonRunConfiguration)getPeer()); + + // If configuration wants to take care about rerun itself + if (configuration instanceof TestRunConfigurationReRunResponsible) { + // TODO: Extract method + final Set<PsiElement> failedTestElements = new HashSet<PsiElement>(); + for (final AbstractTestProxy proxy : getFailedTests(getProject())) { + final Location<?> location = proxy.getLocation(getProject(), GlobalSearchScope.allScope(getProject())); + if (location != null) { + failedTestElements.add(location.getPsiElement()); + } + } + return ((TestRunConfigurationReRunResponsible)configuration).rerunTests(executor, env, failedTestElements); + } return new FailedPythonTestCommandLineStateBase(configuration, env, (PythonTestCommandLineStateBase)configuration.getState(executor, env)); } diff --git a/python/src/com/jetbrains/python/testing/PythonTestCommandLineStateBase.java b/python/src/com/jetbrains/python/testing/PythonTestCommandLineStateBase.java index 1a5aafe59051..c73a488dc7f5 100644 --- a/python/src/com/jetbrains/python/testing/PythonTestCommandLineStateBase.java +++ b/python/src/com/jetbrains/python/testing/PythonTestCommandLineStateBase.java @@ -63,6 +63,7 @@ public abstract class PythonTestCommandLineStateBase extends PythonCommandLineSt myConfiguration = configuration; } + @Override @NotNull protected ConsoleView createAndAttachConsole(Project project, ProcessHandler processHandler, Executor executor) throws ExecutionException { @@ -89,6 +90,7 @@ public abstract class PythonTestCommandLineStateBase extends PythonCommandLineSt return new PythonTRunnerConsoleProperties(myConfiguration, executor, false); } + @Override public GeneralCommandLine generateCommandLine() throws ExecutionException { GeneralCommandLine cmd = super.generateCommandLine(); @@ -135,7 +137,7 @@ public abstract class PythonTestCommandLineStateBase extends PythonCommandLineSt PyRerunFailedTestsAction rerunFailedTestsAction = new PyRerunFailedTestsAction(console); if (console instanceof SMTRunnerConsoleView) { - rerunFailedTestsAction.init(((BaseTestsOutputConsoleView)console).getProperties(), getEnvironment()); + rerunFailedTestsAction.init(((BaseTestsOutputConsoleView)console).getProperties()); rerunFailedTestsAction.setModelProvider(new Getter<TestFrameworkRunningModel>() { @Override public TestFrameworkRunningModel get() { diff --git a/python/src/com/jetbrains/python/testing/TestRunConfigurationReRunResponsible.java b/python/src/com/jetbrains/python/testing/TestRunConfigurationReRunResponsible.java new file mode 100644 index 000000000000..a8e9e1f8d5e4 --- /dev/null +++ b/python/src/com/jetbrains/python/testing/TestRunConfigurationReRunResponsible.java @@ -0,0 +1,30 @@ +package com.jetbrains.python.testing; + +import com.intellij.execution.ExecutionException; +import com.intellij.execution.Executor; +import com.intellij.execution.configurations.RunProfileState; +import com.intellij.execution.runners.ExecutionEnvironment; +import com.intellij.psi.PsiElement; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; + +/** + * Configuration that handles rerun failed tests itself. + * + * @author Ilya.Kazakevich + */ +public interface TestRunConfigurationReRunResponsible { + /** + * Rerun failed tests + * @param executor test executor + * @param environment test environment + * @param failedTests a pack of psi elements, indicating failed tests (to retrn) + * @return state to run or null if no rerun actions found (i.e. no errors in failedTest, empty etc) + * @throws ExecutionException failed to run + */ + @Nullable + RunProfileState rerunTests(@NotNull final Executor executor, @NotNull final ExecutionEnvironment environment, + @NotNull Collection<PsiElement> failedTests) throws ExecutionException; +} diff --git a/python/src/com/jetbrains/python/testing/VFSTestFrameworkListener.java b/python/src/com/jetbrains/python/testing/VFSTestFrameworkListener.java index 9d763e2ce286..64adf2093028 100644 --- a/python/src/com/jetbrains/python/testing/VFSTestFrameworkListener.java +++ b/python/src/com/jetbrains/python/testing/VFSTestFrameworkListener.java @@ -35,7 +35,6 @@ import com.intellij.util.ui.update.Update; import com.jetbrains.python.PyNames; import com.jetbrains.python.packaging.PyExternalProcessException; import com.jetbrains.python.packaging.PyPackageManager; -import com.jetbrains.python.packaging.PyPackageManagerImpl; import com.jetbrains.python.sdk.PySdkUtil; import com.jetbrains.python.sdk.PythonSdkType; import org.jetbrains.annotations.NotNull; @@ -131,9 +130,9 @@ public class VFSTestFrameworkListener implements ApplicationComponent { LOG.info("Searching test runner in empty sdk"); return null; } - final PyPackageManagerImpl packageManager = (PyPackageManagerImpl)PyPackageManager.getInstance(sdk); + final PyPackageManager packageManager = PyPackageManager.getInstance(sdk); try { - return packageManager.findInstalledPackage(testPackageName) != null; + return packageManager.findPackage(testPackageName, false) != null; } catch (PyExternalProcessException e) { LOG.info("Can't load package list " + e.getMessage()); diff --git a/python/src/com/jetbrains/python/testing/pytest/PyTestConfigurationProducer.java b/python/src/com/jetbrains/python/testing/pytest/PyTestConfigurationProducer.java index f988dbd315e6..8dc27d4fd995 100644 --- a/python/src/com/jetbrains/python/testing/pytest/PyTestConfigurationProducer.java +++ b/python/src/com/jetbrains/python/testing/pytest/PyTestConfigurationProducer.java @@ -31,7 +31,6 @@ import com.intellij.webcore.packaging.PackageVersionComparator; import com.jetbrains.python.packaging.PyExternalProcessException; import com.jetbrains.python.packaging.PyPackage; import com.jetbrains.python.packaging.PyPackageManager; -import com.jetbrains.python.packaging.PyPackageManagerImpl; import com.jetbrains.python.psi.PyClass; import com.jetbrains.python.psi.PyFile; import com.jetbrains.python.psi.PyFunction; @@ -97,9 +96,9 @@ public class PyTestConfigurationProducer extends PythonTestConfigurationProducer if (pyFunction != null) { keywords = pyFunction.getName(); if (pyClass != null) { - final PyPackageManagerImpl packageManager = (PyPackageManagerImpl)PyPackageManager.getInstance(sdk); + final PyPackageManager packageManager = PyPackageManager.getInstance(sdk); try { - final PyPackage pytestPackage = packageManager.findInstalledPackage("pytest"); + final PyPackage pytestPackage = packageManager.findPackage("pytest", false); if (pytestPackage != null && PackageVersionComparator.VERSION_COMPARATOR.compare(pytestPackage.getVersion(), "2.3.3") >= 0) { keywords = pyClass.getName() + " and " + keywords; } diff --git a/python/src/com/jetbrains/python/validation/Pep8ExternalAnnotator.java b/python/src/com/jetbrains/python/validation/Pep8ExternalAnnotator.java index a7faa6ebb98f..22bb1d308d2c 100644 --- a/python/src/com/jetbrains/python/validation/Pep8ExternalAnnotator.java +++ b/python/src/com/jetbrains/python/validation/Pep8ExternalAnnotator.java @@ -15,6 +15,7 @@ */ package com.jetbrains.python.validation; +import com.google.common.collect.ImmutableMap; import com.intellij.codeHighlighting.HighlightDisplayLevel; import com.intellij.codeInsight.daemon.HighlightDisplayKey; import com.intellij.codeInsight.intention.IntentionAction; @@ -173,7 +174,7 @@ public class Pep8ExternalAnnotator extends ExternalAnnotator<Pep8ExternalAnnotat options.add("-"); ProcessOutput output = PySdkUtil.getProcessOutput(new File(collectedInfo.interpreterPath).getParent(), ArrayUtil.toStringArray(options), - new String[] { "PYTHONUNBUFFERED=1" }, + ImmutableMap.of("PYTHONBUFFERED", "1"), 10000, collectedInfo.fileText.getBytes(), false); diff --git a/python/src/com/jetbrains/python/validation/StringLiteralQuotesAnnotator.java b/python/src/com/jetbrains/python/validation/StringLiteralQuotesAnnotator.java index 0666e8319caa..997123351d36 100644 --- a/python/src/com/jetbrains/python/validation/StringLiteralQuotesAnnotator.java +++ b/python/src/com/jetbrains/python/validation/StringLiteralQuotesAnnotator.java @@ -18,8 +18,10 @@ package com.jetbrains.python.validation; import com.intellij.lang.ASTNode; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.text.StringUtil; +import com.jetbrains.python.PyBundle; import com.jetbrains.python.psi.PyStringLiteralExpression; import com.jetbrains.python.psi.impl.PyStringLiteralExpressionImpl; +import org.jetbrains.annotations.NotNull; import java.util.List; @@ -29,17 +31,16 @@ import java.util.List; * @author dcheryasov */ public class StringLiteralQuotesAnnotator extends PyAnnotator { - public static final String MISSING_Q = "Missing closing quote"; private static final String TRIPLE_QUOTES = "\"\"\""; private static final String TRIPLE_APOS = "'''"; public void visitPyStringLiteralExpression(final PyStringLiteralExpression node) { - List<ASTNode> stringNodes = node.getStringNodes(); + final List<ASTNode> stringNodes = node.getStringNodes(); for (ASTNode stringNode : stringNodes) { - boolean foundError; - String nodeText = stringNode.getText(); - int index = PyStringLiteralExpressionImpl.getPrefixLength(nodeText); - String unprefixed = nodeText.substring(index); + final String nodeText = stringNode.getText(); + final int index = PyStringLiteralExpressionImpl.getPrefixLength(nodeText); + final String unprefixed = nodeText.substring(index); + final boolean foundError; if (StringUtil.startsWith(unprefixed, TRIPLE_QUOTES)) { foundError = checkTripleQuotedString(stringNode, unprefixed, TRIPLE_QUOTES); } @@ -49,23 +50,33 @@ public class StringLiteralQuotesAnnotator extends PyAnnotator { else { foundError = checkQuotedString(stringNode, unprefixed); } - if (foundError) break; + if (foundError) { + break; + } } } - private boolean checkQuotedString(ASTNode stringNode, String nodeText) { - char firstQuote = nodeText.charAt(0); - int lastChar = nodeText.length()-1; - if (lastChar == 0 || nodeText.charAt(lastChar) != firstQuote || - (nodeText.charAt(lastChar-1) == '\\' && (lastChar == 1 || nodeText.charAt(lastChar-2) != '\\'))) { - getHolder().createErrorAnnotation(stringNode, MISSING_Q + " [" + firstQuote + "]"); + private boolean checkQuotedString(@NotNull ASTNode stringNode, @NotNull String nodeText) { + final char firstQuote = nodeText.charAt(0); + final char lastChar = nodeText.charAt(nodeText.length() - 1); + int precedingBackslashCount = 0; + for (int i = nodeText.length() - 2; i >= 0; i--) { + if (nodeText.charAt(i) == '\\') { + precedingBackslashCount++; + } + else { + break; + } + } + if (nodeText.length() == 1 || lastChar != firstQuote || precedingBackslashCount % 2 != 0) { + getHolder().createErrorAnnotation(stringNode, PyBundle.message("ANN.missing.closing.quote", firstQuote)); return true; } return false; } - private boolean checkTripleQuotedString(ASTNode stringNode, String text, final String quotes) { - if (text.length() < 6 || !text.endsWith(quotes)) { + private boolean checkTripleQuotedString(@NotNull ASTNode stringNode, @NotNull String text, @NotNull String quotes) { + if (text.length() < 6 || !text.endsWith(quotes)) { int startOffset = StringUtil.trimTrailing(stringNode.getText()).lastIndexOf('\n'); if (startOffset < 0) { startOffset = stringNode.getTextRange().getStartOffset(); @@ -73,8 +84,8 @@ public class StringLiteralQuotesAnnotator extends PyAnnotator { else { startOffset = stringNode.getTextRange().getStartOffset() + startOffset + 1; } - TextRange highlightRange = new TextRange(startOffset, stringNode.getTextRange().getEndOffset()); - getHolder().createErrorAnnotation(highlightRange, "Missing closing triple quotes"); + final TextRange highlightRange = new TextRange(startOffset, stringNode.getTextRange().getEndOffset()); + getHolder().createErrorAnnotation(highlightRange, PyBundle.message("ANN.missing.closing.triple.quotes")); return true; } return false; diff --git a/python/testData/codeInsight/smartEnter/withOnlyColonMissing.py b/python/testData/codeInsight/smartEnter/withOnlyColonMissing.py new file mode 100644 index 000000000000..7fc16ea3bb68 --- /dev/null +++ b/python/testData/codeInsight/smartEnter/withOnlyColonMissing.py @@ -0,0 +1 @@ +with open('file.txt') as f<caret>
\ No newline at end of file diff --git a/python/testData/codeInsight/smartEnter/withOnlyColonMissing_after.py b/python/testData/codeInsight/smartEnter/withOnlyColonMissing_after.py new file mode 100644 index 000000000000..09a090ef65d3 --- /dev/null +++ b/python/testData/codeInsight/smartEnter/withOnlyColonMissing_after.py @@ -0,0 +1,2 @@ +with open('file.txt') as f: + <caret>
\ No newline at end of file diff --git a/python/testData/hierarchy/call/Static/ArgumentList/ArgumentList_callee_verification.xml b/python/testData/hierarchy/call/Static/ArgumentList/ArgumentList_callee_verification.xml new file mode 100644 index 000000000000..28d3684cdb10 --- /dev/null +++ b/python/testData/hierarchy/call/Static/ArgumentList/ArgumentList_callee_verification.xml @@ -0,0 +1 @@ +<node text="target_func() (hierarchy.call.Static.ArgumentList.file_1)" base="true"/>
\ No newline at end of file diff --git a/python/testData/hierarchy/call/Static/ArgumentList/ArgumentList_caller_verification.xml b/python/testData/hierarchy/call/Static/ArgumentList/ArgumentList_caller_verification.xml new file mode 100644 index 000000000000..28d3684cdb10 --- /dev/null +++ b/python/testData/hierarchy/call/Static/ArgumentList/ArgumentList_caller_verification.xml @@ -0,0 +1 @@ +<node text="target_func() (hierarchy.call.Static.ArgumentList.file_1)" base="true"/>
\ No newline at end of file diff --git a/python/testData/hierarchy/call/Static/ArgumentList/file_1.py b/python/testData/hierarchy/call/Static/ArgumentList/file_1.py new file mode 100644 index 000000000000..ea497204d2ca --- /dev/null +++ b/python/testData/hierarchy/call/Static/ArgumentList/file_1.py @@ -0,0 +1,8 @@ +def target_func(): + pass + +def func1(f): + f() + +def func2(f): + return f
\ No newline at end of file diff --git a/python/testData/hierarchy/call/Static/ArgumentList/main.py b/python/testData/hierarchy/call/Static/ArgumentList/main.py new file mode 100644 index 000000000000..b479ea6f9d3a --- /dev/null +++ b/python/testData/hierarchy/call/Static/ArgumentList/main.py @@ -0,0 +1,8 @@ +from file_1 import * + +target_<caret>func() +func1(target_func) +func1(target_func()) +func2(target_func) +func2(target_func()) + diff --git a/python/testData/hierarchy/call/Static/Constructor/Constructor_callee_verification.xml b/python/testData/hierarchy/call/Static/Constructor/Constructor_callee_verification.xml new file mode 100644 index 000000000000..1b720a99aee9 --- /dev/null +++ b/python/testData/hierarchy/call/Static/Constructor/Constructor_callee_verification.xml @@ -0,0 +1,8 @@ +<node text="A.__init__(self) (hierarchy.call.Static.Constructor.main)" base="true"> + <node text="invoke1(p) (hierarchy.call.Static.Constructor.main)"> + <node text="A.method1(self) (hierarchy.call.Static.Constructor.main)"/> + </node> + <node text="invoke2(p) (hierarchy.call.Static.Constructor.main)"> + <node text="A.method2(self) (hierarchy.call.Static.Constructor.main)"/> + </node> +</node>
\ No newline at end of file diff --git a/python/testData/hierarchy/call/Static/Constructor/Constructor_caller_verification.xml b/python/testData/hierarchy/call/Static/Constructor/Constructor_caller_verification.xml new file mode 100644 index 000000000000..b872e20e6c7b --- /dev/null +++ b/python/testData/hierarchy/call/Static/Constructor/Constructor_caller_verification.xml @@ -0,0 +1,6 @@ +<node text="A.__init__(self) (hierarchy.call.Static.Constructor.main)" base="true"> + <node text="invokeA() (hierarchy.call.Static.Constructor.main)"> + <node text="C.bar(self) (hierarchy.call.Static.Constructor.main)"/> + </node> + <node text="C.bar(self) (hierarchy.call.Static.Constructor.main)"/> +</node>
\ No newline at end of file diff --git a/python/testData/hierarchy/call/Static/Constructor/main.py b/python/testData/hierarchy/call/Static/Constructor/main.py new file mode 100644 index 000000000000..1c2e14776c25 --- /dev/null +++ b/python/testData/hierarchy/call/Static/Constructor/main.py @@ -0,0 +1,32 @@ +class A(): + def __init__(self): + invoke1(self) + invoke2(self) + + def method1(self): + pass + + def method2(self): + pass + +def invoke1(p): + p.method1() + + +def invoke2(p): + p.method2() + + +def invokeA(): + a = A() + a.method1() + a.method2() + + def new_class_func(): + class C(): + def bar(self): + invokeA(A()) + return C() + +a = A() +A.__init_<caret>_(a)
\ No newline at end of file diff --git a/python/testData/hierarchy/call/Static/DefaultValue/DefaultValue_callee_verification.xml b/python/testData/hierarchy/call/Static/DefaultValue/DefaultValue_callee_verification.xml new file mode 100644 index 000000000000..23fc966f8913 --- /dev/null +++ b/python/testData/hierarchy/call/Static/DefaultValue/DefaultValue_callee_verification.xml @@ -0,0 +1 @@ +<node text="target_func() (hierarchy.call.Static.DefaultValue.main)" base="true"/>
\ No newline at end of file diff --git a/python/testData/hierarchy/call/Static/DefaultValue/DefaultValue_caller_verification.xml b/python/testData/hierarchy/call/Static/DefaultValue/DefaultValue_caller_verification.xml new file mode 100644 index 000000000000..2cb93be11635 --- /dev/null +++ b/python/testData/hierarchy/call/Static/DefaultValue/DefaultValue_caller_verification.xml @@ -0,0 +1,4 @@ +<node text="target_func() (hierarchy.call.Static.DefaultValue.main)" base="true"> + <node text="func2() (hierarchy.call.Static.DefaultValue.main)"/> + <node text="func4() (hierarchy.call.Static.DefaultValue.main)"/> +</node>
\ No newline at end of file diff --git a/python/testData/hierarchy/call/Static/DefaultValue/main.py b/python/testData/hierarchy/call/Static/DefaultValue/main.py new file mode 100644 index 000000000000..223642493ef4 --- /dev/null +++ b/python/testData/hierarchy/call/Static/DefaultValue/main.py @@ -0,0 +1,30 @@ +def target_func(): + pass + + +def func1(): + def inner_func1(x=target_func): + pass + + return inner_func1(target_func) + + +def func2(): + def inner_func2(x=target_func()): + pass + + return inner_func2(target_func) + + +def func3(x=target_func()): + pass + + +def func4(): + def inner_func4(x=target_func): + pass + + return inner_func4(target_func()) + + +target_<caret>func()
\ No newline at end of file diff --git a/python/testData/hierarchy/call/Static/Inheritance/Inheritance_callee_verification.xml b/python/testData/hierarchy/call/Static/Inheritance/Inheritance_callee_verification.xml new file mode 100644 index 000000000000..cf65c63a4d04 --- /dev/null +++ b/python/testData/hierarchy/call/Static/Inheritance/Inheritance_callee_verification.xml @@ -0,0 +1 @@ +<node text="A.target_func(self) (hierarchy.call.Static.Inheritance.main)" base="true"/>
\ No newline at end of file diff --git a/python/testData/hierarchy/call/Static/Inheritance/Inheritance_caller_verification.xml b/python/testData/hierarchy/call/Static/Inheritance/Inheritance_caller_verification.xml new file mode 100644 index 000000000000..a81d537dcc27 --- /dev/null +++ b/python/testData/hierarchy/call/Static/Inheritance/Inheritance_caller_verification.xml @@ -0,0 +1,4 @@ +<node text="A.target_func(self) (hierarchy.call.Static.Inheritance.main)" base="true"> + <node text="C.func(self, a) (hierarchy.call.Static.Inheritance.main)"/> + <node text="foo2(b) (hierarchy.call.Static.Inheritance.main)"/> +</node>
\ No newline at end of file diff --git a/python/testData/hierarchy/call/Static/Inheritance/main.py b/python/testData/hierarchy/call/Static/Inheritance/main.py new file mode 100644 index 000000000000..89c37de9bceb --- /dev/null +++ b/python/testData/hierarchy/call/Static/Inheritance/main.py @@ -0,0 +1,36 @@ + +class A(object): + def target_func(self): + pass + + +class B(A): + pass + + +class C(object): + def func(self, a): + a.target_func() + + +def foo1(b): + f = b.target_func + + +def foo2(b): + b.target_func() + + +def bar1(*args): + pass + + +def bar2(*args): + pass + + +b = B() +foo1(b) +foo2(b) +bar1(b.target_<caret>func) +bar2(b.target_func())
\ No newline at end of file diff --git a/python/testData/hierarchy/call/Static/InnerFunction/InnerFunction_callee_verification.xml b/python/testData/hierarchy/call/Static/InnerFunction/InnerFunction_callee_verification.xml new file mode 100644 index 000000000000..551a5bd0fdf0 --- /dev/null +++ b/python/testData/hierarchy/call/Static/InnerFunction/InnerFunction_callee_verification.xml @@ -0,0 +1,5 @@ +<node text="target_func() (hierarchy.call.Static.InnerFunction.main)" base="true"> + <node text="foo() (hierarchy.call.Static.InnerFunction.main)"> + <node text="bar() (hierarchy.call.Static.InnerFunction.main)"/> + </node> +</node>
\ No newline at end of file diff --git a/python/testData/hierarchy/call/Static/InnerFunction/InnerFunction_caller_verification.xml b/python/testData/hierarchy/call/Static/InnerFunction/InnerFunction_caller_verification.xml new file mode 100644 index 000000000000..f01b7f50e287 --- /dev/null +++ b/python/testData/hierarchy/call/Static/InnerFunction/InnerFunction_caller_verification.xml @@ -0,0 +1 @@ +<node text="target_func() (hierarchy.call.Static.InnerFunction.main)" base="true"/>
\ No newline at end of file diff --git a/python/testData/hierarchy/call/Static/InnerFunction/main.py b/python/testData/hierarchy/call/Static/InnerFunction/main.py new file mode 100644 index 000000000000..7ffc14aaf745 --- /dev/null +++ b/python/testData/hierarchy/call/Static/InnerFunction/main.py @@ -0,0 +1,9 @@ +def bar(): + pass + +def target_func(): + def foo(): + return bar() + foo() + +target_<caret>func()
\ No newline at end of file diff --git a/python/testData/hierarchy/call/Static/Lambda/Lambda_callee_verification.xml b/python/testData/hierarchy/call/Static/Lambda/Lambda_callee_verification.xml new file mode 100644 index 000000000000..c1b05951f7d4 --- /dev/null +++ b/python/testData/hierarchy/call/Static/Lambda/Lambda_callee_verification.xml @@ -0,0 +1,8 @@ +<node text="target_func(x=func1, y=func2(), z=lambda: func3, w=lambda: func4()) (hierarchy.call.Static.Lambda.main)" base="true"> + <node text="func8() (hierarchy.call.Static.Lambda.file_1)"/> + <node text="func13() (hierarchy.call.Static.Lambda.file_1)"/> + <node text="func15() (hierarchy.call.Static.Lambda.file_1)"/> + <node text="inner(ix=func7, iy=func8(), iz=lambda: func9, iw=lambda: func10()) (hierarchy.call.Static.Lambda.main)"> + <node text="func11() (hierarchy.call.Static.Lambda.file_1)"/> + </node> +</node>
\ No newline at end of file diff --git a/python/testData/hierarchy/call/Static/Lambda/Lambda_caller_verification.xml b/python/testData/hierarchy/call/Static/Lambda/Lambda_caller_verification.xml new file mode 100644 index 000000000000..d54b9674b47d --- /dev/null +++ b/python/testData/hierarchy/call/Static/Lambda/Lambda_caller_verification.xml @@ -0,0 +1 @@ +<node text="target_func(x=func1, y=func2(), z=lambda: func3, w=lambda: func4()) (hierarchy.call.Static.Lambda.main)" base="true"/>
\ No newline at end of file diff --git a/python/testData/hierarchy/call/Static/Lambda/file_1.py b/python/testData/hierarchy/call/Static/Lambda/file_1.py new file mode 100644 index 000000000000..bf1185212963 --- /dev/null +++ b/python/testData/hierarchy/call/Static/Lambda/file_1.py @@ -0,0 +1,18 @@ +def func1(): pass +def func2(): pass +def func3(): pass +def func4(): pass +def func5(): pass +def func6(): pass +def func7(): pass +def func8(): pass +def func9(): pass +def func10(): pass +def func11(): pass +def func12(): pass +def func13(): pass +def func14(): pass +def func15(): pass +def func16(): pass +def func17(): pass +def func18(): pass diff --git a/python/testData/hierarchy/call/Static/Lambda/main.py b/python/testData/hierarchy/call/Static/Lambda/main.py new file mode 100644 index 000000000000..7dab14e27566 --- /dev/null +++ b/python/testData/hierarchy/call/Static/Lambda/main.py @@ -0,0 +1,18 @@ +from file_1 import * + + +def target_func(x=func1, y=func2(), z=lambda: func3, w=lambda: func4()): + p1 = lambda: func5() + p2 = lambda: func6 + p1(), p2() + def inner(ix=func7, iy=func8(), iz=lambda: func9, iw=lambda: func10()): + func11() + ip = lambda: func12() + ip() + func13() + inner(func14, func15(), lambda: func16, lambda: func17()) + + return func18 + + +target_<caret>func()
\ No newline at end of file diff --git a/python/testData/hierarchy/call/Static/NestedCall/NestedCall_callee_verification.xml b/python/testData/hierarchy/call/Static/NestedCall/NestedCall_callee_verification.xml new file mode 100644 index 000000000000..4e1813bf0bd7 --- /dev/null +++ b/python/testData/hierarchy/call/Static/NestedCall/NestedCall_callee_verification.xml @@ -0,0 +1,12 @@ +<node text="target_func() (hierarchy.call.Static.NestedCall.main)" base="true"> + <node text="func5(*args) (hierarchy.call.Static.NestedCall.file_1)"/> + <node text="func3(*args) (hierarchy.call.Static.NestedCall.file_1)"/> + <node text="func6(*args) (hierarchy.call.Static.NestedCall.file_1)"/> + <node text="func7(*args) (hierarchy.call.Static.NestedCall.file_1)"/> + <node text="func2(*args) (hierarchy.call.Static.NestedCall.file_1)"/> + <node text="func1(*args) (hierarchy.call.Static.NestedCall.file_1)"/> + <node text="func10(*args) (hierarchy.call.Static.NestedCall.file_1)"/> + <node text="inner(*args) (hierarchy.call.Static.NestedCall.main)"> + <node text="func1(*args) (hierarchy.call.Static.NestedCall.file_1)"/> + </node> +</node>
\ No newline at end of file diff --git a/python/testData/hierarchy/call/Static/NestedCall/NestedCall_caller_verification.xml b/python/testData/hierarchy/call/Static/NestedCall/NestedCall_caller_verification.xml new file mode 100644 index 000000000000..ea843209ce23 --- /dev/null +++ b/python/testData/hierarchy/call/Static/NestedCall/NestedCall_caller_verification.xml @@ -0,0 +1 @@ +<node text="target_func() (hierarchy.call.Static.NestedCall.main)" base="true"/>
\ No newline at end of file diff --git a/python/testData/hierarchy/call/Static/NestedCall/file_1.py b/python/testData/hierarchy/call/Static/NestedCall/file_1.py new file mode 100644 index 000000000000..3e89b6b290d0 --- /dev/null +++ b/python/testData/hierarchy/call/Static/NestedCall/file_1.py @@ -0,0 +1,10 @@ +def func1(*args): pass +def func2(*args): pass +def func3(*args): pass +def func4(*args): pass +def func5(*args): pass +def func6(*args): pass +def func7(*args): pass +def func8(*args): pass +def func9(*args): pass +def func10(*args): pass
\ No newline at end of file diff --git a/python/testData/hierarchy/call/Static/NestedCall/main.py b/python/testData/hierarchy/call/Static/NestedCall/main.py new file mode 100644 index 000000000000..aca993a9d6a1 --- /dev/null +++ b/python/testData/hierarchy/call/Static/NestedCall/main.py @@ -0,0 +1,10 @@ +from file_1 import * + +def target_func(): + def inner(*args): + return func1() + + return inner(func1(func2(func3(func4, func5()), func6(), (((func7)))()), func8), func9, func10()) + + +target_<caret>func()
\ No newline at end of file diff --git a/python/testData/hierarchy/call/Static/OverriddenMethod/OverriddenMethod_callee_verification.xml b/python/testData/hierarchy/call/Static/OverriddenMethod/OverriddenMethod_callee_verification.xml new file mode 100644 index 000000000000..04af9fdd87ed --- /dev/null +++ b/python/testData/hierarchy/call/Static/OverriddenMethod/OverriddenMethod_callee_verification.xml @@ -0,0 +1,3 @@ +<node text="B.target_func(self, p) (hierarchy.call.Static.OverriddenMethod.main)" base="true"> + <node text="A.another_func(self) (hierarchy.call.Static.OverriddenMethod.file_1)"/> +</node>
\ No newline at end of file diff --git a/python/testData/hierarchy/call/Static/OverriddenMethod/OverriddenMethod_caller_verification.xml b/python/testData/hierarchy/call/Static/OverriddenMethod/OverriddenMethod_caller_verification.xml new file mode 100644 index 000000000000..ddf9369f2766 --- /dev/null +++ b/python/testData/hierarchy/call/Static/OverriddenMethod/OverriddenMethod_caller_verification.xml @@ -0,0 +1,5 @@ +<node text="B.target_func(self, p) (hierarchy.call.Static.OverriddenMethod.main)" base="true"> + <node text="C.func1(self, a) (hierarchy.call.Static.OverriddenMethod.main)"/> + <node text="bar1(a) (hierarchy.call.Static.OverriddenMethod.main)"/> + <node text="C.func2(self) (hierarchy.call.Static.OverriddenMethod.main)"/> +</node>
\ No newline at end of file diff --git a/python/testData/hierarchy/call/Static/OverriddenMethod/file_1.py b/python/testData/hierarchy/call/Static/OverriddenMethod/file_1.py new file mode 100644 index 000000000000..e1e4fb90b949 --- /dev/null +++ b/python/testData/hierarchy/call/Static/OverriddenMethod/file_1.py @@ -0,0 +1,8 @@ + +class A(object): + def target_func(self, p): + pass + + def another_func(self): + pass + diff --git a/python/testData/hierarchy/call/Static/OverriddenMethod/main.py b/python/testData/hierarchy/call/Static/OverriddenMethod/main.py new file mode 100644 index 000000000000..288d638e0fea --- /dev/null +++ b/python/testData/hierarchy/call/Static/OverriddenMethod/main.py @@ -0,0 +1,29 @@ +from file_1 import A + + +class B(A): + def target_func(self, p): + p.another_func() + + +class C(object): + def func1(self, a): + a.target_func(A()) + + def func2(self): + a = A() + b = B() + a.target_func(b) + + +def bar1(a): + a.target_func(a) + + +def bar2(a, b): + atf, btf = a.target_func, b.target_func + + +bar1(A()) +bar2(A(), B()) +B().target_<caret>func(A())
\ No newline at end of file diff --git a/python/testData/hierarchy/call/Static/Parentheses/Parentheses_callee_verification.xml b/python/testData/hierarchy/call/Static/Parentheses/Parentheses_callee_verification.xml new file mode 100644 index 000000000000..662bc7a74da8 --- /dev/null +++ b/python/testData/hierarchy/call/Static/Parentheses/Parentheses_callee_verification.xml @@ -0,0 +1,3 @@ +<node text="target_func() (hierarchy.call.Static.Parentheses.file_1)" base="true"> + <node text="nothing(x) (hierarchy.call.Static.Parentheses.main)"/> +</node>
\ No newline at end of file diff --git a/python/testData/hierarchy/call/Static/Parentheses/Parentheses_caller_verification.xml b/python/testData/hierarchy/call/Static/Parentheses/Parentheses_caller_verification.xml new file mode 100644 index 000000000000..36b8e32e9363 --- /dev/null +++ b/python/testData/hierarchy/call/Static/Parentheses/Parentheses_caller_verification.xml @@ -0,0 +1,3 @@ +<node text="target_func() (hierarchy.call.Static.Parentheses.file_1)" base="true"> + <node text="foo(x=bar()) (hierarchy.call.Static.Parentheses.file_1)"/> +</node>
\ No newline at end of file diff --git a/python/testData/hierarchy/call/Static/Parentheses/file_1.py b/python/testData/hierarchy/call/Static/Parentheses/file_1.py new file mode 100644 index 000000000000..ed3de6593a59 --- /dev/null +++ b/python/testData/hierarchy/call/Static/Parentheses/file_1.py @@ -0,0 +1,20 @@ +import main + +def target_func(): + main.nothing(None) + + +def nothing(x): + pass + + +def foo(x=bar()): + ((target_func))() + + +def bar(): + main.nothing((target_func)) + + +def another(): + ((((target_func), 1))(), 2)()
\ No newline at end of file diff --git a/python/testData/hierarchy/call/Static/Parentheses/main.py b/python/testData/hierarchy/call/Static/Parentheses/main.py new file mode 100644 index 000000000000..37fe5bf0819d --- /dev/null +++ b/python/testData/hierarchy/call/Static/Parentheses/main.py @@ -0,0 +1,7 @@ +from file_1 import target_func + +def nothing(x): + pass + + +target_<caret>func()
\ No newline at end of file diff --git a/python/testData/hierarchy/call/Static/Simple/Simple_callee_verification.xml b/python/testData/hierarchy/call/Static/Simple/Simple_callee_verification.xml new file mode 100644 index 000000000000..c009281e2fa2 --- /dev/null +++ b/python/testData/hierarchy/call/Static/Simple/Simple_callee_verification.xml @@ -0,0 +1,3 @@ +<node text="target_func() (hierarchy.call.Static.Simple.main)" base="true"> + <node text="func1() (hierarchy.call.Static.Simple.main)"/> +</node>
\ No newline at end of file diff --git a/python/testData/hierarchy/call/Static/Simple/Simple_caller_verification.xml b/python/testData/hierarchy/call/Static/Simple/Simple_caller_verification.xml new file mode 100644 index 000000000000..a0f06c09b00a --- /dev/null +++ b/python/testData/hierarchy/call/Static/Simple/Simple_caller_verification.xml @@ -0,0 +1,3 @@ +<node text="target_func() (hierarchy.call.Static.Simple.main)" base="true"> + <node text="func2() (hierarchy.call.Static.Simple.main)"/> +</node>
\ No newline at end of file diff --git a/python/testData/hierarchy/call/Static/Simple/main.py b/python/testData/hierarchy/call/Static/Simple/main.py new file mode 100644 index 000000000000..9073f3df5d87 --- /dev/null +++ b/python/testData/hierarchy/call/Static/Simple/main.py @@ -0,0 +1,10 @@ +def func1(): + pass + +def func2(): + target_func() + +def target_func(): + func1() + +target_<caret>func() diff --git a/python/testData/highlighting/multipleEscapedBackslashes.py b/python/testData/highlighting/multipleEscapedBackslashes.py new file mode 100644 index 000000000000..4eb30abdcdb4 --- /dev/null +++ b/python/testData/highlighting/multipleEscapedBackslashes.py @@ -0,0 +1,2 @@ +S1 = 'This literal contains even number of backslashes\\\\\\' +S2 = <error descr="Missing closing quote [']">'This literal contains odd number of backslashes\\\\\\\'</error>
\ No newline at end of file diff --git a/python/testData/inspections/DefaultArgumentEmptyList.py b/python/testData/inspections/DefaultArgumentEmptyList.py index 55748eb449d5..a351c5134c90 100644 --- a/python/testData/inspections/DefaultArgumentEmptyList.py +++ b/python/testData/inspections/DefaultArgumentEmptyList.py @@ -1 +1 @@ -def foo(args=<warning descr="Default argument value is mutable">[<caret>]</warning>):<EOLError descr="Indent expected"></EOLError>
\ No newline at end of file +def bar(args=<warning descr="Default argument value is mutable">[<caret>]</warning>):<EOLError descr="Indent expected"></EOLError>
\ No newline at end of file diff --git a/python/testData/inspections/DefaultArgumentEmptyList_after.py b/python/testData/inspections/DefaultArgumentEmptyList_after.py index 2de1bb88ef92..ada9baaa7865 100644 --- a/python/testData/inspections/DefaultArgumentEmptyList_after.py +++ b/python/testData/inspections/DefaultArgumentEmptyList_after.py @@ -1,3 +1,3 @@ -def foo(args=None): +def bar(args=None): if not args: args = []
\ No newline at end of file diff --git a/python/testData/mover/outsideFromDict.py b/python/testData/mover/outsideFromDict.py new file mode 100644 index 000000000000..52f81fd16a8e --- /dev/null +++ b/python/testData/mover/outsideFromDict.py @@ -0,0 +1,5 @@ +a = { + 1:1, + # te<caret>st + 2:2 +} diff --git a/python/testData/mover/outsideFromDict_afterDown.py b/python/testData/mover/outsideFromDict_afterDown.py new file mode 100644 index 000000000000..551adfcd7894 --- /dev/null +++ b/python/testData/mover/outsideFromDict_afterDown.py @@ -0,0 +1,5 @@ +a = { + 1:1, + 2:2 + # te<caret>st +} diff --git a/python/testData/mover/outsideFromDict_afterUp.py b/python/testData/mover/outsideFromDict_afterUp.py new file mode 100644 index 000000000000..36f665bfc69f --- /dev/null +++ b/python/testData/mover/outsideFromDict_afterUp.py @@ -0,0 +1,5 @@ +a = { + # te<caret>st + 1:1, + 2:2 +} diff --git a/python/testData/mover/sameLevelAsDict.py b/python/testData/mover/sameLevelAsDict.py new file mode 100644 index 000000000000..fd5b54ec4e82 --- /dev/null +++ b/python/testData/mover/sameLevelAsDict.py @@ -0,0 +1,9 @@ +a = { + 'c': 99999 +} +print "Hello, there." +a = { + 'b': 1, + 'c': 2 +} +print <caret>a['c']
\ No newline at end of file diff --git a/python/testData/mover/sameLevelAsDict_afterDown.py b/python/testData/mover/sameLevelAsDict_afterDown.py new file mode 100644 index 000000000000..602da928bab7 --- /dev/null +++ b/python/testData/mover/sameLevelAsDict_afterDown.py @@ -0,0 +1,9 @@ +a = { + 'c': 99999 +} +print "Hello, there." +a = { + 'b': 1, + 'c': 2 +} +print a['c']
\ No newline at end of file diff --git a/python/testData/mover/sameLevelAsDict_afterUp.py b/python/testData/mover/sameLevelAsDict_afterUp.py new file mode 100644 index 000000000000..c4817e7a6474 --- /dev/null +++ b/python/testData/mover/sameLevelAsDict_afterUp.py @@ -0,0 +1,9 @@ +a = { + 'c': 99999 +} +print "Hello, there." +print a['c'] +a = { + 'b': 1, + 'c': 2 +} diff --git a/python/testSrc/com/jetbrains/env/python/PyPackagingTest.java b/python/testSrc/com/jetbrains/env/python/PyPackagingTest.java index 54afb702322b..8dcc2ec69f6f 100644 --- a/python/testSrc/com/jetbrains/env/python/PyPackagingTest.java +++ b/python/testSrc/com/jetbrains/env/python/PyPackagingTest.java @@ -43,7 +43,7 @@ public class PyPackagingTest extends PyEnvTestCase { final Sdk sdk = createTempSdk(sdkHome, SdkCreationType.EMPTY_SDK); List<PyPackage> packages = null; try { - packages = ((PyPackageManagerImpl)PyPackageManager.getInstance(sdk)).getPackages(); + packages = PyPackageManager.getInstance(sdk).getPackages(false); } catch (PyExternalProcessException e) { final int retcode = e.getRetcode(); @@ -74,13 +74,13 @@ public class PyPackagingTest extends PyEnvTestCase { } final File tempDir = FileUtil.createTempDirectory(getTestName(false), null); final File venvDir = new File(tempDir, "venv"); - final String venvSdkHome = ((PyPackageManagerImpl)PyPackageManagerImpl.getInstance(sdk)).createVirtualEnv(venvDir.toString(), - false); + final String venvSdkHome = PyPackageManager.getInstance(sdk).createVirtualEnv(venvDir.toString(), + false); final Sdk venvSdk = createTempSdk(venvSdkHome, SdkCreationType.EMPTY_SDK); assertNotNull(venvSdk); assertTrue(PythonSdkType.isVirtualEnv(venvSdk)); assertInstanceOf(PythonSdkFlavor.getPlatformIndependentFlavor(venvSdk.getHomePath()), VirtualEnvSdkFlavor.class); - final List<PyPackage> packages = ((PyPackageManagerImpl)PyPackageManagerImpl.getInstance(venvSdk)).getPackages(); + final List<PyPackage> packages = PyPackageManager.getInstance(venvSdk).getPackages(false); final PyPackage setuptools = findPackage("setuptools", packages); assertNotNull(setuptools); assertEquals("setuptools", setuptools.getName()); @@ -109,15 +109,15 @@ public class PyPackagingTest extends PyEnvTestCase { try { final File tempDir = FileUtil.createTempDirectory(getTestName(false), null); final File venvDir = new File(tempDir, "venv"); - final String venvSdkHome = ((PyPackageManagerImpl)PyPackageManager.getInstance(sdk)).createVirtualEnv(venvDir.getPath(), false); + final String venvSdkHome = PyPackageManager.getInstance(sdk).createVirtualEnv(venvDir.getPath(), false); final Sdk venvSdk = createTempSdk(venvSdkHome, SdkCreationType.EMPTY_SDK); assertNotNull(venvSdk); - final PyPackageManagerImpl manager = (PyPackageManagerImpl)PyPackageManager.getInstance(venvSdk); - final List<PyPackage> packages1 = manager.getPackages(); + final PyPackageManager manager = PyPackageManager.getInstance(venvSdk); + final List<PyPackage> packages1 = manager.getPackages(false); // TODO: Install Markdown from a local file manager.install(list(PyRequirement.fromString("Markdown<2.2"), new PyRequirement("httplib2")), Collections.<String>emptyList()); - final List<PyPackage> packages2 = manager.getPackages(); + final List<PyPackage> packages2 = manager.getPackages(false); final PyPackage markdown2 = findPackage("Markdown", packages2); assertNotNull(markdown2); assertTrue(markdown2.isInstalled()); @@ -126,7 +126,7 @@ public class PyPackagingTest extends PyEnvTestCase { assertEquals("pip", pip1.getName()); assertEquals(PyPackageManagerImpl.PIP_VERSION, pip1.getVersion()); manager.uninstall(list(pip1)); - final List<PyPackage> packages3 = manager.getPackages(); + final List<PyPackage> packages3 = manager.getPackages(false); final PyPackage pip2 = findPackage("pip", packages3); assertNull(pip2); } diff --git a/python/testSrc/com/jetbrains/env/python/PythonDebuggerTest.java b/python/testSrc/com/jetbrains/env/python/PythonDebuggerTest.java index 4372715371eb..e29ab4cfdc8b 100644 --- a/python/testSrc/com/jetbrains/env/python/PythonDebuggerTest.java +++ b/python/testSrc/com/jetbrains/env/python/PythonDebuggerTest.java @@ -13,7 +13,7 @@ import com.jetbrains.python.console.pydev.PydevCompletionVariant; import com.jetbrains.python.debugger.PyDebuggerException; import com.jetbrains.python.debugger.PyExceptionBreakpointProperties; import com.jetbrains.python.debugger.PyExceptionBreakpointType; -import com.jetbrains.python.debugger.pydev.ProcessDebugger; +import com.jetbrains.python.debugger.pydev.PyDebugCallback; import com.jetbrains.python.sdk.flavors.PythonSdkFlavor; import java.util.List; @@ -52,8 +52,12 @@ public class PythonDebuggerTest extends PyEnvTestCase { }); } - public void testDebugger() { - runPythonTest(new PyUnitTestTask("", "test_debug.py") { + public void testPydevTests_Debugger() { + unittests("tests_python/test_debugger.py"); + } + + private void unittests(final String script) { + runPythonTest(new PyUnitTestTask("", script) { @Override protected String getTestDataPath() { return PythonHelpersLocator.getPythonCommunityPath() + "/helpers/pydev"; @@ -63,9 +67,18 @@ public class PythonDebuggerTest extends PyEnvTestCase { public void after() { allTestsPassed(); } + + @Override + protected int getTestTimeout() { + return 600000; + } }); } + public void testDebug() { //TODO: merge it into pydev tests + unittests("test_debug.py"); + } + public void testConditionalBreakpoint() throws Exception { runPythonTest(new PyDebuggerTask("/debug", "test1.py") { @Override @@ -128,7 +141,7 @@ public class PythonDebuggerTest extends PyEnvTestCase { } private void consoleExec(String command) { - myDebugProcess.consoleExec(command, new ProcessDebugger.DebugCallback<String>() { + myDebugProcess.consoleExec(command, new PyDebugCallback<String>() { @Override public void ok(String value) { diff --git a/python/testSrc/com/jetbrains/env/ut/PyUnitTestTask.java b/python/testSrc/com/jetbrains/env/ut/PyUnitTestTask.java index 01401ca1ca9a..550ca6690cfd 100644 --- a/python/testSrc/com/jetbrains/env/ut/PyUnitTestTask.java +++ b/python/testSrc/com/jetbrains/env/ut/PyUnitTestTask.java @@ -1,8 +1,11 @@ package com.jetbrains.env.ut; import com.google.common.collect.Lists; -import com.intellij.execution.*; +import com.intellij.execution.RunManager; +import com.intellij.execution.RunManagerEx; +import com.intellij.execution.RunnerAndConfigurationSettings; import com.intellij.execution.configurations.ConfigurationFactory; +import com.intellij.execution.configurations.RunConfiguration; import com.intellij.execution.executors.DefaultRunExecutor; import com.intellij.execution.process.ProcessAdapter; import com.intellij.execution.process.ProcessEvent; @@ -29,20 +32,37 @@ import com.jetbrains.python.sdk.flavors.PythonSdkFlavor; import com.jetbrains.python.testing.AbstractPythonTestRunConfiguration; import com.jetbrains.python.testing.PythonTestConfigurationType; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.junit.Assert; /** + * Tasks to run unit test configurations. + * You should extend it either implementing {@link #after()} and {@link #before()} or implement {@link #runTestOn(String)} + * yourself and use {@link #runConfiguration(com.intellij.execution.configurations.ConfigurationFactory, String, com.intellij.openapi.project.Project)} + * or {@link #runConfiguration(com.intellij.execution.RunnerAndConfigurationSettings, com.intellij.execution.configurations.RunConfiguration)} . + * Use {@link #myDescriptor} and {@link #myConsoleView} to check output + * * @author traff */ public abstract class PyUnitTestTask extends PyExecutionFixtureTestTask { protected ProcessHandler myProcessHandler; private boolean shouldPrintOutput = false; - private SMTestProxy.SMRootTestProxy myTestProxy; - private boolean mySetUp = false; - private SMTRunnerConsoleView myConsoleView; - private RunContentDescriptor myDescriptor; + /** + * Test root node + */ + protected SMTestProxy.SMRootTestProxy myTestProxy; + /** + * Output test console + */ + protected SMTRunnerConsoleView myConsoleView; + /** + * Test run descriptor + */ + protected RunContentDescriptor myDescriptor; + private StringBuilder myOutput; + private boolean mySetUp = false; public PyUnitTestTask() { } @@ -83,30 +103,30 @@ public abstract class PyUnitTestTask extends PyExecutionFixtureTestTask { @Override public void tearDown() throws Exception { UIUtil.invokeAndWaitIfNeeded(new Runnable() { - @Override - public void run() { - try { - if (mySetUp) { - if (myConsoleView != null) { - Disposer.dispose(myConsoleView); - myConsoleView = null; - } - if (myDescriptor != null) { - Disposer.dispose(myDescriptor); - myDescriptor = null; - } - - - PyUnitTestTask.super.tearDown(); - - mySetUp = false; - } - } - catch (Exception e) { - throw new RuntimeException(e); - } - } - } + @Override + public void run() { + try { + if (mySetUp) { + if (myConsoleView != null) { + Disposer.dispose(myConsoleView); + myConsoleView = null; + } + if (myDescriptor != null) { + Disposer.dispose(myDescriptor); + myDescriptor = null; + } + + + PyUnitTestTask.super.tearDown(); + + mySetUp = false; + } + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + } ); } @@ -151,10 +171,30 @@ public abstract class PyUnitTestTask extends PyExecutionFixtureTestTask { } }.execute(); - final ExecutionEnvironment environment = ExecutionEnvironmentBuilder.create(DefaultRunExecutor.getRunExecutorInstance(), settings).build(); + runConfiguration(settings, config); + } + + /** + * Run configuration. + * + * @param settings settings (if have any, null otherwise) + * @param config configuration to run + * @throws Exception + */ + protected void runConfiguration(@Nullable final RunnerAndConfigurationSettings settings, + @NotNull final RunConfiguration config) throws Exception { + final ExecutionEnvironment environment; + if (settings == null) { + environment = ExecutionEnvironmentBuilder.create(DefaultRunExecutor.getRunExecutorInstance(), config).build(); + } + else { + environment = ExecutionEnvironmentBuilder.create(DefaultRunExecutor.getRunExecutorInstance(), settings).build(); + } //noinspection ConstantConditions + Assert.assertTrue(environment.getRunner().canRun(DefaultRunExecutor.EXECUTOR_ID, config)); + before(); final com.intellij.util.concurrency.Semaphore s = new com.intellij.util.concurrency.Semaphore(); @@ -177,7 +217,7 @@ public abstract class PyUnitTestTask extends PyExecutionFixtureTestTask { myOutput.append(event.getText()); } }); - myConsoleView = (com.intellij.execution.testframework.sm.runner.ui.SMTRunnerConsoleView)descriptor.getExecutionConsole(); + myConsoleView = (SMTRunnerConsoleView)descriptor.getExecutionConsole(); myTestProxy = myConsoleView.getResultsViewer().getTestsRootNode(); myConsoleView.getResultsViewer().addEventsListener(new TestResultsViewer.SMEventsAdapter() { @Override @@ -194,7 +234,7 @@ public abstract class PyUnitTestTask extends PyExecutionFixtureTestTask { } }); - Assert.assertTrue(s.waitFor(60000)); + Assert.assertTrue(s.waitFor(getTestTimeout())); XDebuggerTestUtil.waitForSwing(); @@ -207,6 +247,10 @@ public abstract class PyUnitTestTask extends PyExecutionFixtureTestTask { disposeProcess(myProcessHandler); } + protected int getTestTimeout() { + return 60000; + } + protected void configure(AbstractPythonTestRunConfiguration config) { } diff --git a/python/testSrc/com/jetbrains/python/PySmartEnterTest.java b/python/testSrc/com/jetbrains/python/PySmartEnterTest.java index 09e0317051a0..e58988010645 100644 --- a/python/testSrc/com/jetbrains/python/PySmartEnterTest.java +++ b/python/testSrc/com/jetbrains/python/PySmartEnterTest.java @@ -184,4 +184,9 @@ public class PySmartEnterTest extends PyTestCase { public void testWithExpressionMissing() { doTest(); } + + // PY-12877 + public void testWithOnlyColonMissing() { + doTest(); + } } diff --git a/python/testSrc/com/jetbrains/python/PyStatementMoverTest.java b/python/testSrc/com/jetbrains/python/PyStatementMoverTest.java index 3539150731b6..220c08ef2d24 100644 --- a/python/testSrc/com/jetbrains/python/PyStatementMoverTest.java +++ b/python/testSrc/com/jetbrains/python/PyStatementMoverTest.java @@ -260,6 +260,14 @@ public class PyStatementMoverTest extends PyTestCase { doTest(); } + public void testOutsideFromDict() { + doTest(); + } + + public void testSameLevelAsDict() { + doTest(); + } + public void testWith() { // PY-5202 try { setLanguageLevel(LanguageLevel.PYTHON27); diff --git a/python/testSrc/com/jetbrains/python/PythonHighlightingTest.java b/python/testSrc/com/jetbrains/python/PythonHighlightingTest.java index e1fa67de63e4..73ab49e28659 100644 --- a/python/testSrc/com/jetbrains/python/PythonHighlightingTest.java +++ b/python/testSrc/com/jetbrains/python/PythonHighlightingTest.java @@ -174,6 +174,10 @@ public class PythonHighlightingTest extends PyTestCase { doTest(true, false); } + public void testMultipleEscapedBackslashes() { + doTest(true, false); + } + public void testUnsupportedFeaturesInPython3() { doTest(LanguageLevel.PYTHON30, true, false); } diff --git a/python/testSrc/com/jetbrains/python/PythonKeywordCompletionTest.java b/python/testSrc/com/jetbrains/python/PythonKeywordCompletionTest.java index 109a1bddd19b..e64f02ad16c3 100644 --- a/python/testSrc/com/jetbrains/python/PythonKeywordCompletionTest.java +++ b/python/testSrc/com/jetbrains/python/PythonKeywordCompletionTest.java @@ -199,6 +199,17 @@ public class PythonKeywordCompletionTest extends PyTestCase { assertContainsElements(doTestByText("for x i<caret>n y:\n pass]"), "in"); } + // PY-11375 + public void testYieldExpression() { + assertContainsElements(doTestByText("def gen(): x = <caret>"), "yield"); + assertDoesntContain(doTestByText("def gen(): x = 1 + <caret>"), "yield"); + assertContainsElements(doTestByText("def gen(): x = 1 + (<caret>"), "yield"); + assertContainsElements(doTestByText("def gen(): x **= <caret>"), "yield"); + assertDoesntContain(doTestByText("def gen(): func(<caret>)"), "yield"); + assertContainsElements(doTestByText("def gen(): func((<caret>"), "yield"); + assertDoesntContain(doTestByText("def gen(): x = y<caret> = 42"), "yield"); + } + public void testExceptAfterElse() { assertDoesntContain(doTestByText("try:\n" + " pass\n" + diff --git a/python/testSrc/com/jetbrains/python/fixtures/PyTestCase.java b/python/testSrc/com/jetbrains/python/fixtures/PyTestCase.java index 8a237b7dfb01..3528d7c6517d 100644 --- a/python/testSrc/com/jetbrains/python/fixtures/PyTestCase.java +++ b/python/testSrc/com/jetbrains/python/fixtures/PyTestCase.java @@ -18,8 +18,14 @@ package com.jetbrains.python.fixtures; import com.intellij.codeInsight.intention.IntentionAction; import com.intellij.codeInspection.LocalQuickFix; import com.intellij.codeInspection.ex.QuickFixWrapper; +import com.intellij.execution.actions.ConfigurationContext; +import com.intellij.execution.actions.ConfigurationFromContext; +import com.intellij.execution.actions.RunConfigurationProducer; +import com.intellij.execution.configurations.RunConfiguration; import com.intellij.find.findUsages.CustomUsageSearcher; import com.intellij.find.findUsages.FindUsagesOptions; +import com.intellij.ide.DataManager; +import com.intellij.openapi.actionSystem.DataContext; import com.intellij.openapi.application.PathManager; import com.intellij.openapi.extensions.Extensions; import com.intellij.openapi.module.Module; @@ -56,6 +62,7 @@ import com.jetbrains.python.PythonTestUtil; import com.jetbrains.python.psi.LanguageLevel; import com.jetbrains.python.psi.PyClass; import com.jetbrains.python.psi.PyFile; +import com.jetbrains.python.psi.PyUtil; import com.jetbrains.python.psi.impl.PyFileImpl; import com.jetbrains.python.psi.impl.PythonLanguageLevelPusher; import org.jetbrains.annotations.NotNull; @@ -64,7 +71,6 @@ import org.jetbrains.annotations.Nullable; import java.io.File; import java.util.ArrayList; import java.util.Collection; -import java.util.List; /** * @author yole @@ -92,12 +98,12 @@ public abstract class PyTestCase extends UsefulTestCase { IdeaTestFixtureFactory factory = IdeaTestFixtureFactory.getFixtureFactory(); TestFixtureBuilder<IdeaProjectTestFixture> fixtureBuilder = factory.createLightFixtureBuilder(getProjectDescriptor()); final IdeaProjectTestFixture fixture = fixtureBuilder.getFixture(); - myFixture = IdeaTestFixtureFactory.getFixtureFactory().createCodeInsightFixture(fixture, - new LightTempDirTestFixtureImpl(true)); - myFixture.setUp(); + myFixture = IdeaTestFixtureFactory.getFixtureFactory().createCodeInsightFixture(fixture, + new LightTempDirTestFixtureImpl(true)); + myFixture.setUp(); - myFixture.setTestDataPath(getTestDataPath()); - } + myFixture.setTestDataPath(getTestDataPath()); + } protected String getTestDataPath() { return PythonTestUtil.getTestDataPath(); @@ -148,8 +154,9 @@ public abstract class PyTestCase extends UsefulTestCase { /** * Searches for quickfix itetion by its class + * * @param clazz quick fix class - * @param <T> quick fix class + * @param <T> quick fix class * @return quick fix or null if nothing found */ @Nullable @@ -170,8 +177,8 @@ public abstract class PyTestCase extends UsefulTestCase { } - protected static void assertNotParsed(PyFile file) { - assertNull(PARSED_ERROR_MSG, ((PyFileImpl)file).getTreeElement()); + protected static void assertNotParsed(PyFile file) { + assertNull(PARSED_ERROR_MSG, ((PyFileImpl)file).getTreeElement()); } /** @@ -184,15 +191,23 @@ public abstract class PyTestCase extends UsefulTestCase { } /** + * @see #moveByText(com.intellij.testFramework.fixtures.CodeInsightTestFixture, String) + */ + protected void moveByText(@NotNull final String testToFind) { + moveByText(myFixture, testToFind); + } + + /** * Finds some text and moves cursor to it (if found) * + * @param fixture test fixture * @param testToFind text to find * @throws AssertionError if element not found */ - protected void moveByText(@NotNull final String testToFind) { - final PsiElement element = myFixture.findElementByText(testToFind, PsiElement.class); + public static void moveByText(@NotNull final CodeInsightTestFixture fixture, @NotNull final String testToFind) { + final PsiElement element = fixture.findElementByText(testToFind, PsiElement.class); assert element != null : "No element found by text: " + testToFind; - myFixture.getEditor().getCaretModel().moveToOffset(element.getTextOffset()); + fixture.getEditor().getCaretModel().moveToOffset(element.getTextOffset()); } /** @@ -263,4 +278,31 @@ public abstract class PyTestCase extends UsefulTestCase { public static String getHelpersPath() { return new File(PythonHelpersLocator.getPythonCommunityPath(), "helpers").getPath(); } + + /** + * Creates run configuration from right click menu + * + * @param fixture test fixture + * @param expectedClass expected class of run configuration + * @param <C> expected class of run configuration + * @return configuration (if created) or null (otherwise) + */ + @Nullable + public static <C extends RunConfiguration> C createRunConfigurationFromContext( + @NotNull final CodeInsightTestFixture fixture, + @NotNull final Class<C> expectedClass) { + final DataContext context = DataManager.getInstance().getDataContext(fixture.getEditor().getComponent()); + for (final RunConfigurationProducer<?> producer : RunConfigurationProducer.EP_NAME.getExtensions()) { + final ConfigurationFromContext fromContext = producer.createConfigurationFromContext(ConfigurationContext.getFromContext(context)); + if (fromContext == null) { + continue; + } + final C result = PyUtil.as(fromContext.getConfiguration(), expectedClass); + if (result != null) { + return result; + } + } + return null; + } } + diff --git a/python/testSrc/com/jetbrains/python/hierarchy/PyCallHierarchyTest.java b/python/testSrc/com/jetbrains/python/hierarchy/PyCallHierarchyTest.java new file mode 100644 index 000000000000..f9f7e226f913 --- /dev/null +++ b/python/testSrc/com/jetbrains/python/hierarchy/PyCallHierarchyTest.java @@ -0,0 +1,158 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jetbrains.python.hierarchy; + +import com.intellij.codeInsight.TargetElementUtilBase; +import com.intellij.ide.hierarchy.HierarchyBrowserBaseEx; +import com.intellij.ide.hierarchy.HierarchyNodeDescriptor; +import com.intellij.ide.hierarchy.HierarchyTreeStructure; +import com.intellij.psi.PsiElement; +import com.jetbrains.python.fixtures.PyTestCase; +import com.jetbrains.python.hierarchy.call.PyCalleeFunctionTreeStructure; +import com.jetbrains.python.hierarchy.call.PyCallerFunctionTreeStructure; +import com.jetbrains.python.psi.PyFunction; +import org.jetbrains.annotations.Nullable; + +/** + * @author novokrest + */ +public class PyCallHierarchyTest extends PyTestCase { + private static final String CALLER_VERIFICATION_SUFFIX = "_caller_verification.xml"; + private static final String CALLEE_VERIFICATION_SUFFIX = "_callee_verification.xml"; + + public static String dump(final HierarchyTreeStructure treeStructure, @Nullable HierarchyNodeDescriptor descriptor) { + StringBuilder s = new StringBuilder(); + dump(treeStructure, descriptor, 0, s); + return s.toString(); + } + + private static void dump(final HierarchyTreeStructure treeStructure, + @Nullable HierarchyNodeDescriptor descriptor, + int level, + StringBuilder b) { + if (level > 10) { + for(int i = 0; i<level; i++) b.append(" "); + b.append("<Probably infinite part skipped>\n"); + return; + } + if(descriptor==null) descriptor = (HierarchyNodeDescriptor)treeStructure.getRootElement(); + for(int i = 0; i<level; i++) b.append(" "); + descriptor.update(); + b.append("<node text=\"").append(descriptor.getHighlightedText().getText()).append("\"") + .append(treeStructure.getBaseDescriptor() == descriptor ? " base=\"true\"" : ""); + + final Object[] children = treeStructure.getChildElements(descriptor); + if(children.length>0) { + b.append(">\n"); + for (Object o : children) { + HierarchyNodeDescriptor d = (HierarchyNodeDescriptor)o; + dump(treeStructure, d, level + 1, b); + } + for(int i = 0; i<level; i++) b.append(" "); + b.append("</node>\n"); + } else { + b.append("/>\n"); + } + } + + private String getBasePath() { + return "hierarchy/call/Static/" + getTestName(false); + } + + private void configureByFiles(String ... fileNames) { + String[] filePaths = new String[fileNames.length]; + int i = 0; + for (String fileName: fileNames) { + filePaths[i] = getBasePath() + "/" + fileName; + i++; + } + myFixture.configureByFiles(filePaths); + } + + private String getVerificationFilePath(final String suffix) { + return getTestDataPath() + "/" + getBasePath() + "/" + getTestName(false) + suffix; + } + + private String getVerificationCallerFilePath() { + return getVerificationFilePath(CALLER_VERIFICATION_SUFFIX); + } + + private String getVerificationCalleeFilePath() { + return getVerificationFilePath(CALLEE_VERIFICATION_SUFFIX); + } + + private void checkHierarchyTreeStructure(PyFunction function) throws Exception { + final PyCallerFunctionTreeStructure callerStructure = new PyCallerFunctionTreeStructure(myFixture.getProject(), function, + HierarchyBrowserBaseEx.SCOPE_PROJECT); + assertSameLinesWithFile(getVerificationCallerFilePath(), dump(callerStructure, null)); + final PyCalleeFunctionTreeStructure calleeStructure = new PyCalleeFunctionTreeStructure(myFixture.getProject(), function, + HierarchyBrowserBaseEx.SCOPE_PROJECT); + assertSameLinesWithFile(getVerificationCalleeFilePath(), dump(calleeStructure, null)); + } + + private void doTestCallHierarchy(String ... fileNames) throws Exception { + configureByFiles(fileNames); + + final PsiElement targetElement = TargetElementUtilBase + .findTargetElement(myFixture.getEditor(), + TargetElementUtilBase.ELEMENT_NAME_ACCEPTED | TargetElementUtilBase.REFERENCED_ELEMENT_ACCEPTED); + assert targetElement != null : "Cannot find referenced element"; + assert targetElement instanceof PyFunction : "Referenced element is not PyFunction"; + + PyFunction function = (PyFunction) targetElement; + checkHierarchyTreeStructure(function); + } + + public void testSimple() throws Exception { + doTestCallHierarchy("main.py"); + } + + public void testArgumentList() throws Exception { + doTestCallHierarchy("main.py", "file_1.py"); + } + + public void testDefaultValue() throws Exception { + doTestCallHierarchy("main.py"); + } + + public void testLambda() throws Exception { + doTestCallHierarchy("main.py", "file_1.py"); + } + + public void testNestedCall() throws Exception { + doTestCallHierarchy("main.py", "file_1.py"); + } + + public void testInheritance() throws Exception { + doTestCallHierarchy("main.py"); + } + + public void testOverriddenMethod() throws Exception { + doTestCallHierarchy("main.py", "file_1.py"); + } + + public void testInnerFunction() throws Exception { + doTestCallHierarchy("main.py"); + } + + public void testConstructor() throws Exception { + doTestCallHierarchy("main.py"); + } + + public void testParentheses() throws Exception { + doTestCallHierarchy("main.py", "file_1.py"); + } +}
\ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/refactoring/PyInlineLocalTest.java b/python/testSrc/com/jetbrains/python/refactoring/PyInlineLocalTest.java index 9a764b94169d..cf6ad38e075d 100644 --- a/python/testSrc/com/jetbrains/python/refactoring/PyInlineLocalTest.java +++ b/python/testSrc/com/jetbrains/python/refactoring/PyInlineLocalTest.java @@ -20,6 +20,7 @@ import com.intellij.openapi.util.Comparing; import com.intellij.psi.PsiElement; import com.intellij.psi.codeStyle.CodeStyleSettings; import com.intellij.psi.codeStyle.CodeStyleSettingsManager; +import com.intellij.psi.codeStyle.CommonCodeStyleSettings; import com.jetbrains.python.PythonLanguage; import com.jetbrains.python.fixtures.PyTestCase; import com.jetbrains.python.refactoring.inline.PyInlineLocalHandler; @@ -97,8 +98,19 @@ public class PyInlineLocalTest extends PyTestCase { // PY-12409 public void testResultExceedsRightMargin() { final CodeStyleSettings settings = CodeStyleSettingsManager.getSettings(myFixture.getProject()); - settings.WRAP_LONG_LINES = true; + final CommonCodeStyleSettings commonSettings = settings.getCommonSettings(PythonLanguage.getInstance()); + + final int oldRightMargin = settings.getRightMargin(PythonLanguage.getInstance()); + final boolean oldWrapLongLines = commonSettings.WRAP_LONG_LINES; + settings.setRightMargin(PythonLanguage.getInstance(), 80); - doTest(); + commonSettings.WRAP_LONG_LINES = true; + try { + doTest(); + } + finally { + commonSettings.WRAP_LONG_LINES = oldWrapLongLines; + settings.setRightMargin(PythonLanguage.getInstance(), oldRightMargin); + } } } diff --git a/python/testSrc/com/jetbrains/python/sdkTools/PyTestSdkTools.java b/python/testSrc/com/jetbrains/python/sdkTools/PyTestSdkTools.java index 2d97e747f00b..0b5cdadbbc43 100644 --- a/python/testSrc/com/jetbrains/python/sdkTools/PyTestSdkTools.java +++ b/python/testSrc/com/jetbrains/python/sdkTools/PyTestSdkTools.java @@ -136,7 +136,7 @@ public final class PyTestSdkTools { final SkeletonVersionChecker checker = new SkeletonVersionChecker(0); final PySkeletonRefresher refresher = new PySkeletonRefresher(project, null, sdk, skeletonsPath, null, null); - final List<String> errors = refresher.regenerateSkeletons(checker, null); + final List<String> errors = refresher.regenerateSkeletons(checker); Assert.assertThat("Errors found", errors, Matchers.empty()); } } |