├── src ├── test │ ├── testData │ │ ├── annotation │ │ │ ├── dummy.py │ │ │ ├── followsImports.py │ │ │ ├── errorMixedIntoStdOut.py │ │ │ ├── white space │ │ │ │ ├── mypy.ini │ │ │ │ ├── mypy_a.py_result.json │ │ │ │ └── mypy │ │ │ ├── a.py │ │ │ ├── e.py │ │ │ ├── d.py │ │ │ ├── b.py │ │ │ ├── c.py │ │ │ ├── mypy_c.py_result.json │ │ │ ├── mypy_d.py_result.json │ │ │ ├── mypy_b.py_result.json │ │ │ ├── mypy_e.py_result.json │ │ │ ├── mypy_a.py_result.json │ │ │ ├── mypy_followsImports.py_result.json │ │ │ ├── mypy_errorMixedIntoStdOut.py_result.json │ │ │ └── mypy │ │ ├── action │ │ │ ├── scan_cli │ │ │ │ ├── excluded_dir │ │ │ │ │ └── should_be_excluded.py │ │ │ │ ├── manualScan.py │ │ │ │ ├── mypy_exit_with_1 │ │ │ │ ├── mypy_exit_with_2 │ │ │ │ ├── mypy_exit_with_3 │ │ │ │ ├── mypy_non_json_output │ │ │ │ ├── manualScan.json │ │ │ │ └── mypy │ │ │ ├── scan_sdk │ │ │ │ ├── excluded_dir │ │ │ │ │ └── should_be_excluded.py │ │ │ │ ├── manualScan.json │ │ │ │ └── MockSdk │ │ │ │ │ └── bin │ │ │ │ │ └── python │ │ │ ├── rescan │ │ │ │ ├── mypy │ │ │ │ ├── mypy2 │ │ │ │ └── results.json │ │ │ └── stop_scan │ │ │ │ └── mypy │ │ └── initialization │ │ │ ├── projectWithLocalSdk │ │ │ ├── MockSdk │ │ │ │ └── bin │ │ │ │ │ └── python │ │ │ └── .idea │ │ │ │ ├── misc.xml │ │ │ │ ├── modules.xml │ │ │ │ └── projectWithLocalSdk.iml │ │ │ ├── projectWithRemoteSdk │ │ │ ├── MockSdk │ │ │ │ └── bin │ │ │ │ │ └── python │ │ │ └── .idea │ │ │ │ ├── misc.xml │ │ │ │ ├── modules.xml │ │ │ │ └── projectWithRemoteSdk.iml │ │ │ └── OldConfiguration │ │ │ └── .idea │ │ │ └── mypy.xml │ └── kotlin │ │ └── works │ │ └── szabope │ │ └── plugins │ │ └── mypy │ │ ├── testutil │ │ ├── notification.kt │ │ ├── MypyPluginPackageManagementServiceStub.kt │ │ ├── TestToolWindowHeadlessManagerImpl.kt │ │ ├── context.kt │ │ ├── TestDialogManager.kt │ │ └── actions.kt │ │ ├── initialization │ │ ├── PluginInitializationFromScratchTest.kt │ │ ├── MypyInitializationFromOldConfigurationTest.kt │ │ ├── MypyInitializationWithLocalPythonSdkTest.kt │ │ └── MypyInitializationWithRemotePythonSdkTest.kt │ │ ├── AbstractMypyHeavyPlatformTestCase.kt │ │ ├── action │ │ ├── StopScanTest.kt │ │ ├── RescanTest.kt │ │ ├── ScanSdkTest.kt │ │ └── ScanCliTest.kt │ │ ├── AbstractToolWindowTestCase.kt │ │ ├── annotator │ │ ├── AnnotatorTest.kt │ │ └── IntentionTest.kt │ │ └── AbstractMypyTestCase.kt └── main │ ├── kotlin │ └── works │ │ └── szabope │ │ └── plugins │ │ └── mypy │ │ ├── services │ │ ├── MypySettingsInvalid.kt │ │ ├── MypyExecutor.kt │ │ ├── MypyExecutorConfiguration.kt │ │ ├── parser │ │ │ ├── MypyMessage.kt │ │ │ ├── MypyMessageConverter.kt │ │ │ └── MypyOutputParser.kt │ │ ├── BalloonUtil.kt │ │ ├── mypySeverityConfigs.kt │ │ ├── MypyPluginPackageManagementService.kt │ │ ├── handleScanException.kt │ │ ├── IncompleteConfigurationNotifier.kt │ │ ├── OldMypySettings.kt │ │ ├── mypyParamListBuilders.kt │ │ ├── AsyncScanService.kt │ │ ├── SyncScanService.kt │ │ └── MypySettings.kt │ │ ├── MypyArgs.kt │ │ ├── toolWindow │ │ ├── MypyToolWindowFactory.kt │ │ ├── MypyTreeModelDataItem.kt │ │ ├── MypyTreeService.kt │ │ └── MypyToolWindowPanel.kt │ │ ├── annotator │ │ ├── MypyInspection.kt │ │ ├── MypyAnnotator.kt │ │ └── MypyIgnoreIntention.kt │ │ ├── action │ │ ├── ScanCurrentlyFocusedOneInEditorAction.kt │ │ ├── RescanAction.kt │ │ ├── ScanJobRegistry.kt │ │ ├── StopScanAction.kt │ │ ├── OpenSettingsAction.kt │ │ ├── InstallMypyAction.kt │ │ └── ScanAction.kt │ │ ├── MypyBundle.kt │ │ ├── activity │ │ └── SettingsInitializationActivity.kt │ │ ├── dialog │ │ ├── DialogManager.kt │ │ └── MypyErrorDialog.kt │ │ └── configurable │ │ ├── MypyConfigurable.kt │ │ └── MypyValidator.kt │ └── resources │ ├── META-INF │ ├── plugin.xml │ ├── pluginIcon.svg │ ├── pluginIcon_dark.svg │ ├── works.szabope.mypy-PythonCore.xml │ └── pluginIcon_LICENSE.txt │ ├── inspectionDescriptions │ └── MypyInspection.html │ ├── icons │ ├── mypyToolWindow_dark.svg │ ├── mypyToolWindow@20x20.svg │ ├── mypyToolWindow@20x20_dark.svg │ ├── mypyToolWindow.svg │ ├── mypyScanAction.svg │ └── mypyScanAction_dark.svg │ └── messages │ └── MypyBundle.properties ├── NOTICE ├── art ├── menu.png ├── results.png ├── settings.png ├── mypy_not_set.png ├── results_lowres.png ├── execute.svg ├── refresh.svg └── mypyScanAction.svg ├── .gitignore ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── settings.gradle.kts ├── CODE_OF_CONDUCT.md ├── .github ├── dependabot.yml └── workflows │ ├── release.yml │ └── build.yml ├── gradle.properties ├── gradlew.bat ├── README.md ├── gradlew └── LICENSE /src/test/testData/annotation/dummy.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Mypy PyCharm Plugin 2 | Copyright 2024, Peter Szabo -------------------------------------------------------------------------------- /src/test/testData/annotation/followsImports.py: -------------------------------------------------------------------------------- 1 | def _(): 2 | pass -------------------------------------------------------------------------------- /src/test/testData/annotation/errorMixedIntoStdOut.py: -------------------------------------------------------------------------------- 1 | def _(): 2 | pass -------------------------------------------------------------------------------- /src/test/testData/action/scan_cli/excluded_dir/should_be_excluded.py: -------------------------------------------------------------------------------- 1 | tutu=1 -------------------------------------------------------------------------------- /src/test/testData/action/scan_sdk/excluded_dir/should_be_excluded.py: -------------------------------------------------------------------------------- 1 | tutu=1 -------------------------------------------------------------------------------- /art/menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szabope/mypy-pycharm-plugin/HEAD/art/menu.png -------------------------------------------------------------------------------- /src/test/testData/annotation/white space/mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | show_column_numbers = True -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /.idea 3 | .intellijPlatform 4 | .qodana 5 | build 6 | .run 7 | .kotlin -------------------------------------------------------------------------------- /art/results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szabope/mypy-pycharm-plugin/HEAD/art/results.png -------------------------------------------------------------------------------- /art/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szabope/mypy-pycharm-plugin/HEAD/art/settings.png -------------------------------------------------------------------------------- /src/test/testData/annotation/a.py: -------------------------------------------------------------------------------- 1 | def lets_have_fun() -> [int]: 2 | return 'fun' 3 | -------------------------------------------------------------------------------- /art/mypy_not_set.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szabope/mypy-pycharm-plugin/HEAD/art/mypy_not_set.png -------------------------------------------------------------------------------- /src/test/testData/action/scan_cli/manualScan.py: -------------------------------------------------------------------------------- 1 | def lets_have_fun() -> [int]: 2 | return 'fun' 3 | -------------------------------------------------------------------------------- /art/results_lowres.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szabope/mypy-pycharm-plugin/HEAD/art/results_lowres.png -------------------------------------------------------------------------------- /src/test/testData/annotation/e.py: -------------------------------------------------------------------------------- 1 | def lets_have_fun() -> [int]: # comment 2 | return 'fun' 3 | -------------------------------------------------------------------------------- /src/test/testData/annotation/d.py: -------------------------------------------------------------------------------- 1 | def more_fun_here() -> str: 2 | return f"""thisonehere{x}""" 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szabope/mypy-pycharm-plugin/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/test/testData/action/rescan/mypy: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ "$1" = "-V" ]; then 3 | echo "mypy 1.11.2 (compiled: yes)" 4 | fi 5 | -------------------------------------------------------------------------------- /src/test/testData/annotation/b.py: -------------------------------------------------------------------------------- 1 | def lets_have_fun() -> [int]: # type: ignore[some-code,another-code, and-a-third-one] 2 | return 'fun' 3 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" 3 | } 4 | rootProject.name = "Mypy PyCharm Plugin" 5 | -------------------------------------------------------------------------------- /src/test/testData/action/scan_cli/mypy_exit_with_1: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ "$1" = "-V" ]; then 3 | echo "mypy 1.11.2 (compiled: yes)" 4 | else 5 | exit 1 6 | fi -------------------------------------------------------------------------------- /src/test/testData/action/scan_cli/mypy_exit_with_2: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ "$1" = "-V" ]; then 3 | echo "mypy 1.11.2 (compiled: yes)" 4 | else 5 | exit 2 6 | fi -------------------------------------------------------------------------------- /src/test/testData/action/scan_cli/mypy_exit_with_3: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ "$1" = "-V" ]; then 3 | echo "mypy 1.11.2 (compiled: yes)" 4 | else 5 | exit 3 6 | fi -------------------------------------------------------------------------------- /src/test/testData/initialization/projectWithLocalSdk/MockSdk/bin/python: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ "$1" = "-V" ]; then 3 | printf "Python 3.12\n" 4 | else 5 | exit 1 6 | fi -------------------------------------------------------------------------------- /src/test/testData/initialization/projectWithRemoteSdk/MockSdk/bin/python: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ "$1" = "-V" ]; then 3 | printf "Python 3.12\n" 4 | else 5 | exit 1 6 | fi -------------------------------------------------------------------------------- /src/test/testData/annotation/c.py: -------------------------------------------------------------------------------- 1 | def more_fun_here() -> str: 2 | return f"""this one here {x} 3 | should be annotated, but 4 | intention should not be available""" 5 | -------------------------------------------------------------------------------- /src/main/kotlin/works/szabope/plugins/mypy/services/MypySettingsInvalid.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.services 2 | 3 | class MypySettingsInvalid(message: String) : RuntimeException(message) -------------------------------------------------------------------------------- /src/test/testData/annotation/mypy_c.py_result.json: -------------------------------------------------------------------------------- 1 | {"file": "/src/c.py", "line": 2, "column": 11, "message": "Name \"x\" is not defined", "hint": null, "code": "name-defined", "severity": "error"} -------------------------------------------------------------------------------- /src/test/testData/annotation/mypy_d.py_result.json: -------------------------------------------------------------------------------- 1 | {"file": "/src/d.py", "line": 2, "column": 11, "message": "Name \"x\" is not defined", "hint": null, "code": "name-defined", "severity": "error"} -------------------------------------------------------------------------------- /src/test/testData/action/rescan/mypy2: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ "$1" = "-V" ]; then 3 | echo "mypy 1.11.2 (compiled: yes)" 4 | else 5 | cat "$(dirname "$(realpath -s "$0")")/results.json" 6 | fi 7 | -------------------------------------------------------------------------------- /src/test/testData/action/scan_cli/mypy_non_json_output: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ "$1" = "-V" ]; then 3 | echo "mypy 1.11.2 (compiled: yes)" 4 | else 5 | echo "This is an error printed to stdout" 6 | fi -------------------------------------------------------------------------------- /src/test/testData/action/stop_scan/mypy: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ "$1" = "-V" ]; then 3 | echo "mypy 1.11.2 (compiled: yes)" 4 | else 5 | while : 6 | do 7 | sleep 0.1 8 | done 9 | fi 10 | -------------------------------------------------------------------------------- /src/main/kotlin/works/szabope/plugins/mypy/MypyArgs.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy 2 | 3 | object MypyArgs { 4 | const val MYPY_MANDATORY_COMMAND_ARGS = "--show-column-numbers --show-absolute-path --output json" 5 | } -------------------------------------------------------------------------------- /src/test/testData/action/rescan/results.json: -------------------------------------------------------------------------------- 1 | {"file": "src/a.py", "line": 1, "column": -1, "message": "Bracketed expression \"[...]\" is not valid as a type", "hint": "Did you mean \"List[...]\"?", "code": "valid-type", "severity": "error"} 2 | -------------------------------------------------------------------------------- /src/test/testData/action/scan_cli/manualScan.json: -------------------------------------------------------------------------------- 1 | {"file": "src/a.py", "line": 1, "column": -1, "message": "Bracketed expression \"[...]\" is not valid as a type", "hint": "Did you mean \"List[...]\"?", "code": "valid-type", "severity": "error"} -------------------------------------------------------------------------------- /src/test/testData/annotation/mypy_b.py_result.json: -------------------------------------------------------------------------------- 1 | {"file": "/src/b.py", "line": 1, "column": -1, "message": "Bracketed expression \"[...]\" is not valid as a type", "hint": "Did you mean \"List[...]\"?", "code": "valid-type", "severity": "error"} -------------------------------------------------------------------------------- /src/test/testData/annotation/mypy_e.py_result.json: -------------------------------------------------------------------------------- 1 | {"file": "/src/e.py", "line": 1, "column": -1, "message": "Bracketed expression \"[...]\" is not valid as a type", "hint": "Did you mean \"List[...]\"?", "code": "valid-type", "severity": "error"} -------------------------------------------------------------------------------- /src/test/testData/action/scan_sdk/manualScan.json: -------------------------------------------------------------------------------- 1 | {"file": "src/a.py", "line": 1, "column": -1, "message": "Bracketed expression \"[...]\" is not valid as a type", "hint": "Did you mean \"List[...]\"?", "code": "valid-type", "severity": "error"} 2 | -------------------------------------------------------------------------------- /src/test/testData/annotation/mypy_a.py_result.json: -------------------------------------------------------------------------------- 1 | {"file": "/src/a.py", "line": 1, "column": -1, "message": "Bracketed expression \"[...]\" is not valid as a type", "hint": "Did you mean \"List[...]\"?", "code": "valid-type", "severity": "error"} 2 | -------------------------------------------------------------------------------- /src/test/testData/annotation/white space/mypy_a.py_result.json: -------------------------------------------------------------------------------- 1 | {"file": "/src/a.py", "line": 1, "column": -1, "message": "Bracketed expression \"[...]\" is not valid as a type", "hint": "Did you mean \"List[...]\"?", "code": "valid-type", "severity": "error"} 2 | -------------------------------------------------------------------------------- /src/test/testData/initialization/projectWithLocalSdk/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | 3 | This project and the corresponding community is governed by the [JetBrains Open Source and Community Code of Conduct](https://confluence.jetbrains.com/display/ALL/JetBrains+Open+Source+and+Community+Code+of+Conduct). Please make sure you read it. 4 | 5 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /src/test/testData/initialization/projectWithRemoteSdk/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /art/execute.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/test/testData/annotation/mypy_followsImports.py_result.json: -------------------------------------------------------------------------------- 1 | {"file": "/src/followsImports.py", "line": 1, "column": 1, "message": "Name \"x\" is not defined", "hint": null, "code": "name-defined", "severity": "error"} 2 | {"file": "/src/dummy.py", "line": 1, "column": 1, "message": "Name \"x\" is not defined", "hint": null, "code": "name-defined", "severity": "error"} 3 | -------------------------------------------------------------------------------- /src/test/testData/initialization/projectWithLocalSdk/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/test/testData/initialization/projectWithRemoteSdk/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/test/testData/initialization/OldConfiguration/.idea/mypy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | -------------------------------------------------------------------------------- /src/test/testData/annotation/mypy_errorMixedIntoStdOut.py_result.json: -------------------------------------------------------------------------------- 1 | {"file": "/src/errorMixedIntoStdOut.py", "line": 1, "column": 1, "message": "Name \"x\" is not defined", "hint": null, "code": "name-defined", "severity": "error"} 2 | Blind adherence to standards is the refuge of martinets. 3 | {"file": "/src/errorMixedIntoStdOut.py", "line": 2, "column": 1, "message": "Name \"x\" is not defined", "hint": null, "code": "name-defined", "severity": "error"} 4 | -------------------------------------------------------------------------------- /src/test/testData/annotation/mypy: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ "$1" = "-V" ]; then 3 | echo "mypy 1.11.2 (compiled: yes)" 4 | else 5 | for last; do true; done 6 | filename=$(basename "$last") 7 | # Let's extract original filename from temp file's name, e.g. pycharm_mypy_15708631520625806318_d.py -> d.py 8 | filename="${filename/pycharm_mypy_/}" 9 | filename="${filename//[0-9]/}" 10 | filename="${filename/_/}" 11 | cat "$0_${filename}_result.json" 12 | fi -------------------------------------------------------------------------------- /src/main/kotlin/works/szabope/plugins/mypy/toolWindow/MypyToolWindowFactory.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.toolWindow 2 | 3 | import com.intellij.openapi.project.Project 4 | import works.szabope.plugins.common.toolWindow.MyToolWindowFactory 5 | import works.szabope.plugins.mypy.MypyBundle 6 | 7 | class MypyToolWindowFactory : MyToolWindowFactory(MypyBundle.message("mypy.toolwindow.name")) { 8 | override fun createPanel(project: Project) = MypyToolWindowPanel(project) 9 | } -------------------------------------------------------------------------------- /src/main/kotlin/works/szabope/plugins/mypy/annotator/MypyInspection.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.annotator 2 | 3 | import com.intellij.codeInspection.ex.ExternalAnnotatorBatchInspection 4 | import com.jetbrains.python.inspections.PyInspection 5 | 6 | const val MypyInspectionId = "MypyInspection" 7 | 8 | internal class MypyInspection : PyInspection(), ExternalAnnotatorBatchInspection { 9 | 10 | override fun getShortName(): String { 11 | return MypyInspectionId 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/test/testData/action/scan_cli/mypy: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ "$1" = "-V" ]; then 3 | echo "mypy 1.11.2 (compiled: yes)" 4 | else 5 | my_dir=$(dirname "$(realpath -s "$0")") 6 | for last; do :; done 7 | expected_args="--show-column-numbers --show-absolute-path --output json --exclude excluded_dir $last" 8 | if [ "$*" != "$expected_args" ]; then 9 | >&2 echo "expected: $expected_args" 10 | >&2 echo "received: $*" 11 | exit 2 # because 1 needs to be treated as being fine 12 | fi 13 | cat "$my_dir/manualScan.json" 14 | fi -------------------------------------------------------------------------------- /src/main/kotlin/works/szabope/plugins/mypy/services/MypyExecutor.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.services 2 | 3 | import com.intellij.execution.process.ProcessEvent 4 | import com.intellij.openapi.project.Project 5 | import works.szabope.plugins.common.run.ToolExecutor 6 | 7 | class MypyExecutor(project: Project) : ToolExecutor(project, "mypy") { 8 | override fun isError(event: ProcessEvent): Boolean { 9 | // exit code 1 should be fine https://github.com/python/mypy/issues/6003 10 | return event.exitCode > 2 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | works.szabope.mypy 4 | Mypy 5 | Peter Szabo 6 | com.intellij.modules.platform 7 | com.intellij.modules.lang 8 | com.intellij.modules.python 9 | 10 | -------------------------------------------------------------------------------- /src/test/testData/action/scan_sdk/MockSdk/bin/python: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ "$1" = "-V" ]; then 3 | printf "Python 3.11.2\n" 4 | else 5 | my_dir=$(dirname "$(realpath -s "$0")") 6 | for last; do :; done 7 | expected_args="-m mypy --show-column-numbers --show-absolute-path --output json --exclude excluded_dir $last" 8 | if [ "$*" != "$expected_args" ]; then 9 | >&2 echo "expected: $expected_args" 10 | >&2 echo "received: $*" 11 | exit 2 # because 1 needs to be treated as being fine 12 | fi 13 | cat "$my_dir/../../manualScan.json" 14 | fi -------------------------------------------------------------------------------- /src/test/testData/initialization/projectWithLocalSdk/.idea/projectWithLocalSdk.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Dependabot configuration: 2 | # https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | # Maintain dependencies for Gradle dependencies 7 | - package-ecosystem: "gradle" 8 | directory: "/" 9 | target-branch: "next" 10 | schedule: 11 | interval: "daily" 12 | # Maintain dependencies for GitHub Actions 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | target-branch: "next" 16 | schedule: 17 | interval: "daily" -------------------------------------------------------------------------------- /src/main/kotlin/works/szabope/plugins/mypy/services/MypyExecutorConfiguration.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.services 2 | 3 | import works.szabope.plugins.common.services.ImmutableSettingsData 4 | 5 | data class MypyExecutorConfiguration( 6 | override val executablePath: String, 7 | override val useProjectSdk: Boolean, 8 | override val configFilePath: String, 9 | override val arguments: String, 10 | override val workingDirectory: String, 11 | override val excludeNonProjectFiles: Boolean, 12 | override val scanBeforeCheckIn: Boolean 13 | ) : ImmutableSettingsData -------------------------------------------------------------------------------- /src/test/testData/initialization/projectWithRemoteSdk/.idea/projectWithRemoteSdk.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/kotlin/works/szabope/plugins/mypy/action/ScanCurrentlyFocusedOneInEditorAction.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.action 2 | 3 | import com.intellij.openapi.actionSystem.AnActionEvent 4 | import com.intellij.openapi.fileEditor.FileEditorManager 5 | import com.intellij.openapi.vfs.VirtualFile 6 | 7 | class ScanCurrentlyFocusedOneInEditorAction : ScanAction() { 8 | override fun listTargets(event: AnActionEvent): List? { 9 | val project = event.project ?: return null 10 | return FileEditorManager.getInstance(project).selectedTextEditor?.virtualFile?.let { listOf(it) } 11 | } 12 | } -------------------------------------------------------------------------------- /src/main/kotlin/works/szabope/plugins/mypy/action/RescanAction.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.action 2 | 3 | import com.intellij.openapi.actionSystem.AnActionEvent 4 | import com.intellij.openapi.vfs.VirtualFile 5 | import works.szabope.plugins.mypy.toolWindow.MypyTreeService 6 | 7 | class RescanAction : ScanAction() { 8 | 9 | override fun listTargets(event: AnActionEvent): Collection { 10 | return MypyTreeService.getInstance(event.project ?: return emptyList()).getRootScanPaths() 11 | } 12 | 13 | companion object { 14 | const val ID = "works.szabope.plugins.mypy.action.RescanAction" 15 | } 16 | } -------------------------------------------------------------------------------- /src/test/kotlin/works/szabope/plugins/mypy/testutil/notification.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.testutil 2 | 3 | import com.intellij.notification.ActionCenter 4 | import com.intellij.notification.Notification 5 | import com.intellij.openapi.project.Project 6 | import works.szabope.plugins.mypy.MypyBundle 7 | 8 | fun getMypyConfigurationNotCompleteNotification(project: Project): Notification { 9 | return ActionCenter.getNotifications(project).single { 10 | MypyBundle.message("notification.group.mypy.group") == it.groupId && MypyBundle.message("mypy.notification.incomplete_configuration") == it.content && !it.isExpired 11 | } 12 | } -------------------------------------------------------------------------------- /src/main/kotlin/works/szabope/plugins/mypy/services/parser/MypyMessage.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.services.parser 2 | 3 | import kotlinx.serialization.Serializable 4 | import works.szabope.plugins.common.annotator.ToolMessage 5 | 6 | // https://github.com/Kotlin/kotlinx.serialization/issues/2808 7 | @Suppress("PROVIDED_RUNTIME_TOO_LOW", "INLINE_CLASSES_NOT_SUPPORTED") 8 | @Serializable 9 | data class MypyMessage( 10 | val file: String, 11 | override var line: Int, 12 | override val column: Int, 13 | override val message: String, 14 | val hint: String?, 15 | val code: String, 16 | var severity: String 17 | ) : ToolMessage 18 | -------------------------------------------------------------------------------- /src/main/kotlin/works/szabope/plugins/mypy/toolWindow/MypyTreeModelDataItem.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.toolWindow 2 | 3 | import works.szabope.plugins.common.services.SeverityConfig 4 | import works.szabope.plugins.common.toolWindow.TreeModelDataItem 5 | 6 | data class MypyTreeModelDataItem( 7 | override val file: String, 8 | override val line: Int, 9 | override val column: Int, 10 | override val message: String, 11 | override val code: String, 12 | override val severity: SeverityConfig, 13 | val hint: String?, 14 | ) : TreeModelDataItem { 15 | override fun toRepresentation() = "$message [$code] ($line:$column) ${hint?.replace("\n", " ")}" 16 | } -------------------------------------------------------------------------------- /src/test/kotlin/works/szabope/plugins/mypy/testutil/MypyPluginPackageManagementServiceStub.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.testutil 2 | 3 | import com.intellij.openapi.project.Project 4 | import com.jetbrains.python.packaging.PyRequirement 5 | import com.jetbrains.python.packaging.pyRequirement 6 | import com.jetbrains.python.packaging.requirement.PyRequirementRelation 7 | import works.szabope.plugins.common.test.services.AbstractPluginPackageManagementServiceStub 8 | 9 | class MypyPluginPackageManagementServiceStub(project: Project) : AbstractPluginPackageManagementServiceStub(project) { 10 | override fun getRequirement(): PyRequirement { 11 | return pyRequirement("mypy", PyRequirementRelation.GTE, "1.11") 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/kotlin/works/szabope/plugins/mypy/toolWindow/MypyTreeService.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.toolWindow 2 | 3 | import com.intellij.openapi.components.Service 4 | import com.intellij.openapi.components.service 5 | import com.intellij.openapi.project.Project 6 | import works.szabope.plugins.common.toolWindow.AbstractTreeService 7 | import works.szabope.plugins.common.toolWindow.ITreeService 8 | import works.szabope.plugins.mypy.services.mypySeverityConfigs 9 | 10 | @Service(Service.Level.PROJECT) 11 | class MypyTreeService : AbstractTreeService(mypySeverityConfigs.keys) { 12 | companion object { 13 | @JvmStatic 14 | fun getInstance(project: Project): ITreeService = project.service() 15 | } 16 | } -------------------------------------------------------------------------------- /src/main/kotlin/works/szabope/plugins/mypy/services/BalloonUtil.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.services 2 | 3 | import com.intellij.openapi.project.Project 4 | import com.intellij.openapi.ui.MessageType 5 | import com.intellij.openapi.wm.ToolWindowManager 6 | import works.szabope.plugins.mypy.toolWindow.MypyToolWindowPanel 7 | import javax.swing.event.HyperlinkEvent 8 | 9 | fun showClickableBalloonError(project: Project, balloonMessage: String, onClick: () -> Unit) { 10 | ToolWindowManager.getInstance(project).notifyByBalloon( 11 | MypyToolWindowPanel.ID, MessageType.ERROR, balloonMessage, null 12 | ) { 13 | if (it.eventType == HyperlinkEvent.EventType.ACTIVATED) { 14 | onClick() 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/main/kotlin/works/szabope/plugins/mypy/MypyBundle.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy 2 | 3 | import com.intellij.DynamicBundle 4 | import org.jetbrains.annotations.NonNls 5 | import org.jetbrains.annotations.PropertyKey 6 | 7 | @NonNls 8 | private const val BUNDLE = "messages.MypyBundle" 9 | 10 | object MypyBundle { 11 | 12 | private val bundle = DynamicBundle(MypyBundle.javaClass, BUNDLE) 13 | 14 | @JvmStatic 15 | fun message(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any) = 16 | bundle.getMessage(key, *params) 17 | 18 | @Suppress("unused") 19 | @JvmStatic 20 | fun messagePointer(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any) = 21 | bundle.getLazyMessage(key, *params) 22 | } 23 | -------------------------------------------------------------------------------- /src/main/kotlin/works/szabope/plugins/mypy/action/ScanJobRegistry.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.action 2 | 3 | import kotlinx.coroutines.Job 4 | import kotlinx.coroutines.cancelAndJoin 5 | 6 | class ScanJobRegistry { 7 | private var job: Job? = null 8 | 9 | fun set(job: Job) { 10 | if (!isAvailable()) { 11 | throw IllegalStateException("Current job has not been completed!") 12 | } 13 | this.job = job 14 | } 15 | 16 | fun isAvailable() = job?.isCompleted ?: true 17 | 18 | fun isActive() = job?.isActive ?: false 19 | 20 | suspend fun cancel() { 21 | job?.cancelAndJoin() 22 | } 23 | 24 | companion object { 25 | val INSTANCE: ScanJobRegistry by lazy { ScanJobRegistry() } 26 | } 27 | } -------------------------------------------------------------------------------- /src/test/kotlin/works/szabope/plugins/mypy/initialization/PluginInitializationFromScratchTest.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.initialization 2 | 3 | import works.szabope.plugins.mypy.AbstractToolWindowTestCase 4 | import works.szabope.plugins.mypy.MypyBundle 5 | import works.szabope.plugins.mypy.testutil.getMypyConfigurationNotCompleteNotification 6 | 7 | class PluginInitializationFromScratchTest : AbstractToolWindowTestCase() { 8 | 9 | fun `test plugin initialized from scratch (no python sdk) results in notification`() { 10 | val actions = getMypyConfigurationNotCompleteNotification(project).actions 11 | assertEquals( 12 | MypyBundle.message("mypy.intention.complete_configuration.text"), 13 | actions.single().templatePresentation.text 14 | ) 15 | } 16 | } -------------------------------------------------------------------------------- /src/test/testData/annotation/white space/mypy: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ "$1" = "-V" ]; then 3 | echo "mypy 1.11.2 (compiled: yes)" 4 | else 5 | penult=${*: -2: 1} 6 | my_dir=$(dirname "$(realpath -s "$0")") 7 | expected_args="--show-column-numbers --show-absolute-path --output json --config-file $my_dir/mypy.ini --shadow-file /src/a.py $penult /src/a.py" 8 | if [ "$*" != "$expected_args" ]; then 9 | >&2 echo "expected: $expected_args" 10 | >&2 echo "received: $*" 11 | exit 1 12 | fi 13 | for last; do :; done 14 | filename=$(basename "$last") 15 | # Let's extract original filename from temp file's name, e.g. pycharm_mypy_15708631520625806318_d.py -> d.py 16 | filename="${filename/pycharm_mypy_/}" 17 | filename="${filename//[0-9]/}" 18 | filename="${filename/_/}" 19 | cat "$0_${filename}_result.json" 20 | fi -------------------------------------------------------------------------------- /src/main/resources/inspectionDescriptions/MypyInspection.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | This inspection integrates Mypy and reports in real-time on problems against the current Mypy profile. 20 | 21 | 22 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | # libraries 3 | junit = "4.13.2" 4 | mockk = "1.14.4" 5 | myPluginCommon = "1.0.11" 6 | 7 | # plugins 8 | changelog = "2.5.0" 9 | intelliJPlatform = "2.10.5" 10 | kotlin = "2.2.21" 11 | kover = "0.9.3" 12 | serialization = "2.2.0" 13 | 14 | [libraries] 15 | junit = { group = "junit", name = "junit", version.ref = "junit" } 16 | mockk = { group = "io.mockk", name = "mockk-jvm", version.ref = "mockk" } 17 | myPluginCommon = { group = "works.szabope.plugins", name = "common", version.ref = "myPluginCommon" } 18 | 19 | [plugins] 20 | changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" } 21 | intelliJPlatform = { id = "org.jetbrains.intellij.platform", version.ref = "intelliJPlatform" } 22 | kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 23 | kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } 24 | serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "serialization" } 25 | -------------------------------------------------------------------------------- /src/main/kotlin/works/szabope/plugins/mypy/annotator/MypyAnnotator.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.annotator 2 | 3 | import com.intellij.openapi.project.Project 4 | import works.szabope.plugins.common.annotator.ToolAnnotator 5 | import works.szabope.plugins.common.services.ImmutableSettingsData 6 | import works.szabope.plugins.mypy.services.MypySettings 7 | import works.szabope.plugins.mypy.services.SyncScanService 8 | import works.szabope.plugins.mypy.services.parser.MypyMessage 9 | 10 | class MypyAnnotator : ToolAnnotator() { 11 | override val inspectionId = MypyInspectionId 12 | 13 | override fun getSettingsInstance(project: Project) = MypySettings.getInstance(project) 14 | 15 | override fun scan(info: AnnotatorInfo, configuration: ImmutableSettingsData) = 16 | SyncScanService.getInstance(info.project).scan(listOf(info.file), configuration)[info.file] ?: emptyList() 17 | 18 | override fun createIntention(message: MypyMessage) = MypyIgnoreIntention(message) 19 | } 20 | -------------------------------------------------------------------------------- /src/test/kotlin/works/szabope/plugins/mypy/testutil/TestToolWindowHeadlessManagerImpl.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.testutil 2 | 3 | import com.intellij.openapi.project.Project 4 | import com.intellij.openapi.wm.ToolWindowBalloonShowOptions 5 | import com.intellij.toolWindow.ToolWindowHeadlessManagerImpl 6 | import org.junit.Assert.assertNull 7 | import works.szabope.plugins.mypy.toolWindow.MypyToolWindowPanel 8 | 9 | class TestToolWindowHeadlessManagerImpl(project: Project) : ToolWindowHeadlessManagerImpl(project) { 10 | private val myHandlers = hashMapOf Unit>() 11 | 12 | override fun notifyByBalloon(options: ToolWindowBalloonShowOptions) { 13 | myHandlers[options.toolWindowId]?.invoke(options) 14 | } 15 | 16 | fun onBalloon(handler: (ToolWindowBalloonShowOptions) -> Unit) { 17 | assertNull(myHandlers.put(MypyToolWindowPanel.ID, handler)) 18 | } 19 | 20 | fun cleanup() { 21 | myHandlers.clear() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/kotlin/works/szabope/plugins/mypy/services/parser/MypyMessageConverter.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.services.parser 2 | 3 | import works.szabope.plugins.common.messages.MessageConverter 4 | import works.szabope.plugins.common.toolWindow.TreeModelDataItem 5 | import works.szabope.plugins.mypy.services.mypySeverityConfigs 6 | import works.szabope.plugins.mypy.toolWindow.MypyTreeModelDataItem 7 | 8 | object MypyMessageConverter : MessageConverter { 9 | override fun convert(message: MypyMessage): TreeModelDataItem { 10 | val severity = requireNotNull(mypySeverityConfigs[message.severity]) { 11 | """Mypy message with type '${message.severity}' is not supported. Please, report this issue at 12 | |https://github.com/szabope/mypy-pycharm-plugin/issues""".trimMargin() 13 | } 14 | return with(message) { 15 | MypyTreeModelDataItem(file, line, column, this.message, code, severity, hint) 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/main/kotlin/works/szabope/plugins/mypy/services/mypySeverityConfigs.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.services 2 | 3 | import com.intellij.icons.AllIcons 4 | import works.szabope.plugins.common.services.SeverityConfig 5 | import works.szabope.plugins.mypy.MypyBundle 6 | 7 | val mypySeverityConfigs = mapOf( 8 | "ERROR" to SeverityConfig( 9 | "ERROR", 10 | MypyBundle.message("action.MyPyDisplayErrorsAction.text"), 11 | MypyBundle.message("action.MyPyDisplayErrorsAction.description"), 12 | AllIcons.General.Error 13 | ), 14 | 15 | "WARNING" to SeverityConfig( 16 | "WARNING", 17 | MypyBundle.message("action.MypyDisplayWarningsAction.text"), 18 | MypyBundle.message("action.MypyDisplayWarningsAction.description"), 19 | AllIcons.General.Warning 20 | ), 21 | 22 | "NOTE" to SeverityConfig( 23 | "NOTE", 24 | MypyBundle.message("action.MypyDisplayNoteAction.text"), 25 | MypyBundle.message("action.MypyDisplayNoteAction.description"), 26 | AllIcons.General.Information 27 | ) 28 | ) 29 | -------------------------------------------------------------------------------- /art/refresh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/kotlin/works/szabope/plugins/mypy/services/MypyPluginPackageManagementService.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.services 2 | 3 | import com.intellij.openapi.components.Service 4 | import com.intellij.openapi.components.service 5 | import com.intellij.openapi.project.Project 6 | import com.jetbrains.python.packaging.PyRequirement 7 | import com.jetbrains.python.packaging.pyRequirement 8 | import com.jetbrains.python.packaging.requirement.PyRequirementRelation 9 | import works.szabope.plugins.common.services.AbstractPluginPackageManagementService 10 | 11 | @Service(Service.Level.PROJECT) 12 | class MypyPluginPackageManagementService(override val project: Project) : AbstractPluginPackageManagementService() { 13 | 14 | override fun getRequirement(): PyRequirement { 15 | return pyRequirement("mypy", PyRequirementRelation.GTE, MINIMUM_VERSION) 16 | } 17 | 18 | companion object { 19 | const val MINIMUM_VERSION = "1.11" 20 | 21 | @JvmStatic 22 | fun getInstance(project: Project): AbstractPluginPackageManagementService = 23 | project.service() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/test/kotlin/works/szabope/plugins/mypy/testutil/context.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.testutil 2 | 3 | import com.intellij.ide.ui.IdeUiService 4 | import com.intellij.openapi.actionSystem.CommonDataKeys 5 | import com.intellij.openapi.actionSystem.DataContext 6 | import com.intellij.openapi.actionSystem.impl.SimpleDataContext 7 | import com.intellij.openapi.project.Project 8 | import com.intellij.openapi.wm.ToolWindowManager 9 | import works.szabope.plugins.mypy.toolWindow.MypyToolWindowPanel 10 | 11 | fun dataContext( 12 | project: Project, customizer: SimpleDataContext.Builder.() -> Unit 13 | ): DataContext { 14 | val panel = ToolWindowManager.getInstance(project) 15 | .getToolWindow(MypyToolWindowPanel.ID)!!.contentManager.contents.single().component as MypyToolWindowPanel 16 | val panelContext = IdeUiService.getInstance().createUiDataContext(panel) 17 | val testContext = SimpleDataContext.builder().setParent(panelContext).add(CommonDataKeys.PROJECT, project).build() 18 | val builder = SimpleDataContext.builder().setParent(testContext) 19 | builder.apply { } 20 | customizer(builder) 21 | return builder.build() 22 | } 23 | -------------------------------------------------------------------------------- /src/main/kotlin/works/szabope/plugins/mypy/action/StopScanAction.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.action 2 | 3 | import com.intellij.openapi.actionSystem.ActionUpdateThread 4 | import com.intellij.openapi.actionSystem.AnActionEvent 5 | import com.intellij.openapi.progress.currentThreadCoroutineScope 6 | import com.intellij.openapi.project.DumbAwareAction 7 | import kotlinx.coroutines.future.future 8 | import works.szabope.plugins.mypy.toolWindow.MypyTreeService 9 | 10 | class StopScanAction : DumbAwareAction() { 11 | 12 | override fun actionPerformed(event: AnActionEvent) { 13 | currentThreadCoroutineScope().future { 14 | ScanJobRegistry.INSTANCE.cancel() 15 | event.project?.let { MypyTreeService.getInstance(it) }?.lock() 16 | }.get() 17 | } 18 | 19 | override fun update(event: AnActionEvent) { 20 | event.presentation.isEnabled = ScanJobRegistry.INSTANCE.isActive() 21 | } 22 | 23 | override fun getActionUpdateThread(): ActionUpdateThread { 24 | return ActionUpdateThread.BGT 25 | } 26 | 27 | companion object { 28 | const val ID = "works.szabope.plugins.mypy.action.StopScanAction" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/kotlin/works/szabope/plugins/mypy/toolWindow/MypyToolWindowPanel.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.toolWindow 2 | 3 | import com.intellij.openapi.project.Project 4 | import com.intellij.ui.treeStructure.Tree 5 | import org.jetbrains.annotations.VisibleForTesting 6 | import works.szabope.plugins.common.services.Settings 7 | import works.szabope.plugins.common.toolWindow.AbstractToolWindowPanel 8 | import works.szabope.plugins.common.toolWindow.ITreeService 9 | import works.szabope.plugins.mypy.services.MypySettings 10 | 11 | class MypyToolWindowPanel(private val project: Project, @VisibleForTesting val tree: Tree = Tree()) : 12 | AbstractToolWindowPanel(project, tree) { 13 | 14 | override val treeService: ITreeService 15 | get() = MypyTreeService.getInstance(project) 16 | override val settings: Settings 17 | get() = MypySettings.getInstance(project) 18 | 19 | init { 20 | super.init(ID, MAIN_ACTION_GROUP, SCROLL_TO_SOURCE_ID) 21 | } 22 | 23 | companion object { 24 | private const val MAIN_ACTION_GROUP: String = "works.szabope.plugins.mypy.MypyPluginActions" 25 | const val ID = "Mypy " 26 | const val SCROLL_TO_SOURCE_ID = "works.szabope.plugins.mypy.action.ScrollToSourceAction" 27 | } 28 | } -------------------------------------------------------------------------------- /src/main/kotlin/works/szabope/plugins/mypy/action/OpenSettingsAction.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.action 2 | 3 | import com.intellij.notification.Notification 4 | import com.intellij.openapi.actionSystem.ActionUpdateThread 5 | import com.intellij.openapi.actionSystem.AnAction 6 | import com.intellij.openapi.actionSystem.AnActionEvent 7 | import com.intellij.openapi.options.ShowSettingsUtil 8 | import com.intellij.openapi.wm.ToolWindowManager 9 | import works.szabope.plugins.mypy.MypyBundle 10 | import works.szabope.plugins.mypy.configurable.MypyConfigurable 11 | 12 | class OpenSettingsAction : AnAction(MypyBundle.message("mypy.intention.complete_configuration.text")) { 13 | override fun actionPerformed(e: AnActionEvent) { 14 | val project = e.project ?: return 15 | ToolWindowManager.getInstance(project).invokeLater { 16 | e.getData(Notification.KEY)?.expire() 17 | ShowSettingsUtil.getInstance().showSettingsDialog(project, MypyConfigurable::class.java) 18 | } 19 | } 20 | 21 | override fun getActionUpdateThread(): ActionUpdateThread { 22 | return ActionUpdateThread.BGT 23 | } 24 | 25 | companion object { 26 | const val ID = "works.szabope.plugins.mypy.action.OpenSettingsAction" 27 | } 28 | } -------------------------------------------------------------------------------- /src/main/kotlin/works/szabope/plugins/mypy/services/handleScanException.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.services 2 | 3 | import com.intellij.openapi.project.Project 4 | import kotlinx.coroutines.flow.FlowCollector 5 | import works.szabope.plugins.common.run.ToolExecutionTerminatedException 6 | import works.szabope.plugins.common.services.ImmutableSettingsData 7 | import works.szabope.plugins.mypy.MypyBundle 8 | import works.szabope.plugins.mypy.dialog.DialogManager 9 | 10 | inline fun handleScanException( 11 | project: Project, configuration: ImmutableSettingsData, stdErr: StringBuilder 12 | ): suspend FlowCollector.(Throwable) -> Unit = { 13 | if (it is ToolExecutionTerminatedException) { 14 | showClickableBalloonError(project, MypyBundle.message("mypy.toolwindow.balloon.external_error")) { 15 | DialogManager.showToolExecutionErrorDialog( 16 | configuration, stdErr.toString(), it.exitCode 17 | ) 18 | } 19 | } else { 20 | // Unexpected exception 21 | showClickableBalloonError( 22 | project, MypyBundle.message("mypy.toolwindow.balloon.failed_to_execute") 23 | ) { 24 | DialogManager.showFailedToExecuteErrorDialog( 25 | it.message ?: MypyBundle.message("mypy.please_report_this_issue") 26 | ) 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/main/kotlin/works/szabope/plugins/mypy/activity/SettingsInitializationActivity.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.activity 2 | 3 | import com.intellij.openapi.application.ApplicationManager 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.openapi.startup.ProjectActivity 6 | import works.szabope.plugins.mypy.services.IncompleteConfigurationNotifier 7 | import works.szabope.plugins.mypy.services.MypyPluginPackageManagementService 8 | import works.szabope.plugins.mypy.services.MypySettings 9 | import works.szabope.plugins.mypy.services.OldMypySettings 10 | 11 | class SettingsInitializationActivity : ProjectActivity { 12 | 13 | override suspend fun execute(project: Project) { 14 | if (project.isDefault) { 15 | return 16 | } 17 | if (!ApplicationManager.getApplication().isUnitTestMode) { 18 | MypyPluginPackageManagementService.getInstance(project).reloadPackages() 19 | } 20 | val settings = MypySettings.getInstance(project) 21 | // we trust in old settings validity 22 | settings.initSettings(OldMypySettings.getInstance(project)) 23 | if (settings.getValidConfiguration().isFailure) { 24 | val canInstall = MypyPluginPackageManagementService.getInstance(project).canInstall() 25 | IncompleteConfigurationNotifier.notify(project, canInstall) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/test/kotlin/works/szabope/plugins/mypy/initialization/MypyInitializationFromOldConfigurationTest.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.initialization 2 | 3 | import com.intellij.openapi.vfs.newvfs.impl.VfsRootAccess 4 | import com.intellij.testFramework.PlatformTestUtil 5 | import works.szabope.plugins.mypy.AbstractMypyHeavyPlatformTestCase 6 | import works.szabope.plugins.mypy.services.MypySettings 7 | import java.nio.file.Path 8 | 9 | class MypyInitializationFromOldConfigurationTest : AbstractMypyHeavyPlatformTestCase() { 10 | 11 | override fun setUpProject() { 12 | VfsRootAccess.allowRootAccess(testRootDisposable, "/usr/bin") 13 | myProject = PlatformTestUtil.loadAndOpenProject(Path.of(PROJECT_PATH), getTestRootDisposable()) 14 | } 15 | 16 | fun `test plugin initialized from old configuration`() { 17 | with(MypySettings.getInstance(project)) { 18 | PlatformTestUtil.waitWhileBusy { !isInitialized() } 19 | assertFalse(useProjectSdk) 20 | assertEquals("$PROJECT_PATH/.venv/bin/mypy", executablePath) 21 | assertEquals("--show-column-numbers", arguments) 22 | assertFalse(scanBeforeCheckIn) 23 | assertEquals("$PROJECT_PATH/mypy.conf", configFilePath) 24 | } 25 | } 26 | 27 | companion object { 28 | const val PROJECT_PATH = "src/test/testData/initialization/OldConfiguration" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/kotlin/works/szabope/plugins/mypy/services/IncompleteConfigurationNotifier.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.services 2 | 3 | import com.intellij.notification.NotificationGroupManager 4 | import com.intellij.notification.NotificationType 5 | import com.intellij.openapi.actionSystem.ActionManager 6 | import com.intellij.openapi.project.Project 7 | import works.szabope.plugins.mypy.MypyBundle 8 | import works.szabope.plugins.mypy.action.InstallMypyAction 9 | import works.szabope.plugins.mypy.action.OpenSettingsAction 10 | 11 | class IncompleteConfigurationNotifier { 12 | companion object { 13 | @JvmStatic 14 | fun notify(project: Project, canInstall: Boolean) { 15 | val openSettingsAction = ActionManager.getInstance().getAction(OpenSettingsAction.ID) 16 | val notificationGroup = NotificationGroupManager.getInstance() 17 | .getNotificationGroup(MypyBundle.message("notification.group.mypy.group")) 18 | val notification = notificationGroup.createNotification( 19 | MypyBundle.message("mypy.notification.incomplete_configuration"), NotificationType.WARNING 20 | ).addAction(openSettingsAction) 21 | if (canInstall) { 22 | val installAction = ActionManager.getInstance().getAction(InstallMypyAction.ID) 23 | notification.addAction(installAction) 24 | } 25 | notification.notify(project) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/kotlin/works/szabope/plugins/mypy/services/OldMypySettings.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.services 2 | 3 | import com.intellij.openapi.components.* 4 | import com.intellij.openapi.project.Project 5 | import org.jetbrains.annotations.ApiStatus 6 | import works.szabope.plugins.common.services.BasicSettingsData 7 | 8 | @Service(Service.Level.PROJECT) 9 | @State( 10 | name = "MypyConfigService", 11 | storages = [Storage("mypy.xml", deprecated = true)], 12 | category = SettingsCategory.PLUGINS, 13 | allowLoadInTests = true, 14 | ) 15 | class OldMypySettings : SimplePersistentStateComponent( 16 | OldMypySettingsState() 17 | ), BasicSettingsData { 18 | 19 | @ApiStatus.Internal 20 | class OldMypySettingsState : BaseState() { 21 | var customMypyPath by string() 22 | var mypyConfigFilePath by string() 23 | var mypyArguments by string() 24 | } 25 | 26 | override val executablePath: String? 27 | get() = state.customMypyPath 28 | override val configFilePath: String? 29 | get() = state.mypyConfigFilePath 30 | override val arguments: String? 31 | get() = state.mypyArguments 32 | override val scanBeforeCheckIn: Boolean 33 | get() = throw UnsupportedOperationException("Old Mypy plugin never supported this") 34 | 35 | companion object { 36 | @JvmStatic 37 | fun getInstance(project: Project): OldMypySettings = project.service() 38 | } 39 | } -------------------------------------------------------------------------------- /src/test/kotlin/works/szabope/plugins/mypy/AbstractMypyHeavyPlatformTestCase.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy 2 | 3 | import com.intellij.openapi.project.Project 4 | import com.intellij.openapi.vfs.newvfs.impl.VfsRootAccess 5 | import com.intellij.testFramework.HeavyPlatformTestCase 6 | import io.mockk.clearAllMocks 7 | import io.mockk.every 8 | import io.mockk.mockkObject 9 | import io.mockk.unmockkAll 10 | import works.szabope.plugins.common.services.AbstractPluginPackageManagementService 11 | import works.szabope.plugins.mypy.services.MypyPluginPackageManagementService 12 | import works.szabope.plugins.mypy.testutil.MypyPluginPackageManagementServiceStub 13 | 14 | abstract class AbstractMypyHeavyPlatformTestCase : HeavyPlatformTestCase() { 15 | 16 | // local variables are not supported in mockk answer, yet 17 | private lateinit var mypyPackageManagementServiceStub: AbstractPluginPackageManagementService 18 | 19 | override fun setUp() { 20 | VfsRootAccess.allowRootAccess(testRootDisposable, "/usr/bin") 21 | mockkObject(MypyPluginPackageManagementService.Companion) 22 | every { MypyPluginPackageManagementService.getInstance(any(Project::class)) } answers { 23 | if (!::mypyPackageManagementServiceStub.isInitialized) { 24 | mypyPackageManagementServiceStub = MypyPluginPackageManagementServiceStub( 25 | firstArg() 26 | ) 27 | } 28 | mypyPackageManagementServiceStub 29 | } 30 | super.setUp() 31 | } 32 | 33 | override fun tearDown() { 34 | clearAllMocks() 35 | unmockkAll() 36 | super.tearDown() 37 | } 38 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # IntelliJ Platform Artifacts Repositories -> https://plugins.jetbrains.com/docs/intellij/intellij-artifacts.html 2 | pluginGroup=works.szabope.pycharm 3 | pluginName=mypy 4 | pluginRepositoryUrl=https://github.com/szabope/mypy-pycharm-plugin 5 | # SemVer format -> https://semver.org 6 | pluginVersion=2.1.8 7 | # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html 8 | pluginSinceBuild=253.28294.237 9 | pluginUntilBuild=253.* 10 | # IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension 11 | platformType=PY 12 | platformVersion=2025.3 13 | # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html 14 | # Example: platformPlugins = com.jetbrains.php:203.4449.22, org.intellij.scala:2023.3.27@EAP 15 | platformPlugins= 16 | # Example: platformBundledPlugins = com.intellij.java 17 | platformBundledPlugins=PythonCore 18 | # Example: platformBundledModules = intellij.spellchecker 19 | platformBundledModules= 20 | # Gradle Releases -> https://github.com/gradle/gradle/releases 21 | gradleVersion=8.13 22 | # Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib 23 | kotlin.stdlib.default.dependency=false 24 | # Disable incremental compilation, it caused failing tests 25 | kotlin.incremental=false 26 | # Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html 27 | org.gradle.configuration-cache=false 28 | # Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html 29 | org.gradle.caching=false 30 | -------------------------------------------------------------------------------- /src/test/kotlin/works/szabope/plugins/mypy/testutil/TestDialogManager.kt: -------------------------------------------------------------------------------- 1 | // inspired by idea/243.19420.21 git4idea.test.TestDialogManager 2 | package works.szabope.plugins.mypy.testutil 3 | 4 | import works.szabope.plugins.common.dialog.PluginDialog 5 | import works.szabope.plugins.common.services.ImmutableSettingsData 6 | import works.szabope.plugins.common.services.PluginPackageManagementException 7 | import works.szabope.plugins.common.test.dialog.AbstractTestDialogManager 8 | import works.szabope.plugins.common.test.dialog.TestDialogWrapper 9 | import works.szabope.plugins.mypy.dialog.* 10 | 11 | class TestDialogManager : AbstractTestDialogManager() { 12 | override fun createPyPackageInstallationErrorDialog(exception: PluginPackageManagementException.InstallationFailedException) = 13 | TestDialogWrapper( 14 | MypyPackageInstallationErrorDialog::class.java, exception 15 | ) 16 | 17 | override fun createToolExecutionErrorDialog(configuration: ImmutableSettingsData, result: String, resultCode: Int) = 18 | TestDialogWrapper(MypyExecutionErrorDialog::class.java, configuration, result, resultCode) 19 | 20 | override fun createFailedToExecuteErrorDialog(message: String): PluginDialog = 21 | TestDialogWrapper(FailedToExecuteErrorDialog::class.java, message) 22 | 23 | override fun createToolOutputParseErrorDialog( 24 | configuration: ImmutableSettingsData, targets: String, json: String, error: String 25 | ) = TestDialogWrapper(MypyParseErrorDialog::class.java, configuration, targets, json, error) 26 | 27 | override fun createGeneralErrorDialog(failure: Throwable) = 28 | TestDialogWrapper(MypyGeneralErrorDialog::class.java, failure) 29 | } 30 | -------------------------------------------------------------------------------- /src/main/kotlin/works/szabope/plugins/mypy/action/InstallMypyAction.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.action 2 | 3 | import com.intellij.openapi.diagnostic.thisLogger 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.openapi.ui.MessageType 6 | import com.intellij.openapi.wm.ToolWindowManager 7 | import works.szabope.plugins.common.action.AbstractInstallToolAction 8 | import works.szabope.plugins.common.services.PluginPackageManagementException 9 | import works.szabope.plugins.mypy.MypyBundle 10 | import works.szabope.plugins.mypy.dialog.DialogManager 11 | import works.szabope.plugins.mypy.services.MypyPluginPackageManagementService 12 | import works.szabope.plugins.mypy.toolWindow.MypyToolWindowPanel 13 | 14 | class InstallMypyAction : AbstractInstallToolAction(MypyBundle.message("action.InstallMypyAction.done_html")) { 15 | override fun getPackageManager(project: Project) = MypyPluginPackageManagementService.getInstance(project) 16 | 17 | override fun handleFailure(failure: Throwable) { 18 | when (failure) { 19 | is PluginPackageManagementException.InstallationFailedException -> { 20 | DialogManager.showPyPackageInstallationErrorDialog(failure) 21 | } 22 | 23 | else -> { 24 | thisLogger().error(failure) 25 | DialogManager.showGeneralErrorDialog(failure) 26 | } 27 | } 28 | } 29 | 30 | override fun notifyPanel(project: Project, message: String) { 31 | ToolWindowManager.getInstance(project).notifyByBalloon( 32 | MypyToolWindowPanel.ID, MessageType.INFO, message 33 | ) 34 | } 35 | 36 | companion object { 37 | const val ID = "works.szabope.plugins.mypy.action.InstallMypyAction" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/test/kotlin/works/szabope/plugins/mypy/action/StopScanTest.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.action 2 | 3 | import com.intellij.openapi.actionSystem.CommonDataKeys 4 | import com.intellij.testFramework.PlatformTestUtil 5 | import com.intellij.testFramework.TestDataPath 6 | import works.szabope.plugins.mypy.AbstractToolWindowTestCase 7 | import works.szabope.plugins.mypy.services.MypySettings 8 | import works.szabope.plugins.mypy.testutil.dataContext 9 | import works.szabope.plugins.mypy.testutil.scan 10 | import works.szabope.plugins.mypy.testutil.stopScan 11 | import java.nio.file.Paths 12 | import kotlin.io.path.absolutePathString 13 | 14 | @TestDataPath($$"$CONTENT_ROOT/testData/action/stop_scan") 15 | class StopScanTest : AbstractToolWindowTestCase() { 16 | 17 | override fun getTestDataPath() = "src/test/testData/action/stop_scan" 18 | 19 | override fun setUp() { 20 | super.setUp() 21 | with(MypySettings.getInstance(project)) { 22 | useProjectSdk = false 23 | executablePath = Paths.get(testDataPath).resolve("mypy").absolutePathString() 24 | workingDirectory = Paths.get(testDataPath).absolutePathString() 25 | } 26 | } 27 | 28 | /** 29 | * For the infinite loop check `mypy` shell script on testDataPath 30 | */ 31 | fun `test that we can stop an external process that runs an infinite loop`() { 32 | val file = myFixture.configureByText("a.py", "doesn't matter").virtualFile 33 | scan(dataContext(project) { add(CommonDataKeys.VIRTUAL_FILE_ARRAY, arrayOf(file)) }) 34 | PlatformTestUtil.waitWhileBusy { !ScanJobRegistry.INSTANCE.isActive() } // make sure that scan has been started 35 | stopScan(dataContext(project) {}) 36 | PlatformTestUtil.waitWhileBusy { !ScanJobRegistry.INSTANCE.isAvailable() } 37 | } 38 | } -------------------------------------------------------------------------------- /src/test/kotlin/works/szabope/plugins/mypy/initialization/MypyInitializationWithLocalPythonSdkTest.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.initialization 2 | 3 | import com.intellij.openapi.application.runWriteActionAndWait 4 | import com.intellij.openapi.projectRoots.ProjectJdkTable 5 | import com.intellij.testFramework.PlatformTestUtil 6 | import com.jetbrains.python.psi.LanguageLevel 7 | import com.jetbrains.python.sdk.pythonSdk 8 | import works.szabope.plugins.common.test.sdk.PythonMockSdk 9 | import works.szabope.plugins.mypy.AbstractMypyHeavyPlatformTestCase 10 | import works.szabope.plugins.mypy.testutil.getMypyConfigurationNotCompleteNotification 11 | import java.nio.file.Path 12 | 13 | class MypyInitializationWithLocalPythonSdkTest : AbstractMypyHeavyPlatformTestCase() { 14 | 15 | override fun tearDown() { 16 | val mockSdk = project.pythonSdk!! 17 | project.pythonSdk = null 18 | module?.pythonSdk = null 19 | runWriteActionAndWait { 20 | ProjectJdkTable.getInstance().removeJdk(mockSdk) 21 | } 22 | super.tearDown() 23 | } 24 | 25 | override fun setUpProject() { 26 | val mockSdk = PythonMockSdk.create("Python 3.12", "$PROJECT_PATH/MockSdk", LanguageLevel.PYTHON312) 27 | runWriteActionAndWait { 28 | ProjectJdkTable.getInstance().addJdk(mockSdk) 29 | } 30 | myProject = PlatformTestUtil.loadAndOpenProject(Path.of(PROJECT_PATH), getTestRootDisposable()) 31 | } 32 | 33 | fun `test plugin initialized for project with python sdk results in notification`() { 34 | val actions = getMypyConfigurationNotCompleteNotification(project).actions 35 | assertEquals(2, actions.size) 36 | } 37 | 38 | companion object { 39 | const val PROJECT_PATH = "src/test/testData/initialization/projectWithLocalSdk" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/kotlin/works/szabope/plugins/mypy/dialog/DialogManager.kt: -------------------------------------------------------------------------------- 1 | // inspired by idea/243.19420.21 git4idea.DialogManager 2 | package works.szabope.plugins.mypy.dialog 3 | 4 | import com.intellij.openapi.ui.DialogWrapper 5 | import works.szabope.plugins.common.dialog.IDialogManager 6 | import works.szabope.plugins.common.dialog.IDialogManager.IShowDialog 7 | import works.szabope.plugins.common.dialog.PluginDialog 8 | import works.szabope.plugins.common.services.ImmutableSettingsData 9 | import works.szabope.plugins.common.services.PluginPackageManagementException 10 | 11 | private fun DialogWrapper.toMypyDialog() = object : PluginDialog { 12 | override fun show() = this@toMypyDialog.show() 13 | } 14 | 15 | class DialogManager : IDialogManager { 16 | override fun showDialog(dialog: PluginDialog) = dialog.show() 17 | 18 | override fun createPyPackageInstallationErrorDialog(exception: PluginPackageManagementException.InstallationFailedException) = 19 | MypyPackageInstallationErrorDialog(exception.message).toMypyDialog() 20 | 21 | override fun createToolExecutionErrorDialog( 22 | configuration: ImmutableSettingsData, 23 | result: String, 24 | resultCode: Int 25 | ) = MypyExecutionErrorDialog(configuration, result, resultCode).toMypyDialog() 26 | 27 | override fun createFailedToExecuteErrorDialog(message: String) = 28 | FailedToExecuteErrorDialog(message).toMypyDialog() 29 | 30 | override fun createToolOutputParseErrorDialog( 31 | configuration: ImmutableSettingsData, 32 | targets: String, 33 | json: String, 34 | error: String 35 | ) = MypyParseErrorDialog(configuration, targets, json, error).toMypyDialog() 36 | 37 | override fun createGeneralErrorDialog(failure: Throwable) = MypyGeneralErrorDialog(failure).toMypyDialog() 38 | 39 | companion object : IShowDialog { 40 | override val dialogManager: IDialogManager by lazy { DialogManager() } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/kotlin/works/szabope/plugins/mypy/services/parser/MypyOutputParser.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.services.parser 2 | 3 | import kotlinx.serialization.SerializationException 4 | import kotlinx.serialization.json.Json 5 | 6 | /** 7 | * Created when a line of mypy output cannot be parsed for some reason. 8 | * The goal is to handle cases when mypy fails to distinguish between throwing an exception and reporting a hit. 9 | * - mypy exceptions _sometimes_ printed to stdout, mixing them into normal output, in which case even `-O json` is ignored 10 | * - and sometimes after such and exception comes valuable json output 11 | */ 12 | class MypyParseException(val sourceJson: String, override val cause: SerializationException) : 13 | SerializationException(cause.message, cause) 14 | 15 | object MypyOutputParser { 16 | 17 | private val withUnknownKeys = Json { ignoreUnknownKeys = true } 18 | 19 | /** 20 | * @throws SerializationException mypy _sometimes_ prints its own errors to stdout, mixing them into normal output, 21 | * in which case even `-O json` is ignored. 22 | */ 23 | @Throws(MypyParseException::class) 24 | fun parse(json: String): Result { 25 | val result = try { 26 | withUnknownKeys.decodeFromString(MypyMessage.serializer(), json) 27 | } catch (e: SerializationException) { 28 | return Result.failure(MypyParseException(json, e)) 29 | } 30 | return Result.success(adjustForPlatform(result)) 31 | } 32 | 33 | /** 34 | * Adjust line numbers 35 | * from mypy: 1-based 36 | * to intellij: 0-based 37 | */ 38 | private fun adjustForPlatform(message: MypyMessage): MypyMessage { 39 | return message.copy( 40 | file = message.file, 41 | line = message.line - 1, 42 | column = message.column, 43 | message = message.message, 44 | hint = message.hint, 45 | code = message.code, 46 | severity = message.severity.uppercase() 47 | ) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/resources/icons/mypyToolWindow_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 13 | 17 | 21 | 22 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/pluginIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 13 | 17 | 21 | 22 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/pluginIcon_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 13 | 17 | 21 | 22 | -------------------------------------------------------------------------------- /src/main/resources/icons/mypyToolWindow@20x20.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 13 | 17 | 21 | 22 | -------------------------------------------------------------------------------- /src/main/resources/icons/mypyToolWindow@20x20_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 13 | 17 | 21 | 22 | -------------------------------------------------------------------------------- /src/test/kotlin/works/szabope/plugins/mypy/AbstractToolWindowTestCase.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy 2 | 3 | import com.intellij.openapi.wm.ToolWindowManager 4 | import com.intellij.testFramework.PlatformTestUtil 5 | import com.intellij.testFramework.replaceService 6 | import com.intellij.toolWindow.ToolWindowHeadlessManagerImpl 7 | import com.intellij.ui.tree.TreeTestUtil 8 | import works.szabope.plugins.mypy.services.mypySeverityConfigs 9 | import works.szabope.plugins.mypy.testutil.TestToolWindowHeadlessManagerImpl 10 | import works.szabope.plugins.mypy.toolWindow.MypyToolWindowFactory 11 | import works.szabope.plugins.mypy.toolWindow.MypyToolWindowPanel 12 | import works.szabope.plugins.mypy.toolWindow.MypyTreeService 13 | 14 | abstract class AbstractToolWindowTestCase : AbstractMypyTestCase() { 15 | 16 | protected lateinit var treeUtil: TreeTestUtil 17 | protected lateinit var toolWindowManager: TestToolWindowHeadlessManagerImpl 18 | 19 | override fun setUp() { 20 | super.setUp() 21 | toolWindowManager = TestToolWindowHeadlessManagerImpl(project) 22 | project.replaceService(ToolWindowManager::class.java, toolWindowManager, testRootDisposable) 23 | setUpToolWindow() 24 | val panel = ToolWindowManager.getInstance(project) 25 | .getToolWindow(MypyToolWindowPanel.ID)!!.contentManager.contents.single().component as MypyToolWindowPanel 26 | treeUtil = TreeTestUtil(panel.tree) 27 | // ensure severities are on default setting 28 | with(MypyTreeService.getInstance(project)) { 29 | mypySeverityConfigs.keys.forEach { assertTrue(isSeverityLevelDisplayed(it)) } 30 | } 31 | PlatformTestUtil.waitForAllBackgroundActivityToCalmDown() 32 | } 33 | 34 | override fun tearDown() { 35 | toolWindowManager.cleanup() 36 | super.tearDown() 37 | } 38 | 39 | private fun setUpToolWindow() { 40 | val toolWindowManager = ToolWindowManager.getInstance(project) as ToolWindowHeadlessManagerImpl 41 | val toolWindow = toolWindowManager.doRegisterToolWindow(MypyToolWindowPanel.ID) 42 | MypyToolWindowFactory().createToolWindowContent(myFixture.project, toolWindow) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/resources/icons/mypyToolWindow.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 13 | 17 | 21 | 22 | -------------------------------------------------------------------------------- /src/test/kotlin/works/szabope/plugins/mypy/action/RescanTest.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.action 2 | 3 | import com.intellij.openapi.actionSystem.CommonDataKeys 4 | import com.intellij.testFramework.PlatformTestUtil 5 | import com.intellij.testFramework.TestDataPath 6 | import works.szabope.plugins.mypy.AbstractToolWindowTestCase 7 | import works.szabope.plugins.mypy.services.MypySettings 8 | import works.szabope.plugins.mypy.testutil.dataContext 9 | import works.szabope.plugins.mypy.testutil.scan 10 | import java.nio.file.Paths 11 | import kotlin.io.path.absolutePathString 12 | 13 | @TestDataPath($$"$CONTENT_ROOT/testData/action/rescan") 14 | class RescanTest : AbstractToolWindowTestCase() { 15 | 16 | override fun getTestDataPath() = "src/test/testData/action/rescan" 17 | 18 | override fun setUp() { 19 | super.setUp() 20 | with(MypySettings.getInstance(project)) { 21 | useProjectSdk = false 22 | executablePath = Paths.get(testDataPath).resolve("mypy").absolutePathString() 23 | workingDirectory = Paths.get(testDataPath).absolutePathString() 24 | } 25 | val file = myFixture.configureByText("a.py", "doesn't matter").virtualFile 26 | scan(dataContext(project) { add(CommonDataKeys.VIRTUAL_FILE_ARRAY, arrayOf(file)) }) 27 | PlatformTestUtil.waitWhileBusy { ScanJobRegistry.INSTANCE.isActive() } 28 | } 29 | 30 | /** 31 | * To be sure that rescan was actually called, we reconfigure executable to a version that returns mypy results: `mypy2` 32 | * `mypy` executable returns no results 33 | */ 34 | fun `test rescan running for the same file scan did`() { 35 | MypySettings.getInstance(project).executablePath = Paths.get(testDataPath).resolve("mypy2").absolutePathString() 36 | PlatformTestUtil.invokeNamedAction(RescanAction.ID) 37 | PlatformTestUtil.waitWhileBusy { ScanJobRegistry.INSTANCE.isActive() } 38 | treeUtil.assertStructure("+Found 1 issue(s) in 1 file(s)\n") 39 | treeUtil.expandAll() 40 | treeUtil.assertStructure( 41 | """|-Found 1 issue(s) in 1 file(s) 42 | | -src/a.py 43 | | Bracketed expression "[...]" is not valid as a type [valid-type] (0:-1) Did you mean "List[...]"? 44 | |""".trimMargin() 45 | ) 46 | } 47 | } -------------------------------------------------------------------------------- /art/mypyScanAction.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 13 | 17 | 23 | 27 | 28 | -------------------------------------------------------------------------------- /src/main/resources/icons/mypyScanAction.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 13 | 17 | 23 | 27 | 28 | -------------------------------------------------------------------------------- /src/main/resources/icons/mypyScanAction_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 13 | 17 | 23 | 27 | 28 | -------------------------------------------------------------------------------- /src/main/kotlin/works/szabope/plugins/mypy/services/mypyParamListBuilders.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.services 2 | 3 | import com.intellij.openapi.project.Project 4 | import com.intellij.openapi.util.io.toCanonicalPath 5 | import com.intellij.openapi.vfs.VirtualFile 6 | import com.intellij.platform.backend.workspace.virtualFile 7 | import com.intellij.platform.workspace.jps.entities.ExcludeUrlEntity 8 | import com.intellij.platform.workspace.jps.entities.contentRoot 9 | import com.intellij.util.execution.ParametersListUtil 10 | import com.intellij.util.text.nullize 11 | import works.szabope.plugins.common.run.Exclusions 12 | import works.szabope.plugins.common.services.ImmutableSettingsData 13 | import works.szabope.plugins.mypy.MypyArgs 14 | import java.nio.file.Path 15 | import kotlin.io.path.pathString 16 | 17 | context(project: Project) 18 | fun buildMypyParamList(configuration: ImmutableSettingsData, shadowMap: Map): List { 19 | val shadowParameters = shadowMap.flatMap { (shadowedOriginal, shadowCastingOne) -> 20 | listOf("--shadow-file", shadowedOriginal.path, shadowCastingOne.pathString) 21 | } 22 | return buildMypyParamList(configuration, shadowMap.keys, shadowParameters) 23 | } 24 | 25 | context(project: Project) 26 | fun buildMypyParamList( 27 | configuration: ImmutableSettingsData, targets: Collection, extraArgs: Collection = emptyList() 28 | ) = with(configuration) { 29 | val params = MypyArgs.MYPY_MANDATORY_COMMAND_ARGS.split(" ").toMutableList() 30 | configFilePath.nullize(true)?.let { params.add("--config-file"); params.add(it) } 31 | arguments.nullize(true)?.let { params.addAll(ParametersListUtil.parse(it)) } 32 | if (excludeNonProjectFiles) { 33 | Exclusions(project).findAll(targets).mapNotNull { getRelativePathFromContentRoot(it)?.toCanonicalPath() } 34 | .forEach { params.add("--exclude"); params.add(it) } 35 | } 36 | params.addAll(extraArgs) 37 | targets.map { requireNotNull(it.canonicalPath) }.let { params.addAll(it) } 38 | params 39 | } 40 | 41 | // mypy's `--exclude` doesn't work with absolute paths 42 | private fun getRelativePathFromContentRoot(excludeUrlEntity: ExcludeUrlEntity): Path? { 43 | val contentRootPath = 44 | excludeUrlEntity.contentRoot?.url?.virtualFile?.path?.let { kotlin.io.path.Path(it) } ?: return null 45 | val exclusionPath = excludeUrlEntity.url.virtualFile?.path?.let { kotlin.io.path.Path(it) } ?: return null 46 | return contentRootPath.relativize(exclusionPath) 47 | } 48 | -------------------------------------------------------------------------------- /src/test/kotlin/works/szabope/plugins/mypy/initialization/MypyInitializationWithRemotePythonSdkTest.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | package works.szabope.plugins.mypy.initialization 4 | 5 | import com.intellij.openapi.application.runWriteActionAndWait 6 | import com.intellij.openapi.projectRoots.ProjectJdkTable 7 | import com.intellij.testFramework.PlatformTestUtil 8 | import com.jetbrains.python.psi.LanguageLevel 9 | import com.jetbrains.python.remote.PyRemoteSdkAdditionalData 10 | import com.jetbrains.python.sdk.pythonSdk 11 | import com.jetbrains.python.sdk.sdkFlavor 12 | import io.mockk.every 13 | import io.mockk.mockk 14 | import io.mockk.mockkObject 15 | import works.szabope.plugins.common.test.sdk.PythonMockSdk 16 | import works.szabope.plugins.mypy.AbstractMypyHeavyPlatformTestCase 17 | import works.szabope.plugins.mypy.testutil.getMypyConfigurationNotCompleteNotification 18 | import java.nio.file.Path 19 | 20 | class MypyInitializationWithRemotePythonSdkTest : AbstractMypyHeavyPlatformTestCase() { 21 | 22 | override fun tearDown() { 23 | val mockSdk = project.pythonSdk!! 24 | project.pythonSdk = null 25 | module?.pythonSdk = null 26 | runWriteActionAndWait { 27 | ProjectJdkTable.getInstance().removeJdk(mockSdk) 28 | } 29 | super.tearDown() 30 | } 31 | 32 | override fun setUpProject() { 33 | val mockSdk = PythonMockSdk.create( 34 | "Remote Python 3.13.1 Docker (python:latest) (3)", 35 | "$PROJECT_PATH/MockSdk", 36 | LanguageLevel.PYTHON313 37 | ) 38 | // let's lie about locality, see com.jetbrains.python.sdk.PythonSdkUtil#isRemote(Sdk) 39 | val mockAdditionalData = mockk() 40 | every { mockAdditionalData.sdkId } returns "Python something" 41 | every { mockAdditionalData.flavor } returns mockSdk.sdkFlavor 42 | mockkObject(mockSdk) 43 | every { mockSdk.sdkAdditionalData } returns mockAdditionalData 44 | runWriteActionAndWait { 45 | ProjectJdkTable.getInstance().addJdk(mockSdk) 46 | } 47 | myProject = PlatformTestUtil.loadAndOpenProject(Path.of(PROJECT_PATH), getTestRootDisposable()) 48 | } 49 | 50 | fun `test plugin initialized for project with python sdk results in notification`() { 51 | val actions = getMypyConfigurationNotCompleteNotification(project).actions 52 | assertEquals(1, actions.size) 53 | } 54 | 55 | companion object { 56 | const val PROJECT_PATH = "src/test/testData/initialization/projectWithRemoteSdk" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/kotlin/works/szabope/plugins/mypy/configurable/MypyConfigurable.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.configurable 2 | 3 | import com.intellij.openapi.project.Project 4 | import com.intellij.openapi.ui.TextFieldWithBrowseButton 5 | import com.intellij.openapi.util.SystemInfo 6 | import com.intellij.ui.layout.ValidationInfoBuilder 7 | import works.szabope.plugins.common.configurable.ConfigurableConfiguration 8 | import works.szabope.plugins.common.configurable.GeneralConfigurable 9 | import works.szabope.plugins.common.trimToNull 10 | import works.szabope.plugins.common.validator.FileValidator 11 | import works.szabope.plugins.mypy.MypyBundle 12 | import works.szabope.plugins.mypy.action.InstallMypyAction 13 | import works.szabope.plugins.mypy.services.MypyPluginPackageManagementService 14 | import works.szabope.plugins.mypy.services.MypySettings 15 | 16 | class MypyConfigurable(private val project: Project) : GeneralConfigurable( 17 | project, ConfigurableConfiguration( 18 | MypyBundle.message("mypy.configuration.name"), 19 | MypyBundle.message("mypy.configuration.name"), 20 | ID, 21 | InstallMypyAction.ID, 22 | MypyBundle.message("mypy.intention.install_mypy.text"), 23 | MypyBundle.message("mypy.configuration.mypy_picker_title"), 24 | MypyBundle.message("mypy.configuration.path_to_executable.label"), 25 | FileFilter( 26 | if (SystemInfo.isWindows) { 27 | listOf("mypy.exe", "mypyc.exe", "mypy.bat") 28 | } else { 29 | listOf("mypy", "mypyc") 30 | } 31 | ), 32 | MypyBundle.message("mypy.configuration.path_to_executable.empty_warning"), 33 | MypyBundle.message("mypy.configuration.path_to_executable.version_validation_title"), 34 | MypyBundle.message("mypy.configuration.use_project_sdk"), 35 | MypyBundle.message("mypy.configuration.config_file.comment"), 36 | MypyBundle.message("mypy.configuration.config_file.help") 37 | ) 38 | ) { 39 | override val settings get() = MypySettings.getInstance(project) 40 | override val packageManager get() = MypyPluginPackageManagementService.getInstance(project) 41 | 42 | override fun validateExecutable(path: String?) = with(MypyValidator(project)) { 43 | path?.trimToNull()?.let { path -> 44 | validateExecutablePath(path) ?: validateMypyVersion(path) 45 | } 46 | } 47 | 48 | override fun validateLocalSdk() = MypyValidator(project).validateProjectSdk() 49 | 50 | override fun validateConfigFilePath( 51 | builder: ValidationInfoBuilder, field: TextFieldWithBrowseButton 52 | ) = FileValidator().validateConfigFilePath(field.text.trimToNull())?.let { builder.error(it) } 53 | 54 | companion object { 55 | const val ID = "Settings.Mypy" 56 | } 57 | } -------------------------------------------------------------------------------- /src/main/kotlin/works/szabope/plugins/mypy/services/AsyncScanService.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.services 2 | 3 | import com.intellij.openapi.components.Service 4 | import com.intellij.openapi.components.service 5 | import com.intellij.openapi.diagnostic.thisLogger 6 | import com.intellij.openapi.project.Project 7 | import com.intellij.openapi.vfs.VirtualFile 8 | import kotlinx.coroutines.flow.* 9 | import works.szabope.plugins.common.services.ImmutableSettingsData 10 | import works.szabope.plugins.mypy.MypyBundle 11 | import works.szabope.plugins.mypy.dialog.DialogManager 12 | import works.szabope.plugins.mypy.services.parser.MypyMessage 13 | import works.szabope.plugins.mypy.services.parser.MypyOutputParser 14 | import works.szabope.plugins.mypy.services.parser.MypyParseException 15 | 16 | @Service(Service.Level.PROJECT) 17 | class AsyncScanService(private val project: Project) { 18 | 19 | suspend fun scan(targets: Collection, configuration: ImmutableSettingsData): List { 20 | // Why? See MypyParseException 21 | // So let's collect parse failures and report them. 22 | // If you have a better idea, please let me know. 23 | val unparsableLinesOfStdout = StringBuilder() 24 | val parameters = with(project) { buildMypyParamList(configuration, targets) } 25 | val stdErr = StringBuilder() 26 | return MypyExecutor(project).execute(configuration, parameters).filter { it.text.isNotBlank() } 27 | .transform { line -> 28 | if (line.isError) { 29 | stdErr.append(line.text) 30 | return@transform 31 | } 32 | MypyOutputParser.parse(line.text).onSuccess { emit(it) }.onFailure { 33 | when (it) { 34 | is MypyParseException -> { 35 | unparsableLinesOfStdout.appendLine("${it.sourceJson} failed with ${it.message}") 36 | } 37 | 38 | else -> { 39 | thisLogger().error(MypyBundle.message("mypy.please_report_this_issue"), it) 40 | } 41 | } 42 | } 43 | }.onCompletion { 44 | if (unparsableLinesOfStdout.isNotEmpty()) { 45 | showClickableBalloonError(project, MypyBundle.message("mypy.toolwindow.balloon.parse_error")) { 46 | DialogManager.showToolOutputParseErrorDialog( 47 | configuration, 48 | targets.joinToString("\n"), 49 | unparsableLinesOfStdout.toString(), 50 | "" 51 | ) 52 | } 53 | } 54 | }.catch(handleScanException(project, configuration, stdErr)).toList(ArrayList()) 55 | } 56 | 57 | companion object { 58 | @JvmStatic 59 | fun getInstance(project: Project): AsyncScanService = project.service() 60 | } 61 | } -------------------------------------------------------------------------------- /src/test/kotlin/works/szabope/plugins/mypy/testutil/actions.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.testutil 2 | 3 | import com.intellij.openapi.actionSystem.* 4 | import com.intellij.openapi.actionSystem.ex.ActionUtil.performAction 5 | import com.intellij.openapi.actionSystem.ex.ActionUtil.updateAction 6 | import com.intellij.testFramework.PlatformTestUtil 7 | import org.junit.Assert 8 | import works.szabope.plugins.mypy.action.InstallMypyAction 9 | import works.szabope.plugins.mypy.action.ScanAction 10 | import works.szabope.plugins.mypy.action.StopScanAction 11 | 12 | fun waitForIt(actionId: String, context: DataContext) { 13 | val action = ActionManager.getInstance().getAction(actionId) 14 | val event = AnActionEvent.createEvent(context, null, "", ActionUiKind.NONE, null) 15 | updateAction(action, event) 16 | PlatformTestUtil.waitWhileBusy { !event.presentation.isEnabled } 17 | } 18 | 19 | fun scan(context: DataContext) { 20 | val action = ActionManager.getInstance().getAction(ScanAction.ID) 21 | val event = AnActionEvent.createEvent(context, null, "", ActionUiKind.NONE, null) 22 | updateAction(action, event) 23 | Assert.assertTrue(event.presentation.isEnabled) 24 | performAction(action, event) 25 | } 26 | 27 | fun stopScan(context: DataContext) { 28 | val action = ActionManager.getInstance().getAction(StopScanAction.ID) 29 | val event = AnActionEvent.createEvent(context, null, ActionPlaces.EDITOR_TAB, ActionUiKind.NONE, null) 30 | updateAction(action, event) 31 | Assert.assertTrue(event.presentation.isEnabled) 32 | performAction(action, event) 33 | } 34 | 35 | fun installMypy(context: DataContext) { 36 | val action = ActionManager.getInstance().getAction(InstallMypyAction.ID) 37 | val event = AnActionEvent.createEvent(context, null, ActionPlaces.NOTIFICATION, ActionUiKind.NONE, null) 38 | updateAction(action, event) 39 | Assert.assertTrue(event.presentation.isEnabled) 40 | performAction(action, event) 41 | } 42 | 43 | fun markExcluded(context: DataContext) { 44 | if (context.getData(CommonDataKeys.VIRTUAL_FILE_ARRAY)?.isNotEmpty() != true) { 45 | throw IllegalArgumentException("Use `CommonDataKeys.VIRTUAL_FILE_ARRAY` for virtual files to exclude them") 46 | } 47 | val event = AnActionEvent.createEvent(context, null, "", ActionUiKind.NONE, null) 48 | val action = ActionManager.getInstance().getAction("MarkExcludeRoot") 49 | updateAction(action, event) 50 | Assert.assertTrue(event.presentation.isEnabled) 51 | performAction(action, event) 52 | } 53 | 54 | fun unmark(context: DataContext) { 55 | val event = AnActionEvent.createEvent(context, null, "", ActionUiKind.NONE, null) 56 | if (event.getData(CommonDataKeys.VIRTUAL_FILE_ARRAY).isNullOrEmpty()) { 57 | throw IllegalArgumentException("Use `CommonDataKeys.VIRTUAL_FILE_ARRAY` for virtual files to (un)mark them") 58 | } 59 | val action = ActionManager.getInstance().getAction("UnmarkRoot") 60 | updateAction(action, event) 61 | Assert.assertTrue(event.presentation.isEnabled) 62 | performAction(action, event) 63 | } -------------------------------------------------------------------------------- /src/test/kotlin/works/szabope/plugins/mypy/annotator/AnnotatorTest.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.annotator 2 | 3 | import com.intellij.testFramework.LightVirtualFile 4 | import com.intellij.testFramework.TestDataPath 5 | import junit.framework.AssertionFailedError 6 | import works.szabope.plugins.mypy.AbstractToolWindowTestCase 7 | import works.szabope.plugins.mypy.services.MypySettings 8 | import java.nio.file.Paths 9 | import kotlin.io.path.absolutePathString 10 | 11 | @TestDataPath($$"$CONTENT_ROOT/testData/annotation") 12 | class AnnotatorTest : AbstractToolWindowTestCase() { 13 | companion object { 14 | val DOESNT_MATTER = """|def lets_have_fun() -> [int]: 15 | | return 'fun' 16 | |""".trimMargin() 17 | } 18 | 19 | override fun getTestDataPath() = "src/test/testData/annotation" 20 | 21 | override fun setUp() { 22 | super.setUp() 23 | myFixture.enableInspections(MypyInspection()) 24 | } 25 | 26 | fun `test MypyAnnotator does not fail with incomplete settings`() { 27 | with(MypySettings.getInstance(project)) { 28 | executablePath = "" 29 | useProjectSdk = false 30 | } 31 | myFixture.configureByText("a.py", DOESNT_MATTER) 32 | assertEmpty(myFixture.filterAvailableIntentions("Suppress mypy ")) 33 | } 34 | 35 | fun `test MypyAnnotator does not fail if mypy executable path has a space in it`() { 36 | with(MypySettings.getInstance(project)) { 37 | executablePath = Paths.get(testDataPath).resolve("white space/mypy").absolutePathString() 38 | configFilePath = Paths.get(testDataPath).resolve("white space/mypy.ini").absolutePathString() 39 | workingDirectory = Paths.get(testDataPath).absolutePathString() 40 | arguments = "" 41 | useProjectSdk = false 42 | } 43 | myFixture.configureByFile("a.py") 44 | @Suppress("UnstableApiUsage") val mypyAnnotations = 45 | myFixture.doHighlighting().filter { it.toolId == MypyAnnotator::class.java } 46 | assertEquals(1, mypyAnnotations.size) 47 | } 48 | 49 | fun `test MypyAnnotator does not run for in-memory target`() { 50 | with(MypySettings.getInstance(project)) { 51 | executablePath = Paths.get(testDataPath).resolve("does_not_exist").absolutePathString() 52 | workingDirectory = Paths.get(testDataPath).absolutePathString() 53 | arguments = "" 54 | useProjectSdk = false 55 | } 56 | var assertionError: Error? = null 57 | toolWindowManager.onBalloon { 58 | assertionError = AssertionFailedError("Should not happen: $it") 59 | } 60 | val inMemoryTarget = LightVirtualFile("file-in-memory.py", "") 61 | myFixture.configureFromExistingVirtualFile(inMemoryTarget) 62 | @Suppress("UnstableApiUsage") val mypyAnnotations = 63 | myFixture.doHighlighting().filter { it.toolId == MypyAnnotator::class.java } 64 | assertEquals(0, mypyAnnotations.size) 65 | assertionError?.let { throw it } 66 | } 67 | } -------------------------------------------------------------------------------- /src/test/kotlin/works/szabope/plugins/mypy/AbstractMypyTestCase.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy 2 | 3 | import com.intellij.openapi.application.runWriteActionAndWait 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.openapi.projectRoots.ProjectJdkTable 6 | import com.intellij.openapi.projectRoots.Sdk 7 | import com.intellij.openapi.vfs.newvfs.impl.VfsRootAccess 8 | import com.intellij.testFramework.LightProjectDescriptor 9 | import com.intellij.testFramework.fixtures.BasePlatformTestCase 10 | import com.jetbrains.python.sdk.pythonSdk 11 | import io.mockk.clearAllMocks 12 | import io.mockk.every 13 | import io.mockk.mockkObject 14 | import io.mockk.unmockkAll 15 | import works.szabope.plugins.common.services.AbstractPluginPackageManagementService 16 | import works.szabope.plugins.common.test.sdk.PythonMockSdk 17 | import works.szabope.plugins.mypy.services.MypyPluginPackageManagementService 18 | import works.szabope.plugins.mypy.services.MypySettings 19 | import works.szabope.plugins.mypy.testutil.MypyPluginPackageManagementServiceStub 20 | 21 | abstract class AbstractMypyTestCase : BasePlatformTestCase() { 22 | 23 | // local variables are not supported in mockk answer, yet 24 | private lateinit var mypyPackageManagementServiceStub: AbstractPluginPackageManagementService 25 | 26 | override fun setUp() { 27 | // FIXME: this is a duct tape for 28 | // com.intellij.python.community.services.systemPython.searchPythonsPhysicallyNoCache 29 | // accessing /usr/bin/python3(\.\d+)? which is not allowed from tests 30 | VfsRootAccess.allowRootAccess(testRootDisposable, "/usr/bin") 31 | mockkObject(MypyPluginPackageManagementService.Companion) 32 | every { MypyPluginPackageManagementService.getInstance(any(Project::class)) } answers { 33 | if (!::mypyPackageManagementServiceStub.isInitialized) { 34 | mypyPackageManagementServiceStub = MypyPluginPackageManagementServiceStub( 35 | firstArg() 36 | ) 37 | } 38 | mypyPackageManagementServiceStub 39 | } 40 | super.setUp() 41 | MypySettings.getInstance(project).reset() 42 | } 43 | 44 | override fun tearDown() { 45 | clearAllMocks() 46 | unmockkAll() 47 | super.tearDown() 48 | } 49 | 50 | /** 51 | * https://youtrack.jetbrains.com/issue/IJPL-197007 52 | */ 53 | override fun getProjectDescriptor(): LightProjectDescriptor? { 54 | return LightProjectDescriptor() 55 | } 56 | 57 | fun withMockSdk(path: String, action: (Sdk) -> Unit) { 58 | val mockSdk = PythonMockSdk.create(path) 59 | runWriteActionAndWait { 60 | ProjectJdkTable.getInstance().addJdk(mockSdk) 61 | } 62 | project.pythonSdk = mockSdk 63 | module.pythonSdk = mockSdk 64 | try { 65 | action(mockSdk) 66 | } finally { 67 | project.pythonSdk = null 68 | module.pythonSdk = null 69 | runWriteActionAndWait { 70 | ProjectJdkTable.getInstance().removeJdk(mockSdk) 71 | } 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | 74 | 75 | @rem Execute Gradle 76 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 77 | 78 | :end 79 | @rem End local scope for the variables with windows NT shell 80 | if %ERRORLEVEL% equ 0 goto mainEnd 81 | 82 | :fail 83 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 84 | rem the _cmd.exe /c_ return code! 85 | set EXIT_CODE=%ERRORLEVEL% 86 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 87 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 88 | exit /b %EXIT_CODE% 89 | 90 | :mainEnd 91 | if "%OS%"=="Windows_NT" endlocal 92 | 93 | :omega 94 | -------------------------------------------------------------------------------- /src/main/kotlin/works/szabope/plugins/mypy/dialog/MypyErrorDialog.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.dialog 2 | 3 | import com.intellij.openapi.ui.DialogWrapper 4 | import com.intellij.openapi.util.NlsContexts.DetailedDescription 5 | import com.intellij.openapi.util.NlsContexts.DialogTitle 6 | import com.intellij.ui.dsl.builder.Align 7 | import com.intellij.ui.dsl.builder.panel 8 | import com.intellij.util.ui.JBUI 9 | import com.jetbrains.rd.generator.nova.GenerationSpec.Companion.nullIfEmpty 10 | import works.szabope.plugins.common.services.ImmutableSettingsData 11 | import works.szabope.plugins.mypy.MypyBundle 12 | 13 | data class MypyErrorDescription( 14 | @DetailedDescription val details: String?, @DetailedDescription val message: String? = null 15 | ) 16 | 17 | class MypyPackageInstallationErrorDialog(message: String) : MypyErrorDialog( 18 | MypyBundle.message("mypy.dialog.installation_error.title"), 19 | MypyErrorDescription(message, MypyBundle.message("mypy.dialog.installation_error.message")) 20 | ) 21 | 22 | class FailedToExecuteErrorDialog(message: String) : MypyErrorDialog( 23 | MypyBundle.message("mypy.dialog.failed_to_execute.title"), MypyErrorDescription( 24 | message, MypyBundle.message("mypy.dialog.failed_to_execute.message") 25 | ) 26 | ) 27 | 28 | class MypyExecutionErrorDialog( 29 | configuration: ImmutableSettingsData, result: String, resultCode: Int? 30 | ) : MypyErrorDialog( 31 | MypyBundle.message("mypy.dialog.execution_error.title"), MypyErrorDescription( 32 | MypyBundle.message("mypy.dialog.execution_error.content", configuration, result), 33 | resultCode?.let { MypyBundle.message("mypy.dialog.execution_error.status_code", it) }) 34 | ) 35 | 36 | class MypyParseErrorDialog( 37 | configuration: ImmutableSettingsData, targets: String, json: String, error: String 38 | ) : MypyErrorDialog( 39 | MypyBundle.message("mypy.dialog.parse_error.title"), MypyErrorDescription( 40 | MypyBundle.message("mypy.dialog.parse_error.details", configuration, targets, json), 41 | error.nullIfEmpty()?.let { MypyBundle.message("mypy.dialog.parse_error.message", it) }) 42 | ) 43 | 44 | class MypyGeneralErrorDialog(throwable: Throwable) : MypyErrorDialog( 45 | MypyBundle.message("mypy.dialog.general_error.title"), MypyErrorDescription( 46 | MypyBundle.message( 47 | "mypy.dialog.general_error.details", throwable.message!!, throwable.stackTraceToString() 48 | ), MypyBundle.message("mypy.please_report_this_issue") 49 | ) 50 | ) 51 | 52 | open class MypyErrorDialog( 53 | title: @DialogTitle String, private val description: MypyErrorDescription 54 | ) : DialogWrapper(false) { 55 | 56 | init { 57 | setTitle(title) 58 | super.init() 59 | setErrorText(description.message) 60 | } 61 | 62 | override fun createCenterPanel() = description.details?.let { details -> 63 | panel { 64 | row { 65 | textArea().applyToComponent { 66 | text = details 67 | isEditable = false 68 | lineWrap = true 69 | wrapStyleWord = true 70 | setSize(JBUI.scale(800), 0) 71 | }.align(Align.FILL) 72 | } 73 | }.withPreferredSize(JBUI.scale(800), 0) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/kotlin/works/szabope/plugins/mypy/annotator/MypyIgnoreIntention.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.annotator 2 | 3 | import com.intellij.codeInsight.intention.IntentionAction 4 | import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction 5 | import com.intellij.openapi.editor.Editor 6 | import com.intellij.openapi.project.Project 7 | import com.intellij.psi.PsiElement 8 | import com.intellij.psi.util.PsiTreeUtil 9 | import com.intellij.psi.util.startOffset 10 | import com.jetbrains.python.psi.PyFormattedStringElement 11 | import com.jetbrains.python.psi.PyStringLiteralExpression 12 | import com.jetbrains.python.psi.PyUtil.StringNodeInfo 13 | import com.jetbrains.python.psi.impl.PyPsiUtils 14 | import works.szabope.plugins.mypy.MypyBundle 15 | import works.szabope.plugins.mypy.services.parser.MypyMessage 16 | 17 | /** 18 | * Intention action to append `# type: ignore[...]` comment to suppress Mypy annotations. 19 | */ 20 | class MypyIgnoreIntention(private val issue: MypyMessage) : PsiElementBaseIntentionAction(), IntentionAction { 21 | 22 | companion object { 23 | @JvmStatic 24 | val COMMENT_REGEX = "^#\\s+type:\\s+ignore(\\[(?[a-zA-Z\\s,-]+)])?".toRegex() 25 | } 26 | 27 | override fun getText(): String { 28 | return MypyBundle.message("mypy.intention.ignore.text", issue.code) 29 | } 30 | 31 | override fun getFamilyName(): String { 32 | return MypyBundle.message("mypy.intention.ignore.family_name") 33 | } 34 | 35 | override fun isAvailable(project: Project, editor: Editor?, element: PsiElement): Boolean { 36 | return !isTripleQuotedMultilineString(element) 37 | } 38 | 39 | override fun invoke(project: Project, editor: Editor?, element: PsiElement) { 40 | val existingComment = PyPsiUtils.findSameLineComment(element) 41 | val existingMypyIgnoreComment = existingComment?.text?.let { COMMENT_REGEX.find(it) } 42 | val existingCodes = existingMypyIgnoreComment?.groups["codes"]?.value 43 | val comment = if (existingCodes != null) { 44 | existingMypyIgnoreComment.value.replace(existingCodes, "$existingCodes,${issue.code}") 45 | } else { // (existingMypyIgnoreComment != null) { IMPOSSIBLE, we cannot end up here with `# type: ignore` 46 | "# type: ignore[${issue.code}]" 47 | } 48 | existingMypyIgnoreComment?.run { 49 | element.containingFile.fileDocument.deleteString( 50 | existingComment.textRange.startOffset, existingComment.textRange.endOffset 51 | ) 52 | } 53 | val codeLineEndOffset = 54 | existingComment?.startOffset ?: element.containingFile.fileDocument.getLineEndOffset(issue.line) 55 | val spaces = " ".repeat(existingComment?.let { 0 } ?: 2) 56 | element.containingFile.fileDocument.insertString(codeLineEndOffset, "$spaces$comment ") 57 | } 58 | 59 | /** mypy does not support `#type: ignore` on multiline triple quoted elements */ 60 | private fun isTripleQuotedMultilineString(element: PsiElement): Boolean { 61 | try { 62 | val elements = PsiTreeUtil.collectParents(element, PyFormattedStringElement::class.java, true) { 63 | it is PyStringLiteralExpression 64 | } 65 | return elements.any { StringNodeInfo(it).isTripleQuoted && it.text.contains('\n') } 66 | } catch (_: IllegalArgumentException) { 67 | return false 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /src/main/kotlin/works/szabope/plugins/mypy/configurable/MypyValidator.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.configurable 2 | 3 | import com.intellij.execution.process.CapturingProcessHandler 4 | import com.intellij.execution.target.TargetedCommandLineBuilder 5 | import com.intellij.execution.target.local.LocalTargetEnvironment 6 | import com.intellij.execution.target.local.LocalTargetEnvironmentRequest 7 | import com.intellij.openapi.project.Project 8 | import com.jetbrains.python.packaging.PyPackage 9 | import works.szabope.plugins.common.services.PluginPackageManagementException 10 | import works.szabope.plugins.mypy.MypyBundle 11 | import works.szabope.plugins.mypy.services.MypyPluginPackageManagementService 12 | import java.io.File 13 | 14 | class MypyValidator(private val project: Project) { 15 | fun validateExecutablePath(path: String?): String? { 16 | val path = path ?: return null 17 | require(path.isNotBlank()) 18 | val file = File(path) 19 | if (!file.exists()) { 20 | return MypyBundle.message("mypy.configuration.path_to_executable.not_exists") 21 | } 22 | if (file.isDirectory) { 23 | return MypyBundle.message("mypy.configuration.path_to_executable.is_directory") 24 | } 25 | if (!file.canExecute()) { 26 | return MypyBundle.message("mypy.configuration.path_to_executable.not_executable") 27 | } 28 | return null 29 | } 30 | 31 | fun validateMypyVersion(path: String): String? { 32 | val mypyVersion = getVersionForExecutable(path) 33 | ?: return MypyBundle.message("mypy.configuration.path_to_executable.unknown_version") 34 | if (!MypyPluginPackageManagementService.getInstance(project).getRequirement() 35 | .match(PyPackage("mypy", mypyVersion)) 36 | ) { 37 | return MypyBundle.message("mypy.configuration.mypy_invalid_version") 38 | } 39 | 40 | return null 41 | } 42 | 43 | private fun getVersionForExecutable(pathToExecutable: String): String? { 44 | val targetEnvRequest = LocalTargetEnvironmentRequest() 45 | val targetEnvironment = LocalTargetEnvironment(LocalTargetEnvironmentRequest()) 46 | 47 | val commandLineBuilder = TargetedCommandLineBuilder(targetEnvRequest) 48 | commandLineBuilder.setExePath(pathToExecutable) 49 | commandLineBuilder.addParameters("-V") 50 | 51 | val targetCMD = commandLineBuilder.build() 52 | 53 | val process = targetEnvironment.createProcess(targetCMD) 54 | 55 | return runCatching { 56 | val processHandler = CapturingProcessHandler( 57 | process, targetCMD.charset, targetCMD.getCommandPresentation(targetEnvironment) 58 | ) 59 | val processOutput = processHandler.runProcess(5000, true).stdout 60 | "(\\d+.\\d+.\\d+)".toRegex().find(processOutput)?.groups?.last()?.value 61 | }.getOrNull() 62 | } 63 | 64 | fun validateProjectSdk(): String? { 65 | MypyPluginPackageManagementService.getInstance(project).checkInstalledRequirement().onFailure { 66 | when (it) { 67 | is PluginPackageManagementException.PackageNotInstalledException -> return MypyBundle.message( 68 | "mypy.configuration.mypy_not_installed" 69 | ) 70 | 71 | is PluginPackageManagementException.PackageVersionObsoleteException -> return MypyBundle.message( 72 | "mypy.configuration.mypy_invalid_version" 73 | ) 74 | } 75 | } 76 | return null 77 | } 78 | } -------------------------------------------------------------------------------- /src/main/kotlin/works/szabope/plugins/mypy/action/ScanAction.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.action 2 | 3 | import com.intellij.openapi.actionSystem.ActionUpdateThread 4 | import com.intellij.openapi.actionSystem.AnActionEvent 5 | import com.intellij.openapi.actionSystem.CommonDataKeys 6 | import com.intellij.openapi.application.EDT 7 | import com.intellij.openapi.application.WriteIntentReadAction 8 | import com.intellij.openapi.fileEditor.FileDocumentManager 9 | import com.intellij.openapi.progress.currentThreadCoroutineScope 10 | import com.intellij.openapi.project.DumbAwareAction 11 | import com.intellij.openapi.project.Project 12 | import com.intellij.openapi.vfs.VirtualFile 13 | import com.intellij.openapi.wm.ToolWindowManager 14 | import com.jetbrains.python.PythonFileType 15 | import com.jetbrains.python.pyi.PyiFileType 16 | import kotlinx.coroutines.Dispatchers 17 | import kotlinx.coroutines.launch 18 | import kotlinx.coroutines.withContext 19 | import works.szabope.plugins.mypy.services.AsyncScanService 20 | import works.szabope.plugins.mypy.services.MypySettings 21 | import works.szabope.plugins.mypy.services.parser.MypyMessageConverter 22 | import works.szabope.plugins.mypy.toolWindow.MypyToolWindowPanel 23 | import works.szabope.plugins.mypy.toolWindow.MypyTreeService 24 | 25 | val SUPPORTED_FILE_TYPES = arrayOf(PythonFileType.INSTANCE, PyiFileType.INSTANCE) 26 | 27 | open class ScanAction : DumbAwareAction() { 28 | 29 | override fun actionPerformed(event: AnActionEvent) { 30 | val targets = listTargets(event) ?: return 31 | val project = event.project ?: return 32 | val treeService = MypyTreeService.getInstance(project) 33 | treeService.reinitialize(targets) 34 | WriteIntentReadAction.run { FileDocumentManager.getInstance().saveAllDocuments() } 35 | val job = currentThreadCoroutineScope().launch(Dispatchers.IO) { 36 | val configuration = MypySettings.getInstance(project).getValidConfiguration().getOrNull() ?: return@launch 37 | AsyncScanService.getInstance(project).scan(targets, configuration).forEach { 38 | val mypyMessage = MypyMessageConverter.convert(it) 39 | withContext(Dispatchers.EDT) { 40 | treeService.add(mypyMessage) 41 | } 42 | } 43 | treeService.lock() 44 | } 45 | ScanJobRegistry.INSTANCE.set(job) 46 | ToolWindowManager.getInstance(project).getToolWindow(MypyToolWindowPanel.ID)?.show() 47 | } 48 | 49 | override fun update(event: AnActionEvent) { 50 | val targets = listTargets(event) ?: return 51 | event.presentation.isEnabled = event.project?.let { isReadyToScan(it, targets) } == true 52 | } 53 | 54 | override fun getActionUpdateThread(): ActionUpdateThread { 55 | return ActionUpdateThread.BGT 56 | } 57 | 58 | protected open fun listTargets(event: AnActionEvent): Collection? { 59 | return event.getData(CommonDataKeys.VIRTUAL_FILE_ARRAY)?.asList() 60 | } 61 | 62 | private fun isReadyToScan(project: Project, targets: Collection): Boolean { 63 | return targets.isNotEmpty() && ScanJobRegistry.INSTANCE.isAvailable() && MypySettings.getInstance(project) 64 | .getValidConfiguration().isSuccess && isEligibleTargets(targets) 65 | } 66 | 67 | private fun isEligibleTargets(targets: Collection) = targets.map { isEligible(it) }.all { it } 68 | 69 | private fun isEligible(virtualFile: VirtualFile) = 70 | virtualFile.fileType in SUPPORTED_FILE_TYPES || virtualFile.isDirectory 71 | 72 | companion object { 73 | const val ID = "works.szabope.plugins.mypy.action.ScanAction" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/works.szabope.mypy-PythonCore.xml: -------------------------------------------------------------------------------- 1 | 2 | messages.MypyBundle 3 | 4 | 5 | 7 | 13 | 14 | 18 | 19 | 21 | 22 | 23 | 24 | 25 | 28 | 31 | 34 | 35 | 38 | 39 | 40 | 41 | 44 | 45 | 48 | 49 | 50 | 51 | 52 | 53 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/test/kotlin/works/szabope/plugins/mypy/action/ScanSdkTest.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.action 2 | 3 | import com.intellij.openapi.actionSystem.CommonDataKeys 4 | import com.intellij.openapi.vfs.ex.temp.TempFileSystem 5 | import com.intellij.testFramework.PlatformTestUtil 6 | import com.intellij.testFramework.TestDataPath 7 | import com.intellij.testFramework.common.waitUntilAssertSucceeds 8 | import io.mockk.every 9 | import io.mockk.mockkObject 10 | import junit.framework.Assert 11 | import junit.framework.AssertionFailedError 12 | import kotlinx.coroutines.runBlocking 13 | import works.szabope.plugins.mypy.AbstractToolWindowTestCase 14 | import works.szabope.plugins.mypy.MypyBundle 15 | import works.szabope.plugins.mypy.dialog.DialogManager 16 | import works.szabope.plugins.mypy.services.MypySettings 17 | import works.szabope.plugins.mypy.testutil.* 18 | import java.nio.file.Paths 19 | import kotlin.io.path.absolutePathString 20 | 21 | @TestDataPath($$"$CONTENT_ROOT/testData/action/scan_sdk") 22 | class ScanSdkTest : AbstractToolWindowTestCase() { 23 | 24 | private val dialogManager = TestDialogManager() 25 | 26 | override fun getTestDataPath() = "src/test/testData/action/scan_sdk" 27 | 28 | override fun setUp() { 29 | mockkObject(DialogManager.Companion) 30 | every { DialogManager.dialogManager } answers { dialogManager } 31 | super.setUp() 32 | } 33 | 34 | @Suppress("removal") 35 | fun testManualScan() = withMockSdk("${Paths.get(testDataPath).absolutePathString()}/MockSdk") { 36 | myFixture.copyDirectoryToProject("/", "/") 37 | installMypy(dataContext(project) { add(CommonDataKeys.PROJECT, project) }) 38 | setUpSettings() 39 | val excludedDir = TempFileSystem.getInstance().findFileByPath("/src/excluded_dir")!! 40 | val exclusionContext = dataContext(project) { 41 | add(CommonDataKeys.VIRTUAL_FILE_ARRAY, arrayOf(excludedDir)) 42 | } 43 | markExcluded(exclusionContext) 44 | var assertionError: Error? = null 45 | toolWindowManager.onBalloon { 46 | val expected = MypyBundle.message("action.InstallMypyAction.done_html") 47 | if (expected != it.htmlBody) { 48 | assertionError = AssertionFailedError(Assert.format("Should not happen", expected, it.htmlBody)) 49 | } 50 | } 51 | val target = TempFileSystem.getInstance().findFileByPath("/src")!! 52 | val context = dataContext(project) { add(CommonDataKeys.VIRTUAL_FILE_ARRAY, arrayOf(target)) } 53 | waitForIt(ScanAction.ID, context) 54 | scan(context) 55 | PlatformTestUtil.waitWhileBusy { ScanJobRegistry.INSTANCE.isActive() } 56 | assertionError?.let { throw it } 57 | runBlocking { 58 | waitUntilAssertSucceeds { 59 | treeUtil.assertStructure("+Found 1 issue(s) in 1 file(s)\n") 60 | }.also { 61 | treeUtil.expandAll() 62 | treeUtil.assertStructure( 63 | """|-Found 1 issue(s) in 1 file(s) 64 | | -src/a.py 65 | | Bracketed expression "[...]" is not valid as a type [valid-type] (0:-1) Did you mean "List[...]"? 66 | |""".trimMargin() 67 | ) 68 | } 69 | } 70 | unmark(exclusionContext) 71 | } 72 | 73 | private fun setUpSettings() { 74 | with(MypySettings.getInstance(project)) { 75 | executablePath = "" 76 | workingDirectory = Paths.get(testDataPath).absolutePathString() 77 | useProjectSdk = true 78 | configFilePath = "" 79 | scanBeforeCheckIn = false 80 | arguments = "" 81 | excludeNonProjectFiles = true 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow created for handling the release process based on the draft release prepared with the Build workflow. 2 | # Running the publishPlugin task requires all the following secrets to be provided: PUBLISH_TOKEN, PRIVATE_KEY, PRIVATE_KEY_PASSWORD, CERTIFICATE_CHAIN. 3 | # See https://plugins.jetbrains.com/docs/intellij/plugin-signing.html for more information. 4 | 5 | name: Release 6 | on: 7 | # Enable manual trigger 8 | workflow_dispatch: 9 | release: 10 | types: [prereleased, released] 11 | 12 | jobs: 13 | 14 | # Prepare and publish the plugin to JetBrains Marketplace repository 15 | release: 16 | name: Publish Plugin 17 | runs-on: ubuntu-latest 18 | permissions: 19 | contents: write 20 | pull-requests: write 21 | steps: 22 | 23 | # Free GitHub Actions Environment Disk Space 24 | - name: Maximize Build Space 25 | uses: jlumbroso/free-disk-space@v1.3.1 26 | with: 27 | tool-cache: false 28 | large-packages: false 29 | 30 | # Check out the current repository 31 | - name: Fetch Sources 32 | uses: actions/checkout@v5 33 | with: 34 | ref: ${{ github.event.release.tag_name }} 35 | 36 | # Set up the Java environment for the next steps 37 | - name: Setup Java 38 | uses: actions/setup-java@v5 39 | with: 40 | distribution: zulu 41 | java-version: 21 42 | 43 | # Setup Gradle 44 | - name: Setup Gradle 45 | uses: gradle/actions/setup-gradle@v5 46 | with: 47 | cache-read-only: true 48 | 49 | # Update the Unreleased section with the current release note 50 | - name: Patch Changelog 51 | if: ${{ github.event.release.body != '' }} 52 | env: 53 | CHANGELOG: ${{ github.event.release.body }} 54 | run: | 55 | RELEASE_NOTE="./build/tmp/release_note.txt" 56 | mkdir -p "$(dirname "$RELEASE_NOTE")" 57 | echo "$CHANGELOG" > $RELEASE_NOTE 58 | 59 | ./gradlew patchChangelog --release-note-file=$RELEASE_NOTE 60 | 61 | # Publish the plugin to JetBrains Marketplace 62 | - name: Publish Plugin 63 | env: 64 | PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} 65 | CERTIFICATE_CHAIN: ${{ secrets.CERTIFICATE_CHAIN }} 66 | PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} 67 | PRIVATE_KEY_PASSWORD: ${{ secrets.PRIVATE_KEY_PASSWORD }} 68 | GPR_USERNAME: ${{secrets.GPR_USERNAME}} 69 | GPR_TOKEN: ${{secrets.GPR_TOKEN}} 70 | run: ./gradlew publishPlugin 71 | 72 | # Upload an artifact as a release asset 73 | - name: Upload Release Asset 74 | env: 75 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 76 | run: gh release upload ${{ github.event.release.tag_name }} ./build/distributions/* 77 | 78 | # Create a pull request 79 | - name: Create Pull Request 80 | if: ${{ github.event.release.body != '' }} 81 | env: 82 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 83 | run: | 84 | VERSION="${{ github.event.release.tag_name }}" 85 | BRANCH="changelog-update-$VERSION" 86 | LABEL="release changelog" 87 | 88 | git config user.email "action@github.com" 89 | git config user.name "GitHub Action" 90 | 91 | git checkout -b $BRANCH 92 | git commit -am "Changelog update - $VERSION" 93 | git push --set-upstream origin $BRANCH 94 | 95 | gh label create "$LABEL" \ 96 | --description "Pull requests with release changelog update" \ 97 | --force \ 98 | || true 99 | 100 | gh pr create \ 101 | --title "Changelog update - \`$VERSION\`" \ 102 | --body "Current pull request contains patched \`CHANGELOG.md\` file for the \`$VERSION\` version." \ 103 | --label "$LABEL" \ 104 | --head $BRANCH 105 | -------------------------------------------------------------------------------- /src/test/kotlin/works/szabope/plugins/mypy/annotator/IntentionTest.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.annotator 2 | 3 | import com.intellij.testFramework.PsiTestUtil 4 | import com.intellij.testFramework.TestDataPath 5 | import com.intellij.testFramework.fixtures.BasePlatformTestCase 6 | import works.szabope.plugins.mypy.MypyBundle 7 | import works.szabope.plugins.mypy.services.MypySettings 8 | import java.nio.file.Paths 9 | import kotlin.io.path.absolutePathString 10 | 11 | @TestDataPath($$"$CONTENT_ROOT/testData/annotation") 12 | class IntentionTest : BasePlatformTestCase() { 13 | 14 | override fun getTestDataPath() = "src/test/testData/annotation" 15 | 16 | override fun setUp() { 17 | super.setUp() 18 | myFixture.enableInspections(MypyInspection()) 19 | with(MypySettings.getInstance(myFixture.project)) { 20 | executablePath = Paths.get(myFixture.testDataPath).resolve("mypy").absolutePathString() 21 | workingDirectory = Paths.get(testDataPath).absolutePathString() 22 | arguments = "" 23 | useProjectSdk = false 24 | } 25 | } 26 | 27 | fun `test function annotated`() { 28 | myFixture.configureByFile("a.py") 29 | assertNotEmpty(myFixture.doHighlighting()) 30 | val intention = myFixture.findSingleIntention(MypyBundle.message("mypy.intention.ignore.text", "valid-type")) 31 | assertNotNull(intention) 32 | myFixture.launchAction(intention) 33 | myFixture.checkResult( 34 | """|def lets_have_fun() -> [int]: # type: ignore[valid-type] 35 | | return 'fun' 36 | |""".trimMargin() 37 | ) 38 | PsiTestUtil.checkFileStructure(myFixture.file) 39 | } 40 | 41 | fun `test function annotated with comment`() { 42 | myFixture.configureByFile("e.py") 43 | assertNotEmpty(myFixture.doHighlighting()) 44 | val intention = myFixture.findSingleIntention(MypyBundle.message("mypy.intention.ignore.text", "valid-type")) 45 | assertNotNull(intention) 46 | myFixture.launchAction(intention) 47 | myFixture.checkResult( 48 | """|def lets_have_fun() -> [int]: # type: ignore[valid-type] # comment 49 | | return 'fun' 50 | |""".trimMargin() 51 | ) 52 | PsiTestUtil.checkFileStructure(myFixture.file) 53 | } 54 | 55 | fun `test existing ignore with codes gets extended`() { 56 | myFixture.configureByFile("b.py") 57 | assertNotEmpty(myFixture.doHighlighting()) 58 | val intention = myFixture.findSingleIntention(MypyBundle.message("mypy.intention.ignore.text", "valid-type")) 59 | assertNotNull(intention) 60 | myFixture.launchAction(intention) 61 | myFixture.checkResult( 62 | """|def lets_have_fun() -> [int]: # type: ignore[some-code,another-code, and-a-third-one,valid-type] 63 | | return 'fun' 64 | |""".trimMargin() 65 | ) 66 | PsiTestUtil.checkFileStructure(myFixture.file) 67 | } 68 | 69 | fun `test triple-quoted string annotated, but no intention available`() { 70 | myFixture.configureByFile("c.py") 71 | assertNotEmpty(myFixture.doHighlighting()) 72 | assertEmpty(myFixture.filterAvailableIntentions("Suppress mypy ")) 73 | } 74 | 75 | fun `test single line triple-quoted string annotated with intention available`() { 76 | myFixture.configureByFile("d.py") 77 | assertNotEmpty(myFixture.doHighlighting()) 78 | val intention = myFixture.findSingleIntention(MypyBundle.message("mypy.intention.ignore.text", "name-defined")) 79 | assertNotNull(intention) 80 | } 81 | 82 | fun `test annotations are processed even with mypy mixing non-json stuff into stdout`() { 83 | myFixture.configureByFile("errorMixedIntoStdOut.py") 84 | @Suppress("UnstableApiUsage") val mypyAnnotations = 85 | myFixture.doHighlighting().filter { it.toolId == MypyAnnotator::class.java } 86 | assertEquals(2, mypyAnnotations.size) 87 | } 88 | 89 | fun `test annotations for anything but target are skipped`() { 90 | myFixture.configureByFiles("followsImports.py", "dummy.py") 91 | @Suppress("UnstableApiUsage") val mypyAnnotations = 92 | myFixture.doHighlighting().filter { it.toolId == MypyAnnotator::class.java } 93 | assertEquals(1, mypyAnnotations.size) 94 | } 95 | } -------------------------------------------------------------------------------- /src/main/kotlin/works/szabope/plugins/mypy/services/SyncScanService.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.services 2 | 3 | import com.intellij.openapi.application.ApplicationManager 4 | import com.intellij.openapi.components.Service 5 | import com.intellij.openapi.components.service 6 | import com.intellij.openapi.diagnostic.thisLogger 7 | import com.intellij.openapi.fileEditor.FileDocumentManager 8 | import com.intellij.openapi.project.Project 9 | import com.intellij.openapi.vfs.VfsUtil 10 | import com.intellij.openapi.vfs.VirtualFile 11 | import com.intellij.openapi.vfs.ex.temp.TempFileSystem 12 | import kotlinx.coroutines.CoroutineScope 13 | import kotlinx.coroutines.flow.* 14 | import kotlinx.coroutines.future.future 15 | import works.szabope.plugins.common.services.ImmutableSettingsData 16 | import works.szabope.plugins.mypy.MypyBundle 17 | import works.szabope.plugins.mypy.services.parser.MypyMessage 18 | import works.szabope.plugins.mypy.services.parser.MypyOutputParser 19 | import works.szabope.plugins.mypy.services.parser.MypyParseException 20 | import java.nio.file.Path 21 | import kotlin.io.path.Path 22 | import kotlin.io.path.deleteIfExists 23 | import kotlin.io.path.writeText 24 | 25 | @Service(Service.Level.PROJECT) 26 | class SyncScanService(private val project: Project, private val cs: CoroutineScope) { 27 | 28 | fun scan( 29 | targets: Collection, configuration: ImmutableSettingsData 30 | ): Map> { 31 | val shadowedTargetMap = targets.associateWith { 32 | copyTempFrom(it) 33 | } 34 | val parameters = with(project) { buildMypyParamList(configuration, shadowedTargetMap) } 35 | val stdErr = StringBuilder() 36 | val flow: Flow> = 37 | MypyExecutor(project).execute(configuration, parameters).filter { it.text.isNotBlank() }.transform { line -> 38 | if (line.isError) { 39 | stdErr.append(line.text) 40 | return@transform 41 | } 42 | MypyOutputParser.parse(line.text).onSuccess { message -> 43 | findFile(Path(message.file))?.let { virtualFile -> 44 | emit(virtualFile to message) 45 | } ?: thisLogger().warn("Can't find VirtualFile at ${message.file}") 46 | }.onFailure { 47 | when (it) { 48 | is MypyParseException -> { 49 | thisLogger().warn( 50 | MypyBundle.message("mypy.executable.parsing-result-failed", configuration), it 51 | ) 52 | } 53 | 54 | else -> { 55 | thisLogger().error(MypyBundle.message("mypy.please_report_this_issue"), it) 56 | } 57 | } 58 | } 59 | }.onCompletion { 60 | // cleanup 61 | shadowedTargetMap.values.onEach { shadowFile -> shadowFile.deleteIfExists() } 62 | }.catch(handleScanException(project, configuration, stdErr)) 63 | return cs.future { 64 | flow.fold(mutableMapOf>()) { acc, (k, v) -> 65 | acc.getOrPut(k) { mutableListOf() }.add(v) 66 | acc 67 | }.mapValues { (_, v) -> v.toList() } 68 | }.get() 69 | } 70 | 71 | private fun findFile(path: Path): VirtualFile? { 72 | return if (ApplicationManager.getApplication().isUnitTestMode) { 73 | @Suppress("UnstableApiUsage") 74 | TempFileSystem.getInstance().findFileByNioFile(path) 75 | } else { 76 | VfsUtil.findFile(path, false) 77 | } 78 | } 79 | 80 | private fun copyTempFrom(file: VirtualFile): Path { 81 | val document = requireNotNull(FileDocumentManager.getInstance().getCachedDocument(file)) { 82 | MypyBundle.message("mypy.please_report_this_issue") 83 | } 84 | val tempFile = kotlin.io.path.createTempFile(prefix = "pycharm_mypy_", suffix = "_" + file.name) 85 | tempFile.toFile().deleteOnExit() 86 | tempFile.writeText(document.charsSequence) 87 | return tempFile 88 | } 89 | 90 | companion object { 91 | @JvmStatic 92 | fun getInstance(project: Project): SyncScanService = project.service() 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/resources/messages/MypyBundle.properties: -------------------------------------------------------------------------------- 1 | mypy.configuration.name=Mypy 2 | mypy.notification.incomplete_configuration=Mypy configuration is incomplete 3 | mypy.configuration.use_project_sdk=Use project SDK 4 | mypy.configuration.mypy_picker_title=Execute Mypy as 5 | mypy.configuration.path_to_executable.label=Mypy executable: 6 | mypy.configuration.path_to_executable.empty_warning=Mypy is not specified 7 | mypy.configuration.path_to_executable.unknown_version=Unable to identify mypy version (-V) 8 | mypy.configuration.config_file.comment=Mypy automatically applies config from working directory. Read more... 9 | mypy.configuration.config_file.help=Module specific config takes precedence. Read more... 10 | mypy.inspection.group_name=Mypy 11 | mypy.inspection.name=Mypy real-time scan 12 | mypy.intention.ignore.family_name=TypeIgnoreIntention 13 | mypy.intention.ignore.text=Suppress mypy [{0}] 14 | mypy.configuration.mypy_invalid_version=Obsolete mypy version. Expected >=1.11 15 | mypy.intention.complete_configuration.text=Complete configuration 16 | mypy.intention.install_mypy.text=Install mypy 17 | mypy.configuration.path_to_executable.not_exists=Selected file does not exist 18 | mypy.configuration.path_to_executable.is_directory=Selected item is a directory 19 | mypy.configuration.path_to_executable.not_executable=Selected file is not executable 20 | mypy.toolwindow.name=Scan 21 | mypy.toolwindow.balloon.external_error=Mypy has thrown an error Details 22 | mypy.toolwindow.balloon.failed_to_execute=Failed to execute mypy Details 23 | action.works.szabope.plugins.mypy.action.ScrollToSourceAction.text=Autoscroll to Source 24 | action.works.szabope.plugins.mypy.action.ScrollToSourceAction.description=Auto-scroll to the source location of errors and warnings 25 | action.MyPyDisplayErrorsAction.text=Display Errors 26 | action.MyPyDisplayErrorsAction.description=Display error results 27 | action.MypyDisplayWarningsAction.text=Display Warnings 28 | action.MypyDisplayWarningsAction.description=Display warning results 29 | action.MypyDisplayNoteAction.text=Display Notes 30 | action.MypyDisplayNoteAction.description=Display Note results 31 | action.works.szabope.plugins.mypy.action.RescanAction.text=Rescan Latest 32 | action.works.szabope.plugins.mypy.action.RescanAction.description=Run Mypy on the latest scan target 33 | action.works.szabope.plugins.mypy.action.StopScanAction.text=Stop the Running Scan 34 | action.works.szabope.plugins.mypy.action.StopScanAction.description=Stop the scan currently being run 35 | action.works.szabope.plugins.mypy.action.ScanCurrentlyFocusedOneInEditorAction.text=Scan Editor 36 | action.works.szabope.plugins.mypy.action.ScanCurrentlyFocusedOneInEditorAction.description=Scan the currently focused file within editor 37 | action.works.szabope.plugins.mypy.action.ScanAction.text=Scan with Mypy 38 | action.works.szabope.plugins.mypy.action.ScanAction.description=Run Mypy on selected target(s) 39 | action.works.szabope.plugins.mypy.action.OpenSettingsAction.text=Open Settings 40 | action.works.szabope.plugins.mypy.action.OpenSettingsAction.description=Open the Mypy settings window 41 | action.works.szabope.plugins.mypy.action.InstallMypyAction.text=Install Mypy 42 | action.InstallMypyAction.done_html=Mypy Installed 43 | mypy.dialog.installation_error.title=Failed to Install Mypy 44 | mypy.dialog.installation_error.message=Installation failed 45 | mypy.dialog.failed_to_execute.title=Failed to Execute Mypy 46 | mypy.dialog.failed_to_execute.message=Calling mypy failed 47 | mypy.dialog.execution_error.title=Mypy Execution Error 48 | mypy.dialog.execution_error.status_code=Process exited with status code: {0} 49 | mypy.dialog.general_error.title=General Error 50 | mypy.dialog.general_error.details=Message: {0}\nStacktrace: {1} 51 | mypy.please_report_this_issue=Please, report this issue at https://github.com/szabope/mypy-pycharm-plugin/issues 52 | mypy.configuration.path_to_executable.version_validation_title=Validating mypy version 53 | mypy.configuration.mypy_not_installed=Mypy not installed 54 | mypy.dialog.execution_error.content=Mypy configuration:\n{0}\n\nExecution result:\n{1} 55 | mypy.dialog.parse_error.title=Mypy Parse Error 56 | mypy.dialog.parse_error.details=Configuration:\n{0}\nTargets:\n{1}\nResult:\n{2} 57 | mypy.dialog.parse_error.message=Parse failed with: {0} 58 | mypy.toolwindow.balloon.parse_error=Parsing mypy output failed Details 59 | mypy.executable.parsing-result-failed=Parsing mypy result failed!\nConfiguration: {0} 60 | # This one has to be the same as plugin.xml > extensions > notificationGroup > id 61 | notification.group.mypy.group=Mypy group 62 | 63 | -------------------------------------------------------------------------------- /src/main/kotlin/works/szabope/plugins/mypy/services/MypySettings.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.services 2 | 3 | import com.intellij.openapi.components.* 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.openapi.project.guessProjectDir 6 | import com.jetbrains.python.sdk.pythonSdk 7 | import org.jetbrains.annotations.TestOnly 8 | import works.szabope.plugins.common.services.BasicSettingsData 9 | import works.szabope.plugins.common.services.ImmutableSettingsData 10 | import works.szabope.plugins.common.services.Settings 11 | 12 | @Service(Service.Level.PROJECT) 13 | @State(name = "MypySettings", storages = [Storage("MypyPlugin.xml")], category = SettingsCategory.PLUGINS) 14 | class MypySettings(internal val project: Project) : SimplePersistentStateComponent(MypyState()), 15 | Settings { 16 | 17 | private var initialized = false 18 | 19 | class MypyState : BaseState() { 20 | var mypyExecutable by string() 21 | var useProjectSdk by property(true) 22 | var configFilePath by string() 23 | var arguments by string() 24 | var autoScrollToSource by property(false) 25 | var excludeNonProjectFiles by property(true) 26 | var projectDirectory by string() 27 | var scanBeforeCheckIn by property(false) 28 | } 29 | 30 | override var useProjectSdk 31 | get() = state.useProjectSdk 32 | set(value) { 33 | state.useProjectSdk = value 34 | } 35 | 36 | override var executablePath 37 | get() = state.mypyExecutable?.trim() ?: "" 38 | set(value) { 39 | // workaround for string() normalizes empty string to null 40 | state.mypyExecutable = value.ifBlank { " " } 41 | } 42 | 43 | override var configFilePath 44 | get() = state.configFilePath?.trim() ?: "" 45 | set(value) { 46 | // workaround for string() normalizes empty string to null 47 | state.configFilePath = value.ifBlank { " " } 48 | } 49 | 50 | override var arguments 51 | get() = state.arguments?.trim() ?: "" 52 | set(value) { 53 | // workaround for string() normalizes empty string to null 54 | state.arguments = value.ifBlank { " " } 55 | } 56 | 57 | override var isAutoScrollToSource 58 | get() = state.autoScrollToSource 59 | set(value) { 60 | state.autoScrollToSource = value 61 | } 62 | 63 | override var excludeNonProjectFiles 64 | get() = state.excludeNonProjectFiles 65 | set(value) { 66 | state.excludeNonProjectFiles = value 67 | } 68 | 69 | override var workingDirectory 70 | get() = state.projectDirectory 71 | set(value) { 72 | state.projectDirectory = value 73 | } 74 | 75 | override var scanBeforeCheckIn 76 | get() = state.scanBeforeCheckIn 77 | set(value) { 78 | state.scanBeforeCheckIn = value 79 | } 80 | 81 | override suspend fun initSettings(oldSettings: BasicSettingsData) { 82 | if (state.mypyExecutable == null && oldSettings.executablePath != null) { 83 | executablePath = oldSettings.executablePath!! 84 | } 85 | if (executablePath.isNotBlank() && project.pythonSdk == null) { 86 | useProjectSdk = false 87 | } 88 | if (state.configFilePath == null && oldSettings.configFilePath != null) { 89 | configFilePath = oldSettings.configFilePath!! 90 | } 91 | if (state.arguments == null && oldSettings.arguments != null) { 92 | arguments = oldSettings.arguments!! 93 | } 94 | if (state.projectDirectory == null) { 95 | workingDirectory = project.guessProjectDir()?.canonicalPath 96 | } 97 | initialized = true 98 | } 99 | 100 | override fun getValidConfiguration(): Result { 101 | val workingDirectory = workingDirectory 102 | if (workingDirectory.isNullOrBlank()) { 103 | return Result.failure(MypySettingsInvalid("Working directory is required")) 104 | } 105 | if (!isMypySet()) { 106 | return Result.failure(MypySettingsInvalid("Mypy tool is not set")) 107 | } 108 | 109 | return MypyExecutorConfiguration( 110 | executablePath, 111 | useProjectSdk, 112 | configFilePath, 113 | arguments, 114 | workingDirectory, 115 | excludeNonProjectFiles, 116 | scanBeforeCheckIn 117 | ).let { Result.success(it) } 118 | } 119 | 120 | private fun isMypySet(): Boolean { 121 | return if (useProjectSdk) { 122 | project.pythonSdk != null && MypyPluginPackageManagementService.getInstance(project) 123 | .checkInstalledRequirement().isSuccess 124 | } else { 125 | executablePath.isNotBlank() 126 | } 127 | } 128 | 129 | @TestOnly 130 | fun reset() { 131 | loadState(MypyState()) 132 | } 133 | 134 | fun isInitialized() = initialized 135 | 136 | companion object { 137 | @JvmStatic 138 | fun getInstance(project: Project): MypySettings = project.service() 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mypy-pycharm 2 | [![Apache-2.0 license](https://img.shields.io/github/license/szabope/mypy-pycharm-plugin.svg?style=plastic)](https://github.com/szabope/mypy-pycharm-plugin/blob/master/LICENSE) 3 | 4 | 5 | This plugin provides PyCharm with both real-time and on-demand scanning capabilities using an external mypy tool.\ 6 | It is the rework of [Roberto Leinardi](https://github.com/szabope/mypy-pycharm-plugin?tab=readme-ov-file#acknowledgements)'s [mypy-pycharm](https://github.com/leinardi/mypy-pycharm) plugin.[ Click here](https://github.com/szabope/mypy-pycharm-plugin?tab=readme-ov-file#differences-from-the-original-plugin) to see differences. 7 | 8 | [Mypy](https://github.com/python/mypy), as described by its authors: 9 | >Mypy is a static type checker for Python. 10 | > 11 | >Type checkers help ensure that you're using variables and functions in your code correctly. With mypy, add type hints (PEP 484) to your Python programs, and mypy will warn you when you use those types incorrectly. 12 | 13 | ![low_res_mypy plugin screenshot](https://raw.githubusercontent.com/szabope/mypy-pycharm-plugin/3b600281f84ecec09d345ec7541c39c6b705ddff/art/results_lowres.png) 14 | 15 | 16 | ## Requirements 17 | - mypy >= 1.11.0 18 | - mypy must be executable by the IDE. *e.g. mypy in WSL won't work with IDE running on Windows* 19 | - mypy does not need to be installed into the project's environment, it can be configured independently 20 | 21 | 22 | ## Installation steps 23 | https://www.jetbrains.com/help/pycharm/managing-plugins.html#Managing_Plugins.topic 24 | 25 | ## Configuration 26 | Configuration is done on a project basis. `mypy` executable validation **executes the candidate** with `-V` to validate its version. 27 | 28 | ### Automated configuration 29 | Upon project load, the plugin looks for existing settings for Leinardi's mypy plugin and makes a copy of them. Executable only set if the version of mypy is supported.\ 30 | In case such configuration was not found `Use project SDK` option is selected. 31 | If there is no python SDK set for the project or `mypy` is not installed for it, the user gets notified: 32 | ![mypy_plugin_incomplete_configuration_screenshot](https://raw.githubusercontent.com/szabope/mypy-pycharm-plugin/3b600281f84ecec09d345ec7541c39c6b705ddff/art/mypy_not_set.png) 33 | 34 | ### Manual configuration 35 | You can modify settings at [Tools](https://www.jetbrains.com/help/pycharm/settings-tools.html#Settings_Tools.topic) / **Mypy**. 36 | ![mypy plugin screenshot](https://raw.githubusercontent.com/szabope/mypy-pycharm-plugin/829f08ceec6ba06c8b7b25a9c819b5c7fc9350ef/art/settings.png) 37 | 38 | ### Inspection severity 39 | Mypy severity level is set to `Error` by default. You can change this in [inspection settings](https://www.jetbrains.com/help/pycharm/inspections-settings.html#Inspections_Settings.topic). 40 | 41 | ## Usage 42 | 43 | **Scan with Mypy** ![](https://raw.githubusercontent.com/szabope/mypy-pycharm-plugin/338908f67473081858a50cf55ecf6e4c37e69fd4/art/mypyScanAction.svg) 44 | action is available in right-click menus for the Python file loaded into the editor, its tab, 45 | and Python files and directories in the project and changes views. You may select multiple targets, 46 | but all of them has to be either a Python file or a directory.\ 47 | **Rescan Latest** ![](https://raw.githubusercontent.com/szabope/mypy-pycharm-plugin/338908f67473081858a50cf55ecf6e4c37e69fd4/art/refresh.svg) 48 | action is available within Mypy toolwindow. It clears the results and re-runs mypy for the latest target. 49 | Mypy configuration is not retained from the previous run.\ 50 | **Scan Editor** ![](https://raw.githubusercontent.com/szabope/mypy-pycharm-plugin/338908f67473081858a50cf55ecf6e4c37e69fd4/art/execute.svg) 51 | action is available within Mypy toolwindow. It clears the results and runs mypy for the one file that is open 52 | and currently focused in the Editor. 53 | 54 | ![mypy plugin screenshot](https://raw.githubusercontent.com/szabope/mypy-pycharm-plugin/3b600281f84ecec09d345ec7541c39c6b705ddff/art/menu.png) 55 | 56 | ![mypy plugin screenshot](https://raw.githubusercontent.com/szabope/mypy-pycharm-plugin/3b600281f84ecec09d345ec7541c39c6b705ddff/art/results.png) 57 | 58 | ## FAQ 59 | ### Scan fails with: `External tool failed with error.` or `External tool returned unexpected output.` 60 | This indicates that the external mypy tool has exited with an error. The plugin can't fix these. 61 | #### Details may contain something like this: `mypy: "mypy/typeshed/stubs/mypy-extensions/mypy_extensions.pyi" shadows library module "mypy_extensions"` 62 | In this case you may want to add `--exclude \.pyi$` to the arguments in mypy settings. 63 | Another switch `--explicit-package-bases` may also work. 64 | #### Or details may be like `Duplicate module named "a"` 65 | You can exclude containing directory: 66 | - make sure that `Settings > Tools > Mypy > Exclude non-project files` is checked, so all directories that are marked as excluded will also be excluded from mypy scan. 67 | - `Mark Directory as > Excluded` 68 | 69 | For further mypy configuration options, please see `mypy -h` 70 | 71 | You may get more insight into the plugin here: [Debug](https://github.com/szabope/mypy-pycharm-plugin?tab=readme-ov-file#debug) 72 | 73 | ## Debug 74 | Open `Help > Diagnostic Tools > Debug Log Settings...`\ 75 | Enter `works.szabope.plugins.mypy:trace`\ 76 | Hit `[Ok]`\ 77 | Then you can see debug logs in idea.log (`Help > Open Log in Editor`)\ 78 | **_Keep in mind that the log may contain sensitive information._** 79 | 80 | ## Differences from the original plugin 81 | - Toolbar actions were simplified: 82 | - Close toolbar: **removed** 83 | - Check module: **removed** 84 | - Check project: **removed** 85 | - Check all modified files: **removed** 86 | - Check files in the current changelist: **removed** 87 | - Clear all: **removed** 88 | - Severity filters: **removed** 89 | - Rescan: **added** 90 | 91 | [//]: # ( TODO - severity filter: **grouped**) 92 | - Scan can now be started from the right-click menu within the editor, on an editor tab, and on files or directories 93 | in the project and changes views. 94 | 95 | ## Acknowledgements 96 | A huge thanks to [Roberto Leinardi](https://github.com/leinardi) for the creation and maintenance of the original plugin and for the support and guidance in the rework. 97 | 98 | ## License 99 | ``` 100 | Copyright 2024 Peter Szabo. 101 | 102 | Licensed to the Apache Software Foundation (ASF) under one or more contributor 103 | license agreements. See the NOTICE file distributed with this work for 104 | additional information regarding copyright ownership. The ASF licenses this 105 | file to you under the Apache License, Version 2.0 (the "License"); you may not 106 | use this file except in compliance with the License. You may obtain a copy of 107 | the License at 108 | 109 | http://www.apache.org/licenses/LICENSE-2.0 110 | 111 | Unless required by applicable law or agreed to in writing, software 112 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 113 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 114 | License for the specific language governing permissions and limitations under 115 | the License. 116 | ``` 117 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow is created for testing and preparing the plugin release in the following steps: 2 | # - Validate Gradle Wrapper. 3 | # - Run 'test' and 'verifyPlugin' tasks. 4 | # - Run the 'buildPlugin' task and prepare artifact for further tests. 5 | # - Run the 'runPluginVerifier' task. 6 | # - Create a draft release. 7 | # 8 | # The workflow is triggered on push and pull_request events. 9 | # 10 | # GitHub Actions reference: https://help.github.com/en/actions 11 | # 12 | ## JBIJPPTPL 13 | 14 | name: Build 15 | on: 16 | # Enable manual trigger 17 | workflow_dispatch: 18 | # Trigger the workflow on pushes to only the 'master' branch (this avoids duplicate checks being run e.g., for dependabot pull requests) 19 | push: 20 | branches: [ master ] 21 | # Trigger the workflow on any pull request 22 | pull_request: 23 | 24 | concurrency: 25 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 26 | cancel-in-progress: true 27 | 28 | jobs: 29 | 30 | # Prepare the environment and build the plugin 31 | build: 32 | name: Build 33 | runs-on: ubuntu-latest 34 | steps: 35 | 36 | # Free GitHub Actions Environment Disk Space 37 | - name: Maximize Build Space 38 | uses: jlumbroso/free-disk-space@v1.3.1 39 | with: 40 | tool-cache: false 41 | large-packages: false 42 | 43 | # Check out the current repository 44 | - name: Fetch Sources 45 | uses: actions/checkout@v5 46 | 47 | # Set up the Java environment for the next steps 48 | - name: Setup Java 49 | uses: actions/setup-java@v5 50 | with: 51 | distribution: zulu 52 | java-version: 21 53 | 54 | # Setup Gradle 55 | - name: Setup Gradle 56 | uses: gradle/actions/setup-gradle@v5 57 | 58 | # Build plugin 59 | - name: Build plugin 60 | env: 61 | GPR_USERNAME: ${{secrets.GPR_USERNAME}} 62 | GPR_TOKEN: ${{secrets.GPR_TOKEN}} 63 | run: ./gradlew buildPlugin 64 | 65 | # Prepare plugin archive content for creating artifact 66 | - name: Prepare Plugin Artifact 67 | id: artifact 68 | shell: bash 69 | run: | 70 | cd ${{ github.workspace }}/build/distributions 71 | FILENAME=`ls *.zip` 72 | unzip "$FILENAME" -d content 73 | 74 | echo "filename=${FILENAME:0:-4}" >> $GITHUB_OUTPUT 75 | 76 | # Store an already-built plugin as an artifact for downloading 77 | - name: Upload artifact 78 | uses: actions/upload-artifact@v5 79 | with: 80 | name: ${{ steps.artifact.outputs.filename }} 81 | path: ./build/distributions/content/*/* 82 | 83 | # Run tests and upload a code coverage report 84 | test: 85 | name: Test 86 | needs: [ build ] 87 | runs-on: ubuntu-latest 88 | steps: 89 | 90 | # Free GitHub Actions Environment Disk Space 91 | - name: Maximize Build Space 92 | uses: jlumbroso/free-disk-space@v1.3.1 93 | with: 94 | tool-cache: false 95 | large-packages: false 96 | 97 | # Check out the current repository 98 | - name: Fetch Sources 99 | uses: actions/checkout@v5 100 | 101 | # Set up the Java environment for the next steps 102 | - name: Setup Java 103 | uses: actions/setup-java@v5 104 | with: 105 | distribution: zulu 106 | java-version: 21 107 | 108 | # Setup Gradle 109 | - name: Setup Gradle 110 | uses: gradle/actions/setup-gradle@v5 111 | with: 112 | cache-read-only: true 113 | 114 | # Run tests 115 | - name: Run Tests 116 | env: 117 | GPR_USERNAME: ${{secrets.GPR_USERNAME}} 118 | GPR_TOKEN: ${{secrets.GPR_TOKEN}} 119 | run: ./gradlew check 120 | 121 | # Collect Tests Result of failed tests 122 | - name: Collect Tests Result 123 | if: ${{ failure() }} 124 | uses: actions/upload-artifact@v5 125 | with: 126 | name: tests-result 127 | path: ${{ github.workspace }}/build/reports/tests 128 | 129 | # Upload the Kover report to CodeCov 130 | - name: Upload Code Coverage Report 131 | uses: codecov/codecov-action@v5 132 | with: 133 | files: ${{ github.workspace }}/build/reports/kover/report.xml 134 | token: ${{ secrets.CODECOV_TOKEN }} 135 | 136 | # Run plugin structure verification along with IntelliJ Plugin Verifier 137 | verify: 138 | name: Verify plugin 139 | needs: [ build ] 140 | runs-on: ubuntu-latest 141 | steps: 142 | 143 | # Free GitHub Actions Environment Disk Space 144 | - name: Maximize Build Space 145 | uses: jlumbroso/free-disk-space@v1.3.1 146 | with: 147 | tool-cache: false 148 | large-packages: false 149 | 150 | # Check out the current repository 151 | - name: Fetch Sources 152 | uses: actions/checkout@v5 153 | 154 | # Set up the Java environment for the next steps 155 | - name: Setup Java 156 | uses: actions/setup-java@v5 157 | with: 158 | distribution: zulu 159 | java-version: 21 160 | 161 | # Setup Gradle 162 | - name: Setup Gradle 163 | uses: gradle/actions/setup-gradle@v5 164 | with: 165 | cache-read-only: true 166 | 167 | # Run Verify Plugin task and IntelliJ Plugin Verifier tool 168 | - name: Run Plugin Verification tasks 169 | env: 170 | GPR_USERNAME: ${{secrets.GPR_USERNAME}} 171 | GPR_TOKEN: ${{secrets.GPR_TOKEN}} 172 | run: ./gradlew verifyPlugin 173 | 174 | # Collect Plugin Verifier Result 175 | - name: Collect Plugin Verifier Result 176 | if: ${{ always() }} 177 | uses: actions/upload-artifact@v5 178 | with: 179 | name: pluginVerifier-result 180 | path: ${{ github.workspace }}/build/reports/pluginVerifier 181 | 182 | # Prepare a draft release for GitHub Releases page for the manual verification 183 | # If accepted and published, the release workflow would be triggered 184 | releaseDraft: 185 | name: Release draft 186 | if: github.event_name != 'pull_request' 187 | needs: [ build, test, verify ] 188 | runs-on: ubuntu-latest 189 | permissions: 190 | contents: write 191 | steps: 192 | 193 | # Check out the current repository 194 | - name: Fetch Sources 195 | uses: actions/checkout@v5 196 | 197 | # Remove old release drafts by using the curl request for the available releases with a draft flag 198 | - name: Remove Old Release Drafts 199 | env: 200 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 201 | run: | 202 | gh api repos/{owner}/{repo}/releases \ 203 | --jq '.[] | select(.draft == true) | .id' \ 204 | | xargs -I '{}' gh api -X DELETE repos/{owner}/{repo}/releases/{} 205 | 206 | # Create a new release draft which is not publicly visible and requires manual acceptance 207 | - name: Create Release Draft 208 | env: 209 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 210 | run: | 211 | VERSION=$(./gradlew properties --property version --quiet --console=plain | tail -n 1 | cut -f2- -d ' ') 212 | RELEASE_NOTE="./build/tmp/release_note.txt" 213 | ./gradlew getChangelog --unreleased --no-header --quiet --console=plain --output-file=$RELEASE_NOTE 214 | 215 | gh release create $VERSION \ 216 | --draft \ 217 | --title $VERSION \ 218 | --notes-file $RELEASE_NOTE 219 | -------------------------------------------------------------------------------- /src/test/kotlin/works/szabope/plugins/mypy/action/ScanCliTest.kt: -------------------------------------------------------------------------------- 1 | package works.szabope.plugins.mypy.action 2 | 3 | import com.intellij.openapi.actionSystem.CommonDataKeys 4 | import com.intellij.openapi.ui.DialogWrapper 5 | import com.intellij.openapi.vfs.ex.temp.TempFileSystem 6 | import com.intellij.platform.backend.workspace.WorkspaceModel 7 | import com.intellij.platform.backend.workspace.virtualFile 8 | import com.intellij.platform.workspace.jps.entities.ContentRootEntity 9 | import com.intellij.testFramework.PlatformTestUtil 10 | import com.intellij.testFramework.TestDataPath 11 | import io.mockk.every 12 | import io.mockk.mockkObject 13 | import junit.framework.AssertionFailedError 14 | import org.jetbrains.concurrency.asPromise 15 | import works.szabope.plugins.common.test.dialog.TestDialogWrapper 16 | import works.szabope.plugins.mypy.AbstractToolWindowTestCase 17 | import works.szabope.plugins.mypy.dialog.DialogManager 18 | import works.szabope.plugins.mypy.dialog.FailedToExecuteErrorDialog 19 | import works.szabope.plugins.mypy.dialog.MypyExecutionErrorDialog 20 | import works.szabope.plugins.mypy.dialog.MypyParseErrorDialog 21 | import works.szabope.plugins.mypy.services.MypySettings 22 | import works.szabope.plugins.mypy.testutil.* 23 | import java.net.URI 24 | import java.nio.file.Paths 25 | import java.util.concurrent.CompletableFuture 26 | import javax.swing.event.HyperlinkEvent 27 | import kotlin.io.path.absolutePathString 28 | 29 | @TestDataPath($$"$CONTENT_ROOT/testData/action/scan_cli") 30 | class ScanCliTest : AbstractToolWindowTestCase() { 31 | 32 | private val dialogManager = TestDialogManager() 33 | 34 | override fun getTestDataPath() = "src/test/testData/action/scan_cli" 35 | 36 | override fun setUp() { 37 | mockkObject(DialogManager.Companion) 38 | every { DialogManager.dialogManager } answers { dialogManager } 39 | super.setUp() 40 | } 41 | 42 | @Suppress("removal") 43 | fun testManualScan() { 44 | myFixture.copyDirectoryToProject("/", "/") 45 | val excludedDir = TempFileSystem.getInstance().findFileByPath("/src/excluded_dir")!! 46 | setUpSettings("mypy") 47 | val exclusionContext = dataContext(project) { 48 | add(CommonDataKeys.VIRTUAL_FILE_ARRAY, arrayOf(excludedDir)) 49 | } 50 | markExcluded(exclusionContext) 51 | var assertionError: Error? = null 52 | toolWindowManager.onBalloon { 53 | assertionError = AssertionFailedError("Should not happen: $it") 54 | } 55 | val target = TempFileSystem.getInstance().findFileByPath("/src")!! 56 | scan(dataContext(project) { add(CommonDataKeys.VIRTUAL_FILE_ARRAY, arrayOf(target)) }) 57 | PlatformTestUtil.waitWhileBusy { ScanJobRegistry.INSTANCE.isActive() } 58 | assertionError?.let { throw it } 59 | treeUtil.assertStructure("+Found 1 issue(s) in 1 file(s)\n") 60 | treeUtil.expandAll() 61 | treeUtil.assertStructure( 62 | """|-Found 1 issue(s) in 1 file(s) 63 | | -src/a.py 64 | | Bracketed expression "[...]" is not valid as a type [valid-type] (0:-1) Did you mean "List[...]"? 65 | |""".trimMargin() 66 | ) 67 | unmark(exclusionContext) 68 | } 69 | 70 | fun `test mypy returning non-json result with exit code 0 results in Dialog of The Parse Error`() { 71 | myFixture.copyDirectoryToProject("/", "/") 72 | setUpSettings("mypy_non_json_output") 73 | toolWindowManager.onBalloon { 74 | it.listener?.hyperlinkUpdate( 75 | HyperlinkEvent( 76 | "dumb", HyperlinkEvent.EventType.ACTIVATED, URI("http://localhost").toURL() 77 | ) 78 | ) 79 | } 80 | val dialogShown = CompletableFuture() 81 | dialogManager.onDialog(MypyParseErrorDialog::class.java) { 82 | it.close(DialogWrapper.OK_EXIT_CODE) 83 | dialogShown.complete(it) 84 | it.getExitCode() 85 | } 86 | val target = WorkspaceModel.getInstance(project).currentSnapshot.entities(ContentRootEntity::class.java) 87 | .first().url.virtualFile!! 88 | scan(dataContext(project) { add(CommonDataKeys.VIRTUAL_FILE_ARRAY, arrayOf(target)) }) 89 | PlatformTestUtil.assertPromiseSucceeds(dialogShown.asPromise()) 90 | assertTrue(dialogShown.isDone && with(dialogShown.get()) { isShown() && getExitCode() == DialogWrapper.OK_EXIT_CODE }) 91 | } 92 | 93 | // https://github.com/python/mypy/issues/6003 94 | fun `test mypy returning with an exit code 1 is fine`() { 95 | myFixture.copyDirectoryToProject("/", "/") 96 | setUpSettings("mypy_exit_with_1") 97 | var assertionError: Error? = null 98 | toolWindowManager.onBalloon { 99 | assertionError = AssertionFailedError("Should not happen") 100 | } 101 | val target = WorkspaceModel.getInstance(project).currentSnapshot.entities(ContentRootEntity::class.java) 102 | .first().url.virtualFile!! 103 | scan(dataContext(project) { add(CommonDataKeys.VIRTUAL_FILE_ARRAY, arrayOf(target)) }) 104 | PlatformTestUtil.waitWhileBusy { ScanJobRegistry.INSTANCE.isActive() } 105 | assertionError?.let { throw it } 106 | } 107 | 108 | // syntax error results in exit 2 109 | fun `test mypy returning with an exit code 2 is also fine`() { 110 | myFixture.copyDirectoryToProject("/", "/") 111 | setUpSettings("mypy_exit_with_2") 112 | var assertionError: Error? = null 113 | toolWindowManager.onBalloon { 114 | assertionError = AssertionFailedError("Should not happen") 115 | } 116 | val target = WorkspaceModel.getInstance(project).currentSnapshot.entities(ContentRootEntity::class.java) 117 | .first().url.virtualFile!! 118 | scan(dataContext(project) { add(CommonDataKeys.VIRTUAL_FILE_ARRAY, arrayOf(target)) }) 119 | PlatformTestUtil.waitWhileBusy { ScanJobRegistry.INSTANCE.isActive() } 120 | assertionError?.let { throw it } 121 | } 122 | 123 | fun `test mypy returning with an exit code other than 0, 1, and 2 results in dialog`() { 124 | myFixture.copyDirectoryToProject("/", "/") 125 | setUpSettings("mypy_exit_with_3") 126 | toolWindowManager.onBalloon { 127 | it.listener?.hyperlinkUpdate( 128 | HyperlinkEvent( 129 | "dumb", HyperlinkEvent.EventType.ACTIVATED, URI("http://localhost").toURL() 130 | ) 131 | ) 132 | } 133 | val dialogShown = CompletableFuture() 134 | dialogManager.onDialog(MypyExecutionErrorDialog::class.java) { 135 | it.close(DialogWrapper.OK_EXIT_CODE) 136 | dialogShown.complete(it) 137 | it.getExitCode() 138 | } 139 | val target = WorkspaceModel.getInstance(project).currentSnapshot.entities(ContentRootEntity::class.java) 140 | .first().url.virtualFile!! 141 | scan(dataContext(project) { add(CommonDataKeys.VIRTUAL_FILE_ARRAY, arrayOf(target)) }) 142 | PlatformTestUtil.assertPromiseSucceeds(dialogShown.asPromise()) 143 | assertTrue(dialogShown.isDone && with(dialogShown.get()) { isShown() && getExitCode() == DialogWrapper.OK_EXIT_CODE }) 144 | } 145 | 146 | fun `test executable path does not exist results in dialog`() { 147 | myFixture.copyDirectoryToProject("/", "/") 148 | with(MypySettings.getInstance(project)) { 149 | executablePath = "/does/not/exist" 150 | workingDirectory = Paths.get(testDataPath).absolutePathString() 151 | useProjectSdk = false 152 | configFilePath = "" 153 | scanBeforeCheckIn = false 154 | arguments = "" 155 | excludeNonProjectFiles = true 156 | } 157 | toolWindowManager.onBalloon { 158 | it.listener?.hyperlinkUpdate( 159 | HyperlinkEvent( 160 | "dumb", HyperlinkEvent.EventType.ACTIVATED, URI("http://localhost").toURL() 161 | ) 162 | ) 163 | } 164 | val dialogShown = CompletableFuture() 165 | dialogManager.onDialog(FailedToExecuteErrorDialog::class.java) { 166 | it.close(DialogWrapper.OK_EXIT_CODE) 167 | dialogShown.complete(it) 168 | it.getExitCode() 169 | } 170 | val target = WorkspaceModel.getInstance(project).currentSnapshot.entities(ContentRootEntity::class.java) 171 | .first().url.virtualFile!! 172 | scan(dataContext(project) { add(CommonDataKeys.VIRTUAL_FILE_ARRAY, arrayOf(target)) }) 173 | PlatformTestUtil.assertPromiseSucceeds(dialogShown.asPromise()) 174 | assertTrue(dialogShown.isDone && with(dialogShown.get()) { isShown() && getExitCode() == DialogWrapper.OK_EXIT_CODE }) 175 | } 176 | 177 | private fun setUpSettings(executable: String) { 178 | with(MypySettings.getInstance(project)) { 179 | executablePath = Paths.get(testDataPath).resolve(executable).absolutePathString() 180 | workingDirectory = Paths.get(testDataPath).absolutePathString() 181 | useProjectSdk = false 182 | configFilePath = "" 183 | scanBeforeCheckIn = false 184 | arguments = "" 185 | excludeNonProjectFiles = true 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | 118 | 119 | # Determine the Java command to use to start the JVM. 120 | if [ -n "$JAVA_HOME" ] ; then 121 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 122 | # IBM's JDK on AIX uses strange locations for the executables 123 | JAVACMD=$JAVA_HOME/jre/sh/java 124 | else 125 | JAVACMD=$JAVA_HOME/bin/java 126 | fi 127 | if [ ! -x "$JAVACMD" ] ; then 128 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 129 | 130 | Please set the JAVA_HOME variable in your environment to match the 131 | location of your Java installation." 132 | fi 133 | else 134 | JAVACMD=java 135 | if ! command -v java >/dev/null 2>&1 136 | then 137 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 138 | 139 | Please set the JAVA_HOME variable in your environment to match the 140 | location of your Java installation." 141 | fi 142 | fi 143 | 144 | # Increase the maximum file descriptors if we can. 145 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 146 | case $MAX_FD in #( 147 | max*) 148 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 149 | # shellcheck disable=SC2039,SC3045 150 | MAX_FD=$( ulimit -H -n ) || 151 | warn "Could not query maximum file descriptor limit" 152 | esac 153 | case $MAX_FD in #( 154 | '' | soft) :;; #( 155 | *) 156 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 157 | # shellcheck disable=SC2039,SC3045 158 | ulimit -n "$MAX_FD" || 159 | warn "Could not set maximum file descriptor limit to $MAX_FD" 160 | esac 161 | fi 162 | 163 | # Collect all arguments for the java command, stacking in reverse order: 164 | # * args from the command line 165 | # * the main class name 166 | # * -classpath 167 | # * -D...appname settings 168 | # * --module-path (only if needed) 169 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 170 | 171 | # For Cygwin or MSYS, switch paths to Windows format before running java 172 | if "$cygwin" || "$msys" ; then 173 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 214 | "$@" 215 | 216 | # Stop when "xargs" is not available. 217 | if ! command -v xargs >/dev/null 2>&1 218 | then 219 | die "xargs is not available" 220 | fi 221 | 222 | # Use "xargs" to parse quoted args. 223 | # 224 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 225 | # 226 | # In Bash we could simply go: 227 | # 228 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 229 | # set -- "${ARGS[@]}" "$@" 230 | # 231 | # but POSIX shell has neither arrays nor command substitution, so instead we 232 | # post-process each arg (as a line of input to sed) to backslash-escape any 233 | # character that might be a shell metacharacter, then use eval to reverse 234 | # that process (while maintaining the separation between arguments), and wrap 235 | # the whole thing up as a single "set" statement. 236 | # 237 | # This will of course break if any of these variables contains a newline or 238 | # an unmatched quote. 239 | # 240 | 241 | eval "set -- $( 242 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 243 | xargs -n1 | 244 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 245 | tr '\n' ' ' 246 | )" '"$@"' 247 | 248 | exec "$JAVACMD" "$@" 249 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/pluginIcon_LICENSE.txt: -------------------------------------------------------------------------------- 1 | Mypy (and mypyc) are licensed under the terms of the MIT license, reproduced below. 2 | 3 | = = = = = 4 | 5 | The MIT License 6 | 7 | Copyright (c) 2012-2023 Jukka Lehtosalo and contributors 8 | Copyright (c) 2015-2023 Dropbox, Inc. 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a 11 | copy of this software and associated documentation files (the "Software"), 12 | to deal in the Software without restriction, including without limitation 13 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 14 | and/or sell copies of the Software, and to permit persons to whom the 15 | Software is furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in 18 | all copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 26 | DEALINGS IN THE SOFTWARE. 27 | 28 | = = = = = 29 | 30 | Portions of mypy and mypyc are licensed under different licenses. 31 | The files 32 | mypyc/lib-rt/pythonsupport.h, mypyc/lib-rt/getargs.c and 33 | mypyc/lib-rt/getargsfast.c are licensed under the PSF 2 License, reproduced 34 | below. 35 | 36 | = = = = = 37 | 38 | PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 39 | -------------------------------------------- 40 | 41 | 1. This LICENSE AGREEMENT is between the Python Software Foundation 42 | ("PSF"), and the Individual or Organization ("Licensee") accessing and 43 | otherwise using this software ("Python") in source or binary form and 44 | its associated documentation. 45 | 46 | 2. Subject to the terms and conditions of this License Agreement, PSF hereby 47 | grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, 48 | analyze, test, perform and/or display publicly, prepare derivative works, 49 | distribute, and otherwise use Python alone or in any derivative version, 50 | provided, however, that PSF's License Agreement and PSF's notice of copyright, 51 | i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 52 | 2011, 2012 Python Software Foundation; All Rights Reserved" are retained in Python 53 | alone or in any derivative version prepared by Licensee. 54 | 55 | 3. In the event Licensee prepares a derivative work that is based on 56 | or incorporates Python or any part thereof, and wants to make 57 | the derivative work available to others as provided herein, then 58 | Licensee hereby agrees to include in any such work a brief summary of 59 | the changes made to Python. 60 | 61 | 4. PSF is making Python available to Licensee on an "AS IS" 62 | basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR 63 | IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND 64 | DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS 65 | FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT 66 | INFRINGE ANY THIRD PARTY RIGHTS. 67 | 68 | 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON 69 | FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS 70 | A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, 71 | OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. 72 | 73 | 6. This License Agreement will automatically terminate upon a material 74 | breach of its terms and conditions. 75 | 76 | 7. Nothing in this License Agreement shall be deemed to create any 77 | relationship of agency, partnership, or joint venture between PSF and 78 | Licensee. This License Agreement does not grant permission to use PSF 79 | trademarks or trade name in a trademark sense to endorse or promote 80 | products or services of Licensee, or any third party. 81 | 82 | 8. By copying, installing or otherwise using Python, Licensee 83 | agrees to be bound by the terms and conditions of this License 84 | Agreement. 85 | 86 | 87 | BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 88 | ------------------------------------------- 89 | 90 | BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 91 | 92 | 1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an 93 | office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the 94 | Individual or Organization ("Licensee") accessing and otherwise using 95 | this software in source or binary form and its associated 96 | documentation ("the Software"). 97 | 98 | 2. Subject to the terms and conditions of this BeOpen Python License 99 | Agreement, BeOpen hereby grants Licensee a non-exclusive, 100 | royalty-free, world-wide license to reproduce, analyze, test, perform 101 | and/or display publicly, prepare derivative works, distribute, and 102 | otherwise use the Software alone or in any derivative version, 103 | provided, however, that the BeOpen Python License is retained in the 104 | Software, alone or in any derivative version prepared by Licensee. 105 | 106 | 3. BeOpen is making the Software available to Licensee on an "AS IS" 107 | basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR 108 | IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND 109 | DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS 110 | FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT 111 | INFRINGE ANY THIRD PARTY RIGHTS. 112 | 113 | 4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE 114 | SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS 115 | AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY 116 | DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. 117 | 118 | 5. This License Agreement will automatically terminate upon a material 119 | breach of its terms and conditions. 120 | 121 | 6. This License Agreement shall be governed by and interpreted in all 122 | respects by the law of the State of California, excluding conflict of 123 | law provisions. Nothing in this License Agreement shall be deemed to 124 | create any relationship of agency, partnership, or joint venture 125 | between BeOpen and Licensee. This License Agreement does not grant 126 | permission to use BeOpen trademarks or trade names in a trademark 127 | sense to endorse or promote products or services of Licensee, or any 128 | third party. As an exception, the "BeOpen Python" logos available at 129 | http://www.pythonlabs.com/logos.html may be used according to the 130 | permissions granted on that web page. 131 | 132 | 7. By copying, installing or otherwise using the software, Licensee 133 | agrees to be bound by the terms and conditions of this License 134 | Agreement. 135 | 136 | 137 | CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 138 | --------------------------------------- 139 | 140 | 1. This LICENSE AGREEMENT is between the Corporation for National 141 | Research Initiatives, having an office at 1895 Preston White Drive, 142 | Reston, VA 20191 ("CNRI"), and the Individual or Organization 143 | ("Licensee") accessing and otherwise using Python 1.6.1 software in 144 | source or binary form and its associated documentation. 145 | 146 | 2. Subject to the terms and conditions of this License Agreement, CNRI 147 | hereby grants Licensee a nonexclusive, royalty-free, world-wide 148 | license to reproduce, analyze, test, perform and/or display publicly, 149 | prepare derivative works, distribute, and otherwise use Python 1.6.1 150 | alone or in any derivative version, provided, however, that CNRI's 151 | License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) 152 | 1995-2001 Corporation for National Research Initiatives; All Rights 153 | Reserved" are retained in Python 1.6.1 alone or in any derivative 154 | version prepared by Licensee. Alternately, in lieu of CNRI's License 155 | Agreement, Licensee may substitute the following text (omitting the 156 | quotes): "Python 1.6.1 is made available subject to the terms and 157 | conditions in CNRI's License Agreement. This Agreement together with 158 | Python 1.6.1 may be located on the Internet using the following 159 | unique, persistent identifier (known as a handle): 1895.22/1013. This 160 | Agreement may also be obtained from a proxy server on the Internet 161 | using the following URL: http://hdl.handle.net/1895.22/1013". 162 | 163 | 3. In the event Licensee prepares a derivative work that is based on 164 | or incorporates Python 1.6.1 or any part thereof, and wants to make 165 | the derivative work available to others as provided herein, then 166 | Licensee hereby agrees to include in any such work a brief summary of 167 | the changes made to Python 1.6.1. 168 | 169 | 4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" 170 | basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR 171 | IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND 172 | DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS 173 | FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT 174 | INFRINGE ANY THIRD PARTY RIGHTS. 175 | 176 | 5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON 177 | 1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS 178 | A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, 179 | OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. 180 | 181 | 6. This License Agreement will automatically terminate upon a material 182 | breach of its terms and conditions. 183 | 184 | 7. This License Agreement shall be governed by the federal 185 | intellectual property law of the United States, including without 186 | limitation the federal copyright law, and, to the extent such 187 | U.S. federal law does not apply, by the law of the Commonwealth of 188 | Virginia, excluding Virginia's conflict of law provisions. 189 | Notwithstanding the foregoing, with regard to derivative works based 190 | on Python 1.6.1 that incorporate non-separable material that was 191 | previously distributed under the GNU General Public License (GPL), the 192 | law of the Commonwealth of Virginia shall govern this License 193 | Agreement only as to issues arising under or with respect to 194 | Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this 195 | License Agreement shall be deemed to create any relationship of 196 | agency, partnership, or joint venture between CNRI and Licensee. This 197 | License Agreement does not grant permission to use CNRI trademarks or 198 | trade name in a trademark sense to endorse or promote products or 199 | services of Licensee, or any third party. 200 | 201 | 8. By clicking on the "ACCEPT" button where indicated, or by copying, 202 | installing or otherwise using Python 1.6.1, Licensee agrees to be 203 | bound by the terms and conditions of this License Agreement. 204 | 205 | ACCEPT 206 | 207 | 208 | CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 209 | -------------------------------------------------- 210 | 211 | Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, 212 | The Netherlands. All rights reserved. 213 | 214 | Permission to use, copy, modify, and distribute this software and its 215 | documentation for any purpose and without fee is hereby granted, 216 | provided that the above copyright notice appear in all copies and that 217 | both that copyright notice and this permission notice appear in 218 | supporting documentation, and that the name of Stichting Mathematisch 219 | Centrum or CWI not be used in advertising or publicity pertaining to 220 | distribution of the software without specific, written prior 221 | permission. 222 | 223 | STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO 224 | THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 225 | FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE 226 | FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 227 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 228 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT 229 | OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 230 | --------------------------------------------------------------------------------