diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1712404 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +insert_final_newline = true +max_line_length = 120 + +[{*.kt,*.kts}] +ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL +ij_kotlin_name_count_to_use_star_import = 2147483647 +ij_kotlin_name_count_to_use_star_import_for_members = 2147483647 +ij_kotlin_packages_to_use_import_on_demand = unset +ij_kotlin_imports_layout=java.**,javax.**,|,*,|,com.intellij.**,com.jetbrains.**,|,com.jetbrains.python.**,|,com.github.pyvenvmanage.**,|^ \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index 5f8af70..0000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1 +0,0 @@ -* @gaborbernat @andrask diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..ccea671 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,43 @@ +name: Bug report +description: Create a report to help us improve +labels: ["bug"] +body: + - type: input + id: gradle + attributes: + label: Used within IDE (name and version) + placeholder: PyCharm 2024.2 + validations: + required: true + + - type: dropdown + id: os + attributes: + label: Operating System + options: + - macOS + - Linux + - Windows + + - type: input + id: version + attributes: + label: PyVenv Plugin version + placeholder: 2.0.0 + validations: + required: true + + - type: textarea + id: issue + attributes: + label: What happened? + description: A clear and concise description of what the bug is. + validations: + required: true + + - type: textarea + id: logs + attributes: + label: Relevant log output or stack trace + description: If there is an exception stack trace post it here. + render: shell diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..b4e8e12 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Documentation + url: https://github.com/tox-dev/PyVenvManage?tab=readme-ov-file#pyvenvmanage + about: Check the README file in the first place. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..4fcfeb5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,31 @@ +name: Feature request +description: Suggest an idea for this project +labels: ["enhancement"] +body: + - type: textarea + id: cause + attributes: + label: Describe the need of your request + description: A clear and concise description of what the need or problem is. + validations: + required: true + + - type: textarea + id: solution + attributes: + label: Proposed solution + description: A clear and concise description of what you want to happen. + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives you've considered + description: What did you try so far to accomplish the goal? + + - type: textarea + id: context + attributes: + label: Additional context + description: Add any other context or screenshots about the feature request here. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 1230149..9be205f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,6 +1,17 @@ +# Dependabot configuration: +# https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/configuration-options-for-dependency-updates + version: 2 updates: + # Maintain dependencies for Gradle dependencies + - package-ecosystem: "gradle" + directory: "/" + target-branch: "next" + schedule: + interval: "daily" + # Maintain dependencies for GitHub Actions - package-ecosystem: "github-actions" directory: "/" + target-branch: "next" schedule: interval: "daily" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..1618cee --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,227 @@ +name: Build +on: + push: + branches: [main] + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Build plugin + runs-on: ubuntu-latest + steps: + - name: Checkout git repository + uses: actions/checkout@v4 + - name: Validate Gradle wrapper + uses: gradle/actions/wrapper-validation@v4 + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: zulu + java-version: 17 + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + - name: Build plugin + run: ./gradlew buildPlugin + - name: Prepare Plugin Artifact + id: artifact + shell: bash + run: | + cd ${{ github.workspace }}/build/distributions + FILENAME=`ls *.zip` + unzip "$FILENAME" -d content + echo "filename=${FILENAME:0:-4}" >> $GITHUB_OUTPUT + - name: Upload artifact for later download + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.artifact.outputs.filename }} + path: ./build/distributions/content/*/* + info: + name: Get plugin info + runs-on: ubuntu-latest + outputs: + version: ${{ steps.properties.outputs.version }} + changelog: ${{ steps.properties.outputs.changelog }} + pluginVerifierHomeDir: ${{ steps.properties.outputs.pluginVerifierHomeDir }} + steps: + - name: Checkout git repository + uses: actions/checkout@v4 + - name: Validate Gradle wrapper + uses: gradle/actions/wrapper-validation@v4 + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: zulu + java-version: 17 + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + - name: Export plugin properties + id: properties + shell: bash + run: | + PROPERTIES="$(./gradlew properties --console=plain -q)" + VERSION="$(echo "$PROPERTIES" | grep "^version:" | cut -f2- -d ' ')" + CHANGELOG="$(./gradlew getChangelog --unreleased --no-header --console=plain -q)" + + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "pluginVerifierHomeDir=~/.pluginVerifier" >> $GITHUB_OUTPUT + echo "changelog<> $GITHUB_OUTPUT + echo "$CHANGELOG" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + verify: + name: Verify plugin + needs: [build, info] + runs-on: ubuntu-latest + steps: + - name: Maximize Build Space + uses: jlumbroso/free-disk-space@main + with: + tool-cache: false + large-packages: false + - name: Checkout git repository + uses: actions/checkout@v4 + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: zulu + java-version: 17 + + # Set up Gradle + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v3 + with: + gradle-home-cache-cleanup: true + + # Cache Plugin Verifier IDEs + - name: Set up Plugin Verifier IDEs Cache + uses: actions/cache@v4 + with: + path: ${{ needs.info.outputs.pluginVerifierHomeDir }}/ides + key: plugin-verifier-${{ hashFiles('build/listProductsReleases.txt') }} + + # Run Verify Plugin task and IntelliJ Plugin Verifier tool + - name: Run Plugin Verification tasks + run: ./gradlew verifyPlugin -Dplugin.verifier.home.dir=${{ needs.info.outputs.pluginVerifierHomeDir }} + + # Collect Plugin Verifier Result + - name: Collect Plugin Verifier Result + if: ${{ always() }} + uses: actions/upload-artifact@v4 + with: + name: pluginVerifier-result + path: ${{ github.workspace }}/build/reports/pluginVerifier + + # Run tests and upload a code coverage report + test: + name: Test + needs: [build] + runs-on: ubuntu-latest + steps: + # Check out the current repository + - name: Checkout git repository + uses: actions/checkout@v4 + + # Set up Java environment for the next steps + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: zulu + java-version: 17 + + # Set up Gradle + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v3 + with: + gradle-home-cache-cleanup: true + + # Run tests + - name: Run Tests + run: ./gradlew check + + # Collect Tests Result of failed tests + - name: Collect Tests Result + if: ${{ failure() }} + uses: actions/upload-artifact@v4 + with: + name: tests-result + path: ${{ github.workspace }}/build/reports/tests + + # Upload the Kover report to CodeCov + - name: Upload Code Coverage Report + uses: codecov/codecov-action@v4 + with: + files: ${{ github.workspace }}/build/reports/kover/report.xml + + # Run Qodana inspections and provide report + inspectCode: + name: Inspect code + needs: [build] + runs-on: ubuntu-latest + permissions: + contents: write + checks: write + pull-requests: write + steps: + # Free GitHub Actions Environment Disk Space + - name: Maximize Build Space + uses: jlumbroso/free-disk-space@main + with: + tool-cache: false + large-packages: false + + # Check out the current repository + - name: Checkout git repository + uses: actions/checkout@v4 + + # Set up Java environment for the next steps + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: zulu + java-version: 17 + + # Run Qodana inspections + - name: Qodana - Code Inspection + uses: JetBrains/qodana-action@v2024.1.5 + with: + cache-default-branch-only: true + + # Prepare a draft release for GitHub Releases page for the manual verification + # If accepted and published, release workflow would be triggered + releaseDraft: + name: Release draft + if: github.event_name != 'pull_request' + needs: [build, info, test, inspectCode, verify] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + # Check out the current repository + - name: Checkout git repository + uses: actions/checkout@v4 + + # Remove old release drafts by using the curl request for the available releases with a draft flag + - name: Remove Old Release Drafts + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh api repos/{owner}/{repo}/releases \ + --jq '.[] | select(.draft == true) | .id' \ + | xargs -I '{}' gh api -X DELETE repos/{owner}/{repo}/releases/{} + + # Create a new release draft which is not publicly visible and requires manual acceptance + - name: Create Release Draft + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create "v${{ needs.info.outputs.version }}" \ + --draft \ + --title "v${{ needs.info.outputs.version }}" \ + --notes "$(cat << 'EOM' + ${{ needs.info.outputs.changelog }} + EOM + )" diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml deleted file mode 100644 index 5cd7b23..0000000 --- a/.github/workflows/check.yaml +++ /dev/null @@ -1,35 +0,0 @@ -name: check -on: - push: - pull_request: - schedule: - - cron: "0 8 * * *" - -concurrency: - group: "${{ github.workflow }}-${{ github.ref }}" - cancel-in-progress: true - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: 17 - - - name: Setup Gradle - uses: gradle/gradle-build-action@v3 - - - name: Build plugin - run: ./gradlew buildPlugin - - - name: Run plugin verifier - run: ./gradlew runPluginVerifier - - - uses: actions/upload-artifact@v4 - with: - name: plugin - path: build/distributions/*.zip - retention-days: 10 diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml deleted file mode 100644 index 433114e..0000000 --- a/.github/workflows/publish.yaml +++ /dev/null @@ -1,33 +0,0 @@ -name: publish -on: - workflow_dispatch: - -jobs: - publish: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: 17 - - - name: Setup Gradle - uses: gradle/gradle-build-action@v3 - - - name: Build plugin - run: ./gradlew buildPlugin - - - name: Run plugin verifier - run: ./gradlew runPluginVerifier - - - uses: actions/upload-artifact@v4 - with: - name: plugin - path: build/distributions/*.zip - retention-days: 10 - - - name: Publish plugin - env: - PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} - run: ./gradlew publishPlugin diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..424ff9b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,101 @@ +# GitHub Actions Workflow created for handling the release process based on the draft release prepared with the Build workflow. +# Running the publishPlugin task requires all following secrets to be provided: PUBLISH_TOKEN, PRIVATE_KEY, PRIVATE_KEY_PASSWORD, CERTIFICATE_CHAIN. +# See https://plugins.jetbrains.com/docs/intellij/plugin-signing.html for more information. + +name: Release +on: + release: + types: [prereleased, released] + +jobs: + # Prepare and publish the plugin to JetBrains Marketplace repository + release: + name: Publish Plugin + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + # Check out the current repository + - name: Fetch Sources + uses: actions/checkout@v4 + with: + ref: ${{ github.event.release.tag_name }} + + # Set up Java environment for the next steps + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: zulu + java-version: 17 + + # Setup Gradle + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + with: + gradle-home-cache-cleanup: true + + # Set environment variables + - name: Export Properties + id: properties + shell: bash + run: | + CHANGELOG="$(cat << 'EOM' | sed -e 's/^[[:space:]]*$//g' -e '/./,$!d' + ${{ github.event.release.body }} + EOM + )" + + echo "changelog<> $GITHUB_OUTPUT + echo "$CHANGELOG" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + # Update the Unreleased section with the current release note + - name: Patch Changelog + if: ${{ steps.properties.outputs.changelog != '' }} + env: + CHANGELOG: ${{ steps.properties.outputs.changelog }} + run: | + ./gradlew patchChangelog --release-note="$CHANGELOG" + + # Publish the plugin to JetBrains Marketplace + - name: Publish Plugin + env: + PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} + CERTIFICATE_CHAIN: ${{ secrets.CERTIFICATE_CHAIN }} + PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} + PRIVATE_KEY_PASSWORD: ${{ secrets.PRIVATE_KEY_PASSWORD }} + run: ./gradlew publishPlugin + + # Upload artifact as a release asset + - name: Upload Release Asset + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh release upload ${{ github.event.release.tag_name }} ./build/distributions/* + + # Create a pull request + - name: Create Pull Request + if: ${{ steps.properties.outputs.changelog != '' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + VERSION="${{ github.event.release.tag_name }}" + BRANCH="changelog-update-$VERSION" + LABEL="release changelog" + + git config user.email "action@github.com" + git config user.name "GitHub Action" + + git checkout -b $BRANCH + git commit -am "Changelog update - $VERSION" + git push --set-upstream origin $BRANCH + + gh label create "$LABEL" \ + --description "Pull requests with release changelog update" \ + --force \ + || true + + gh pr create \ + --title "Changelog update - \`$VERSION\`" \ + --body "Current pull request contains patched \`CHANGELOG.md\` file for the \`$VERSION\` version." \ + --label "$LABEL" \ + --head $BRANCH diff --git a/.github/workflows/run-ui-tests.yml b/.github/workflows/run-ui-tests.yml new file mode 100644 index 0000000..cac2070 --- /dev/null +++ b/.github/workflows/run-ui-tests.yml @@ -0,0 +1,53 @@ +name: Run UI Tests +on: workflow_dispatch + +jobs: + testUI: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + runIde: | + export DISPLAY=:99.0 + Xvfb -ac :99 -screen 0 1920x1080x16 & + gradle runIdeForUiTests & + - os: windows-latest + runIde: start gradlew.bat runIdeForUiTests + - os: macos-latest + runIde: ./gradlew runIdeForUiTests & + + steps: + # Check out the current repository + - name: Fetch Sources + uses: actions/checkout@v4 + + # Set up Java environment for the next steps + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: zulu + java-version: 17 + + # Setup Gradle + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + with: + gradle-home-cache-cleanup: true + + # Run IDEA prepared for UI testing + - name: Run IDE + run: ${{ matrix.runIde }} + + # Wait for IDEA to be started + - name: Health Check + uses: jtalk/url-health-check-action@v4 + with: + url: http://127.0.0.1:8082 + max-attempts: 15 + retry-delay: 30s + + # Run tests + - name: Tests + run: ./gradlew test diff --git a/.gitignore b/.gitignore index e0d53d8..ffabde6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .gradle .idea +.intellijPlatform +.qodana build diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..b0a8df7 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,14 @@ +repos: + - repo: git@bbgithub.dev.bloomberg.com:pre-commit/mirrors-prettier.git + rev: "v4.0.0-alpha.8" + hooks: + - id: prettier + additional_dependencies: + - "prettier@3.3.3" + - "@prettier/plugin-xml@3.4.1" + args: ["--print-width=120", "--prose-wrap=always"] + - repo: https://github.com/python-jsonschema/check-jsonschema + rev: 0.29.1 + hooks: + - id: check-github-workflows + args: ["--verbose"] diff --git a/.run/Run Plugin.run.xml b/.run/Run Plugin.run.xml new file mode 100644 index 0000000..d15ff68 --- /dev/null +++ b/.run/Run Plugin.run.xml @@ -0,0 +1,24 @@ + + + + + + + + true + true + false + + + \ No newline at end of file diff --git a/.run/Run Tests.run.xml b/.run/Run Tests.run.xml new file mode 100644 index 0000000..f281bdc --- /dev/null +++ b/.run/Run Tests.run.xml @@ -0,0 +1,25 @@ + + + + + + + + true + true + false + true + + + diff --git a/.run/Run Verifications.run.xml b/.run/Run Verifications.run.xml new file mode 100644 index 0000000..32783f5 --- /dev/null +++ b/.run/Run Verifications.run.xml @@ -0,0 +1,25 @@ + + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/.sdkmanrc b/.sdkmanrc new file mode 100644 index 0000000..8a88afd --- /dev/null +++ b/.sdkmanrc @@ -0,0 +1,3 @@ +# Enable auto-env through the sdkman_auto_env config +# Add key=value pairs of SDKs to use below +java=17.0.11-jbr diff --git a/CHANGELOG.md b/CHANGELOG.md index 66522c1..7d21e17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,38 +1,46 @@ - - # PyVenvManage Changelog +## [Unreleased] + +- Move to Kotlin +- Move to + ## [1.4.0] + ### Fixed + - Fix support for 2024.1 -- + ## [1.3.4] + ### Fixed + - Fix support for 2021.3 ## [1.3.3] + ### Fixed + - https://github.com/nokia/PyVenvManage/pull/18 Upgrade gradle and support 2021.3 + ### Thanks + - I would like to express my gratitude towards @gaborbernat for his continued support of the plugin. ## [1.3.2] - 2021-06-08 + ### Fixed + - [16](https://github.com/nokia/PyVenvManage/issues/16) PyCharm 2021.1.2 ## [1.3.1] - 2021-02-20 + ### Fixed + - [13](https://github.com/nokia/PyVenvManage/issues/13) Enable compatibility with 2021.1 EAP ## [1.3.0] - 2020-11-11 -### Fixed -- Removed the usage of the deprecated PythonSdkType.getPythonExecutable API - +- Removed the usage of the deprecated PythonSdkType.getPythonExecutable API diff --git a/LICENSE b/LICENSE index 5052afb..70b85cb 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,3 @@ - BSD 3-Clause License Copyright (c) 2017, Nokia All rights reserved. diff --git a/PyVenvManage.iml b/PyVenvManage.iml deleted file mode 100644 index 305d1b0..0000000 --- a/PyVenvManage.iml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/README.md b/README.md index 2a34556..9275274 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,12 @@ ## Introduction + **PyVenvManage** is a plugin for managing the Python interpreter of Pycharm Projects. -It is a general issue that Python projects may have several interpreters in different -virtual environments for the various versions of the language. Managing these venvs -is easily done with `tox`, but configuring the project in Pycharm is painful. +It is a general issue that Python projects may have several interpreters in different virtual environments for the +various versions of the language. Managing these venvs is easily done with `tox`, but configuring the project in Pycharm +is painful. With PyVenvManage the selection and setup of the venv is a few clicks without dialog boxes. @@ -27,7 +28,7 @@ The official plugin page is at https://plugins.jetbrains.com/plugin/20536-pyvenv ![usage video](anim.gif?raw=true) - ## License -This project is licensed under the BSD-3-Clause license - see the [LICENSE](https://github.com/pyvenvmanage/PyVenvManage/blob/main/LICENSE). +This project is licensed under the BSD-3-Clause license - see the +[LICENSE](https://github.com/pyvenvmanage/PyVenvManage/blob/main/LICENSE). diff --git a/build.gradle.kts b/build.gradle.kts index 2fc6eb3..600117c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,94 +1,154 @@ -import io.gitlab.arturbosch.detekt.Detekt -import org.jetbrains.changelog.date -import org.jetbrains.intellij.tasks.PatchPluginXmlTask -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +import org.jetbrains.changelog.Changelog +import org.jetbrains.changelog.markdownToHTML +import org.jetbrains.intellij.platform.gradle.Constants.Constraints +import org.jetbrains.intellij.platform.gradle.TestFrameworkType plugins { - id("java") - id("org.jetbrains.kotlin.jvm") version "1.9.23" - id("org.jetbrains.intellij") version "1.17.3" - id("org.jetbrains.changelog") version "1.3.1" - id("io.gitlab.arturbosch.detekt") version "1.23.6" - id("org.jlleitschuh.gradle.ktlint") version "11.6.1" + alias(libs.plugins.kotlin) + alias(libs.plugins.intelliJPlatform) + alias(libs.plugins.changelog) + alias(libs.plugins.qodana) + alias(libs.plugins.testLogger) + alias(libs.plugins.kover) + alias(libs.plugins.ktlint) +// alias(libs.plugins.taskinfo) // cache incompatible https://gitlab.com/barfuin/gradle-taskinfo/-/issues/23 } -val pluginGroup: String = "com.github.pyvenvmanage.pyvenv" -val pluginNameG: String = "PyVenv Manage" -val pluginVersion: String = "1.4.0" -val pluginSinceBuild = "241" -val pluginUntilBuild = "" -// https://www.jetbrains.com/idea/download/other.html -val pluginVerifierIdeVersions = "241.14494.240" -val platformType = "IC" -val platformVersion = "2024.1" -// PythonCore https://plugins.jetbrains.com/plugin/631-python/versions -var usePlugins = "PythonCore:241.14494.240" - -group = pluginGroup -version = pluginVersion +group = providers.gradleProperty("pluginGroup").get() +version = providers.gradleProperty("pluginVersion").get() +kotlin { + jvmToolchain(17) +} repositories { mavenCentral() + intellijPlatform { + defaultRepositories() + } } dependencies { - detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.21.0") -} + testImplementation("com.squareup.okhttp3:okhttp:4.12.0") // needed for connecting to remote robot + testImplementation(libs.jupiter) + testRuntimeOnly(libs.jupiterEngine) + testRuntimeOnly("junit:junit:4.13.2") // https://github.com/JetBrains/intellij-platform-gradle-plugin/issues/1711 + testImplementation(libs.remoteRobot) + testImplementation(libs.remoteRobotFixtures) -tasks { - withType { - sourceCompatibility = "17" - targetCompatibility = "17" - } - listOf("compileKotlin", "compileTestKotlin").forEach { - getByName(it) { - kotlinOptions.jvmTarget = "17" - } + intellijPlatform { + create(providers.gradleProperty("platformType"), providers.gradleProperty("platformVersion")) + plugins(providers.gradleProperty("platformPlugins").map { it.split(',') }) + instrumentationTools() + pluginVerifier() + zipSigner() + testFramework(TestFrameworkType.JUnit5) } - withType { - jvmTarget = "17" - reports { - html.required.set(true) +} + +intellijPlatform { + pluginConfiguration { + version = providers.gradleProperty("pluginVersion") + description = + providers.fileContents(layout.projectDirectory.file("README.md")).asText.map { + val start = "" + val end = "" + with(it.lines()) { + if (!containsAll(listOf(start, end))) { + throw GradleException("Plugin description section not found in README.md:\n$start ... $end") + } + subList(indexOf(start) + 1, indexOf(end)).joinToString("\n").let(::markdownToHTML) + } + } + + val changelog = project.changelog + changeNotes = + providers.gradleProperty("pluginVersion").map { pluginVersion -> + with(changelog) { + renderItem( + (getOrNull(pluginVersion) ?: getUnreleased()) + .withHeader(false) + .withEmptySections(false), + Changelog.OutputType.HTML, + ) + } + } + + ideaVersion { + sinceBuild = providers.gradleProperty("pluginSinceBuild") + untilBuild = provider { null } } } - patchPluginXml { - version.set(pluginVersion) - sinceBuild.set(pluginSinceBuild) - untilBuild.set(pluginUntilBuild) - } - withType { - pluginDescription.set(provider { file("description.html").readText() }) + signing { + certificateChain = providers.environmentVariable("CERTIFICATE_CHAIN") + privateKey = providers.environmentVariable("PRIVATE_KEY") + password = providers.environmentVariable("PRIVATE_KEY_PASSWORD") } - runPluginVerifier { - ideVersions.set(listOf(pluginVerifierIdeVersions)) + publishing { + token = providers.environmentVariable("PUBLISH_TOKEN") + channels = + providers.gradleProperty("pluginVersion").map { + listOf( + it + .substringAfter('-', "") + .substringBefore('.') + .ifEmpty { "default" }, + ) + } } - publishPlugin { - dependsOn("patchChangelog") - token.set(System.getenv("PUBLISH_TOKEN")) - channels.set(listOf(pluginVersion.split('-').getOrElse(1) { "default" }.split('.').first())) + + pluginVerification { + ides { + recommended() + } } } -intellij { - pluginName.set(pluginNameG) - version.set(platformVersion) - type.set(platformType) - plugins.set(listOf(usePlugins)) +changelog { + groups.empty() + repositoryUrl = providers.gradleProperty("pluginRepositoryUrl") +} +kover { + reports { + total { + xml { + onCheck = true + } + } + } } -detekt { - config.setFrom("./detekt-config.yml") - buildUponDefaultConfig = true +tasks { + wrapper { + gradleVersion = providers.gradleProperty("gradleVersion").get() + } + publishPlugin { + dependsOn(patchChangelog) + } + buildSearchableOptions { + enabled = false + } + test { + useJUnitPlatform() + } } -changelog { - version.set(pluginVersion) - path.set("${project.projectDir}/CHANGELOG.md") - header.set(provider { "[${version.get()}] - ${date()}" }) - itemPrefix.set("-") - keepUnreleasedSection.set(true) - unreleasedTerm.set("[Unreleased]") - groups.set(listOf("Added", "Changed", "Deprecated", "Removed", "Fixed", "Security")) +val runIdeForUiTests by intellijPlatformTesting.runIde.registering { + task { + jvmArgumentProviders += + CommandLineArgumentProvider { + listOf( + "-Drobot-server.port=8082", + "-Dide.mac.message.dialogs.as.sheets=false", + "-Djb.privacy.policy.text=", + "-Djb.consents.confirmation.enabled=false", + ) + } + } + + plugins { + robotServerPlugin(Constraints.LATEST_VERSION) + } } diff --git a/description.html b/description.html deleted file mode 100644 index 5bd3ae4..0000000 --- a/description.html +++ /dev/null @@ -1,13 +0,0 @@ -PyVenvManage 2 is a plugin for managing the Python interpreter of Python projects. - -It is a general issue that Python projects may have several interpreters in different -virtual environments for the various versions of the language. Managing these venvs -is easily done with `tox`, but configuring the interpreter for the project or module is painful. - -With PyVenvManage 2 the selection and setup of the venv is a few clicks without unwanted dialog boxes. - -

