diff --git a/python/src/META-INF/python-plugin-common.xml b/python/src/META-INF/python-plugin-common.xml index 2b34f46dd9a9..5acbea33de8b 100644 --- a/python/src/META-INF/python-plugin-common.xml +++ b/python/src/META-INF/python-plugin-common.xml @@ -694,6 +694,10 @@ + + diff --git a/python/src/com/jetbrains/python/PyBundle.properties b/python/src/com/jetbrains/python/PyBundle.properties index 35f2ed9fe6d7..8755ee1028bb 100644 --- a/python/src/com/jetbrains/python/PyBundle.properties +++ b/python/src/com/jetbrains/python/PyBundle.properties @@ -194,6 +194,7 @@ surround.with.try.except.template=try / except ### Inspections: INSP ### INSP.GROUP.python=Python +INSP.GROUP.mako=Mako # PyArgumentListInspection INSP.NAME.incorrect.call.arguments=Incorrect call arguments @@ -600,6 +601,8 @@ sdk.error.invalid.interpreter.name.$0=Invalid Python interpeter name '{0}'! sdk.select.path=Select Python Interpreter runcfg.unittest.dlg.pattern=Pattern: +runcfg.testing.no.test.framework=No {0} runner found in selected interpreter + # Consoles messages python.console=Python Console django.console=Django Console diff --git a/python/src/com/jetbrains/python/configuration/PyIntegratedToolsConfigurable.form b/python/src/com/jetbrains/python/configuration/PyIntegratedToolsConfigurable.form index 750d85d328ee..1dfead35678a 100644 --- a/python/src/com/jetbrains/python/configuration/PyIntegratedToolsConfigurable.form +++ b/python/src/com/jetbrains/python/configuration/PyIntegratedToolsConfigurable.form @@ -1,6 +1,6 @@
- + @@ -56,7 +56,7 @@ - + @@ -76,6 +76,14 @@ + + + + + + + + diff --git a/python/src/com/jetbrains/python/configuration/PyIntegratedToolsConfigurable.java b/python/src/com/jetbrains/python/configuration/PyIntegratedToolsConfigurable.java index 524f6e3546db..5ddbf2cac530 100644 --- a/python/src/com/jetbrains/python/configuration/PyIntegratedToolsConfigurable.java +++ b/python/src/com/jetbrains/python/configuration/PyIntegratedToolsConfigurable.java @@ -1,39 +1,34 @@ package com.jetbrains.python.configuration; -import com.google.common.collect.Lists; import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.editor.ex.EditorEx; -import com.intellij.openapi.editor.highlighter.EditorHighlighter; -import com.intellij.openapi.editor.highlighter.EditorHighlighterFactory; -import com.intellij.openapi.editor.impl.EditorFactoryImpl; +import com.intellij.facet.impl.ui.FacetErrorPanel; +import com.intellij.facet.ui.FacetEditorValidator; +import com.intellij.facet.ui.ValidationResult; import com.intellij.openapi.fileChooser.FileChooserDescriptor; import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory; -import com.intellij.openapi.options.Configurable; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.module.ModuleManager; import com.intellij.openapi.options.ConfigurationException; import com.intellij.openapi.options.NonDefaultProjectConfigurable; import com.intellij.openapi.options.SearchableConfigurable; import com.intellij.openapi.project.Project; -import com.intellij.openapi.roots.ContentIterator; -import com.intellij.openapi.roots.ProjectRootManager; +import com.intellij.openapi.projectRoots.Sdk; import com.intellij.openapi.ui.TextFieldWithBrowseButton; import com.intellij.openapi.util.Comparing; -import com.intellij.openapi.vfs.VirtualFile; import com.intellij.ui.CollectionComboBoxModel; -import com.intellij.util.FileContentUtil; -import com.jetbrains.django.lang.template.DjangoTemplateFileType; -import com.jetbrains.python.templateLanguages.TemplatesConfigurationsModel; -import com.jetbrains.python.templateLanguages.TemplatesService; +import com.jetbrains.python.PyBundle; import com.jetbrains.python.documentation.DocStringFormat; import com.jetbrains.python.documentation.PyDocumentationSettings; +import com.jetbrains.python.sdk.PythonSdkType; import com.jetbrains.python.testing.PythonTestConfigurationsModel; import com.jetbrains.python.testing.TestRunnerService; +import com.jetbrains.python.testing.VFSTestFrameworkListener; import com.jetbrains.rest.ReSTService; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; import javax.swing.*; +import java.awt.*; import java.util.List; /** @@ -48,6 +43,7 @@ public class PyIntegratedToolsConfigurable implements SearchableConfigurable, No private final PyDocumentationSettings myDocumentationSettings; private TextFieldWithBrowseButton myWorkDir; private JCheckBox txtIsRst; + private JPanel myErrorPanel; public PyIntegratedToolsConfigurable(Project project) { myProject = project; @@ -59,8 +55,41 @@ public class PyIntegratedToolsConfigurable implements SearchableConfigurable, No ReSTService service = ReSTService.getInstance(myProject); myWorkDir.setText(service.getWorkdir()); txtIsRst.setSelected(service.txtIsRst()); + initErrorValidation(); } + private void initErrorValidation() { + FacetErrorPanel facetErrorPanel = new FacetErrorPanel(); + myErrorPanel.add(facetErrorPanel.getComponent(), BorderLayout.CENTER); + + facetErrorPanel.getValidatorsManager().registerValidator(new FacetEditorValidator() { + @Override + public ValidationResult check() { + Module[] modules = ModuleManager.getInstance(myProject).getModules(); + if (modules.length == 0) return ValidationResult.OK; + final Sdk sdk = PythonSdkType.findPythonSdk(modules[0]); + if (sdk != null) { + if (myTestRunnerComboBox.getSelectedItem() == PythonTestConfigurationsModel.PY_TEST_NAME) { + if (!VFSTestFrameworkListener.getInstance().isPyTestInstalled(sdk.getHomePath())) + return new ValidationResult(PyBundle.message("runcfg.testing.no.test.framework", "py.test")); + } + else if (myTestRunnerComboBox.getSelectedItem() == PythonTestConfigurationsModel.PYTHONS_NOSETEST_NAME) { + if (!VFSTestFrameworkListener.getInstance().isNoseTestInstalled(sdk.getHomePath())) + return new ValidationResult(PyBundle.message("runcfg.testing.no.test.framework", "nosetest")); + } + else if (myTestRunnerComboBox.getSelectedItem() == PythonTestConfigurationsModel.PYTHONS_ATTEST_NAME) { + if (!VFSTestFrameworkListener.getInstance().isAtTestInstalled(sdk.getHomePath())) + return new ValidationResult(PyBundle.message("runcfg.testing.no.test.framework", "attest")); + } + } + return ValidationResult.OK; + } + }, myTestRunnerComboBox); + + facetErrorPanel.getValidatorsManager().validate(); + } + + @Nls @Override public String getDisplayName() { diff --git a/python/src/com/jetbrains/python/inspections/PyArgumentListInspection.java b/python/src/com/jetbrains/python/inspections/PyArgumentListInspection.java index d72b44aa3659..67091c4ebed4 100644 --- a/python/src/com/jetbrains/python/inspections/PyArgumentListInspection.java +++ b/python/src/com/jetbrains/python/inspections/PyArgumentListInspection.java @@ -6,6 +6,7 @@ import com.intellij.lang.ASTNode; import com.intellij.psi.PsiElementVisitor; import com.intellij.psi.util.PsiTreeUtil; import com.jetbrains.cython.CythonLanguageDialect; +import com.jetbrains.mako.MakoLanguage; import com.jetbrains.python.PyBundle; import com.jetbrains.python.PyNames; import com.jetbrains.python.PyTokenTypes; @@ -43,7 +44,7 @@ public class PyArgumentListInspection extends PyInspection { @Override public void visitPyArgumentList(final PyArgumentList node) { - if (CythonLanguageDialect._isDisabledFor(node)) { + if (CythonLanguageDialect._isDisabledFor(node) || MakoLanguage._isDisabledFor(node)) { return; } // analyze @@ -87,14 +88,18 @@ public class PyArgumentListInspection extends PyInspection { } - public static void inspectPyArgumentList(PyArgumentList node, ProblemsHolder holder, final TypeEvalContext context) { + public static void inspectPyArgumentList(PyArgumentList node, ProblemsHolder holder, final TypeEvalContext context, int implicitOffset) { if (node.getParent() instanceof PyClass) return; // class Foo(object) is also an arg list - CallArgumentsMapping result = node.analyzeCall(PyResolveContext.noImplicits().withTypeEvalContext(context)); + CallArgumentsMapping result = node.analyzeCall(PyResolveContext.noImplicits().withTypeEvalContext(context), implicitOffset); highlightIncorrectArguments(holder, result); highlightMissingArguments(node, holder, result); highlightStarArgumentTypeMismatch(node, holder, context); } + public static void inspectPyArgumentList(PyArgumentList node, ProblemsHolder holder, final TypeEvalContext context) { + inspectPyArgumentList(node, holder, context, 0); + } + private static void highlightIncorrectArguments(ProblemsHolder holder, CallArgumentsMapping result) { for (Map.Entry> arg_entry : result.getArgumentFlags().entrySet()) { EnumSet flags = arg_entry.getValue(); diff --git a/python/src/com/jetbrains/python/lexer/PythonEditorHighlighter.java b/python/src/com/jetbrains/python/lexer/PythonEditorHighlighter.java index 9cdb214c0a3f..5f1e804b4f8f 100644 --- a/python/src/com/jetbrains/python/lexer/PythonEditorHighlighter.java +++ b/python/src/com/jetbrains/python/lexer/PythonEditorHighlighter.java @@ -6,9 +6,13 @@ import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.colors.EditorColorsScheme; import com.intellij.openapi.editor.event.DocumentEvent; import com.intellij.openapi.editor.ex.util.LexerEditorHighlighter; +import com.intellij.openapi.editor.highlighter.HighlighterClient; import com.intellij.openapi.fileTypes.SyntaxHighlighter; import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Key; +import com.intellij.openapi.util.TextRange; import com.intellij.openapi.vfs.VirtualFile; +import com.jetbrains.python.PyNames; import com.jetbrains.python.PythonFileType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -25,6 +29,9 @@ public class PythonEditorHighlighter extends LexerEditorHighlighter { scheme); } + private Boolean hadUnicodeImport = false; + + public static final Key KEY = new Key("python.future.import"); @Override public void documentChanged(DocumentEvent e) { synchronized (this) { @@ -34,10 +41,14 @@ public class PythonEditorHighlighter extends LexerEditorHighlighter { // we should update the whole document if (l instanceof LayeredLexer) { Lexer delegate = ((LayeredLexer)l).getDelegate(); + int offset = e.getOffset(); + int lineNumber = document.getLineNumber(offset); + TextRange tr = new TextRange(document.getLineStartOffset(lineNumber), document.getLineEndOffset(lineNumber)); + document.putUserData(KEY, document.getText(tr).indexOf(PyNames.UNICODE_LITERALS) == -1); + Boolean hasUnicodeImport = document.getUserData(KEY); if (delegate instanceof PythonHighlightingLexer && (((PythonHighlightingLexer)delegate).getImportOffset() > e.getOffset() - || ((PythonHighlightingLexer)delegate).getImportOffset() == -1)) { - + || hasUnicodeImport != hadUnicodeImport)) { ((PythonHighlightingLexer)delegate).clearState(e.getDocument().getTextLength()); setText(document.getCharsSequence()); } @@ -46,4 +57,19 @@ public class PythonEditorHighlighter extends LexerEditorHighlighter { else super.documentChanged(e); } } + + @Override + public void beforeDocumentChange(DocumentEvent e) { + final Document document = e.getDocument(); + hadUnicodeImport = document.getUserData(KEY); + } + + @Override + public void setEditor(HighlighterClient editor) { + Lexer l = getLexer(); + if (l instanceof LayeredLexer) { + editor.getDocument().putUserData(KEY, editor.getDocument().getText().indexOf(PyNames.UNICODE_LITERALS) == -1); + } + super.setEditor(editor); + } } diff --git a/python/src/com/jetbrains/python/psi/PyArgumentList.java b/python/src/com/jetbrains/python/psi/PyArgumentList.java index fad66e5d654b..8d18aecada73 100644 --- a/python/src/com/jetbrains/python/psi/PyArgumentList.java +++ b/python/src/com/jetbrains/python/psi/PyArgumentList.java @@ -30,10 +30,15 @@ public interface PyArgumentList extends PyElement { * Tries to map the argument list to callee's idea of parameters. * @return a result object with mappings and diagnostic flags. * @param resolveContext the reference resolution context + * @param implicitOffset known from the context implicit offset */ + @NotNull + CallArgumentsMapping analyzeCall(PyResolveContext resolveContext, int implicitOffset); + @NotNull CallArgumentsMapping analyzeCall(PyResolveContext resolveContext); + @Nullable ASTNode getClosingParen(); } diff --git a/python/src/com/jetbrains/python/psi/PyCallExpression.java b/python/src/com/jetbrains/python/psi/PyCallExpression.java index 03e84c9d8a28..5547eb09c48f 100644 --- a/python/src/com/jetbrains/python/psi/PyCallExpression.java +++ b/python/src/com/jetbrains/python/psi/PyCallExpression.java @@ -55,6 +55,14 @@ public interface PyCallExpression extends PyExpression { @Nullable PyMarkedCallee resolveCallee(PyResolveContext resolveContext); + /** + * + * @param resolveContext the reference resolve context + * @param implicitOffset known from the context implicit offset + */ + @Nullable + PyMarkedCallee resolveCallee(PyResolveContext resolveContext, int implicitOffset); + /** * Checks if the unqualified name of the callee matches any of the specified names * diff --git a/python/src/com/jetbrains/python/psi/impl/PyArgumentListImpl.java b/python/src/com/jetbrains/python/psi/impl/PyArgumentListImpl.java index 578b101d85c9..f9baf9e4d174 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyArgumentListImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyArgumentListImpl.java @@ -238,6 +238,11 @@ public class PyArgumentListImpl extends PyElementImpl implements PyArgumentList @NotNull public CallArgumentsMapping analyzeCall(PyResolveContext resolveContext) { + return analyzeCall(resolveContext, 0); + } + + @NotNull + public CallArgumentsMapping analyzeCall(PyResolveContext resolveContext, int offset) { final CallArgumentsMappingImpl ret = new CallArgumentsMappingImpl(this); // declaration-based checks // proper arglist is: [positional,...][name=value,...][*tuple,][**dict] @@ -245,7 +250,7 @@ public class PyArgumentListImpl extends PyElementImpl implements PyArgumentList // following the spec: http://docs.python.org/ref/calls.html PyCallExpression call = getCallExpression(); if (call != null) { - PyCallExpression.PyMarkedCallee resolved_callee = call.resolveCallee(resolveContext); + PyCallExpression.PyMarkedCallee resolved_callee = call.resolveCallee(resolveContext, offset); if (resolved_callee != null) ret.mapArguments(resolved_callee, resolveContext.getTypeEvalContext()); } return ret; diff --git a/python/src/com/jetbrains/python/psi/impl/PyCallExpressionHelper.java b/python/src/com/jetbrains/python/psi/impl/PyCallExpressionHelper.java index 4e17002e5b59..5ecf88777b89 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyCallExpressionHelper.java +++ b/python/src/com/jetbrains/python/psi/impl/PyCallExpressionHelper.java @@ -104,6 +104,11 @@ public class PyCallExpressionHelper { @Nullable public static PyCallExpression.PyMarkedCallee resolveCallee(PyCallExpression us, PyResolveContext resolveContext) { + return resolveCallee(us, resolveContext, 0); + } + + @Nullable + public static PyCallExpression.PyMarkedCallee resolveCallee(PyCallExpression us, PyResolveContext resolveContext, int implicitOffset) { PyFunction.Flag wrappedFlag = null; boolean isConstructorCall = false; @@ -152,8 +157,8 @@ public class PyCallExpressionHelper { PyExpression lastQualifier = qualifiers != null && qualifiers.isEmpty() ? null : qualifiers.get(qualifiers.size()-1); boolean isByClass = lastQualifier == null ? false : isQualifiedByClass((Callable)resolved, lastQualifier, resolveContext.getTypeEvalContext()); final Callable callable = (Callable)resolved; - - int implicitOffset = getImplicitArgumentCount(callable, flags, isConstructorCall, isByInstance, isByClass); + + implicitOffset += getImplicitArgumentCount(callable, flags, isConstructorCall, isByInstance, isByClass); implicitOffset = implicitOffset < 0? 0: implicitOffset; // wrong source can trigger strange behaviour return new PyCallExpression.PyMarkedCallee(callable, flags, implicitOffset, resolveResult != null ? resolveResult.isImplicit() : false); diff --git a/python/src/com/jetbrains/python/psi/impl/PyCallExpressionImpl.java b/python/src/com/jetbrains/python/psi/impl/PyCallExpressionImpl.java index 3d50652a3ea7..69cab39d7d34 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyCallExpressionImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyCallExpressionImpl.java @@ -64,6 +64,10 @@ public class PyCallExpressionImpl extends PyElementImpl implements PyCallExpress return PyCallExpressionHelper.resolveCallee(this, resolveContext); } + public PyMarkedCallee resolveCallee(PyResolveContext resolveContext, int offset) { + return PyCallExpressionHelper.resolveCallee(this, resolveContext, offset); + } + public boolean isCalleeText(@NotNull String... nameCandidates) { return PyCallExpressionHelper.isCalleeText(this, nameCandidates); } diff --git a/python/src/com/jetbrains/python/psi/impl/PyDecoratorImpl.java b/python/src/com/jetbrains/python/psi/impl/PyDecoratorImpl.java index e7c46d8c3a6e..b01be5a05a55 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyDecoratorImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyDecoratorImpl.java @@ -112,6 +112,9 @@ public class PyDecoratorImpl extends PyPresentableElementImpl i } public PyMarkedCallee resolveCallee(PyResolveContext resolveContext) { + return resolveCallee(resolveContext, 0); + } + public PyMarkedCallee resolveCallee(PyResolveContext resolveContext, int offset) { PyMarkedCallee callee = PyCallExpressionHelper.resolveCallee(this, resolveContext); if (callee == null) return null; if (!hasArgumentList()) { diff --git a/python/src/com/jetbrains/python/testing/attest/PythonAtTestRunConfiguration.java b/python/src/com/jetbrains/python/testing/attest/PythonAtTestRunConfiguration.java index 8807ec8e10c1..268a7088a961 100644 --- a/python/src/com/jetbrains/python/testing/attest/PythonAtTestRunConfiguration.java +++ b/python/src/com/jetbrains/python/testing/attest/PythonAtTestRunConfiguration.java @@ -7,6 +7,7 @@ import com.intellij.execution.runners.ExecutionEnvironment; import com.intellij.openapi.options.SettingsEditor; import com.intellij.openapi.util.InvalidDataException; import com.intellij.openapi.util.WriteExternalException; +import com.jetbrains.python.PyBundle; import com.jetbrains.python.testing.AbstractPythonTestRunConfiguration; import com.jetbrains.python.testing.VFSTestFrameworkListener; import org.jdom.Element; @@ -62,6 +63,6 @@ public class PythonAtTestRunConfiguration extends AbstractPythonTestRunConfigura public void checkConfiguration() throws RuntimeConfigurationException { super.checkConfiguration(); if (!VFSTestFrameworkListener.getInstance().isAtTestInstalled(getSdkHome())) - throw new RuntimeConfigurationWarning("No attest runner found in selected interpreter"); + throw new RuntimeConfigurationWarning(PyBundle.message("runcfg.testing.no.test.framework", "attest")); } } diff --git a/python/src/com/jetbrains/python/testing/nosetest/PythonNoseTestRunConfiguration.java b/python/src/com/jetbrains/python/testing/nosetest/PythonNoseTestRunConfiguration.java index 43aa66ccb3d4..3de3ab8d5b78 100644 --- a/python/src/com/jetbrains/python/testing/nosetest/PythonNoseTestRunConfiguration.java +++ b/python/src/com/jetbrains/python/testing/nosetest/PythonNoseTestRunConfiguration.java @@ -8,6 +8,7 @@ import com.intellij.openapi.options.SettingsEditor; import com.intellij.openapi.util.InvalidDataException; import com.intellij.openapi.util.JDOMExternalizerUtil; import com.intellij.openapi.util.WriteExternalException; +import com.jetbrains.python.PyBundle; import com.jetbrains.python.testing.AbstractPythonTestRunConfiguration; import com.jetbrains.python.testing.VFSTestFrameworkListener; import org.jdom.Element; @@ -79,7 +80,7 @@ public class PythonNoseTestRunConfiguration extends AbstractPythonTestRunConfigu public void checkConfiguration() throws RuntimeConfigurationException { super.checkConfiguration(); if (!VFSTestFrameworkListener.getInstance().isNoseTestInstalled(getSdkHome())) - throw new RuntimeConfigurationWarning("No nosetest runner found in selected interpreter"); + throw new RuntimeConfigurationWarning(PyBundle.message("runcfg.testing.no.test.framework", "nosetest")); } public boolean useParam() { diff --git a/python/src/com/jetbrains/python/testing/pytest/PyTestRunConfiguration.java b/python/src/com/jetbrains/python/testing/pytest/PyTestRunConfiguration.java index 6ece7cb4bc8f..be0c596ebdc2 100644 --- a/python/src/com/jetbrains/python/testing/pytest/PyTestRunConfiguration.java +++ b/python/src/com/jetbrains/python/testing/pytest/PyTestRunConfiguration.java @@ -9,6 +9,7 @@ import com.intellij.openapi.util.InvalidDataException; import com.intellij.openapi.util.JDOMExternalizerUtil; import com.intellij.openapi.util.WriteExternalException; import com.intellij.openapi.util.text.StringUtil; +import com.jetbrains.python.PyBundle; import com.jetbrains.python.testing.AbstractPythonTestRunConfiguration; import com.jetbrains.python.testing.VFSTestFrameworkListener; import org.jdom.Element; @@ -115,7 +116,7 @@ public class PyTestRunConfiguration extends AbstractPythonTestRunConfiguration i throw new RuntimeConfigurationError("Please specify target folder or script"); } if (!VFSTestFrameworkListener.getInstance().isPyTestInstalled(getSdkHome())) - throw new RuntimeConfigurationWarning("No py.test runner found in selected interpreter"); + throw new RuntimeConfigurationWarning(PyBundle.message("runcfg.testing.no.test.framework", "py.test")); } @Override