diff --git a/python/resources/intentionDescriptions/PyConvertTypeCommentToVariableAnnotation/after.py.template b/python/resources/intentionDescriptions/PyConvertTypeCommentToVariableAnnotation/after.py.template
new file mode 100644
index 000000000000..89023c6848ad
--- /dev/null
+++ b/python/resources/intentionDescriptions/PyConvertTypeCommentToVariableAnnotation/after.py.template
@@ -0,0 +1 @@
+x: Optional[int] = undefined()
\ No newline at end of file
diff --git a/python/resources/intentionDescriptions/PyConvertTypeCommentToVariableAnnotation/before.py.template b/python/resources/intentionDescriptions/PyConvertTypeCommentToVariableAnnotation/before.py.template
new file mode 100644
index 000000000000..e28b15f6074b
--- /dev/null
+++ b/python/resources/intentionDescriptions/PyConvertTypeCommentToVariableAnnotation/before.py.template
@@ -0,0 +1 @@
+x = undefined() # type: Optional[int]
\ No newline at end of file
diff --git a/python/resources/intentionDescriptions/PyConvertTypeCommentToVariableAnnotation/description.html b/python/resources/intentionDescriptions/PyConvertTypeCommentToVariableAnnotation/description.html
new file mode 100644
index 000000000000..2d034cc57781
--- /dev/null
+++ b/python/resources/intentionDescriptions/PyConvertTypeCommentToVariableAnnotation/description.html
@@ -0,0 +1,7 @@
+
+
+
+ This intention convert PEP-484 type comments to Python 3.6 variable annotations
+
+
+
\ No newline at end of file
diff --git a/python/src/META-INF/python-core-common.xml b/python/src/META-INF/python-core-common.xml
index 57df91bfea5e..a2b5c41eb4f0 100644
--- a/python/src/META-INF/python-core-common.xml
+++ b/python/src/META-INF/python-core-common.xml
@@ -311,6 +311,11 @@
Python
+
+ com.jetbrains.python.codeInsight.intentions.PyConvertTypeCommentToVariableAnnotation
+ Python
+
+
diff --git a/python/src/com/jetbrains/python/PyBundle.properties b/python/src/com/jetbrains/python/PyBundle.properties
index e092447a124b..db09df1ac899 100644
--- a/python/src/com/jetbrains/python/PyBundle.properties
+++ b/python/src/com/jetbrains/python/PyBundle.properties
@@ -245,9 +245,15 @@ INTN.convert.variadic.param=Convert from variadic to normal parameter(s)
# PyConvertTripleQuotedStringIntention
INTN.triple.quoted.string=Convert triple-quoted string to single-quoted string
+# PyBaseConvertCollectionLiteralIntention
INTN.convert.collection.literal.family=Convert collection to {0}
INTN.convert.collection.literal.text=Convert {0} to {1}
+# PyConvertTypeCommentToVariableAnnotation
+INTN.convert.type.comment.to.variable.annotation.family=Convert type comment to variable annotation
+INTN.convert.type.comment.to.variable.annotation.text=Convert to variable annotation
+
+
INTN.demorgan.law=DeMorgan law
# PyTransformConditionalExpressionIntention
diff --git a/python/src/com/jetbrains/python/codeInsight/intentions/PyConvertTypeCommentToVariableAnnotation.java b/python/src/com/jetbrains/python/codeInsight/intentions/PyConvertTypeCommentToVariableAnnotation.java
new file mode 100644
index 000000000000..8fb2e15d8127
--- /dev/null
+++ b/python/src/com/jetbrains/python/codeInsight/intentions/PyConvertTypeCommentToVariableAnnotation.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2000-2016 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.codeInsight.intentions;
+
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiComment;
+import com.intellij.psi.PsiDocumentManager;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.util.IncorrectOperationException;
+import com.jetbrains.python.PyBundle;
+import com.jetbrains.python.codeInsight.PyTypingTypeProvider;
+import com.jetbrains.python.psi.*;
+import com.jetbrains.python.psi.impl.PyPsiUtils;
+import org.jetbrains.annotations.Nls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class PyConvertTypeCommentToVariableAnnotation extends PyBaseIntentionAction {
+ @Override
+ public void doInvoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException {
+ final PsiComment comment = findCommentUnderCaret(editor, file);
+ if (comment != null) {
+ final PyTargetExpression target = findTypeCommentTarget(comment);
+ if (target != null) {
+ final String annotation = PyTypingTypeProvider.getTypeCommentValue(comment.getText());
+ final PsiElement prev = PyPsiUtils.getPrevNonWhitespaceSibling(comment);
+ final int commentStart = prev != null ? prev.getTextRange().getEndOffset() : comment.getTextRange().getStartOffset();
+ final int commentEnd = comment.getTextRange().getEndOffset();
+ final Document document = editor.getDocument();
+ runWithDocumentReleasedAndCommitted(project, document, () -> {
+ document.deleteString(commentStart, commentEnd);
+ document.insertString(target.getTextRange().getEndOffset(), ": " + annotation);
+ });
+ }
+ }
+ }
+
+ @NotNull
+ @Override
+ public String getText() {
+ return PyBundle.message("INTN.convert.type.comment.to.variable.annotation.text");
+ }
+
+ @Nls
+ @NotNull
+ @Override
+ public String getFamilyName() {
+ return PyBundle.message("INTN.convert.type.comment.to.variable.annotation.family");
+ }
+
+ @Override
+ public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
+ if (file instanceof PyFile && LanguageLevel.forElement(file).isAtLeast(LanguageLevel.PYTHON36)) {
+ final PsiComment comment = findCommentUnderCaret(editor, file);
+ return comment != null && isSuitableTypeComment(comment);
+ }
+ return false;
+ }
+
+ @Nullable
+ private static PsiComment findCommentUnderCaret(@NotNull Editor editor, @NotNull PsiFile file) {
+ final PsiElement element = file.findElementAt(editor.getCaretModel().getOffset());
+ return PsiTreeUtil.getParentOfType(element, PsiComment.class, false);
+ }
+
+ private boolean isSuitableTypeComment(@NotNull PsiComment comment) {
+ final String annotation = PyTypingTypeProvider.getTypeCommentValue(comment.getText());
+ return annotation != null && findTypeCommentTarget(comment) != null;
+ }
+
+ @Nullable
+ private PyTargetExpression findTypeCommentTarget(@NotNull PsiComment comment) {
+ final PsiElement parent = comment.getParent();
+ if (parent instanceof PyAssignmentStatement) {
+ final PyAssignmentStatement assignment = (PyAssignmentStatement)parent;
+ final PyExpression[] rawTargets = assignment.getRawTargets();
+ if (rawTargets.length == 1 && rawTargets[0] instanceof PyTargetExpression) {
+ final PyTargetExpression target = (PyTargetExpression)rawTargets[0];
+ if (target.getTypeComment() == comment) {
+ return target;
+ }
+ }
+ }
+ return null;
+ }
+
+ public static void runWithDocumentReleasedAndCommitted(@NotNull Project project, @NotNull Document document, @NotNull Runnable runnable) {
+ final PsiDocumentManager manager = PsiDocumentManager.getInstance(project);
+ manager.doPostponedOperationsAndUnblockDocument(document);
+ try {
+ runnable.run();
+ }
+ finally {
+ manager.commitDocument(document);
+ }
+ }
+}
diff --git a/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/assignmentWithUnpacking.py b/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/assignmentWithUnpacking.py
new file mode 100644
index 000000000000..45bdb4e44262
--- /dev/null
+++ b/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/assignmentWithUnpacking.py
@@ -0,0 +1 @@
+x, y = (1, 2) # type: int, int
diff --git a/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/badLanguageLevel.py b/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/badLanguageLevel.py
new file mode 100644
index 000000000000..bfcc3bf3ce4a
--- /dev/null
+++ b/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/badLanguageLevel.py
@@ -0,0 +1 @@
+x = 42 # type: int
\ No newline at end of file
diff --git a/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/chainedAssignment.py b/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/chainedAssignment.py
new file mode 100644
index 000000000000..6f7b13bee865
--- /dev/null
+++ b/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/chainedAssignment.py
@@ -0,0 +1 @@
+x = y = 42 # type: int
\ No newline at end of file
diff --git a/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/multilineAssignment.py b/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/multilineAssignment.py
new file mode 100644
index 000000000000..7495802f2d54
--- /dev/null
+++ b/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/multilineAssignment.py
@@ -0,0 +1,5 @@
+from typing import List
+
+xs = [1,
+ 2,
+ 3] # type: List[int]
diff --git a/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/multilineAssignment_after.py b/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/multilineAssignment_after.py
new file mode 100644
index 000000000000..166ef07c5417
--- /dev/null
+++ b/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/multilineAssignment_after.py
@@ -0,0 +1,5 @@
+from typing import List
+
+xs: List[int] = [1,
+ 2,
+ 3]
diff --git a/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/simpleAssignment.py b/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/simpleAssignment.py
new file mode 100644
index 000000000000..bfcc3bf3ce4a
--- /dev/null
+++ b/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/simpleAssignment.py
@@ -0,0 +1 @@
+x = 42 # type: int
\ No newline at end of file
diff --git a/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/simpleAssignment_after.py b/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/simpleAssignment_after.py
new file mode 100644
index 000000000000..c19c1b98f2a6
--- /dev/null
+++ b/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/simpleAssignment_after.py
@@ -0,0 +1 @@
+x: int = 42
\ No newline at end of file
diff --git a/python/testSrc/com/jetbrains/python/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest.java b/python/testSrc/com/jetbrains/python/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest.java
new file mode 100644
index 000000000000..e912da672ef0
--- /dev/null
+++ b/python/testSrc/com/jetbrains/python/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2000-2016 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.intentions;
+
+import com.jetbrains.python.PyBundle;
+import com.jetbrains.python.psi.LanguageLevel;
+
+public class PyConvertTypeCommentToVariableAnnotationIntentionTest extends PyIntentionTestCase {
+ private void doPositiveTest() {
+ doTest(PyBundle.message("INTN.convert.type.comment.to.variable.annotation.text"), LanguageLevel.PYTHON36);
+ }
+
+ private void doNegativeTest() {
+ runWithLanguageLevel(LanguageLevel.PYTHON36, () -> {
+ doNegativeTest(PyBundle.message("INTN.convert.type.comment.to.variable.annotation.text"));
+ });
+ }
+
+ public void testSimpleAssignment() {
+ doPositiveTest();
+ }
+
+ public void testBadLanguageLevel() {
+ runWithLanguageLevel(LanguageLevel.PYTHON35, () -> {
+ doNegativeTest(PyBundle.message("INTN.convert.type.comment.to.variable.annotation.text"));
+ });
+ }
+
+ public void testChainedAssignment() {
+ doNegativeTest();
+ }
+
+ public void testAssignmentWithUnpacking() {
+ doNegativeTest();
+ }
+
+ public void testMultilineAssignment() {
+ doPositiveTest();
+ }
+}