Features

-
    -
  • Popup menu item to set the project interpreter
  • -
  • Icon provider to indicate virtual environments in the project view
  • -
diff --git a/detekt-config.yml b/detekt-config.yml deleted file mode 100644 index f69e7ca..0000000 --- a/detekt-config.yml +++ /dev/null @@ -1,5 +0,0 @@ -formatting: - Indentation: - continuationIndentSize: 8 - ParameterListWrapping: - indentSize: 8 diff --git a/example-project/.gitignore b/example-project/.gitignore deleted file mode 100644 index 58e7969..0000000 --- a/example-project/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -.tox -*.egg-info - - diff --git a/example-project/code/__init__.py b/example-project/code/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/example-project/requirements.txt b/example-project/requirements.txt deleted file mode 100644 index 3288e92..0000000 --- a/example-project/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -requests - diff --git a/example-project/setup.py b/example-project/setup.py deleted file mode 100644 index 5ca68fb..0000000 --- a/example-project/setup.py +++ /dev/null @@ -1,11 +0,0 @@ -from setuptools import setup, find_packages - -setup( - name='ExamplePackageForPyVenvManage', - version='1.0.0', - url='https://github.com/nokia/PyVenvManage', - author='Andras Kovi', - author_email='akovi@nokia.com', - description='Use this for testing the plugin', - packages=find_packages(), -) diff --git a/example-project/tox.ini b/example-project/tox.ini deleted file mode 100644 index b6cedd8..0000000 --- a/example-project/tox.ini +++ /dev/null @@ -1,14 +0,0 @@ -[tox] -envlist = py3 -minversion = 2.0 -skipsdist = True - -[testenv] -usedevelop = True -install_command = pip install {opts} {packages} -deps = - -r{toxinidir}/requirements.txt -commands = - true -allowlist_externals = - true diff --git a/gradle.properties b/gradle.properties index e5d9a95..7b8ce75 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1,16 @@ +pluginGroup = com.github.pyvenvmanage +pluginName = PyVenv Manage 2 +pluginRepositoryUrl = https://github.com/pyvenvmanage/PyVenvManage +pluginVersion = 2.0.0 + +pluginSinceBuild = 241 +platformType = IC +platformVersion = 2024.1 +# https://plugins.jetbrains.com/plugin/7322-python-community-edition/versions/stable pick compatible with IC +platformPlugins = PythonCore:241.14494.240 + +gradleVersion = 8.9 kotlin.stdlib.default.dependency = false +org.gradle.configuration-cache = true +org.gradle.caching = true +org.gradle.welcome-message = never diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..f51b719 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,27 @@ +[versions] +jupiter = "5.10.3" +changelog = "2.2.1" +intelliJPlatform = "2.0.1" +kotlin = "1.9.24" +kover = "0.8.3" +qodana = "2024.1.9" +remoteRobot = "0.11.23" +ktlint = "12.1.1" +taskinfo = "2.2.0" +testLogger = "4.0.0" + +[libraries] +jupiter = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "jupiter" } +jupiterEngine = { group = "org.junit.jupiter", name = "junit-jupiter-engine", version.ref = "jupiter" } +remoteRobot = { group = "com.intellij.remoterobot", name = "remote-robot", version.ref = "remoteRobot" } +remoteRobotFixtures = { group = "com.intellij.remoterobot", name = "remote-fixtures", version.ref = "remoteRobot" } + +[plugins] +changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" } +intelliJPlatform = { id = "org.jetbrains.intellij.platform", version.ref = "intelliJPlatform" } +kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } +qodana = { id = "org.jetbrains.qodana", version.ref = "qodana" } +ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } +#taskinfo = { id = "org.barfuin.gradle.taskinfo", version.ref = "taskinfo" } +testLogger = { id = "com.adarshr.test-logger", version.ref = "testLogger" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 943f0cb..2c35211 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 20db9ad..dedd5d1 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 65dcd68..f5feea6 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -83,10 +85,9 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,10 +134,13 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. @@ -144,7 +148,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +156,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -197,11 +201,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/gradlew.bat b/gradlew.bat index 6689b85..9b42019 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/qodana.yml b/qodana.yml new file mode 100644 index 0000000..7a28535 --- /dev/null +++ b/qodana.yml @@ -0,0 +1,9 @@ +version: 1.0 +linter: jetbrains/qodana-jvm-community:latest +projectJDK: "17" +profile: + name: qodana.recommended +exclude: + - name: All + paths: + - .qodana diff --git a/settings.gradle.kts b/settings.gradle.kts index d8d2d32..b7286f5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1 @@ -rootProject.name = "PyVenvManage" +rootProject.name = "PyVenv Manage 2" diff --git a/src/main/java/com/github/pyvenvmanage/ConfigureModulePythonVenv.java b/src/main/java/com/github/pyvenvmanage/ConfigureModulePythonVenv.java deleted file mode 100644 index 01b22c6..0000000 --- a/src/main/java/com/github/pyvenvmanage/ConfigureModulePythonVenv.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.github.pyvenvmanage; - -import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.actionSystem.CommonDataKeys; -import com.intellij.openapi.module.Module; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.projectRoots.Sdk; -import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil; -import com.intellij.openapi.roots.ModuleRootModificationUtil; -import com.intellij.openapi.roots.ProjectFileIndex; -import com.intellij.openapi.vfs.VirtualFile; -import com.jetbrains.python.sdk.PythonSdkType; -import org.jetbrains.annotations.NotNull; - -import java.text.MessageFormat; - -public class ConfigureModulePythonVenv extends ConfigurePythonVenv { - - protected void setInterpreter(Project project, VirtualFile file, String pythonExecutable) { - Sdk sdk = findExistingSdkForExecutable(pythonExecutable, project); - if (sdk == null) { - sdk = SdkConfigurationUtil.createAndAddSDK(pythonExecutable, PythonSdkType.getInstance()); - } - PythonSdkType.getInstance().setupSdkPaths(sdk); - - Module module = ProjectFileIndex.getInstance(project).getModuleForFile(file, false); - - if (null != module) { - ModuleRootModificationUtil.setModuleSdk(module, sdk); - String message = MessageFormat.format("Updated SDK for module {0} to: {1}", module.getName(), sdk.getName()); - showNotification(project, message); - } - } - - @Override - public void update(@NotNull AnActionEvent e) { - if (isEventOnVenvDir(e)) { - VirtualFile file = e.getData(CommonDataKeys.VIRTUAL_FILE); - Project project = e.getProject(); - Module module = ProjectFileIndex.getInstance(project).getModuleForFile(file, false); - if (null != module) { - String POPUP_ITEM_TEXT = "Set module venv for %s"; - e.getPresentation().setText(String.format(POPUP_ITEM_TEXT, module.getName())); - e.getPresentation().setEnabledAndVisible(true); - return; - } - } - e.getPresentation().setEnabledAndVisible(false); - } - -} diff --git a/src/main/java/com/github/pyvenvmanage/ConfigureProjectPythonVenv.java b/src/main/java/com/github/pyvenvmanage/ConfigureProjectPythonVenv.java deleted file mode 100644 index c67c19a..0000000 --- a/src/main/java/com/github/pyvenvmanage/ConfigureProjectPythonVenv.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.github.pyvenvmanage; - -import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.projectRoots.Sdk; -import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil; -import com.intellij.openapi.vfs.VirtualFile; -import com.jetbrains.python.sdk.PythonSdkType; - -import java.text.MessageFormat; - -public class ConfigureProjectPythonVenv extends ConfigurePythonVenv { - - protected void setInterpreter(Project project, VirtualFile file, String pythonExecutable) { - Sdk sdk = findExistingSdkForExecutable(pythonExecutable, project); - if (sdk == null) { - sdk = SdkConfigurationUtil.createAndAddSDK(pythonExecutable, PythonSdkType.getInstance()); - } - - SdkConfigurationUtil.setDirectoryProjectSdk(project, sdk); - String message = MessageFormat.format("Updated SDK for project {0} to: {1}", project.getName(), sdk.getName()); - showNotification(project, message); - } - - @Override - public void update(AnActionEvent e) { - e.getPresentation().setEnabledAndVisible(isEventOnVenvDir(e)); - } - -} diff --git a/src/main/java/com/github/pyvenvmanage/ConfigurePythonVenv.java b/src/main/java/com/github/pyvenvmanage/ConfigurePythonVenv.java deleted file mode 100644 index b54f71d..0000000 --- a/src/main/java/com/github/pyvenvmanage/ConfigurePythonVenv.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (C) 2017 Nokia - * Copyright (C) 2022 PyVenvManage Org - */ - -package com.github.pyvenvmanage; - -import com.intellij.notification.NotificationGroupManager; -import com.intellij.openapi.actionSystem.AnAction; -import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.actionSystem.CommonDataKeys; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.projectRoots.Sdk; -import com.intellij.openapi.ui.MessageType; -import com.intellij.openapi.vfs.VirtualFile; -import com.jetbrains.python.configuration.PyConfigurableInterpreterList; -import com.jetbrains.python.sdk.PythonSdkUtil; - -import java.io.File; -import java.util.Collection; - -/** - * Configures the selected directory as the virtual environment for the containing project. - */ -public abstract class ConfigurePythonVenv extends AnAction { - - @Override - public void actionPerformed(AnActionEvent e) { - final Project project = e.getData(CommonDataKeys.PROJECT); - - if (project == null) { - return; - } - - VirtualFile file = e.getData(CommonDataKeys.VIRTUAL_FILE); - - if (file == null) { - return; - } - - if (!file.isDirectory()) { - file = file.getParent(); - } - - final String pythonExecutable = PythonSdkUtil.getPythonExecutable(file.getPath()); - if (pythonExecutable != null) { - setInterpreter(project, file, pythonExecutable); - } - } - - abstract void setInterpreter(Project project, VirtualFile file, String pythonExecutable); - - Sdk findExistingSdkForExecutable(String pythonExecutablePath, Project project) { - final PyConfigurableInterpreterList interpreterList = PyConfigurableInterpreterList.getInstance(project); - if (interpreterList == null) { - return null; - } - Collection sdks = interpreterList.getModel().getProjectSdks().values(); - for (Sdk sdk : sdks) { - if (pythonExecutablePath.equals(sdk.getHomePath())) { - return sdk; - } - } - return null; - } - - void showNotification(Project project, String message) { - NotificationGroupManager.getInstance() - .getNotificationGroup("SDK changed notification") - .createNotification(message, MessageType.INFO) - .notify(project); - } - - boolean isEventOnVenvDir(AnActionEvent e) { - VirtualFile file = e.getData(CommonDataKeys.VIRTUAL_FILE); - - if (file == null) { - return false; - } - - if (!file.isDirectory() && new File(file.getPath()).isFile()) { - return PythonSdkUtil.isVirtualEnv(file.getPath()); - } - - if (PythonSdkUtil.getPythonExecutable(file.getPath()) != null) { - e.getPresentation().setEnabledAndVisible(true); - return true; - } - - return false; - } - - @Override - public boolean isDumbAware() { - return false; - } - -} diff --git a/src/main/java/com/github/pyvenvmanage/VenvIconProvider.java b/src/main/java/com/github/pyvenvmanage/VenvIconProvider.java deleted file mode 100644 index f9f6f53..0000000 --- a/src/main/java/com/github/pyvenvmanage/VenvIconProvider.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2020 Nokia - * Copyright (C) 2022 PyVenvManage Org - */ - -package com.github.pyvenvmanage; - -import com.intellij.ide.IconLayerProvider; -import com.intellij.ide.IconProvider; -import com.intellij.openapi.util.Iconable; -import com.intellij.psi.PsiDirectory; -import com.intellij.psi.PsiElement; -import com.jetbrains.python.icons.PythonIcons; -import com.jetbrains.python.sdk.PythonSdkUtil; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import javax.swing.*; - -/** - * Sets the icon of Virtual Environment directories in the project view. - */ -public class VenvIconProvider extends IconProvider implements IconLayerProvider { - @Nullable - @Override - public Icon getIcon(@NotNull PsiElement element, int flags) { - if (element instanceof PsiDirectory) { - final String venvRootPath = ((PsiDirectory) element).getVirtualFile().getPath(); - if (PythonSdkUtil.getPythonExecutable(venvRootPath) != null) { - return PythonIcons.Python.Virtualenv; - } - } - return null; - } - - @Nullable - @Override - public Icon getLayerIcon(@NotNull Iconable element, boolean isLocked) { - if (element instanceof PsiDirectory) { - final String venvRootPath = ((PsiDirectory) element).getVirtualFile().getPath(); - if (PythonSdkUtil.getPythonExecutable(venvRootPath) != null) { - return PythonIcons.Python.Virtualenv; - } - } - return null; - } - - @NotNull - @Override - public String getLayerDescription() { - return "Python venv"; - } -} diff --git a/src/main/kotlin/com/github/pyvenvmanage/actions/ConfigurePythonActionAbstract.kt b/src/main/kotlin/com/github/pyvenvmanage/actions/ConfigurePythonActionAbstract.kt new file mode 100644 index 0000000..fc91561 --- /dev/null +++ b/src/main/kotlin/com/github/pyvenvmanage/actions/ConfigurePythonActionAbstract.kt @@ -0,0 +1,79 @@ +package com.github.pyvenvmanage.actions + +import com.intellij.notification.NotificationGroupManager +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.project.Project +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil +import com.intellij.openapi.ui.MessageType +import com.intellij.openapi.vfs.VirtualFile + +import com.jetbrains.python.configuration.PyConfigurableInterpreterList +import com.jetbrains.python.sdk.PythonSdkType +import com.jetbrains.python.sdk.PythonSdkUtil +import com.jetbrains.python.statistics.executionType +import com.jetbrains.python.statistics.interpreterType + +abstract class ConfigurePythonActionAbstract : AnAction() { + override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT + + override fun update(e: AnActionEvent) { + e.presentation.isEnabledAndVisible = isEventOnVirtualEnvironment(e) + } + + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + var selectedPath = e.getData(CommonDataKeys.VIRTUAL_FILE) ?: return + selectedPath = if (selectedPath.isDirectory) selectedPath else selectedPath.parent + val pythonExecutable = PythonSdkUtil.getPythonExecutable(selectedPath.path) ?: return + val sdk: Sdk = + ( + PyConfigurableInterpreterList + .getInstance(project) + .model.projectSdks.values + .firstOrNull { it.homePath == pythonExecutable } + ?: SdkConfigurationUtil.createAndAddSDK(pythonExecutable, PythonSdkType.getInstance()) + ) ?: return + + val notificationFor = setInterpreter(project, selectedPath, sdk) ?: return + NotificationGroupManager + .getInstance() + .getNotificationGroup("Python SDK change") + .createNotification( + "Updated SDK for $notificationFor to: ${sdk.name} " + + "of type ${sdk.interpreterType.toString().lowercase()} " + + sdk.executionType.toString().lowercase(), + MessageType.INFO, + ).notify(project) + } + + abstract fun setInterpreter( + project: Project, + selectedPath: VirtualFile, + sdk: Sdk, + ): String? + + private fun isEventOnVirtualEnvironment(e: AnActionEvent): Boolean { + // True when the selected path is the: + // - virtual environment root + // - virtual environment binary (Scripts) folder + // - any files within the binary folder. + return when (val selectedPath = e.getData(CommonDataKeys.VIRTUAL_FILE)) { + null -> false + else -> + when (selectedPath.isDirectory) { + true -> { + // check if there is a python executable available under this folder -> name match for binary + PythonSdkUtil.getPythonExecutable(selectedPath.path) != null + } + false -> { + // check for presence of the activate_this.py + activate alongside or pyvenv.cfg above + PythonSdkUtil.isVirtualEnv(selectedPath.path) + } + } + } + } +} diff --git a/src/main/kotlin/com/github/pyvenvmanage/actions/ConfigurePythonActionModule.kt b/src/main/kotlin/com/github/pyvenvmanage/actions/ConfigurePythonActionModule.kt new file mode 100644 index 0000000..414a919 --- /dev/null +++ b/src/main/kotlin/com/github/pyvenvmanage/actions/ConfigurePythonActionModule.kt @@ -0,0 +1,19 @@ +package com.github.pyvenvmanage.actions + +import com.intellij.openapi.project.Project +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.roots.ModuleRootModificationUtil +import com.intellij.openapi.roots.ProjectFileIndex +import com.intellij.openapi.vfs.VirtualFile + +class ConfigurePythonActionModule : ConfigurePythonActionAbstract() { + override fun setInterpreter( + project: Project, + selectedPath: VirtualFile, + sdk: Sdk, + ): String? { + val module = ProjectFileIndex.getInstance(project).getModuleForFile(selectedPath, false) ?: return null + ModuleRootModificationUtil.setModuleSdk(module, sdk) + return "module ${module.name}" + } +} diff --git a/src/main/kotlin/com/github/pyvenvmanage/actions/ConfigurePythonActionProject.kt b/src/main/kotlin/com/github/pyvenvmanage/actions/ConfigurePythonActionProject.kt new file mode 100644 index 0000000..6841ef1 --- /dev/null +++ b/src/main/kotlin/com/github/pyvenvmanage/actions/ConfigurePythonActionProject.kt @@ -0,0 +1,17 @@ +package com.github.pyvenvmanage.actions + +import com.intellij.openapi.project.Project +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil +import com.intellij.openapi.vfs.VirtualFile + +class ConfigurePythonActionProject : ConfigurePythonActionAbstract() { + override fun setInterpreter( + project: Project, + selectedPath: VirtualFile, + sdk: Sdk, + ): String { + SdkConfigurationUtil.setDirectoryProjectSdk(project, sdk) + return "project ${project.name}" + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 7e6ab83..0c06e9c 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -1,43 +1,33 @@ - - com.github.pyvenvmanage.pyvenv PyVenv Manage 2 pyvenvmanage - - - - PythonCore - com.intellij.modules.python + PythonCore + com.intellij.modules.platform - - - + + id="com.github.pyvenvmanage.actions.ConfigurePythonActionProject" + class="com.github.pyvenvmanage.actions.ConfigurePythonActionProject" + text="Set as Project Interpreter" + description="Configure this Python to be the projects interpreter." + icon="com.jetbrains.python.icons.PythonIcons.Python.Virtualenv" + > + id="com.github.pyvenvmanage.actions.ConfigurePythonActionModule" + class="com.github.pyvenvmanage.actions.ConfigurePythonActionModule" + text="Set as Module Interpreter" + description="Configure this Python to be the current modules interpreter." + icon="com.jetbrains.python.icons.PythonIcons.Python.Virtualenv" + > - diff --git a/src/main/resources/META-INF/pluginIcon.svg b/src/main/resources/META-INF/pluginIcon.svg new file mode 100644 index 0000000..3aba0ff --- /dev/null +++ b/src/main/resources/META-INF/pluginIcon.svg @@ -0,0 +1,50 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/kotlin/com/github/pyvenvmanage/UITest.kt b/src/test/kotlin/com/github/pyvenvmanage/UITest.kt new file mode 100644 index 0000000..31f8916 --- /dev/null +++ b/src/test/kotlin/com/github/pyvenvmanage/UITest.kt @@ -0,0 +1,114 @@ +package com.github.pyvenvmanage + +import java.io.File +import java.nio.file.Files +import java.nio.file.Path +import java.time.Duration +import java.time.Duration.ofMinutes +import java.util.concurrent.TimeUnit +import javax.imageio.ImageIO + +import org.assertj.swing.core.MouseButton +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Timeout +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.api.extension.TestWatcher + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.search.locators.byXpath +import com.intellij.remoterobot.steps.CommonSteps +import com.intellij.remoterobot.stepsProcessing.StepLogger +import com.intellij.remoterobot.stepsProcessing.StepWorker +import com.intellij.remoterobot.utils.waitFor + +import com.github.pyvenvmanage.pages.actionMenu +import com.github.pyvenvmanage.pages.actionMenuItem +import com.github.pyvenvmanage.pages.dialog +import com.github.pyvenvmanage.pages.idea +import com.github.pyvenvmanage.pages.welcomeFrame + +@ExtendWith(UITest.IdeTestWatcher::class) +@Timeout(value = 15, unit = TimeUnit.MINUTES) +class UITest { + class IdeTestWatcher : TestWatcher { + override fun testFailed( + context: ExtensionContext, + cause: Throwable?, + ) { + ImageIO.write( + remoteRobot.getScreenshot(), + "png", + File("build/reports", "${context.displayName}.png"), + ) + } + } + + companion object { + private var tmpDir: Path = Files.createTempDirectory("ui-test") + private lateinit var remoteRobot: RemoteRobot + + @BeforeAll + @JvmStatic + fun startIdea() { + // ./gradlew runIdeForUiTests requires already running + StepWorker.registerProcessor(StepLogger()) + remoteRobot = RemoteRobot("http://localhost:8082") + waitFor(Duration.ofSeconds(120), Duration.ofSeconds(5)) { + runCatching { + remoteRobot.callJs("true") + }.getOrDefault(false) + } + remoteRobot.welcomeFrame { + createNewProjectLink.click() + dialog("New Project") { + findText("Python").click() + button(byXpath("//div[@visible_text='Existing']")).click() + textField("Name:", contains = false).text = "demo" + textField("Location:", contains = false).text = tmpDir.toString() + button(byXpath("//div[@visible_text='New']")).click() + button("Create").click() + } + } + remoteRobot.idea { + waitFor(ofMinutes(1)) { isDumbMode().not() } +// step("Create App file") { +// with(projectViewTree) { +// findText(projectName).click(MouseButton.RIGHT_BUTTON) +// } +// remoteRobot.actionMenu("New").click() +// remoteRobot.actionMenuItem("File").click() +// keyboard { +// enterText("main.py") +// enter() +// } +// } + } + } + + @AfterAll + @JvmStatic + fun cleanUp() { + CommonSteps(remoteRobot).closeProject() + remoteRobot.welcomeFrame { + findText("demo").click(MouseButton.RIGHT_BUTTON) + remoteRobot.actionMenuItem("Remove from Recent Projects…").click() + button("Remove").click() + } + tmpDir.toFile().deleteRecursively() + } + } + + @Test + fun testCheck() { + remoteRobot.idea { + with(projectViewTree) { + findText("venv").click(MouseButton.RIGHT_BUTTON) + println("ok") + remoteRobot.actionMenu("Set as Project Interpreter").click() + } + } + } +} diff --git a/src/test/kotlin/com/github/pyvenvmanage/pages/ActionMenuFixture.kt b/src/test/kotlin/com/github/pyvenvmanage/pages/ActionMenuFixture.kt new file mode 100644 index 0000000..40f2889 --- /dev/null +++ b/src/test/kotlin/com/github/pyvenvmanage/pages/ActionMenuFixture.kt @@ -0,0 +1,35 @@ +package com.github.pyvenvmanage.pages +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.ComponentFixture +import com.intellij.remoterobot.fixtures.FixtureName +import com.intellij.remoterobot.search.locators.byXpath +import com.intellij.remoterobot.utils.waitFor + +fun RemoteRobot.actionMenu(text: String): ActionMenuFixture { + val xpath = byXpath("text '$text'", "//div[@class='ActionMenu' and @text='$text']") + waitFor { + findAll(xpath).isNotEmpty() + } + return findAll(xpath).first() +} + +fun RemoteRobot.actionMenuItem(text: String): ActionMenuItemFixture { + val xpath = byXpath("text '$text'", "//div[@class='ActionMenuItem' and @text='$text']") + waitFor { + findAll(xpath).isNotEmpty() + } + return findAll(xpath).first() +} + +@FixtureName("ActionMenu") +class ActionMenuFixture( + remoteRobot: RemoteRobot, + remoteComponent: RemoteComponent, +) : ComponentFixture(remoteRobot, remoteComponent) + +@FixtureName("ActionMenuItem") +class ActionMenuItemFixture( + remoteRobot: RemoteRobot, + remoteComponent: RemoteComponent, +) : ComponentFixture(remoteRobot, remoteComponent) diff --git a/src/test/kotlin/com/github/pyvenvmanage/pages/DialogFixture.kt b/src/test/kotlin/com/github/pyvenvmanage/pages/DialogFixture.kt new file mode 100644 index 0000000..b43a347 --- /dev/null +++ b/src/test/kotlin/com/github/pyvenvmanage/pages/DialogFixture.kt @@ -0,0 +1,31 @@ +package com.github.pyvenvmanage.pages + +import java.time.Duration + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.CommonContainerFixture +import com.intellij.remoterobot.fixtures.ContainerFixture +import com.intellij.remoterobot.fixtures.FixtureName +import com.intellij.remoterobot.search.locators.byXpath +import com.intellij.remoterobot.stepsProcessing.step + +fun ContainerFixture.dialog( + title: String, + timeout: Duration = Duration.ofSeconds(20), + function: DialogFixture.() -> Unit = {}, +): DialogFixture = + step("Search for dialog with title $title") { + find(DialogFixture.byTitle(title), timeout).apply(function) + } + +@FixtureName("Dialog") +class DialogFixture( + remoteRobot: RemoteRobot, + remoteComponent: RemoteComponent, +) : CommonContainerFixture(remoteRobot, remoteComponent) { + companion object { + @JvmStatic + fun byTitle(title: String) = byXpath("title $title", "//div[@title='$title' and @class='MyDialog']") + } +} diff --git a/src/test/kotlin/com/github/pyvenvmanage/pages/IdeaFrame.kt b/src/test/kotlin/com/github/pyvenvmanage/pages/IdeaFrame.kt new file mode 100644 index 0000000..9de7448 --- /dev/null +++ b/src/test/kotlin/com/github/pyvenvmanage/pages/IdeaFrame.kt @@ -0,0 +1,100 @@ +package com.github.pyvenvmanage.pages + +import java.time.Duration + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.CommonContainerFixture +import com.intellij.remoterobot.fixtures.ContainerFixture +import com.intellij.remoterobot.fixtures.DefaultXpath +import com.intellij.remoterobot.fixtures.FixtureName +import com.intellij.remoterobot.fixtures.JMenuBarFixture +import com.intellij.remoterobot.search.locators.byXpath +import com.intellij.remoterobot.stepsProcessing.step +import com.intellij.remoterobot.utils.waitFor + +fun RemoteRobot.idea(function: IdeaFrame.() -> Unit) { + find(timeout = Duration.ofSeconds(10)).apply(function) +} + +@FixtureName("Idea frame") +@DefaultXpath("IdeFrameImpl type", "//div[@class='IdeFrameImpl']") +class IdeaFrame( + remoteRobot: RemoteRobot, + remoteComponent: RemoteComponent, +) : CommonContainerFixture(remoteRobot, remoteComponent) { + val projectViewTree + get() = find(byXpath("ProjectViewTree", "//div[@class='ProjectViewTree']")) + + val projectName + get() = step("Get project name") { return@step callJs("component.getProject().getName()") } + + val menuBar: JMenuBarFixture + get() = + step("Menu...") { + return@step remoteRobot.find(JMenuBarFixture::class.java, JMenuBarFixture.byType()) + } + + @JvmOverloads + fun dumbAware( + timeout: Duration = Duration.ofMinutes(5), + function: () -> Unit, + ) { + step("Wait for smart mode") { + waitFor(duration = timeout, interval = Duration.ofSeconds(5)) { + runCatching { isDumbMode().not() }.getOrDefault(false) + } + function() + step("..wait for smart mode again") { + waitFor(duration = timeout, interval = Duration.ofSeconds(5)) { + isDumbMode().not() + } + } + } + } + + fun isDumbMode(): Boolean = + callJs( + """ + const frameHelper = com.intellij.openapi.wm.impl.ProjectFrameHelper.getFrameHelper(component) + if (frameHelper) { + const project = frameHelper.getProject() + project ? com.intellij.openapi.project.DumbService.isDumb(project) : true + } else { + true + } + """, + true, + ) + + fun openFile(path: String) { + runJs( + """ + importPackage(com.intellij.openapi.fileEditor) + importPackage(com.intellij.openapi.vfs) + importPackage(com.intellij.openapi.wm.impl) + importClass(com.intellij.openapi.application.ApplicationManager) + + const path = '$path' + const frameHelper = ProjectFrameHelper.getFrameHelper(component) + if (frameHelper) { + const project = frameHelper.getProject() + const projectPath = project.getBasePath() + const file = LocalFileSystem.getInstance().findFileByPath(projectPath + '/' + path) + const openFileFunction = new Runnable({ + run: function() { + FileEditorManager.getInstance(project).openTextEditor( + new OpenFileDescriptor( + project, + file + ), true + ) + } + }) + ApplicationManager.getApplication().invokeLater(openFileFunction) + } + """, + true, + ) + } +} diff --git a/src/test/kotlin/com/github/pyvenvmanage/pages/WelcomeFrame.kt b/src/test/kotlin/com/github/pyvenvmanage/pages/WelcomeFrame.kt new file mode 100644 index 0000000..3c2b7da --- /dev/null +++ b/src/test/kotlin/com/github/pyvenvmanage/pages/WelcomeFrame.kt @@ -0,0 +1,25 @@ +package com.github.pyvenvmanage.pages + +import java.time.Duration + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.ActionLinkFixture +import com.intellij.remoterobot.fixtures.CommonContainerFixture +import com.intellij.remoterobot.fixtures.DefaultXpath +import com.intellij.remoterobot.fixtures.FixtureName +import com.intellij.remoterobot.search.locators.byXpath + +fun RemoteRobot.welcomeFrame(function: WelcomeFrame.() -> Unit) { + find(WelcomeFrame::class.java, Duration.ofSeconds(10)).apply(function) +} + +@FixtureName("Welcome Frame") +@DefaultXpath("type", "//div[@class='FlatWelcomeFrame']") +class WelcomeFrame( + remoteRobot: RemoteRobot, + remoteComponent: RemoteComponent, +) : CommonContainerFixture(remoteRobot, remoteComponent) { + private val xpath = "//div[@myiconbutton='createNewProjectTabSelected.svg']" + val createNewProjectLink: ActionLinkFixture get() = actionLink(byXpath("New Project", xpath)) +}