├── .appveyor.yml ├── .github ├── dependabot.yml └── workflows │ └── CI.yml ├── .gitignore ├── .yo-rc.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── doc ├── docs │ ├── about │ │ ├── compatibility.md │ │ ├── history.md │ │ └── license.md │ ├── getting-started.md │ ├── guide │ │ ├── ci.md │ │ ├── configuration.md │ │ ├── docker.md │ │ ├── modules.md │ │ ├── multimodule.md │ │ ├── plugindev.md │ │ ├── python.md │ │ ├── stats.md │ │ └── usage.md │ └── index.md └── mkdocs.yml ├── main └── groovy │ └── ru │ └── vyarus │ └── gradle │ └── plugin │ └── python │ ├── PythonExtension.groovy │ ├── PythonPlugin.groovy │ ├── cmd │ ├── LoggedCommandCleaner.groovy │ ├── Pip.groovy │ ├── Python.groovy │ ├── Venv.groovy │ ├── VirtualTool.groovy │ ├── Virtualenv.groovy │ ├── docker │ │ ├── ContainerManager.groovy │ │ ├── DockerConfig.groovy │ │ ├── DockerFactory.groovy │ │ └── PythonContainer.groovy │ ├── env │ │ ├── Environment.groovy │ │ ├── GradleEnvironment.groovy │ │ └── SimpleEnvironment.groovy │ └── exec │ │ └── PythonBinary.groovy │ ├── service │ ├── EnvService.groovy │ ├── stat │ │ ├── PythonStat.groovy │ │ └── StatsPrinter.groovy │ └── value │ │ ├── CacheValueSource.groovy │ │ └── StatsValueSource.groovy │ ├── task │ ├── BasePythonTask.groovy │ ├── CheckPythonTask.groovy │ ├── PythonTask.groovy │ ├── env │ │ ├── EnvSupport.groovy │ │ ├── FallbackException.groovy │ │ ├── VenvSupport.groovy │ │ └── VirtualenvSupport.groovy │ └── pip │ │ ├── BasePipTask.groovy │ │ ├── PipInstallTask.groovy │ │ ├── PipListTask.groovy │ │ ├── PipModule.groovy │ │ ├── PipUpdatesTask.groovy │ │ └── module │ │ ├── FeaturePipModule.groovy │ │ ├── ModuleFactory.groovy │ │ └── VcsPipModule.groovy │ └── util │ ├── CliUtils.groovy │ ├── DurationFormatter.groovy │ ├── OutputLogger.groovy │ ├── PythonExecutionFailed.groovy │ └── RequirementsReader.groovy └── test └── groovy └── ru └── vyarus └── gradle └── plugin └── python ├── AbsoluteVirtualenvLocationKitTest.groovy ├── AbstractKitTest.groovy ├── AbstractTest.groovy ├── ConfigurationCacheSupportKitTest.groovy ├── GlobalVirtualenvTest.groovy ├── LegacyKitTest.groovy ├── PipUpgradeTest.groovy ├── PythonPluginKitTest.groovy ├── PythonPluginTest.groovy ├── RequirementsKitTest.groovy ├── StatsKitTest.groovy ├── StatsWinKitTest.groovy ├── UpstreamKitTest.groovy ├── UseCustomPythonForTaskKitTest.groovy ├── VenvFromVenvCreationTest.groovy ├── WorkflowKitTest.groovy ├── cmd ├── AbstractCliMockSupport.groovy ├── PipCliTest.groovy ├── PipExecTest.groovy ├── PipExecUnderVirtualenvTest.groovy ├── PythonCliTest.groovy ├── PythonExecTest.groovy ├── VenvCliTest.groovy ├── VenvExecTest.groovy ├── VirtualenvCliTest.groovy └── VirtualenvExecTest.groovy ├── docker ├── DockerAutoRestartKitTest.groovy ├── DockerExclusiveExecutionKitTest.groovy ├── DockerMultiModuleKitTest.groovy └── DockerRunKitTest.groovy ├── multimodule ├── MultiplePythonInstallationsKitTest.groovy ├── ParallelExecutionKitTest.groovy ├── PythonUsedInSubmoduleKitTest.groovy └── RequirementsInSubmoduleKitTest.groovy ├── task ├── CheckTaskKitTest.groovy ├── ModuleParseTest.groovy ├── PipInstallTaskKitTest.groovy ├── PipListTaskKitTest.groovy ├── PipModulesInstallTest.groovy ├── PipUpdatesTaskKitTest.groovy ├── PythonTaskEnvironmentKitTest.groovy └── PythonTaskKitTest.groovy └── util ├── CliUtilsTest.groovy ├── DurationFormatTest.groovy ├── ExecRes.groovy ├── OutputLoggerTest.groovy ├── StatsPrinterTest.groovy └── TestLogger.groovy /.appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{build}' 2 | image: Visual Studio 2019 3 | 4 | environment: 5 | matrix: 6 | - job_name: Java 8, python 3.8 7 | JAVA_HOME: C:\Program Files\Java\jdk1.8.0 8 | PYTHON: "C:\\Python38-x64" 9 | PIP: 24.0 10 | - job_name: Java 11, python 3.11 11 | JAVA_HOME: C:\Program Files\Java\jdk11 12 | PYTHON: "C:\\Python311-x64" 13 | PIP: 24.0 14 | - job_name: Java 17, python 3.12 15 | JAVA_HOME: C:\Program Files\Java\jdk17 16 | appveyor_build_worker_image: Visual Studio 2019 17 | PYTHON: "C:\\Python312-x64" 18 | PIP: 24.0 19 | 20 | install: 21 | - set PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH% 22 | - python --version 23 | - python -m pip install -U pip==%PIP% 24 | - python -m pip --version 25 | - python -m pip install -U virtualenv==20.25.1 26 | 27 | build_script: 28 | - ./gradlew assemble --no-daemon 29 | test_script: 30 | - ./gradlew check --no-daemon 31 | 32 | on_success: 33 | - ./gradlew jacocoTestReport --no-daemon 34 | - ps: | 35 | $ProgressPreference = 'SilentlyContinue' 36 | Invoke-WebRequest -Uri https://uploader.codecov.io/latest/windows/codecov.exe -Outfile codecov.exe 37 | .\codecov.exe -f build\reports\jacoco\test\jacocoTestReport.xml -F windows 38 | 39 | cache: 40 | - C:\Users\appveyor\.gradle\caches 41 | - C:\Users\appveyor\.gradle\wrapper -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gradle 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "23:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | name: Java ${{ matrix.java }}, python ${{ matrix.python }} 11 | strategy: 12 | matrix: 13 | java: [8, 11, 17] 14 | python: ['3.8', '3.11', '3.12'] 15 | pip: ['24.0'] 16 | virtualenv: ['20.25.1'] 17 | 18 | exclude: 19 | - java: 8 20 | python: '3.11' 21 | - java: 8 22 | python: '3.12' 23 | - java: 11 24 | python: '3.8' 25 | - java: 11 26 | python: '3.12' 27 | - java: 17 28 | python: '3.8' 29 | - java: 17 30 | python: '3.11' 31 | 32 | steps: 33 | - uses: actions/checkout@v3 34 | 35 | - name: Set up JDK ${{ matrix.java }} 36 | uses: actions/setup-java@v1 37 | with: 38 | java-version: ${{ matrix.java }} 39 | 40 | - name: Set up Python ${{ matrix.python }} 41 | uses: actions/setup-python@v4 42 | with: 43 | python-version: ${{matrix.python}} 44 | 45 | - name: Build 46 | run: | 47 | chmod +x gradlew 48 | python --version 49 | pip install --upgrade pip==${{ matrix.pip }} 50 | pip --version 51 | pip install virtualenv==${{ matrix.virtualenv }} 52 | ./gradlew assemble --no-daemon 53 | 54 | - name: Test 55 | env: 56 | GH_ACTIONS: true 57 | run: ./gradlew check --no-daemon 58 | 59 | - name: Build coverage report 60 | if: github.ref == 'refs/heads/master' && github.event_name != 'pull_request' 61 | run: ./gradlew jacocoTestReport --no-daemon 62 | 63 | - uses: codecov/codecov-action@v4 64 | if: github.ref == 'refs/heads/master' && github.event_name != 'pull_request' 65 | with: 66 | files: build/reports/jacoco/test/jacocoTestReport.xml 67 | flags: LINUX 68 | fail_ci_if_error: true 69 | token: ${{ secrets.CODECOV_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created with https://www.gitignore.io 2 | 3 | ### Gradle ### 4 | .gradle/ 5 | build/ 6 | 7 | # Ignore Gradle GUI config 8 | gradle-app.setting 9 | 10 | ### JetBrains ### 11 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 12 | 13 | /*.iml 14 | 15 | ## Directory-based project format: 16 | .idea/ 17 | 18 | ## File-based project format: 19 | *.ipr 20 | *.iws 21 | 22 | ## Plugin-specific files: 23 | 24 | # IntelliJ 25 | out/ 26 | 27 | # mpeltonen/sbt-idea plugin 28 | .idea_modules/ 29 | 30 | # JIRA plugin 31 | atlassian-ide-plugin.xml 32 | 33 | # Crashlytics plugin (for Android Studio and IntelliJ) 34 | com_crashlytics_export_strings.xml 35 | 36 | 37 | ### Eclipse ### 38 | *.pydevproject 39 | .metadata 40 | bin/ 41 | tmp/ 42 | *.tmp 43 | *.bak 44 | *.swp 45 | *~.nib 46 | local.properties 47 | .settings/ 48 | .loadpath 49 | 50 | # External tool builders 51 | .externalToolBuilders/ 52 | 53 | # Locally stored "Eclipse launch configurations" 54 | *.launch 55 | 56 | # CDT-specific 57 | .cproject 58 | 59 | # PDT-specific 60 | .buildpath 61 | 62 | # sbteclipse plugin 63 | .target 64 | 65 | # TeXlipse plugin 66 | .texlipse 67 | 68 | ### Java ### 69 | *.class 70 | 71 | # Mobile Tools for Java (J2ME) 72 | .mtj.tmp/ 73 | 74 | # Package Files # 75 | *.war 76 | *.ear 77 | 78 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 79 | hs_err_pid* 80 | 81 | 82 | ### NetBeans ### 83 | nbproject/private/ 84 | nbbuild/ 85 | dist/ 86 | nbdist/ 87 | nbactions.xml 88 | nb-configuration.xml 89 | 90 | 91 | ### OSX ### 92 | .DS_Store 93 | .AppleDouble 94 | .LSOverride 95 | 96 | # Icon must end with two \r 97 | Icon 98 | 99 | 100 | # Thumbnails 101 | ._* 102 | 103 | # Files that might appear on external disk 104 | .Spotlight-V100 105 | .Trashes 106 | 107 | # Directories potentially created on remote AFP share 108 | .AppleDB 109 | .AppleDesktop 110 | Network Trash Folder 111 | Temporary Items 112 | .apdisk 113 | 114 | 115 | ### Windows ### 116 | # Windows image file caches 117 | Thumbs.db 118 | ehthumbs.db 119 | 120 | # Folder config file 121 | Desktop.ini 122 | 123 | # Recycle Bin used on file shares 124 | $RECYCLE.BIN/ 125 | 126 | # Windows Installer files 127 | *.cab 128 | *.msi 129 | *.msm 130 | *.msp 131 | 132 | # Windows shortcuts 133 | *.lnk 134 | 135 | 136 | ### Linux ### 137 | *~ 138 | 139 | # KDE directory preferences 140 | .directory 141 | 142 | ### JEnv ### 143 | # JEnv local Java version configuration file 144 | .java-version 145 | 146 | # Used by previous versions of JEnv 147 | .jenv-version 148 | -------------------------------------------------------------------------------- /.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "generator-gradle-plugin": { 3 | "githubUser": "xvik", 4 | "authorName": "Vyacheslav Rusakov", 5 | "authorEmail": "vyarus@gmail.com", 6 | "projectName": "gradle-use-python-plugin", 7 | "projectGroup": "ru.vyarus", 8 | "projectPackage": "ru.vyarus.gradle.plugin.python", 9 | "projectVersion": "0.1.0", 10 | "projectDesc": "Use python modules in gradle build", 11 | "pluginPortalDesc": "Manage pip dependencies and use python in gradle build", 12 | "pluginPortalTags": "python, virtualenv", 13 | "pluginPortalUseCustomGroup": true, 14 | "usedGeneratorVersion": "2.0.0", 15 | "centralPublish": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-2024, Vyacheslav Rusakov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gradle use-python plugin 2 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](http://www.opensource.org/licenses/MIT) 3 | [![CI](https://github.com/xvik/gradle-use-python-plugin/actions/workflows/CI.yml/badge.svg)](https://github.com/xvik/gradle-use-python-plugin/actions/workflows/CI.yml) 4 | [![Appveyor build status](https://ci.appveyor.com/api/projects/status/github/xvik/gradle-use-python-plugin?svg=true)](https://ci.appveyor.com/project/xvik/gradle-use-python-plugin) 5 | [![codecov](https://codecov.io/gh/xvik/gradle-use-python-plugin/branch/master/graph/badge.svg)](https://codecov.io/gh/xvik/gradle-use-python-plugin) 6 | 7 | **DOCUMENTATION**: https://xvik.github.io/gradle-use-python-plugin/ 8 | 9 | ### About 10 | 11 | Plugin **does not install python and pip** itself and use globally installed python (by default). 12 | It's easier to prepare python manually because python have good compatibility (from user perspective) and does not need to 13 | be updated often. 14 | 15 | Also, plugin could run python inside docker container to avoid local python installation. 16 | 17 | The only plugin intention is to simplify python usage from gradle. By default, plugin creates python virtualenv 18 | inside the project and installs all modules there so each project has its own python (copy) and could not be 19 | affected by other projects or system changes. 20 | 21 | Features: 22 | 23 | * Works with directly installed python or docker container (with python) 24 | * Creates local (project-specific) virtualenv (project-specific python copy) 25 | * Installs required pip modules (venv by default (with fallback to virtualenv), but could be global installation) 26 | * Support requirements.txt file (limited by default) 27 | * Compatible with gradle configuration cache 28 | * Could be used as basement for building plugins for specific python modules (like 29 | [mkdocs plugin](https://github.com/xvik/gradle-mkdocs-plugin)) 30 | 31 | **[Who's using (usage examples)](https://github.com/xvik/gradle-use-python-plugin/discussions/18)** 32 | 33 | ##### Summary 34 | 35 | * Configuration: `python` 36 | * Tasks: 37 | - `checkPython` - validate python installation (and create virtualenv if required) 38 | - `cleanPython` - clean created python environment 39 | - `pipInstall` - install declared pip modules 40 | - `pipUpdates` - show the latest available versions for the registered modules 41 | - `pipList` - show all installed modules (the same as pipInstall shows after installation) 42 | - `type:PythonTask` - call python command/script/module 43 | - `type:PipInstallTask` - may be used for custom pip modules installation workflow 44 | 45 | ### Setup 46 | 47 | [![Maven Central](https://img.shields.io/maven-central/v/ru.vyarus/gradle-use-python-plugin.svg)](https://maven-badges.herokuapp.com/maven-central/ru.vyarus/gradle-use-python-plugin) 48 | [![Gradle Plugin Portal](https://img.shields.io/maven-metadata/v/https/plugins.gradle.org/m2/ru/vyarus/use-python/ru.vyarus.use-python.gradle.plugin/maven-metadata.xml.svg?colorB=007ec6&label=plugins%20portal)](https://plugins.gradle.org/plugin/ru.vyarus.use-python) 49 | 50 | ```groovy 51 | buildscript { 52 | repositories { 53 | mavenCentral() 54 | } 55 | dependencies { 56 | classpath 'ru.vyarus:gradle-use-python-plugin:4.1.0' 57 | } 58 | } 59 | apply plugin: 'ru.vyarus.use-python' 60 | ``` 61 | 62 | OR 63 | 64 | ```groovy 65 | plugins { 66 | id 'ru.vyarus.use-python' version '4.1.0' 67 | } 68 | ``` 69 | 70 | #### Compatibility 71 | 72 | Plugin compiled for java 8, compatible with java 11, 17. 73 | Supports python 2 (not tested anymore, but should work) and 3 on windows and linux (macos) 74 | 75 | Gradle | Version 76 | --------|------- 77 | 7-8 | 4.1.0 78 | 5.3 | [3.0.0](https://xvik.github.io/gradle-use-python-plugin/3.0.0/) 79 | 5-5.2 | [2.3.0](https://xvik.github.io/gradle-use-python-plugin/2.3.0/) 80 | 4.x | [1.2.0](https://github.com/xvik/gradle-use-python-plugin/tree/1.2.0) 81 | 82 | #### Snapshots 83 | 84 |
85 | Snapshots may be used through JitPack 86 | 87 | * Go to [JitPack project page](https://jitpack.io/#ru.vyarus/gradle-use-python-plugin) 88 | * Select `Commits` section and click `Get it` on commit you want to use 89 | or use `master-SNAPSHOT` to use the most recent snapshot 90 | 91 | * Add to `settings.gradle` (top most!) (exact commit hash might be used as version): 92 | 93 | ```groovy 94 | pluginManagement { 95 | resolutionStrategy { 96 | eachPlugin { 97 | if (requested.id.id == 'ru.vyarus.use-python') { 98 | useModule('ru.vyarus:gradle-use-python-plugin:master-SNAPSHOT') 99 | } 100 | } 101 | } 102 | repositories { 103 | gradlePluginPortal() 104 | maven { url 'https://jitpack.io' } 105 | } 106 | } 107 | ``` 108 | * Use plugin without declaring version: 109 | 110 | ```groovy 111 | plugins { 112 | id 'ru.vyarus.use-python' 113 | } 114 | ``` 115 | 116 |
117 | 118 | #### Python & Pip 119 | 120 | Make sure python and pip are installed: 121 | 122 | ```bash 123 | python --version 124 | pip --version 125 | ``` 126 | 127 | On *nix `python` usually reference python2. For python3: 128 | 129 | ```bash 130 | python3 --version 131 | pip3 --version 132 | ``` 133 | 134 | OR enable docker support to run python inside docker container 135 | 136 | ### Usage 137 | 138 | Read [documentation](https://xvik.github.io/gradle-use-python-plugin/) 139 | 140 | ### Might also like 141 | 142 | * [quality-plugin](https://github.com/xvik/gradle-quality-plugin) - java and groovy source quality checks 143 | * [animalsniffer-plugin](https://github.com/xvik/gradle-animalsniffer-plugin) - java compatibility checks 144 | * [pom-plugin](https://github.com/xvik/gradle-pom-plugin) - improves pom generation 145 | * [java-lib-plugin](https://github.com/xvik/gradle-java-lib-plugin) - avoid boilerplate for java or groovy library project 146 | * [github-info-plugin](https://github.com/xvik/gradle-github-info-plugin) - pre-configure common plugins with github related info 147 | 148 | --- 149 | [![gradle plugin generator](http://img.shields.io/badge/Powered%20by-%20Gradle%20plugin%20generator-green.svg?style=flat-square)](https://github.com/xvik/generator-gradle-plugin) 150 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.gradle.plugin-publish' version '1.3.1' 3 | id 'java-gradle-plugin' 4 | id 'groovy' 5 | id 'jacoco' 6 | id 'signing' 7 | id 'net.researchgate.release' version '3.1.0' 8 | id 'ru.vyarus.quality' version '5.0.0' 9 | id 'io.github.gradle-nexus.publish-plugin' version '2.0.0' 10 | id 'ru.vyarus.java-lib' version '3.0.0' 11 | id 'ru.vyarus.github-info' version '2.0.0' 12 | id 'com.github.ben-manes.versions' version '0.52.0' 13 | id "pl.droidsonroids.jacoco.testkit" version "1.0.12" 14 | id 'ru.vyarus.mkdocs' version '4.0.1' 15 | } 16 | 17 | java { 18 | sourceCompatibility = 1.8 19 | } 20 | 21 | wrapper { 22 | gradleVersion = '8.6' 23 | distributionType = Wrapper.DistributionType.BIN 24 | } 25 | 26 | repositories { mavenLocal(); mavenCentral(); gradlePluginPortal() } 27 | dependencies { 28 | implementation 'org.testcontainers:testcontainers:1.21.0' 29 | 30 | testImplementation('org.spockframework:spock-core:2.3-groovy-3.0') { 31 | exclude group: 'org.codehaus.groovy' 32 | } 33 | testImplementation 'net.bytebuddy:byte-buddy:1.17.5' 34 | testImplementation 'org.objenesis:objenesis:3.4' 35 | } 36 | 37 | group = 'ru.vyarus' 38 | description = 'Use python modules in gradle build' 39 | 40 | github { 41 | user 'xvik' 42 | license 'MIT' 43 | } 44 | 45 | mkdocs { 46 | extras = [ 47 | 'version': '4.1.0', 48 | 'image': 'python:3.12.7-alpine3.20' 49 | ] 50 | publish { 51 | docPath = mkdocs.extras['version'] 52 | rootRedirect = true 53 | rootRedirectTo = 'latest' 54 | versionAliases = ['latest'] 55 | hideOldBugfixVersions = true 56 | } 57 | } 58 | 59 | maven.pom { 60 | developers { 61 | developer { 62 | id = 'xvik' 63 | name = 'Vyacheslav Rusakov' 64 | email = 'vyarus@gmail.com' 65 | } 66 | } 67 | } 68 | 69 | nexusPublishing { 70 | repositories { 71 | sonatype { 72 | username = findProperty('sonatypeUser') 73 | password = findProperty('sonatypePassword') 74 | } 75 | } 76 | } 77 | 78 | // skip signing for jitpack (snapshots) 79 | tasks.withType(Sign) {onlyIf { !System.getenv('JITPACK') }} 80 | 81 | // Required signing properties for release: signing.keyId, signing.password and signing.secretKeyRingFile 82 | // (https://docs.gradle.org/current/userguide/signing_plugin.html#sec:signatory_credentials) 83 | 84 | javaLib { 85 | // don't publish gradle metadata artifact 86 | withoutGradleMetadata() 87 | } 88 | 89 | 90 | gradlePlugin { 91 | plugins { 92 | usePythonPlugin { 93 | id = 'ru.vyarus.use-python' 94 | displayName = project.description 95 | description = 'Manage pip dependencies and use python in gradle build' 96 | tags.set(['python', 'virtualenv']) 97 | implementationClass = 'ru.vyarus.gradle.plugin.python.PythonPlugin' 98 | } 99 | } 100 | } 101 | 102 | release.git.requireBranch.set('master') 103 | 104 | afterReleaseBuild { 105 | dependsOn = [ 106 | 'publishMavenPublicationToSonatypeRepository', 107 | 'closeAndReleaseSonatypeStagingRepository', 108 | publishPlugins] 109 | doLast { 110 | logger.warn "RELEASED $project.group:$project.name:$project.version" 111 | } 112 | } 113 | 114 | test { 115 | useJUnitPlatform() 116 | testLogging { 117 | events 'skipped', 'failed' 118 | exceptionFormat 'full' 119 | } 120 | maxHeapSize = '512m' 121 | doLast { 122 | sleep(1000) 123 | } 124 | } 125 | 126 | dependencyUpdates.revision = 'release' 127 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | version=4.1.1-SNAPSHOT -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xvik/gradle-use-python-plugin/121ceab6762567c24c54dd916cf94c90773ec4be/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /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 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | mavenLocal() 4 | gradlePluginPortal() 5 | } 6 | } 7 | 8 | rootProject.name = 'gradle-use-python-plugin' 9 | -------------------------------------------------------------------------------- /src/doc/docs/about/compatibility.md: -------------------------------------------------------------------------------- 1 | # Gradle compatibility 2 | 3 | Plugin compiled for java 8, compatible with java 11 and 17. 4 | Works with python 2 and 3 (but python 2 not tested anymore) on windows and linux (and macos). 5 | 6 | Gradle | Version 7 | --------|------- 8 | 7-8 | 4.1.0 9 | 5.3 | [3.0.0](https://xvik.github.io/gradle-use-python-plugin/3.0.0/) 10 | 5-5.2 | [2.3.0](https://xvik.github.io/gradle-use-python-plugin/2.3.0/) 11 | 4.x | [1.2.0](https://github.com/xvik/gradle-use-python-plugin/tree/1.2.0) 12 | -------------------------------------------------------------------------------- /src/doc/docs/about/license.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-2024, Vyacheslav Rusakov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/doc/docs/guide/ci.md: -------------------------------------------------------------------------------- 1 | # CI 2 | 3 | Example configuration, required to use python on CI servers. 4 | 5 | !!! warning 6 | [Docker support](docker.md) will not work on most **windows CI** servers (like appveyor). 7 | Linux CI is completely ok (e.g. works out of the box on github actions) 8 | 9 | ## GitHub actions 10 | 11 | ```yaml 12 | name: CI 13 | 14 | on: 15 | push: 16 | pull_request: 17 | 18 | jobs: 19 | build: 20 | runs-on: ubuntu-latest 21 | name: Java {{ '${{ matrix.java }}' }}, python {{ '${{ matrix.python }}' }} 22 | strategy: 23 | matrix: 24 | java: [8, 11] 25 | python: ['3.8', '3.12'] 26 | 27 | # reduce matrix, if required 28 | exclude: 29 | - java: 8 30 | python: '3.12' 31 | 32 | steps: 33 | - uses: actions/checkout@v3 34 | 35 | - name: Set up JDK {{ '${{ matrix.java }}' }} 36 | uses: actions/setup-java@v1 37 | with: 38 | java-version: {{ '${{ matrix.java }}' }} 39 | 40 | - name: Set up Python {{ '${{ matrix.python }}' }} 41 | uses: actions/setup-python@v4 42 | with: 43 | python-version: {{ '${{matrix.python}}' }} 44 | 45 | - name: Build 46 | run: | 47 | chmod +x gradlew 48 | python --version 49 | pip --version 50 | ./gradlew assemble --no-daemon 51 | 52 | - name: Test 53 | run: ./gradlew check --no-daemon 54 | ``` 55 | 56 | ## Appveyour 57 | 58 | To make plugin work on [appveyour](https://www.appveyor.com/) you'll need to add python to path: 59 | 60 | ```yaml 61 | environment: 62 | matrix: 63 | - job_name: Java 8, python 3.8 64 | JAVA_HOME: C:\Program Files\Java\jdk1.8.0 65 | PYTHON: "C:\\Python38-x64" 66 | - job_name: Java 17, python 3.12 67 | JAVA_HOME: C:\Program Files\Java\jdk17 68 | appveyor_build_worker_image: Visual Studio 2019 69 | PYTHON: "C:\\Python312-x64" 70 | 71 | install: 72 | - set PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH% 73 | - python --version 74 | ``` 75 | 76 | Now plugin would be able to find python binary. 77 | 78 | See [available pythons matrix](https://www.appveyor.com/docs/windows-images-software/#python) for more info. 79 | 80 | ## Travis 81 | 82 | To make plugin work on [travis](https://travis-ci.org/) you'll need to install python3 packages: 83 | 84 | ```yaml 85 | language: java 86 | dist: bionic 87 | jdk: openjdk8 88 | 89 | addons: 90 | apt: 91 | packages: 92 | - python3 93 | - python3-pip 94 | - python3-setuptools 95 | 96 | before_install: 97 | - python3 --version 98 | - pip3 --version 99 | - pip3 install -U pip 100 | ``` 101 | 102 | It will be python 3.6 by default (for bionic). 103 | 104 | ## Environment caching 105 | 106 | To avoid creating virtual environments on each execution, it makes sense to move 107 | environment location from the default `.gradle/python` (inside project) outside the project: 108 | 109 | ```groovy 110 | python.envPath = '~/.myProjectEnv' 111 | ``` 112 | 113 | Virtual environment created inside the user directory and so could be easily cached now. 114 | 115 | NOTE: Only `envPath` property supports home directory reference (`~/`). If you need it in other places 116 | then use manual workaround: `'~/mypath/'.replace('~', System.getProperty("user.home"))` 117 | 118 | ## System packages 119 | 120 | On linux distributions, some python packages could be managed with external packages 121 | (like python3-venv, python3-virtualenv, etc.). 122 | 123 | If your build is **not using virtual environment** and still needs to install such packages, 124 | it would lead to error: 125 | 126 | ``` 127 | error: externally-managed-environment 128 | 129 | × This environment is externally managed 130 | ╰─> To install Python packages system-wide, try apt install 131 | python3-xyz, where xyz is the package you are trying to 132 | install. 133 | ``` 134 | 135 | To work around this problem, use [breakSystemPackages](https://pip.pypa.io/en/stable/cli/pip_install/#cmdoption-break-system-packages) option: 136 | 137 | 138 | ```groovy 139 | python { 140 | breakSystemPackages = true 141 | } 142 | ``` 143 | -------------------------------------------------------------------------------- /src/doc/docs/guide/multimodule.md: -------------------------------------------------------------------------------- 1 | # Multi-module projects 2 | 3 | When used in multi-module project, plugin will create virtualenv inside the root project directory 4 | in order to share the same environment for all modules. 5 | 6 | This could be changed with `python.envPath` configuration in modules. 7 | 8 | 9 | ## One environment for all modules 10 | 11 | Project with 2 modules (+root): 12 | 13 | ``` 14 | / 15 | /mod1/ 16 | /mod2/ 17 | build.gradle 18 | settings.gradle 19 | ``` 20 | 21 | ```groovy 22 | plugins { 23 | id 'ru.vyarus.use-python' version '{{ gradle.version }}' apply false 24 | } 25 | 26 | subprojects { 27 | apply plugin: 'ru.vyarus.use-python' 28 | 29 | python { 30 | pip 'click:6.7' 31 | } 32 | } 33 | ``` 34 | 35 | Python plugin applied for submodules only (not for root project). One virtualenv will be created (at `/.gradle/python`) and used by both modules. 36 | 37 | Note that plugins section in root project used for plugin version management. 38 | 39 | ## Root project use python too 40 | 41 | If root project must use python tasks then use allprojects section instead: 42 | 43 | ```groovy 44 | plugins { 45 | id 'ru.vyarus.use-python' version '{{ gradle.version }}' apply false 46 | } 47 | 48 | allprojects { 49 | apply plugin: 'ru.vyarus.use-python' 50 | 51 | python { 52 | pip 'click:6.7' 53 | } 54 | } 55 | ``` 56 | 57 | ## Environment in module only 58 | 59 | Suppose we want to use python only in one sub module (for example, for docs generation): 60 | 61 | ``` 62 | / 63 | /doc/ 64 | /mod2/ 65 | build.gradle 66 | settings.gradle 67 | ``` 68 | 69 | ```groovy 70 | plugins { 71 | id 'ru.vyarus.use-python' version '{{ gradle.version }}' apply false 72 | } 73 | 74 | // this may be inside module's build.gradle 75 | project(':doc') { 76 | apply plugin: 'ru.vyarus.use-python' 77 | 78 | python { 79 | pip 'click:6.7' 80 | } 81 | } 82 | ``` 83 | 84 | Python plugin applied only in docs module, but virtualenv will still be created at the root level. 85 | If you want to move virtualenv itself inside module then specify relative path for it: `python.envPath = "python"`. 86 | 87 | ## Use different virtualenvs in modules 88 | 89 | If modules require independent environments (different python versions required or incompatible modules used) then specify relative `envPath` so environment would be created relative to module dir. 90 | 91 | ``` 92 | / 93 | /mod1/ 94 | /mod2/ 95 | build.gradle 96 | settings.gradle 97 | ``` 98 | 99 | ```groovy 100 | plugins { 101 | id 'ru.vyarus.use-python' version '{{ gradle.version }}' apply false 102 | } 103 | 104 | subprojects { 105 | apply plugin: 'ru.vyarus.use-python' 106 | 107 | python { 108 | envPath = 'python' 109 | } 110 | } 111 | 112 | // this may be inside module's build.gradle 113 | project(':mod1') { 114 | python { 115 | pythonPath = "/path/to/python2" 116 | pip 'click:6.6' 117 | } 118 | } 119 | 120 | project(':mod2') { 121 | python { 122 | pythonPath = "/path/to/python3" 123 | pip 'click:6.7' 124 | } 125 | } 126 | ``` 127 | 128 | Here `mod1` will cerate wirtualenv inside `/mod1/python` from python 2 and `mod2` will use its own environment created from python 3. 129 | 130 | ## Problems resolution 131 | 132 | Use python commands statistics report could help detect problems (enabled in root module): 133 | 134 | ```groovy 135 | python.printStats = true 136 | ``` 137 | 138 | [Report](stats.md#duplicates-detection) would show all executed commands and mark commands executed in parallel. -------------------------------------------------------------------------------- /src/doc/docs/guide/python.md: -------------------------------------------------------------------------------- 1 | # Python & Pip 2 | 3 | !!! tip 4 | [Docker might be used](docker.md) instead of direct python installation 5 | 6 | To make sure python and pip are installed: 7 | 8 | ```bash 9 | python --version 10 | pip --version 11 | ``` 12 | 13 | On *nix `python` usually reference python2. For python3: 14 | 15 | ```bash 16 | python3 --version 17 | pip3 --version 18 | ``` 19 | 20 | !!! tip 21 | [Python-related configurations](configuration.md#python-location) 22 | 23 | ## Windows install 24 | 25 | [Download and install](https://www.python.org/downloads/windows/) python manually or use 26 | [chocolately](https://chocolatey.org/packages/python/3.6.3): 27 | 28 | ```bash 29 | choco install python 30 | ``` 31 | 32 | In Windows 10 python 3.9 could be installed from Windows Store: 33 | just type 'python' in console and windows will open Windows Store's python page. 34 | No additional actions required after installation. 35 | 36 | Note that windows store python will require minium virtualenv 20.0.11 (or above). 37 | (if virtualenv not yet installed then no worry - plugin will install the correct version) 38 | 39 | ## Linux/Macos install 40 | 41 | On most *nix distributions python is already installed, but often without pip. 42 | 43 | [Install](https://pip.pypa.io/en/stable/installing/) pip if required (ubuntu example): 44 | 45 | ```bash 46 | sudo apt-get install python3-pip 47 | ``` 48 | 49 | Make sure the latest pip installed (required to overcome some older pip problems): 50 | 51 | ```bash 52 | pip3 install -U pip 53 | ``` 54 | 55 | To install exact pip version: 56 | 57 | ```bash 58 | pip3 install -U pip==20.0.11 59 | ``` 60 | 61 | Note that on ubuntu pip installed with `python3-pip` package is 9.0.1, but it did not(!) downgrade 62 | module versions (e.g. `pip install click 6.6` when click 6.7 is installed will do nothing). 63 | Maybe there are other differences, so it's highly recommended to upgrade pip with `pip3 install -U pip`. 64 | 65 | If you need to switch python versions often, you can use [pyenv](https://github.com/pyenv/pyenv): 66 | see [this article](https://www.liquidweb.com/kb/how-to-install-pyenv-on-ubuntu-18-04/) for ubuntu installation guide. 67 | But pay attention to PATH: plugin may not "see" pyenv due to [different PATH](configuration.md#python-location) (when not launched from shell). 68 | 69 | ### Externally managed environment 70 | 71 | On linux, multiple python packages could be installed. For example: 72 | 73 | ``` 74 | sudo apt install python3.12 75 | ``` 76 | 77 | Install python 3.12 accessible with `python3.12` binary, whereas `python3` would be a different python (e.g. 3.9) 78 | 79 | To use such python specify: 80 | 81 | ```groovy 82 | python { 83 | pythonBinary = 'python3.12' 84 | breakSystemPackages = true 85 | } 86 | ``` 87 | 88 | `breakSystemPackages` is required if you need to install pip modules and target python 89 | does not have virtualenv installed (so plugin would try to install it). 90 | 91 | Without `breakSystemPackages` you'll see the following error: 92 | 93 | ``` 94 | error: externally-managed-environment 95 | 96 | × This environment is externally managed 97 | ╰─> To install Python packages system-wide, try apt install 98 | python3-xyz, where xyz is the package you are trying to 99 | install. 100 | ``` 101 | 102 | ### Possible pip issue warning (linux/macos) 103 | 104 | If `pip3 list -o` fails with: `TypeError: '>' not supported between instances of 'Version' and 'Version'` 105 | Then simply update installed pip version: `python3 -m pip install --upgrade pip` 106 | 107 | This is a [known issue](https://github.com/pypa/pip/issues/3057) related to incorrectly 108 | patched pip packages in some distributions. 109 | 110 | ## Automatic pip upgrade 111 | 112 | As described above, there are different ways of pip installation in linux and, more important, 113 | admin permissions are required to upgrade global pip. So it is impossible to upgrade pip from the plugin (in all cases). 114 | 115 | But, it is possible inside virtualenv or user (--user) scope. Note that plugin creates virtualenv by default (per project independent python environment). 116 | 117 | So, in order to use newer pip simply put it as first dependency: 118 | 119 | ``` 120 | python { 121 | pip 'pip:10.0.1' 122 | pip 'some_module:1.0' 123 | } 124 | ``` 125 | 126 | Here project virtualenv will be created with global pip and newer pip version installed inside environment. 127 | Packages installation is sequential, so all other packages will be installed with newer pip (each installation is independent pip command). 128 | 129 | The same will work for user scope: `python.scope = USER` 130 | 131 | When applying this trick, consider minimal pip version declared in configuration 132 | (`python.minPipVersion='9'` by default) as minimal pip version required for *project setup* 133 | (instead of minimal version required *for work*). 134 | 135 | ## Automatic python install 136 | 137 | Python is assumed to be used as java: install and forget. It perfectly fits user 138 | use case: install python once and plugin will replace all manual work on project environment setup. 139 | 140 | It is also easy to configure python on CI (like travis). 141 | 142 | If you want automatic python installation, try looking on JetBrain's 143 | [python-envs plugin](https://github.com/JetBrains/gradle-python-envs). But be careful because 144 | it has some caveats (for example, on windows python could be installed automatically just once 145 | and requires manual un-installation). 146 | 147 | ## Global python validation 148 | 149 | For global python (when no `pythonPath` configured) plugin would manually search 150 | for python binary in `$PATH` and would throw error if not found containing 151 | entire `$PATH`. This is required for cases when PATH visible for gradle process 152 | is different to your shell path. 153 | 154 | For example, on M1 it could be rosetta path instead of native (see [this issue](https://github.com/xvik/gradle-use-python-plugin/issues/35)). 155 | 156 | Validation could be disabled with: 157 | 158 | ```groovy 159 | python.validateSystemBinary = false 160 | ``` 161 | 162 | !!! note 163 | This option is ignored if [docker support](docker.md) enabled -------------------------------------------------------------------------------- /src/doc/docs/guide/usage.md: -------------------------------------------------------------------------------- 1 | # Call python 2 | 3 | Call python command: 4 | 5 | ```groovy 6 | tasks.register('cmd', PythonTask) { 7 | command = "-c print('sample')" 8 | } 9 | ``` 10 | 11 | called: `python -c print('sample')` on win and `python -c exec("print('sample')")` on *nix (exec applied automatically for compatibility) 12 | 13 | Call multi-line command: 14 | 15 | ```groovy 16 | tasks.register('cmd', PythonTask) { 17 | command = '-c "import sys; print(sys.prefix)"' 18 | } 19 | ``` 20 | 21 | called: `python -c "import sys; print(sys.prefix)"` on win and `python -c exec("import sys; print(sys.prefix)")` on *nix 22 | 23 | !!! note 24 | It is important to wrap script with space in quotes (otherwise parser will incorrectly parse arguments). 25 | 26 | String command is used for simplicity, but it could be array/collection of args: 27 | 28 | ```groovy 29 | tasks.register('script', PythonTask) { 30 | command = ['path/to/script.py', '1', '2'] 31 | } 32 | ``` 33 | 34 | ## Pip module command 35 | 36 | ```groovy 37 | tasks.register('mod', PythonTask) { 38 | module = 'sample' 39 | command = 'mod args' 40 | } 41 | ``` 42 | 43 | called: `python -m sample mod args` 44 | 45 | ## Script 46 | 47 | ```groovy 48 | tasks.register('script', PythonTask) { 49 | command = 'path/to/script.py 1 2' 50 | } 51 | ``` 52 | 53 | called: `python path/to/script.py 1 2` (arguments are optional, just for demo) 54 | 55 | ## Command parsing 56 | 57 | When command passed as string it is manually parsed to arguments array (split by space): 58 | 59 | * Spaces in quotes are ignored: `"quoted space"` or `'quoted space'` 60 | * Escaped spaces are ignored: `with\\ space` (argument will be used with simple space then - escape removed). 61 | * Escaped quotes are ignored: `"with \\"interrnal quotes\\" inside"`. But pay attention that it must be 2 symbols `\\"` and **not** `\"` because otherwise it is impossible to detect escape. 62 | 63 | To view parsed arguments run gradle with `-i` flag (enable info logs). In case when command can't be parsed properly 64 | (bug in parser or unsupported case) use array of arguments instead of string. 65 | 66 | ## Environment variables 67 | 68 | By default, executed python can access system environment variables (same as `System.getenv()`). 69 | 70 | To declare custom (process specific) variables: 71 | 72 | ```groovy 73 | tasks.register('sample', PythonTask) { 74 | command = "-c \"import os;print('variables: '+os.getenv('some', 'null')+' '+os.getenv('foo', 'null'))\"" 75 | environment 'some', 1 76 | environment 'other', 2 77 | environment(['foo': 'bar', 'baz': 'bag']) 78 | } 79 | ``` 80 | 81 | Map based declaration (`environment(['foo': 'bar', 'baz': 'bag'])`) does not remove previously declared variables 82 | (just add all vars from map), but direct assignment `environment = ['foo': 'bar', 'baz': 'bag']` will reset variables. 83 | 84 | System variables will be available even after declaring custom variables (of course, custom variables could override global value). 85 | 86 | !!! note 87 | Environment variable could also be declared in extension to apply for all python commands: 88 | `python.environment 'some', 1` (if environments declared both globally (through extension) and directly on task, they would be merged) 89 | 90 | ### Non-default python 91 | 92 | Python task would use python selected by `checkPython` task (global or detected virtualenv). 93 | If you need to use completely different python for some task, then it should be explicitly stated 94 | with `useCustomPython` property. 95 | 96 | For example, suppose we use virtual environment, but need to use global python 97 | in one task: 98 | 99 | ```groovy 100 | tasks.register('script', PythonTask) { 101 | // global python (it would select python3 automatically on linux) 102 | pythonPath = null 103 | // force custom python for task 104 | useCustomPython = true 105 | command = ['path/to/script.py', '1', '2'] 106 | } 107 | ``` 108 | 109 | Additional property (useCustomPython) is required because normally task's `pythonPath` is ignored 110 | (an actual path is selected by `checkPython` task) -------------------------------------------------------------------------------- /src/doc/docs/index.md: -------------------------------------------------------------------------------- 1 | # Welcome to gradle use-python plugin 2 | 3 | !!! summary "" 4 | Use [python](https://www.python.org/) in gradle build. The only plugin intention is to simplify python usage from gradle (without managing python itself). 5 | 6 | [Release notes](about/history.md) - [Compatibility](about/compatibility.md) - [License](about/license.md) 7 | 8 | **[Who's using](https://github.com/xvik/gradle-use-python-plugin/discussions/18)** 9 | 10 | ## Features 11 | 12 | * Works with [directly installed python](guide/python.md) or [docker container](guide/docker.md) (with python) 13 | * Creates local (project-specific) [virtualenv](guide/configuration.md#virtualenv) (project-specific python copy) 14 | * Installs required [pip modules](guide/modules.md) (venv by default, but could be global installation) 15 | - Support [requirements.txt](guide/modules.md#requirementstxt) file (limited by default) 16 | * Gradle configuration cache supported 17 | * Could be used as basement for [building plugins](guide/plugindev.md) for specific python modules (like 18 | [mkdocs plugin](https://github.com/xvik/gradle-mkdocs-plugin)) 19 | 20 | -------------------------------------------------------------------------------- /src/doc/mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Gradle use-python plugin 2 | 3 | # Meta tags (placed in header) 4 | site_description: Use python modules in gradle build 5 | site_author: Vyacheslav Rusakov 6 | site_url: https://xvik.github.io/gradle-use-python-plugin 7 | 8 | # Repository (add link to repository on each page) 9 | repo_name: gradle-use-python-plugin 10 | repo_url: https://github.com/xvik/gradle-use-python-plugin 11 | edit_uri: edit/master/src/doc/docs/ 12 | 13 | #Copyright (shown at the footer) 14 | copyright: 'Copyright © 2017-2024 Vyacheslav Rusakov' 15 | 16 | # Meterial theme 17 | theme: 18 | name: 'material' 19 | palette: 20 | - media: "(prefers-color-scheme: light)" 21 | scheme: default 22 | toggle: 23 | icon: material/toggle-switch-off-outline 24 | name: Switch to dark mode 25 | - media: "(prefers-color-scheme: dark)" 26 | scheme: slate 27 | toggle: 28 | icon: material/toggle-switch 29 | name: Switch to light mode 30 | features: 31 | #- navigation.tabs 32 | #- navigation.tabs.sticky 33 | #- navigation.instant 34 | - navigation.tracking 35 | - navigation.top 36 | 37 | plugins: 38 | - search 39 | - markdownextradata 40 | 41 | extra: 42 | # palette: 43 | # primary: 'indigo' 44 | # accent: 'indigo' 45 | 46 | version: 47 | provider: mike 48 | 49 | social: 50 | - icon: fontawesome/brands/github 51 | link: https://github.com/xvik 52 | - icon: fontawesome/brands/hashnode 53 | link: https://blog.vyarus.ru 54 | # - icon: fontawesome/brands/twitter 55 | # link: https://twitter.com/vyarus 56 | # 57 | # Google Analytics 58 | # analytics: 59 | # provider: google 60 | # property: UA-XXXXXXXX-X 61 | 62 | markdown_extensions: 63 | # Python Markdown 64 | - abbr 65 | - admonition 66 | - attr_list 67 | - def_list 68 | - footnotes 69 | - meta 70 | - md_in_html 71 | - toc: 72 | permalink: true 73 | 74 | # Python Markdown Extensions 75 | - pymdownx.arithmatex: 76 | generic: true 77 | - pymdownx.betterem: 78 | smart_enable: all 79 | - pymdownx.caret 80 | - pymdownx.details 81 | - pymdownx.emoji: 82 | emoji_index: !!python/name:material.extensions.emoji.twemoji 83 | emoji_generator: !!python/name:material.extensions.emoji.to_svg 84 | - pymdownx.highlight 85 | - pymdownx.inlinehilite 86 | - pymdownx.keys 87 | - pymdownx.mark 88 | - pymdownx.smartsymbols 89 | - pymdownx.superfences 90 | - pymdownx.tabbed: 91 | alternate_style: true 92 | - pymdownx.tasklist: 93 | custom_checkbox: true 94 | - pymdownx.tilde 95 | 96 | dev_addr: 127.0.0.1:3001 97 | 98 | nav: 99 | - Home: index.md 100 | - Getting started: getting-started.md 101 | - User guide: 102 | - Python install: guide/python.md 103 | - Docker: guide/docker.md 104 | - Pip modules: guide/modules.md 105 | - Usage: guide/usage.md 106 | - Multi-module: guide/multimodule.md 107 | - Configuration: guide/configuration.md 108 | - Stats: guide/stats.md 109 | - CI: guide/ci.md 110 | - Plugin development: guide/plugindev.md 111 | - About: 112 | - Release notes: about/history.md 113 | - Compatibility: about/compatibility.md 114 | - License: about/license.md -------------------------------------------------------------------------------- /src/main/groovy/ru/vyarus/gradle/plugin/python/cmd/LoggedCommandCleaner.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.cmd 2 | 3 | /** 4 | * Some python commands may require sensitive data which should not be revealed in logs (all executed commands 5 | * are logged). For example pip may use external index url with auth credentials (in this case password must be 6 | * hidden). 7 | *

8 | * Cleaner must be registered directly into {@link Python} instance with 9 | * {@link Python#logCommandCleaner(ru.vyarus.gradle.plugin.python.cmd.LoggedCommandCleaner)}. 10 | *

11 | * As an example see {@link Pip} constructor which register external index url credentials cleaner into 12 | * provided python instance. For cleaner implementation see 13 | * {@link ru.vyarus.gradle.plugin.python.util.CliUtils#hidePipCredentials(java.lang.String)}. 14 | * 15 | * @author Vyacheslav Rusakov 16 | * @since 27.02.2021 17 | */ 18 | interface LoggedCommandCleaner { 19 | 20 | /** 21 | * Called before logging executed python command into console to hide possible sensitive command parts. 22 | * 23 | * @param cmd executed command 24 | * @return command safe for logging 25 | */ 26 | String clear(String cmd) 27 | } 28 | -------------------------------------------------------------------------------- /src/main/groovy/ru/vyarus/gradle/plugin/python/cmd/Venv.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.cmd 2 | 3 | import groovy.transform.CompileStatic 4 | import ru.vyarus.gradle.plugin.python.cmd.env.Environment 5 | import ru.vyarus.gradle.plugin.python.util.PythonExecutionFailed 6 | 7 | /** 8 | * Venv commands execution utility. Use {@link Python} internally. 9 | *

10 | * Note: venv is not a pip-managed module! 11 | *

12 | * Usually venv is bundled with python (since 3.3), but not always: for example, on ubuntu it is a separate package 13 | * python3-venv. 14 | *

15 | * Tool does not provide its version. 16 | * 17 | * @author Vyacheslav Rusakov 18 | * @since 21.09.2023 19 | */ 20 | @CompileStatic 21 | class Venv extends VirtualTool { 22 | 23 | public static final String NAME = 'venv' 24 | 25 | // module name 26 | final String name = NAME 27 | 28 | Venv(Environment environment, String path) { 29 | this(environment, null, null, path) 30 | } 31 | 32 | /** 33 | * Create venv utility. 34 | * 35 | * @param environment gradle api access object 36 | * @param pythonPath python path (null to use global) 37 | * @param binary python binary name (null to use default python3 or python) 38 | * @param path environment path (relative to project or absolute) 39 | */ 40 | Venv(Environment environment, String pythonPath, String binary, String path) { 41 | super(environment, pythonPath, binary, path) 42 | } 43 | 44 | /** 45 | * Create venv with pip. Do nothing if already exists. 46 | * To copy environment instead of symlinking, use {@code copy (true)} otherwise don't specify parameter. 47 | */ 48 | @SuppressWarnings('BuilderMethodWithSideEffects') 49 | @Override 50 | void create(boolean copy = false) { 51 | create(true, copy) 52 | } 53 | 54 | /** 55 | * Create the lightest env without pip. Do nothing if already exists. 56 | * To copy environment instead of symlinking, use {@code copy (true)} otherwise don't specify parameter. 57 | */ 58 | @SuppressWarnings('BuilderMethodWithSideEffects') 59 | void createPythonOnly(boolean copy = false) { 60 | create(false, copy) 61 | } 62 | 63 | /** 64 | * Create venv. Do nothing if already exists. 65 | * To copy environment instead if symlinking, use {@code copy (? , ? , true)} otherwise omit last parameter. 66 | * 67 | * @param pip do not install pip (--without-pip) 68 | * @param copy copy virtualenv instead if symlink (--copies) 69 | */ 70 | @SuppressWarnings('BuilderMethodWithSideEffects') 71 | void create(boolean pip, boolean copy) { 72 | if (exists()) { 73 | return 74 | } 75 | String cmd = path 76 | if (copy) { 77 | cmd += ' --copies' 78 | } 79 | if (!pip) { 80 | cmd += ' --without-pip' 81 | } 82 | python.callModule(name, cmd) 83 | } 84 | 85 | /** 86 | * On ubuntu venv module is installed as a separate package (python3-venv) and is not visible as pip module. 87 | * So the only way to check its existence is calling it directly. 88 | * 89 | * @return true if venv is present, false if not 90 | */ 91 | boolean isInstalled() { 92 | return python.getOrCompute('venv.installed') { 93 | try { 94 | python.withHiddenLog { 95 | python.callModule(name, '-h') 96 | } 97 | return true 98 | } catch (PythonExecutionFailed ignored) { 99 | return false 100 | } 101 | } 102 | } 103 | 104 | @Override 105 | String toString() { 106 | return env.file(pythonPath).canonicalPath + ' (venv)' 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/groovy/ru/vyarus/gradle/plugin/python/cmd/VirtualTool.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.cmd 2 | 3 | import groovy.transform.CompileStatic 4 | import org.gradle.api.logging.LogLevel 5 | import ru.vyarus.gradle.plugin.python.cmd.docker.DockerConfig 6 | import ru.vyarus.gradle.plugin.python.cmd.env.Environment 7 | import ru.vyarus.gradle.plugin.python.util.CliUtils 8 | 9 | import java.nio.file.Paths 10 | 11 | /** 12 | * Base class for environment virtualization tools (venv, virtualenv). 13 | * 14 | * @author Vyacheslav Rusakov 15 | * @since 19.09.2023 16 | * @param actual tool type 17 | */ 18 | @CompileStatic 19 | abstract class VirtualTool { 20 | 21 | protected final Environment env 22 | protected final Python python 23 | final String path 24 | final File location 25 | 26 | protected VirtualTool(Environment environment, String pythonPath, String binary, String path) { 27 | this.env = environment 28 | this.python = new Python(environment, pythonPath, binary).logLevel(LogLevel.LIFECYCLE) 29 | if (!path) { 30 | throw new IllegalArgumentException('Virtual environment path not set') 31 | } 32 | // for direct tool usage support 33 | this.path = CliUtils.resolveHomeReference(path) 34 | this.location = environment.file(this.path) 35 | environment.debug("${getClass().simpleName} environment init for path '${this.path}' " + 36 | "(python path: '${pythonPath}')") 37 | } 38 | 39 | /** 40 | * System binary search is performed only for global python (when pythonPath is not specified). Enabled by default. 41 | * 42 | * @param validate true to search python binary in system path and fail if not found 43 | * @return cli instance for chained calls 44 | */ 45 | T validateSystemBinary(boolean validate) { 46 | this.python.validateSystemBinary(validate) 47 | return self() 48 | } 49 | 50 | /** 51 | * Enable docker support: all python commands would be executed under docker container. 52 | * 53 | * @param docker docker configuration (may be null) 54 | * @return cli instance for chained calls 55 | */ 56 | T withDocker(DockerConfig docker) { 57 | this.python.withDocker(docker) 58 | return self() 59 | } 60 | 61 | /** 62 | * Shortcut for {@link Python#workDir(java.lang.String)}. 63 | * 64 | * @param workDir python working directory 65 | * @return virtualenv instance for chained calls 66 | */ 67 | T workDir(String workDir) { 68 | python.workDir(workDir) 69 | return self() 70 | } 71 | 72 | /** 73 | * Shortcut for {@link Python#environment(java.util.Map)}. 74 | * 75 | * @param env environment map 76 | * @return pip instance for chained calls 77 | */ 78 | T environment(Map env) { 79 | python.environment(env) 80 | return self() 81 | } 82 | 83 | /** 84 | * Perform pre-initialization and, if required, validate global python binary correctness. Calling this method is 85 | * NOT REQUIRED: initialization will be performed automatically before first execution. But it might be called 86 | * in order to throw possible initialization error before some other logic (related to exception handling). 87 | * 88 | * @return virtualenv instance for chained calls 89 | */ 90 | T validate() { 91 | python.validate() 92 | return self() 93 | } 94 | 95 | /** 96 | * May be used to apply additional virtualenv ({@link Python#extraArgs(java.lang.Object)}) or python 97 | * ({@link Python#pythonArgs(java.lang.Object)}) arguments. 98 | * 99 | * @return python cli instance used to execute commands 100 | */ 101 | Python getPython() { 102 | return python 103 | } 104 | 105 | /** 106 | * @return true if virtualenv exists 107 | */ 108 | boolean exists() { 109 | return location.exists() && location.list().size() > 0 110 | } 111 | 112 | /** 113 | * @return python path to use for environment 114 | */ 115 | String getPythonPath() { 116 | return python.getOrCompute("env.python.path:$env.projectPath") { 117 | String res = CliUtils.pythonBinPath(location.absolutePath, python.windows) 118 | return Paths.get(path).absolute ? res 119 | // use shorter relative path 120 | : env.relativePath(res) 121 | } 122 | } 123 | 124 | /** 125 | * Create virtual environment. Do nothing if already exists. 126 | * To copy environment instead of symlinking, use {@code copy (true)}. 127 | */ 128 | @SuppressWarnings('BuilderMethodWithSideEffects') 129 | abstract void create(boolean copy) 130 | 131 | private T self() { 132 | return (T) this 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/main/groovy/ru/vyarus/gradle/plugin/python/cmd/Virtualenv.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.cmd 2 | 3 | import groovy.transform.CompileStatic 4 | import ru.vyarus.gradle.plugin.python.cmd.env.Environment 5 | 6 | import java.util.regex.Matcher 7 | import java.util.regex.Pattern 8 | 9 | /** 10 | * Virtualenv commands execution utility. Use {@link Python} internally. 11 | * 12 | * @author Vyacheslav Rusakov 13 | * @since 13.12.2017 14 | */ 15 | @CompileStatic 16 | class Virtualenv extends VirtualTool { 17 | 18 | private static final Pattern VERSION = Pattern.compile('virtualenv ([\\d.]+)') 19 | 20 | public static final String PIP_NAME = 'virtualenv' 21 | 22 | // module name 23 | final String name = PIP_NAME 24 | 25 | Virtualenv(Environment environment, String path) { 26 | this(environment, null, null, path) 27 | } 28 | 29 | /** 30 | * Create virtualenv utility. 31 | * 32 | * @param environment gradle api access object 33 | * @param pythonPath python path (null to use global) 34 | * @param binary python binary name (null to use default python3 or python) 35 | * @param path environment path (relative to project or absolute) 36 | */ 37 | Virtualenv(Environment environment, String pythonPath, String binary, String path) { 38 | super(environment, pythonPath, binary, path) 39 | } 40 | 41 | /** 42 | * @return virtualenv version (major.minor.micro) 43 | */ 44 | String getVersion() { 45 | return python.getOrCompute('virtualenv.version') { 46 | // first try to parse line to avoid duplicate python call 47 | Matcher matcher = VERSION.matcher(versionLine) 48 | if (matcher.find()) { 49 | // note: this will drop beta postfix (e.g. for 10.0.0b2 version will be 10.0.0) 50 | return matcher.group(1) 51 | } 52 | // if can't recognize version, ask directly 53 | return python.withHiddenLog { 54 | python.readOutput("-c \"import $name; print(${name}.__version__)\"") 55 | } 56 | } 57 | } 58 | 59 | /** 60 | * @return virtualenv --version output 61 | */ 62 | String getVersionLine() { 63 | return python.getOrCompute('virtualenv.version.line') { 64 | // virtualenv 20 returns long version string including location path 65 | String res = python.withHiddenLog { 66 | python.readOutput("-m $name --version") 67 | } 68 | // virtualenv 16 and below return only raw version (backwards compatibility) 69 | if (!res.startsWith(name)) { 70 | res = "$name $res" 71 | } 72 | return res 73 | } 74 | } 75 | 76 | /** 77 | * Create virtualenv with setuptools and pip. Do nothing if already exists. 78 | * To copy environment instead of symlinking, use {@code copy (true)} otherwise don't specify parameter. 79 | */ 80 | @SuppressWarnings('BuilderMethodWithSideEffects') 81 | @Override 82 | void create(boolean copy = false) { 83 | create(true, true, copy) 84 | } 85 | 86 | /** 87 | * Create the lightest env without setuptools and pip. Do nothing if already exists. 88 | * To copy environment instead of symlinking, use {@code copy (true)} otherwise don't specify parameter. 89 | */ 90 | @SuppressWarnings('BuilderMethodWithSideEffects') 91 | void createPythonOnly(boolean copy = false) { 92 | create(false, false, copy) 93 | } 94 | 95 | /** 96 | * Create virtualenv. Do nothing if already exists. 97 | * To copy environment instead if symlinking, use {@code copy (? , ? , true)} otherwise omit last parameter. 98 | * 99 | * @param setuptools do not install setuptools (--no-setuptools) 100 | * @param pip do not install pip and wheel (--no-pip --no-wheel) 101 | * @param copy copy virtualenv instead if symlink (--always-copy) 102 | */ 103 | @SuppressWarnings('BuilderMethodWithSideEffects') 104 | void create(boolean setuptools, boolean pip, boolean copy = false) { 105 | if (exists()) { 106 | return 107 | } 108 | String cmd = path 109 | if (copy) { 110 | cmd += ' --always-copy' 111 | } 112 | if (!setuptools) { 113 | cmd += ' --no-setuptools' 114 | } 115 | if (!pip) { 116 | cmd += ' --no-pip --no-wheel' 117 | } 118 | python.callModule(name, cmd) 119 | } 120 | 121 | @Override 122 | String toString() { 123 | return env.file(pythonPath).canonicalPath + " (virtualenv $version)" 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/main/groovy/ru/vyarus/gradle/plugin/python/cmd/docker/DockerConfig.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.cmd.docker 2 | 3 | import groovy.transform.CompileStatic 4 | 5 | /** 6 | * Docker configuration for python execution. For documentation see plugin extension object 7 | * {@link ru.vyarus.gradle.plugin.python.PythonExtension#docker} or task docker configuration object 8 | * {@link ru.vyarus.gradle.plugin.python.task.BasePythonTask.DockerEnv}. 9 | *

10 | * Note that such triple duplication of docker configuration objects is required for better customization and 11 | * ability for direct {@link ru.vyarus.gradle.plugin.python.cmd.Python} (and related) object usage. 12 | * 13 | * @author Vyacheslav Rusakov 14 | * @since 27.09.2022 15 | */ 16 | @CompileStatic 17 | class DockerConfig { 18 | 19 | boolean windows 20 | String image 21 | boolean exclusive 22 | // would always be false for mac and win 23 | boolean useHostNetwork 24 | Set ports 25 | } 26 | -------------------------------------------------------------------------------- /src/main/groovy/ru/vyarus/gradle/plugin/python/cmd/docker/DockerFactory.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.cmd.docker 2 | 3 | import groovy.transform.CompileStatic 4 | import ru.vyarus.gradle.plugin.python.cmd.env.Environment 5 | 6 | /** 7 | * Global docker containers manager. All python tasks, requiring the same container (by full image name) would use 8 | * the same instance in order to synchronize calls. The same applies for multi-module projects. 9 | *

10 | * Containers re-use is important not only for synchronization, but to speed-up execution, avoiding re-starting 11 | * container for each python call. 12 | *

13 | * Container might be restarted if target python command requires different environment, work dir or specific 14 | * docker configuration (but, of course, it's better to use the same docker configuration, declared in extension). 15 | *

16 | * If different tasks would require different docker images - different containers would be started and they may 17 | * work concurrently (no synchronization required). 18 | * 19 | * @author Vyacheslav Rusakov 20 | * @since 23.09.2022 21 | */ 22 | @SuppressWarnings('SynchronizedMethod') 23 | @CompileStatic 24 | class DockerFactory { 25 | 26 | private static final Map CONTAINERS = [:] 27 | 28 | /** 29 | * Gets existing or creates new docker container manager. It is assumed that all tasks requiring the same container 30 | * (by image name) would share the same instance. This allows synchronization of running commands inside 31 | * the same container (so in multi-module projects or with parallel execution one container would always 32 | * execute only one python command). 33 | *

34 | * Note that exclusive tasks always spawn new container. 35 | * 36 | * @param config docker configuration (only image name is required) 37 | * @param project project instance 38 | * @return container manager instance (most likely, already started) 39 | */ 40 | static synchronized ContainerManager getContainer(DockerConfig config, Environment environment) { 41 | if (config == null) { 42 | return null 43 | } 44 | String key = config.image 45 | if (!CONTAINERS.containsKey(key)) { 46 | CONTAINERS.put(key, new ContainerManager(config.image, config.windows, environment)) 47 | } 48 | return CONTAINERS.get(key) 49 | } 50 | 51 | /** 52 | * Shuts down started containers. Called at the end of the build. 53 | */ 54 | @SuppressWarnings('UnnecessaryGetter') 55 | static synchronized void shutdownAll() { 56 | if (!CONTAINERS.isEmpty()) { 57 | CONTAINERS.values().each { it.stop() } 58 | CONTAINERS.clear() 59 | } 60 | } 61 | 62 | /** 63 | * @return active containers count (not stopped) 64 | */ 65 | static synchronized int getActiveContainersCount() { 66 | return CONTAINERS.values().stream().filter { !it.started }.count() 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/groovy/ru/vyarus/gradle/plugin/python/cmd/docker/PythonContainer.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.cmd.docker 2 | 3 | import groovy.transform.CompileStatic 4 | import org.slf4j.Logger 5 | import org.slf4j.helpers.NOPLoggerFactory 6 | import org.testcontainers.containers.GenericContainer 7 | import org.testcontainers.containers.InternetProtocol 8 | import org.testcontainers.utility.DockerImageName 9 | 10 | /** 11 | * Special class required to tune default {@link GenericContainer} behaviour. 12 | * 13 | * @author Vyacheslav Rusakov 14 | * @since 28.09.2022 15 | */ 16 | @CompileStatic 17 | class PythonContainer extends GenericContainer { 18 | 19 | PythonContainer(String image) { 20 | super(DockerImageName.parse(image)) 21 | } 22 | 23 | // require only because groovy can't compile otherwise 24 | @SuppressWarnings(['CloseWithoutCloseable', 'UnnecessaryOverridingMethod']) 25 | @Override 26 | void close() { 27 | super.close() 28 | } 29 | 30 | // from deprecated FixedHostPortGenericContainer 31 | // we can't use random ports here because it would require additional api for exposing mappings 32 | // and would be completely unusable for exclusive containers 33 | 34 | /** 35 | * Bind a fixed TCP port on the docker host to a container port 36 | * @param hostPort a port on the docker host, which must be available 37 | * @param containerPort a port in the container 38 | * @return this container 39 | */ 40 | PythonContainer withFixedExposedPort(int hostPort, int containerPort) { 41 | return withFixedExposedPort(hostPort, containerPort, InternetProtocol.TCP) 42 | } 43 | 44 | /** 45 | * Bind a fixed port on the docker host to a container port 46 | * @param hostPort a port on the docker host, which must be available 47 | * @param containerPort a port in the container 48 | * @param protocol an internet protocol (tcp or udp) 49 | * @return this container 50 | */ 51 | PythonContainer withFixedExposedPort(int hostPort, int containerPort, InternetProtocol protocol) { 52 | super.addFixedExposedPort(hostPort, containerPort, protocol) 53 | return self() 54 | } 55 | 56 | @Override 57 | @SuppressWarnings('UnnecessaryGetter') 58 | protected Logger logger() { 59 | // avoid direct logging of errors (prevent duplicates in log) 60 | return NOPLoggerFactory.getConstructor().newInstance().getLogger(PythonContainer.name) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/groovy/ru/vyarus/gradle/plugin/python/cmd/env/Environment.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.cmd.env 2 | 3 | import org.gradle.api.logging.Logger 4 | 5 | import java.util.function.Supplier 6 | 7 | /** 8 | * Environment-specific apis provider. Object used as lightweight alternative to gradle {@link org.gradle.api.Project} 9 | * (which was used before), because project is not compatible with configuration cache. 10 | *

11 | * NOTE: configuration cache stores entire objects (so they are created only when configuration cache is not enabled). 12 | * 13 | * @author Vyacheslav Rusakov 14 | * @since 15.03.2024 15 | */ 16 | interface Environment { 17 | 18 | Logger getLogger() 19 | 20 | /** 21 | * Same as {@code project.rootProject.name}. 22 | * 23 | * @return root project name 24 | */ 25 | String getRootName() 26 | 27 | /** 28 | * Same as {@code project.path}. 29 | * 30 | * @return current project path (e.g. :mod1:sub1) to uniquely identify project 31 | */ 32 | String getProjectPath() 33 | 34 | /** 35 | * Same as {@code project.rootDir}. 36 | * 37 | * @return root project directory 38 | */ 39 | File getRootDir() 40 | 41 | /** 42 | * Same as {@code project.projectDir}. 43 | * 44 | * @return current project directory (might be root or sub module) 45 | */ 46 | File getProjectDir() 47 | 48 | /** 49 | * Same as {@code project.file()}. 50 | * 51 | * @param path absolute or relative path to file (for current project) 52 | * @return file, resolved relative to current project 53 | */ 54 | File file(String path) 55 | 56 | /** 57 | * Same as {@code project.relativePath}. 58 | * 59 | * @param path absolute path 60 | * @return path relative for current project 61 | */ 62 | String relativePath(String path) 63 | 64 | /** 65 | * Same as {@code project.relativePath}. 66 | * 67 | * @param path absolute path 68 | * @return path relative for current project 69 | */ 70 | String relativePath(File file) 71 | 72 | /** 73 | * Rebuild relative or absolute path relative to root project. If path not lying inside root project 74 | * then path remain absolute. 75 | * 76 | * @param path path to convert 77 | * @return path relative to root project 78 | */ 79 | String relativeRootPath(String path) 80 | 81 | /** 82 | * Execute command (external process). 83 | * Same as {@code project.exec}. 84 | * 85 | * @param cmd command 86 | * @param out output stream 87 | * @param err errors stream 88 | * @param workDir work directory (may be null) 89 | * @param envVars environment variables (may be null) 90 | * @return exit code 91 | */ 92 | int exec(String[] cmd, OutputStream out, OutputStream err, String workDir, Map envVars) 93 | 94 | /** 95 | * Compute value or get from project-wide cache. Used to cache values within one project. Unifies cache between 96 | * {@link ru.vyarus.gradle.plugin.python.cmd.Python} instances (created independently for each task). 97 | *

98 | * Used as a replacement for project external property, which is impossible to use due to new limitation of 99 | * not using {@link org.gradle.api.Project} inside task action. 100 | * 101 | * @param key cache key (case sensitive) 102 | * @param value value supplier (used when nothing stored in cache), may be null 103 | * @return project cache 104 | */ 105 | T projectCache(String key, Supplier value) 106 | 107 | /** 108 | * Compute value or get from global-wide cache. Unique cache for all projects in multi-module build 109 | * (to cache values, common for all modules and avoid redundant python calls) 110 | *

111 | * Used as a replacement for project external property, which is impossible to use due to new limitation of 112 | * not using {@link org.gradle.api.Project} inside task action. 113 | * 114 | * @param key cache key (case sensitive) 115 | * @param value value supplier (used when nothing stored in cache), may be null 116 | * @return project cache 117 | */ 118 | T globalCache(String key, Supplier value) 119 | 120 | /** 121 | * Update project cache value (even if it already contains value), 122 | * 123 | * @param key cache key 124 | * @param value value 125 | */ 126 | void updateProjectCache(String key, Object value) 127 | 128 | /** 129 | * Update global cache value (even if it already contains value). 130 | * 131 | * @param key cache key 132 | * @param value value 133 | */ 134 | void updateGlobalCache(String key, Object value) 135 | 136 | /** 137 | * Print debug message if debug enabled. Message would include context project and task. 138 | * 139 | * @param msg message 140 | */ 141 | void debug(String msg) 142 | 143 | /** 144 | * Save command execution stat. Counts only python execution (possible direct docker commands ignored). 145 | * 146 | * @param containerName docker container name 147 | * @param cmd executed command (cleared!) 148 | * @param workDir working directory (may be null) 149 | * @param globalPython true to indicate global python call 150 | * @param start start time 151 | * @param success execution success 152 | */ 153 | @SuppressWarnings('ParameterCount') 154 | void stat(String containerName, String cmd, String workDir, boolean globalPython, long start, boolean success) 155 | 156 | /** 157 | * Prints cache state (for debug), but only if debug enabled in the root project. 158 | */ 159 | void printCacheState() 160 | } 161 | -------------------------------------------------------------------------------- /src/main/groovy/ru/vyarus/gradle/plugin/python/cmd/env/SimpleEnvironment.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.cmd.env 2 | 3 | import groovy.transform.CompileStatic 4 | import org.gradle.api.Project 5 | import org.gradle.api.internal.file.FileOperations 6 | import org.gradle.api.internal.project.DefaultProject 7 | import org.gradle.api.provider.Provider 8 | import org.gradle.process.ExecOperations 9 | import org.gradle.testfixtures.ProjectBuilder 10 | import ru.vyarus.gradle.plugin.python.service.stat.PythonStat 11 | 12 | import java.util.concurrent.ConcurrentHashMap 13 | 14 | /** 15 | * Environment implementation using fake project (in current directory). Might be used for direct python and pip 16 | * tools execution in tests (use global tools). For example, to uninstall global pip package before test. 17 | * 18 | * @author Vyacheslav Rusakov 19 | * @since 04.04.2024 20 | */ 21 | @CompileStatic 22 | class SimpleEnvironment extends GradleEnvironment { 23 | 24 | private final ExecOperations exec 25 | private final FileOperations fs 26 | 27 | SimpleEnvironment() { 28 | this(new File(''), false) 29 | } 30 | 31 | SimpleEnvironment(File projectDir, boolean debug = false) { 32 | this(ProjectBuilder.builder() 33 | .withProjectDir(projectDir) 34 | .build(), debug) 35 | } 36 | 37 | SimpleEnvironment(Project project, boolean debug = false) { 38 | super(project.logger, 39 | project.projectDir, 40 | project.projectDir, 41 | 'local', ':', 'dummy', 42 | null, 43 | { new ConcurrentHashMap<>() } as Provider, 44 | { [] } as Provider, 45 | { debug } as Provider 46 | ) 47 | this.exec = (project as DefaultProject).services.get(ExecOperations) 48 | this.fs = (project as DefaultProject).services.get(FileOperations) 49 | } 50 | 51 | Map getCache() { 52 | return cacheProject.get() 53 | } 54 | 55 | List getStats() { 56 | return super.stats.get() 57 | } 58 | 59 | @Override 60 | protected ExecOperations getExec() { 61 | return exec 62 | } 63 | 64 | @Override 65 | protected FileOperations getFs() { 66 | return fs 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/groovy/ru/vyarus/gradle/plugin/python/service/stat/PythonStat.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.service.stat 2 | 3 | import groovy.transform.CompileStatic 4 | import org.jetbrains.annotations.NotNull 5 | 6 | /** 7 | * Python command execution statistic. Also tracks internal direct commands execution (just simpler to count all), 8 | * 9 | * @author Vyacheslav Rusakov 10 | * @since 22.03.2024 11 | */ 12 | @CompileStatic 13 | class PythonStat implements Comparable { 14 | // docker 15 | String containerName 16 | String projectPath 17 | String taskName 18 | String cmd 19 | String workDir 20 | long start 21 | long duration 22 | boolean success 23 | 24 | boolean parallel 25 | 26 | @Override 27 | int compareTo(@NotNull PythonStat pythonStat) { 28 | return start <=> pythonStat.start 29 | } 30 | 31 | boolean inParallel(PythonStat stat) { 32 | return startIn(this, stat) || startIn(stat, this) 33 | } 34 | 35 | String getFullTaskName() { 36 | return "$projectPath:$taskName".replaceAll('::', ':') 37 | } 38 | 39 | @Override 40 | String toString() { 41 | return "$fullTaskName:${System.identityHashCode(this)}" 42 | } 43 | 44 | private static boolean startIn(PythonStat stat, PythonStat stat2) { 45 | return stat.start <= stat2.start && stat.start + stat.duration > stat2.start 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/groovy/ru/vyarus/gradle/plugin/python/service/stat/StatsPrinter.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.service.stat 2 | 3 | import groovy.transform.CompileStatic 4 | import ru.vyarus.gradle.plugin.python.util.DurationFormatter 5 | 6 | import java.text.SimpleDateFormat 7 | 8 | /** 9 | * Python execution statistics print utility. 10 | * 11 | * @author Vyacheslav Rusakov 12 | * @since 28.03.2024 13 | */ 14 | @CompileStatic 15 | class StatsPrinter { 16 | 17 | @SuppressWarnings(['SimpleDateFormatMissingLocale', 'Println', 'UnnecessaryGetter']) 18 | static String print(List stats) { 19 | if (stats.empty) { 20 | return '' 21 | } 22 | Set sorted = new TreeSet(stats) 23 | StringBuilder res = new StringBuilder('\nPython execution stats:\n\n') 24 | boolean dockerUsed = stats.stream().anyMatch { it.containerName != null } 25 | SimpleDateFormat timeFormat = new SimpleDateFormat('HH:mm:ss:SSS') 26 | StatCollector collector = new StatCollector(sorted) 27 | String format = dockerUsed ? '%-37s %-3s%-12s %-20s %-10s %-8s %s%n' 28 | : '%-37s %-3s%-12s %s %-10s %-8s %s%n' 29 | res.append(String.format( 30 | format, 'task', '', 'started', dockerUsed ? 'docker container' : '', 'duration', '', '')) 31 | 32 | for (PythonStat stat : (sorted)) { 33 | collector.collect() 34 | res.append(String.format(format, stat.fullTaskName, stat.parallel ? '||' : '', 35 | timeFormat.format(stat.start), 36 | stat.containerName ?: '', DurationFormatter.format(stat.duration), 37 | stat.success ? '' : 'FAILED', stat.cmd)) 38 | } 39 | res.append('\n Executed ').append(stats.size()).append(' commands in ') 40 | .append(DurationFormatter.format(collector.overall)).append(' (overall)\n') 41 | 42 | if (!collector.duplicates.isEmpty()) { 43 | res.append('\n Duplicate executions:\n') 44 | collector.duplicates.each { 45 | res.append("\n\t\t$it.key (${it.value.size()})\n") 46 | it.value.each { 47 | res.append("\t\t\t$it.fullTaskName (work dir: ${it.workDir})\n") 48 | } 49 | } 50 | } 51 | return res.toString() 52 | } 53 | 54 | @SuppressWarnings('NestedForLoop') 55 | static class StatCollector { 56 | long overall = 0 57 | 58 | Map> duplicates = [:] 59 | 60 | StatCollector(Set stats) { 61 | for (PythonStat stat : stats) { 62 | for (PythonStat stat2 : stats) { 63 | if (stat != stat2 && stat.inParallel(stat2)) { 64 | stat.parallel = true 65 | stat2.parallel = true 66 | } 67 | } 68 | 69 | List dups = duplicates.get(stat.cmd) 70 | if (dups == null) { 71 | dups = [] 72 | duplicates.put(stat.cmd, dups) 73 | } 74 | dups.add(stat) 75 | 76 | overall += stat.duration 77 | } 78 | duplicates.removeAll { 79 | it.value.size() == 1 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/groovy/ru/vyarus/gradle/plugin/python/service/value/CacheValueSource.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.service.value 2 | 3 | import groovy.transform.CompileStatic 4 | import org.gradle.api.provider.Property 5 | import org.gradle.api.provider.ValueSource 6 | import org.gradle.api.provider.ValueSourceParameters 7 | import ru.vyarus.gradle.plugin.python.service.EnvService 8 | 9 | /** 10 | * Required to prevent configuration cache from storing inner cache maps (which would make all python instances 11 | * depend on its own cache map, making cache useless). 12 | * 13 | * @author Vyacheslav Rusakov 14 | * @since 26.03.2024 15 | */ 16 | @CompileStatic 17 | @SuppressWarnings('AbstractClassWithoutAbstractMethod') 18 | abstract class CacheValueSource implements ValueSource, CacheParams> { 19 | 20 | Map obtain() { 21 | return parameters.service.get() 22 | .getCache(parameters.project.get()) 23 | } 24 | 25 | interface CacheParams extends ValueSourceParameters { 26 | Property getService() 27 | Property getProject() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/groovy/ru/vyarus/gradle/plugin/python/service/value/StatsValueSource.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.service.value 2 | 3 | import groovy.transform.CompileStatic 4 | import org.gradle.api.provider.Property 5 | import org.gradle.api.provider.ValueSource 6 | import org.gradle.api.provider.ValueSourceParameters 7 | import ru.vyarus.gradle.plugin.python.service.EnvService 8 | import ru.vyarus.gradle.plugin.python.service.stat.PythonStat 9 | 10 | /** 11 | * Required to prevent configuration cache from storing inner stats list (which would make all python instances 12 | * depend on its own stats list, hiding stats). 13 | * 14 | * @author Vyacheslav Rusakov 15 | * @since 26.03.2024 16 | */ 17 | @CompileStatic 18 | @SuppressWarnings('AbstractClassWithoutAbstractMethod') 19 | abstract class StatsValueSource implements ValueSource, StatsParams> { 20 | 21 | List obtain() { 22 | return parameters.service.get().stats 23 | } 24 | 25 | interface StatsParams extends ValueSourceParameters { 26 | Property getService() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/groovy/ru/vyarus/gradle/plugin/python/task/PythonTask.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.task 2 | 3 | import groovy.transform.CompileStatic 4 | import org.gradle.api.GradleException 5 | import org.gradle.api.provider.ListProperty 6 | import org.gradle.api.provider.Property 7 | import org.gradle.api.tasks.Input 8 | import org.gradle.api.tasks.Optional 9 | import org.gradle.api.tasks.TaskAction 10 | import ru.vyarus.gradle.plugin.python.cmd.Python 11 | 12 | /** 13 | * Task to execute python command (call module, script) using globally installed python. 14 | * All python tasks are called after default pipInstall task. 15 | *

16 | * In essence, task duplicates {@link Python} utility configuration and use it for execution. 17 | *

18 | * Task may be used as base class for specific modules tasks. 19 | * 20 | * @author Vyacheslav Rusakov 21 | * @since 11.11.2017 22 | */ 23 | @CompileStatic 24 | abstract class PythonTask extends BasePythonTask { 25 | 26 | /** 27 | * Create work directory if it doesn't exist. Enabled by default. 28 | */ 29 | @Input 30 | abstract Property getCreateWorkDir() 31 | 32 | /** 33 | * Module name. If specified, "-m module " will be prepended to specified command (if command not specified then 34 | * modules will be called directly). 35 | */ 36 | @Input 37 | @Optional 38 | abstract Property getModule() 39 | 40 | /** 41 | * Python command to execute. If module name set then it will be module specific command. 42 | * Examples: 43 | *

48 | * Command could be specified as string, array or list (iterable). 49 | */ 50 | @Input 51 | @Optional 52 | abstract Property getCommand() 53 | 54 | /** 55 | * Prefix each line of python output. By default it's '\t' to indicate command output. 56 | */ 57 | @Input 58 | @Optional 59 | abstract Property getOutputPrefix() 60 | 61 | /** 62 | * Extra arguments to append to every called command. 63 | * Useful for pre-configured options, applied to all executed commands 64 | *

65 | * Option not available in {@link BasePythonTask} because of pip tasks which use different set of keys 66 | * for various commands. Special pip tasks like {@link ru.vyarus.gradle.plugin.python.task.pip.PipInstallTask} 67 | * use multiple different calls internally and general extra args would apply to all of them and, most likely, 68 | * crash the build. It is better to implement external arguments support on exact task level (to properly apply it 69 | * to exact executed command and avoid usage confusion). 70 | */ 71 | @Input 72 | @Optional 73 | abstract ListProperty getExtraArgs() 74 | 75 | @TaskAction 76 | void run() { 77 | String mod = module.orNull 78 | Object cmd = command.orNull 79 | if (!mod && !cmd) { 80 | throw new GradleException('Module or command to execute must be defined') 81 | } 82 | initWorkDirIfRequired() 83 | 84 | Python python = python 85 | .outputPrefix(outputPrefix.get()) 86 | .extraArgs(extraArgs.get()) 87 | 88 | // task-specific logger required for exclusive docker usage, because otherwise project logger would 89 | // show output below previous task (in exclusive mode logs would come from separate thread) 90 | if (mod) { 91 | python.callModule(logger, mod, cmd) 92 | } else { 93 | python.exec(logger, cmd) 94 | } 95 | } 96 | 97 | /** 98 | * Add extra arguments, applied to command. 99 | * 100 | * @param args arguments 101 | */ 102 | @SuppressWarnings('ConfusingMethodName') 103 | void extraArgs(String... args) { 104 | if (args) { 105 | extraArgs.addAll(args) 106 | } 107 | } 108 | 109 | @SuppressWarnings('UnnecessaryGetter') 110 | private void initWorkDirIfRequired() { 111 | String dir = getWorkDir().orNull 112 | if (dir && createWorkDir.get()) { 113 | File wrkd = gradleEnv.get().file(dir) 114 | if (!wrkd.exists()) { 115 | wrkd.mkdirs() 116 | } 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/main/groovy/ru/vyarus/gradle/plugin/python/task/env/EnvSupport.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.task.env 2 | 3 | import ru.vyarus.gradle.plugin.python.cmd.Pip 4 | 5 | /** 6 | * Virtual environment creation support. No model objects used for virtualenv settings because the entire check task 7 | * is passed (to re-use its configuration and services). 8 | *

9 | * In essence, this class must create environment if required and, eventually, provide different python path 10 | * to use by plugin. 11 | *

12 | * NOTE: virtual environment detection is actually implemented inside {@link ru.vyarus.gradle.plugin.python.cmd.Python} 13 | * object (by presence of activation script). So only packages using such script are supported (venv, virtualenv). 14 | * 15 | * @author Vyacheslav Rusakov 16 | * @since 01.04.2024 17 | */ 18 | interface EnvSupport { 19 | 20 | /** 21 | * @return true if environment already exists 22 | */ 23 | boolean exists() 24 | 25 | /** 26 | * Create new environment. 27 | * 28 | * @param pip pip instance (to check if required package installed) 29 | * @return true if environment was created 30 | */ 31 | boolean create(Pip pip) 32 | 33 | /** 34 | * @return python path to use (inside environment) 35 | */ 36 | String getPythonPath() 37 | } 38 | -------------------------------------------------------------------------------- /src/main/groovy/ru/vyarus/gradle/plugin/python/task/env/FallbackException.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.task.env 2 | 3 | import groovy.transform.CompileStatic 4 | 5 | /** 6 | * Exception indicate required fallback to virtualenv tool (e.g. when venv is not installed). 7 | * 8 | * @author Vyacheslav Rusakov 9 | * @since 01.04.2024 10 | */ 11 | @CompileStatic 12 | class FallbackException extends RuntimeException { 13 | } 14 | -------------------------------------------------------------------------------- /src/main/groovy/ru/vyarus/gradle/plugin/python/task/env/VenvSupport.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.task.env 2 | 3 | import groovy.transform.CompileStatic 4 | import ru.vyarus.gradle.plugin.python.cmd.Pip 5 | import ru.vyarus.gradle.plugin.python.cmd.Venv 6 | import ru.vyarus.gradle.plugin.python.task.CheckPythonTask 7 | 8 | /** 9 | * Venv support implementation. 10 | * 11 | * @author Vyacheslav Rusakov 12 | * @since 01.04.2024 13 | */ 14 | @CompileStatic 15 | class VenvSupport implements EnvSupport { 16 | private static final String PROP_VENV_INSTALLED = 'venv.installed' 17 | 18 | private final CheckPythonTask task 19 | private final Venv env 20 | 21 | VenvSupport(CheckPythonTask task) { 22 | this.task = task 23 | env = new Venv(task.gradleEnv.get(), task.pythonPath.orNull, task.pythonBinary.orNull, 24 | task.envPath.orNull) 25 | .validateSystemBinary(task.validateSystemBinary.get()) 26 | .withDocker(task.docker.toConfig()) 27 | .workDir(task.workDir.orNull) 28 | .environment(task.environment.get()) 29 | .validate() 30 | } 31 | 32 | @Override 33 | boolean exists() { 34 | return env.exists() 35 | } 36 | 37 | @Override 38 | boolean create(Pip pip) { 39 | // to avoid calling pip in EACH module (in multi-module project) to verify virtualenv existence 40 | Boolean venvInstalled = task.gradleEnv.get().globalCache(PROP_VENV_INSTALLED, null) 41 | if (venvInstalled == null) { 42 | venvInstalled = env.installed 43 | task.gradleEnv.get().updateGlobalCache(PROP_VENV_INSTALLED, venvInstalled) 44 | } 45 | if (!venvInstalled) { 46 | task.logger.warn('WARNING: Venv python module is not found, fallback to virtualenv') 47 | // fallback to virtualenv (no attempt to install it as it could be managed by system package) 48 | throw new FallbackException() 49 | } 50 | 51 | if (pip.python.virtualenv) { 52 | task.logger.error('WARNING: Global python is already a virtualenv: \'{}\'. New environment would be ' + 53 | 'created based on it: \'{}\'. In most cases, everything would work as expected.', 54 | pip.python.binaryDir, task.envPath.get()) 55 | } 56 | 57 | // no version for venv as its synchronized with python 58 | task.logger.lifecycle("Using venv (in '${task.envPath.get()}')") 59 | 60 | // symlink by default (copy if requested by user config) 61 | env.create(task.envCopy.get()) 62 | return true 63 | } 64 | 65 | @Override 66 | String getPythonPath() { 67 | return env.pythonPath 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/groovy/ru/vyarus/gradle/plugin/python/task/env/VirtualenvSupport.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.task.env 2 | 3 | import groovy.transform.CompileStatic 4 | import org.gradle.api.GradleException 5 | import ru.vyarus.gradle.plugin.python.PythonExtension 6 | import ru.vyarus.gradle.plugin.python.cmd.Pip 7 | import ru.vyarus.gradle.plugin.python.cmd.Virtualenv 8 | import ru.vyarus.gradle.plugin.python.task.CheckPythonTask 9 | import ru.vyarus.gradle.plugin.python.util.CliUtils 10 | 11 | /** 12 | * Virtualenv support implementation. 13 | * 14 | * @author Vyacheslav Rusakov 15 | * @since 01.04.2024 16 | */ 17 | @CompileStatic 18 | class VirtualenvSupport implements EnvSupport { 19 | private static final String PROP_VENV_INSTALLED = 'virtualenv.installed' 20 | 21 | private final CheckPythonTask task 22 | private final Virtualenv env 23 | 24 | VirtualenvSupport(CheckPythonTask task) { 25 | this.task = task 26 | env = new Virtualenv(task.gradleEnv.get(), task.pythonPath.orNull, task.pythonBinary.orNull, 27 | task.envPath.orNull) 28 | .validateSystemBinary(task.validateSystemBinary.get()) 29 | .withDocker(task.docker.toConfig()) 30 | .workDir(task.workDir.orNull) 31 | .environment(task.environment.get()) 32 | .validate() 33 | } 34 | 35 | @Override 36 | boolean exists() { 37 | return env.exists() 38 | } 39 | 40 | @Override 41 | boolean create(Pip pip) { 42 | // to avoid calling pip in EACH module (in multi-module project) to verify virtualenv existence 43 | Boolean venvInstalled = task.gradleEnv.get().globalCache(PROP_VENV_INSTALLED, null) 44 | if (venvInstalled == null) { 45 | venvInstalled = pip.isInstalled(env.name) 46 | task.gradleEnv.get().updateGlobalCache(PROP_VENV_INSTALLED, venvInstalled) 47 | } 48 | if (!venvInstalled) { 49 | if (task.installVirtualenv.get()) { 50 | // automatically install virtualenv if allowed (in --user) 51 | // by default, exact (configured) version used to avoid side effects!) 52 | pip.install(env.name + (task.virtualenvVersion.orNull ? "==${task.virtualenvVersion.get()}" : '')) 53 | task.gradleEnv.get().updateGlobalCache(PROP_VENV_INSTALLED, true) 54 | } else if (task.scope.get() == PythonExtension.Scope.VIRTUALENV) { 55 | // virtualenv strictly required - fail 56 | throw new GradleException('Virtualenv is not installed. Please install it ' + 57 | '(https://virtualenv.pypa.io/en/stable/installation/) or change target pip ' + 58 | "scope 'python.scope' from ${PythonExtension.Scope.VIRTUALENV}") 59 | } else { 60 | // not found, but ok (fallback to USER scope) 61 | return false 62 | } 63 | } 64 | 65 | if (pip.python.virtualenv) { 66 | task.logger.error('WARNING: Global python is already a virtualenv: \'{}\'. New environment would be ' + 67 | 'created based on it: \'{}\'. In most cases, everything would work as expected.', 68 | pip.python.binaryDir, task.envPath.get()) 69 | } 70 | 71 | task.logger.lifecycle("Using $env.versionLine (in '${task.envPath.get()}')") 72 | 73 | if (!CliUtils.isVersionMatch(env.version, task.minVirtualenvVersion.orNull)) { 74 | throw new GradleException("Installed virtualenv version $env.version does not match minimal " + 75 | "required version ${task.minVirtualenvVersion.get()}. \nVirtualenv " + 76 | "${task.minVirtualenvVersion.get()} is recommended but older version could also be used. " + 77 | '\nEither configure lower minimal required ' + 78 | "version with [python.minVirtualenvVersion=\'$env.version\'] \nor upgrade installed " + 79 | "virtualenv with [pip install -U virtualenv==${task.virtualenvVersion.get()}] \n(or just remove " + 80 | 'virtualenv with [pip uninstall virtualenv] and plugin will install the correct version itself)') 81 | } 82 | 83 | // symlink by default (copy if requested by user config) 84 | env.create(task.envCopy.get()) 85 | return true 86 | } 87 | 88 | @Override 89 | String getPythonPath() { 90 | return env.pythonPath 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/groovy/ru/vyarus/gradle/plugin/python/task/pip/PipListTask.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.task.pip 2 | 3 | import groovy.transform.CompileStatic 4 | import org.gradle.api.provider.Property 5 | import org.gradle.api.tasks.Input 6 | import org.gradle.api.tasks.TaskAction 7 | 8 | /** 9 | * List all installed modules in current scope. The same is displayed after pipInstall by default. 10 | * Task used just to be able to see installed modules list at any time (because pipInstall will show it only once). 11 | *

12 | * When user scope used, use {@code all = true} to see modules from global scope. 13 | * 14 | * @author Vyacheslav Rusakov 15 | * @since 15.12.2017 16 | */ 17 | @CompileStatic 18 | abstract class PipListTask extends BasePipTask { 19 | 20 | /** 21 | * To see all modules from global scope, when user scope used. 22 | * Note that option will not take effect if global scope is configured or virtualenv is used. 23 | */ 24 | @Input 25 | abstract Property getAll() 26 | 27 | @TaskAction 28 | void run() { 29 | Closure action = { pip.exec('list --format=columns') } 30 | if (all.get()) { 31 | // show global scope 32 | pip.inGlobalScope action 33 | } else { 34 | // show global or user (depends on scope configuration) 35 | action.call() 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/groovy/ru/vyarus/gradle/plugin/python/task/pip/PipModule.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.task.pip 2 | 3 | import groovy.transform.CompileStatic 4 | import ru.vyarus.gradle.plugin.python.task.pip.module.ModuleFactory 5 | 6 | /** 7 | * Pip module declaration pojo. Support parsing 'name:version' format (used for configuration). 8 | * 9 | * @author Vyacheslav Rusakov 10 | * @since 11.11.2017 11 | */ 12 | @CompileStatic 13 | class PipModule { 14 | String name 15 | String version 16 | 17 | /** 18 | * Parse module declaration in format 'module:version' or 'vcs+protocol://repo_url/@vcsVersion#egg=pkg-pkgVersion' 19 | * (for vcs module). 20 | * 21 | * @param declaration module declaration to parse 22 | * @return parsed module pojo 23 | * @throws IllegalArgumentException if module format does not match 24 | * @see ModuleFactory#create(java.lang.String) 25 | */ 26 | static PipModule parse(String declaration) { 27 | return ModuleFactory.create(declaration) 28 | } 29 | 30 | PipModule(String name, String version) { 31 | if (!name) { 32 | throw new IllegalArgumentException('Module name required') 33 | } 34 | if (!version) { 35 | throw new IllegalArgumentException('Module version required') 36 | } 37 | 38 | this.name = name 39 | this.version = version 40 | } 41 | 42 | /** 43 | * @return human readable module declaration 44 | */ 45 | @Override 46 | String toString() { 47 | return "$name $version" 48 | } 49 | 50 | /** 51 | * Must be used for module up to date detection. 52 | * 53 | * @return module declaration in pip format 54 | * @deprecated freeze command output changed in pip 21 for vcs modules and so now exact pip version is required 55 | * for proper up-to-date check 56 | */ 57 | @Deprecated 58 | String toPipString() { 59 | return toFreezeStrings()[0] 60 | } 61 | 62 | /** 63 | * Module record as it appears in {@code pip freeze} command. 64 | * Must be used for module up to date detection. Multiple results required to properly support 65 | * changed pip output syntax between versions. 66 | * 67 | * @param pipVersion current pip version (because command output could change) 68 | * @return list of possible module declarations in the same format as freeze will print 69 | */ 70 | List toFreezeStrings() { 71 | // exact version matching! 72 | // pip will re-install even newer package to an older version 73 | return ["$name==$version" as String] 74 | } 75 | 76 | /** 77 | * Must be used for installation. 78 | * 79 | * @return module installation declaration 80 | */ 81 | String toPipInstallString() { 82 | return "$name==$version" 83 | } 84 | 85 | boolean equals(Object o) { 86 | if (this.is(o)) { 87 | return true 88 | } 89 | if (!getClass().isAssignableFrom(o.class)) { 90 | return false 91 | } 92 | 93 | PipModule pipModule = (PipModule) o 94 | return name == pipModule.name && version == pipModule.version 95 | } 96 | 97 | int hashCode() { 98 | int result 99 | result = name.hashCode() 100 | result = 31 * result + version.hashCode() 101 | return result 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main/groovy/ru/vyarus/gradle/plugin/python/task/pip/PipUpdatesTask.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.task.pip 2 | 3 | import groovy.transform.CompileStatic 4 | import org.gradle.api.provider.Property 5 | import org.gradle.api.tasks.Input 6 | import org.gradle.api.tasks.TaskAction 7 | 8 | /** 9 | * Print available new versions for the registered pip modules. 10 | * 11 | * @author Vyacheslav Rusakov 12 | * @since 01.12.2017 13 | */ 14 | @CompileStatic 15 | abstract class PipUpdatesTask extends BasePipTask { 16 | 17 | /** 18 | * True to show all available updates. By default (false): show only updates for configured modules. 19 | */ 20 | @Input 21 | abstract Property getAll() 22 | 23 | @TaskAction 24 | @SuppressWarnings('DuplicateNumberLiteral') 25 | void run() { 26 | boolean showAll = all.get() 27 | if (!showAll && modulesList.empty) { 28 | logger.lifecycle('No modules declared') 29 | } else { 30 | List res = [] 31 | List updates = pip.readOutput('list -o -l --format=columns').toLowerCase().readLines() 32 | 33 | // when no updates - no output (for all or filtered) 34 | if (showAll || updates.empty) { 35 | res = updates 36 | } else { 37 | // header 38 | res.addAll(updates[0..1]) 39 | 2.times { updates.remove(0) } 40 | 41 | // search for lines matching modules 42 | modulesList.each { PipModule mod -> 43 | String line = updates.find { it =~ /$mod.name\s+/ } 44 | if (line) { 45 | res.add(line) 46 | } 47 | } 48 | } 49 | 50 | if (res.size() > 2) { 51 | logger.lifecycle('The following modules could be updated:\n\n{}', 52 | res.collect { '\t' + it }.join('\n')) 53 | } else { 54 | logger.lifecycle('All modules use the most recent versions') 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/groovy/ru/vyarus/gradle/plugin/python/task/pip/module/FeaturePipModule.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.task.pip.module 2 | 3 | import groovy.transform.CompileStatic 4 | import ru.vyarus.gradle.plugin.python.task.pip.PipModule 5 | 6 | /** 7 | * Feature-enabled modules support. E.g. 'requests[socks,security]:2.18.4'. 8 | * Such declaration should install modified version of requests module. Everything in square brackets is simply 9 | * passed to module's install script as parameters. 10 | *

11 | * As it is not possible to track exact variation of installed module, then module will not be installed if 12 | * default 'requests:2.18.4' is installed. 13 | * 14 | * @author Vyacheslav Rusakov 15 | * @since 23.05.2018 16 | */ 17 | @CompileStatic 18 | class FeaturePipModule extends PipModule { 19 | 20 | private final String qualifier 21 | 22 | FeaturePipModule(String name, String qualifier, String version) { 23 | super(name, version) 24 | this.qualifier = qualifier 25 | } 26 | 27 | @Override 28 | String toString() { 29 | return "${name}[$qualifier] $version" 30 | } 31 | 32 | @Override 33 | String toPipInstallString() { 34 | return "${name}[$qualifier]==$version" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/groovy/ru/vyarus/gradle/plugin/python/task/pip/module/ModuleFactory.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.task.pip.module 2 | 3 | import groovy.transform.CompileStatic 4 | import ru.vyarus.gradle.plugin.python.task.pip.PipModule 5 | 6 | import java.util.regex.Matcher 7 | import java.util.regex.Pattern 8 | 9 | /** 10 | * Module descriptor parser. Supports versioned vcs modules, special exact syntax (name:version) and 11 | * feature-enabled exact syntax (name[feature1,feature2]:version). 12 | * 13 | * @author Vyacheslav Rusakov 14 | * @since 18.05.2018 15 | */ 16 | @CompileStatic 17 | @SuppressWarnings('DuplicateNumberLiteral') 18 | class ModuleFactory { 19 | 20 | private static final Pattern VCS_FORMAT = Pattern.compile('@[^#]+#egg=([^&]+)') 21 | private static final String VERSION_SEPARATOR = ':' 22 | private static final String VCS_VERSION_SEPARATOR = '-' 23 | 24 | private static final Pattern FEATURE_FORMAT = Pattern.compile('(.+)\\[(.+)]\\s*:\\s*(.+)') 25 | private static final String QUALIFIER_START = '[' 26 | private static final String QUALIFIER_END = ']' 27 | 28 | private static final int DECL_PARTS = 2 29 | 30 | /** 31 | * @param descriptor module descriptor string 32 | * @return parsed module instance (normal or vcs) 33 | */ 34 | static PipModule create(String descriptor) { 35 | PipModule res 36 | if (descriptor.contains('#egg=') || descriptor.contains('/')) { 37 | res = parseVcsModule(descriptor) 38 | } else if (descriptor.contains(QUALIFIER_START) && descriptor.contains(QUALIFIER_END)) { 39 | res = parseFeatureModule(descriptor) 40 | } else { 41 | res = parseModule(descriptor) 42 | } 43 | return res 44 | } 45 | 46 | /** 47 | * Search module by name in provided declarations. Supports normal and vcs syntax. 48 | * 49 | * @param name module name 50 | * @param modules module declarations to search in 51 | * @return found module name or null if not found 52 | */ 53 | static String findModuleDeclaration(String name, List modules) { 54 | String nm = name.toLowerCase() + VERSION_SEPARATOR 55 | String qualifNm = name.toLowerCase() + QUALIFIER_START 56 | String vcsNm = "#egg=${name.toLowerCase()}-" 57 | return modules.find { 58 | String mod = it.toLowerCase() 59 | if (mod.contains(QUALIFIER_START)) { 60 | // qualified definition 61 | return mod.startsWith(qualifNm) 62 | } 63 | // vcs and simple definitions 64 | return mod.startsWith(nm) || mod.contains(vcsNm) 65 | } 66 | } 67 | 68 | /** 69 | * Parse vsc module declaration. Only declaration with exact vcs and package versions is acceptable. 70 | * 71 | * @param desc descriptor 72 | * @return parsed module instance 73 | * @see pip vsc support 74 | */ 75 | private static PipModule parseVcsModule(String desc) { 76 | if (!desc.contains('@')) { 77 | throw new IllegalArgumentException("${wrongVcs(desc)} '@version' part is required") 78 | } 79 | Matcher matcher = VCS_FORMAT.matcher(desc) 80 | if (!matcher.find()) { 81 | throw new IllegalArgumentException("${wrongVcs(desc)} Module name not found") 82 | } 83 | String name = matcher.group(1).trim() 84 | // '-' could not appear in module name 85 | if (!name.contains(VCS_VERSION_SEPARATOR)) { 86 | throw new IllegalArgumentException( 87 | "${wrongVcs(desc)} Module version is required in module (#egg=name-version): '$name'. " + 88 | 'This is important to be able to check up-to-date state without python run') 89 | } 90 | String[] split = name.split(VCS_VERSION_SEPARATOR) 91 | String version = split.last().trim() 92 | // remove version part because pip fails to install with it 93 | String pkgName = name[0..name.lastIndexOf(VCS_VERSION_SEPARATOR) - 1].trim() 94 | String shortDesc = desc.replace(name, pkgName) 95 | return new VcsPipModule(shortDesc, pkgName, version) 96 | } 97 | 98 | /** 99 | * Feature enabled module declaration: name[qualifier]:version. 100 | * 101 | * @param desc module descriptor 102 | * @return simple module if qualifier is empty or feature module 103 | */ 104 | private static PipModule parseFeatureModule(String desc) { 105 | Matcher matcher = FEATURE_FORMAT.matcher(desc) 106 | if (!matcher.matches()) { 107 | throw new IllegalArgumentException('Incorrect pip module declaration (expected ' + 108 | "'module[qualifier,qualifier2]:version'): '$desc'") 109 | } 110 | String name = matcher.group(1).trim() 111 | String qualifier = matcher.group(2).trim() 112 | String version = matcher.group(3).trim() 113 | return qualifier ? 114 | new FeaturePipModule(name, qualifier, version) 115 | // no qualifier ([]) - silently create simple module 116 | : new PipModule(name, version) 117 | } 118 | 119 | /** 120 | * Parse module declaration in format 'module:version'. 121 | * 122 | * @param declaration module declaration to parse 123 | * @return parsed module pojo 124 | * @throws IllegalArgumentException if module format does not match 125 | */ 126 | private static PipModule parseModule(String desc) { 127 | String[] parts = desc.split(VERSION_SEPARATOR) 128 | if (parts.length != DECL_PARTS) { 129 | throw new IllegalArgumentException( 130 | "Incorrect pip module declaration (must be 'module:version'): $desc") 131 | } 132 | return new PipModule(parts[0].trim() ?: null, parts[1].trim() ?: null) 133 | } 134 | 135 | private static String wrongVcs(String desc) { 136 | return "Incorrect pip vsc module declaration: '$desc' (required format is " + 137 | "'vcs+protocol://repo_url/@vcsVersion#egg=name-pkgVersion')." 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/main/groovy/ru/vyarus/gradle/plugin/python/task/pip/module/VcsPipModule.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.task.pip.module 2 | 3 | import groovy.transform.CompileStatic 4 | import ru.vyarus.gradle.plugin.python.task.pip.PipModule 5 | 6 | /** 7 | * Supported vsc module format: vcs+protocol://repo_url/@vcsVersion#egg=pkg-pkgVersion. It requires both commit version 8 | * (may be tag or branch name) and package version. Package version is important because otherwise it would be 9 | * impossible to track up-to date state without python run (slow). Vsc version is important for predictable 10 | * builds. 11 | *

12 | * IMPORTANT: if you specify branch version then module will be installed only once because it will rely on 13 | * version declaration in #egg part. The only way to workaround it is to use 14 | * {@code python.alwaysInstallModules = true} option in order to delegate dependency management to pip. 15 | *

16 | * Note: egg=project-version is official convention, but it is not supported, so version part is cut off in actual 17 | * pip install command. 18 | * 19 | * @author Vyacheslav Rusakov 20 | * @since 18.05.2018 21 | * @see pip vsc support 22 | */ 23 | @CompileStatic 24 | class VcsPipModule extends PipModule { 25 | 26 | private final String declaration 27 | 28 | VcsPipModule(String declaration, String name, String version) { 29 | super(name, version) 30 | this.declaration = declaration 31 | } 32 | 33 | @Override 34 | String toString() { 35 | return "$name $version ($declaration)" 36 | } 37 | 38 | @Override 39 | String toPipInstallString() { 40 | return declaration 41 | } 42 | 43 | @Override 44 | List toFreezeStrings() { 45 | List res = super.toFreezeStrings() 46 | // In pip 21 (actually latest 20.x and 19.x too) freeze command shows exact version path, 47 | // instead of pure version! 48 | 49 | // Separator would be always present due to forced validation in 50 | // ru.vyarus.gradle.plugin.python.task.pip.module.ModuleFactory.parseVcsModule 51 | String hash = declaration[0..declaration.lastIndexOf('#') - 1] 52 | // put new syntax first 53 | res.add(0, "$name @ $hash" as String) 54 | return res 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/groovy/ru/vyarus/gradle/plugin/python/util/DurationFormatter.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.util 2 | 3 | import groovy.transform.CompileStatic 4 | 5 | /** 6 | * Copy of gradle's internal {@link org.gradle.internal.time.TimeFormatting} class, which become internal in 7 | * gradle 4.2 and broke compatibility. 8 | *

9 | * Used to pretty print elapsed tile in human readable form. 10 | * 11 | * @author Vyacheslav Rusakov 12 | * @since 21.09.2017 13 | */ 14 | @CompileStatic 15 | class DurationFormatter { 16 | private static final long MILLIS_PER_SECOND = 1000 17 | private static final long MILLIS_PER_MINUTE = 60000 18 | private static final long MILLIS_PER_HOUR = 3600000 19 | private static final long MILLIS_PER_DAY = 86400000 20 | 21 | private DurationFormatter() { 22 | } 23 | 24 | /** 25 | * @param duration duration in milliseconds 26 | * @return human readable (short) duration 27 | */ 28 | static String format(long duration) { 29 | if (duration == 0L) { 30 | return '0ms' 31 | } 32 | 33 | StringBuilder result = new StringBuilder() 34 | long days = (duration / MILLIS_PER_DAY).longValue() 35 | duration %= MILLIS_PER_DAY 36 | if (days > 0L) { 37 | append(result, days, 'd') 38 | } 39 | 40 | long hours = (duration / MILLIS_PER_HOUR).longValue() 41 | duration %= MILLIS_PER_HOUR 42 | if (hours > 0L) { 43 | append(result, hours, 'h') 44 | } 45 | 46 | long minutes = (duration / MILLIS_PER_MINUTE).longValue() 47 | duration %= MILLIS_PER_MINUTE 48 | if (minutes > 0L) { 49 | append(result, minutes, 'm') 50 | } 51 | 52 | boolean secs = false 53 | if (duration >= MILLIS_PER_SECOND) { 54 | // if only secs, show rounded value, otherwise get rid of ms 55 | int secondsScale = result.length() == 0 ? 2 : 0 56 | append(result, 57 | BigDecimal.valueOf(duration) 58 | .divide(BigDecimal.valueOf(MILLIS_PER_SECOND)) 59 | .setScale(secondsScale, 4) 60 | .stripTrailingZeros() 61 | .toPlainString(), 62 | 's') 63 | secs = true 64 | duration %= MILLIS_PER_SECOND 65 | } 66 | 67 | if (!secs && duration > 0) { 68 | result.append(duration + 'ms') 69 | } 70 | return result.toString() 71 | } 72 | 73 | private static void append(StringBuilder builder, Object num, String what) { 74 | if (builder.length() > 0) { 75 | builder.append(' ') 76 | } 77 | builder.append(num) 78 | builder.append(what) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/groovy/ru/vyarus/gradle/plugin/python/util/OutputLogger.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.util 2 | 3 | import groovy.transform.CompileStatic 4 | import org.apache.tools.ant.util.LineOrientedOutputStream 5 | import org.gradle.api.logging.LogLevel 6 | import org.gradle.api.logging.Logger 7 | 8 | /** 9 | * Special output stream to be used instead of system.out to redirect output into gradle logger (by line) 10 | * with prefixing. 11 | * 12 | * @author Vyacheslav Rusakov 13 | * @since 16.11.2017 14 | */ 15 | @CompileStatic 16 | class OutputLogger extends LineOrientedOutputStream { 17 | 18 | private final Logger logger 19 | private final LogLevel level 20 | private final String prefix 21 | 22 | private final StringBuilder source = new StringBuilder() 23 | 24 | OutputLogger(Logger logger, LogLevel level, String prefix) { 25 | this.logger = logger 26 | this.level = level 27 | this.prefix = prefix 28 | } 29 | 30 | @Override 31 | String toString() { 32 | // returns original output 33 | return source.toString().trim() 34 | } 35 | 36 | @Override 37 | protected void processLine(String s) throws IOException { 38 | String msg = prefix ? "$prefix $s" : s 39 | logger.log(level, msg) 40 | source.append(s).append('\n') 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/groovy/ru/vyarus/gradle/plugin/python/util/PythonExecutionFailed.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.util 2 | 3 | import groovy.transform.CompileStatic 4 | import org.gradle.api.GradleException 5 | 6 | /** 7 | * Thrown when python command execution failed. Message will contain entire command. 8 | * 9 | * @author Vyacheslav Rusakov 10 | * @since 15.11.2017 11 | */ 12 | @CompileStatic 13 | class PythonExecutionFailed extends GradleException { 14 | 15 | PythonExecutionFailed(String message) { 16 | super(message) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/groovy/ru/vyarus/gradle/plugin/python/util/RequirementsReader.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.util 2 | 3 | import groovy.transform.CompileStatic 4 | import ru.vyarus.gradle.plugin.python.PythonExtension 5 | import ru.vyarus.gradle.plugin.python.cmd.env.Environment 6 | 7 | /** 8 | * Read requirements file and convert it into plugin's modules declaration syntax (the same as modules declared in 9 | * {@link ru.vyarus.gradle.plugin.python.PythonExtension#pip(java.lang.String [ ])}). 10 | * 11 | * @author Vyacheslav Rusakov 12 | * @since 24.08.2022 13 | * @see requirements files 14 | * @see format 15 | */ 16 | @CompileStatic 17 | class RequirementsReader { 18 | 19 | /** 20 | * Searches for requirements file, counting that requirements files support could be disabled. File is searched 21 | * relative to project root (in case of module - module root). File is not searched for submodule inside root 22 | * project to avoid situation when all modules read requirements from root which must be using different 23 | * set of dependencies. If required, root file could be always manually configured for sub modules. 24 | * 25 | * @param project project 26 | * @param requirements extension 27 | * @return found file or null 28 | */ 29 | static File find(Environment environment, PythonExtension.Requirements requirements) { 30 | if (!requirements.use) { 31 | return null 32 | } 33 | File reqs = environment.file(requirements.file) 34 | return reqs.exists() ? reqs : null 35 | } 36 | 37 | /** 38 | * Reads module declarations from requirements file. Does not perform any validations: it is assumed to 39 | * be used for plugin input which would complain if some declaration is incorrect. 40 | *

41 | * Recognize requirements file references (like "-r some-file.txt") and reads referenced files. Constraint 42 | * files (-c) are not supported! 43 | *

44 | * Returns all non empty and non-comment lines. Only replace '==' into ':' to convert from python declaration 45 | * syntax into plugin syntax. 46 | *

47 | * NOTE: only not quite correct vcs modules syntax is supported: its egg part must contain version (which is wrong 48 | * for pure pip declaration). Anyway, that is the only way for plugin to know vcs module version and 49 | * correctly apply up-to-date checks. 50 | * 51 | * @param file requirements file 52 | * @return module declarations from requirements file or empty list 53 | */ 54 | static List read(File file) { 55 | if (!file || !file.exists()) { 56 | return Collections.emptyList() 57 | } 58 | 59 | // requirements file may use different encoding, but its intentionally not supported 60 | List res = [] 61 | file.readLines('utf-8').each { 62 | String line = it.trim() 63 | if (line) { 64 | if (line.startsWith('-r')) { 65 | String sub = line.split(' ')[1].trim() 66 | // not existing file would be simply ignored 67 | res.addAll(read(new File(file.parent, sub))) 68 | } else if (!line.startsWith('#')) { 69 | // translate python syntax into "plugin syntax" (required only for simple packages) 70 | res.add(line.replace('==', ':')) 71 | } 72 | } 73 | } 74 | return res 75 | } 76 | 77 | /** 78 | * Returns path, relative for current project if file located somewhere inside root project. Otherwise returns 79 | * absolute file path (file located outside project dir) 80 | * 81 | * @param environment gradle environment 82 | * @param file file to get path of 83 | * @return relative file path if file is located inside project or absolute path 84 | */ 85 | static String relativePath(Environment environment, File file) { 86 | if (file.canonicalPath.startsWith(environment.rootDir.canonicalPath)) { 87 | return environment.relativePath(file) 88 | } 89 | return file.canonicalPath 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/test/groovy/ru/vyarus/gradle/plugin/python/AbsoluteVirtualenvLocationKitTest.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python 2 | 3 | import org.gradle.testkit.runner.BuildResult 4 | import org.gradle.testkit.runner.TaskOutcome 5 | import ru.vyarus.gradle.plugin.python.cmd.Venv 6 | import ru.vyarus.gradle.plugin.python.cmd.env.SimpleEnvironment 7 | import ru.vyarus.gradle.plugin.python.util.CliUtils 8 | import spock.lang.TempDir 9 | 10 | /** 11 | * @author Vyacheslav Rusakov 12 | * @since 28.08.2018 13 | */ 14 | class AbsoluteVirtualenvLocationKitTest extends AbstractKitTest { 15 | 16 | @TempDir File envDir 17 | 18 | def "Check virtualenv configuration with absolute path"() { 19 | 20 | setup: 21 | build """ 22 | plugins { 23 | id 'ru.vyarus.use-python' 24 | } 25 | 26 | python { 27 | envPath = "${CliUtils.canonicalPath(envDir).replace('\\', '\\\\')}" 28 | 29 | pip 'extract-msg:0.28.0' 30 | } 31 | 32 | tasks.register('sample', PythonTask) { 33 | command = '-c print(\\'samplee\\')' 34 | } 35 | 36 | """ 37 | 38 | when: "run task" 39 | BuildResult result = run(':sample') 40 | 41 | then: "task successful" 42 | result.task(':sample').outcome == TaskOutcome.SUCCESS 43 | result.output =~ /extract-msg\s+0.28.0/ 44 | result.output.contains('samplee') 45 | 46 | then: "virtualenv created at correct path" 47 | result.output.contains("${CliUtils.canonicalPath(envDir)}${File.separator}") 48 | } 49 | 50 | def "Check user home recognition"() { 51 | setup: 52 | File dir = new File(CliUtils.resolveHomeReference("~/.testuserdir")) 53 | dir.mkdirs() 54 | 55 | build """ 56 | plugins { 57 | id 'ru.vyarus.use-python' 58 | } 59 | 60 | python { 61 | envPath = "~/.testuserdir" 62 | 63 | pip 'extract-msg:0.28.0' 64 | } 65 | 66 | tasks.register('sample', PythonTask) { 67 | command = '-c print(\\'samplee\\')' 68 | } 69 | 70 | """ 71 | 72 | when: "run task" 73 | BuildResult result = run(':sample') 74 | 75 | then: "task successful" 76 | result.task(':sample').outcome == TaskOutcome.SUCCESS 77 | result.output =~ /extract-msg\s+0.28.0/ 78 | result.output.contains('samplee') 79 | 80 | then: "virtualenv created at correct path" 81 | result.output.contains("${CliUtils.canonicalPath(dir)}") 82 | 83 | when: "test virtualenv direct support" 84 | Venv env = new Venv(new SimpleEnvironment(testProjectDir), "~/.testuserdir") 85 | then: "created" 86 | env.exists() 87 | 88 | when: "cleanup directory" 89 | result = run(':cleanPython') 90 | 91 | then: "ok" 92 | result.task(":cleanPython").outcome == TaskOutcome.SUCCESS 93 | result.output.contains("${CliUtils.canonicalPath(dir)}") 94 | 95 | cleanup: 96 | dir.deleteDir() 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/test/groovy/ru/vyarus/gradle/plugin/python/AbstractKitTest.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python 2 | 3 | import org.apache.tools.ant.taskdefs.condition.Os 4 | import org.gradle.api.Project 5 | import org.gradle.testfixtures.ProjectBuilder 6 | import org.gradle.testkit.runner.BuildResult 7 | import org.gradle.testkit.runner.GradleRunner 8 | import ru.vyarus.gradle.plugin.python.cmd.Venv 9 | import ru.vyarus.gradle.plugin.python.cmd.Virtualenv 10 | import ru.vyarus.gradle.plugin.python.cmd.env.Environment 11 | import ru.vyarus.gradle.plugin.python.cmd.env.GradleEnvironment 12 | import ru.vyarus.gradle.plugin.python.service.EnvService 13 | import spock.lang.Specification 14 | import spock.lang.TempDir 15 | 16 | /** 17 | * Base class for Gradle TestKit based tests. 18 | * Useful for full-cycle and files manipulation testing. 19 | * 20 | * @author Vyacheslav Rusakov 21 | * @since 11.11.2017 22 | */ 23 | abstract class AbstractKitTest extends Specification { 24 | 25 | boolean debug 26 | boolean isWin = Os.isFamily(Os.FAMILY_WINDOWS) 27 | 28 | @TempDir File testProjectDir 29 | File buildFile 30 | 31 | def setup() { 32 | buildFile = file('build.gradle') 33 | // jacoco coverage support 34 | fileFromClasspath('gradle.properties', 'testkit-gradle.properties') 35 | } 36 | 37 | def build(String file) { 38 | buildFile << file 39 | } 40 | 41 | File file(String path) { 42 | new File(testProjectDir, path) 43 | } 44 | 45 | File fileFromClasspath(String toFile, String source) { 46 | File target = file(toFile) 47 | target.parentFile.mkdirs() 48 | target.withOutputStream { 49 | it.write((getClass().getResourceAsStream(source) ?: getClass().classLoader.getResourceAsStream(source)).bytes) 50 | } 51 | target 52 | } 53 | 54 | /** 55 | * Enable it and run test with debugger (no manual attach required). Not always enabled to speed up tests during 56 | * normal execution. 57 | */ 58 | def debug() { 59 | debug = true 60 | } 61 | 62 | String projectName() { 63 | return testProjectDir.getName() 64 | } 65 | 66 | GradleRunner gradle(File root, String... commands) { 67 | GradleRunner.create() 68 | .withProjectDir(root) 69 | .withArguments((commands + ['--stacktrace']) as String[]) 70 | .withPluginClasspath() 71 | .withDebug(debug) 72 | .forwardOutput() 73 | } 74 | 75 | GradleRunner gradle(String... commands) { 76 | gradle(testProjectDir, commands) 77 | } 78 | 79 | BuildResult run(String... commands) { 80 | return gradle(commands).build() 81 | } 82 | 83 | BuildResult runFailed(String... commands) { 84 | return gradle(commands).buildAndFail() 85 | } 86 | 87 | BuildResult runVer(String gradleVersion, String... commands) { 88 | println 'Running with GRADLE ' + gradleVersion 89 | return gradle(commands).withGradleVersion(gradleVersion).build() 90 | } 91 | 92 | BuildResult runFailedVer(String gradleVersion, String... commands) { 93 | println 'Running with GRADLE ' + gradleVersion 94 | return gradle(commands).withGradleVersion(gradleVersion).buildAndFail() 95 | } 96 | 97 | protected String unifyString(String input) { 98 | return input 99 | // cleanup win line break for simpler comparisons 100 | .replace("\r", '') 101 | } 102 | 103 | String unifyStats(String text) { 104 | return unifyString(text) 105 | .replaceAll(/\d{2}:\d{2}:\d{2}:\d{3}/, '11:11:11:111') 106 | .replaceAll(/(\d\.?)+(ms|s)\s+/, '11ms ') 107 | .replaceAll(/11ms\s+\(overall\)/, '11ms (overall)') 108 | .replaceAll(/ +\/[a-z_]{2,} +/, " /test_container ") 109 | // workaround for windows paths 110 | .replace('\\', '/') 111 | } 112 | 113 | // custom virtualenv to use for simulations 114 | Virtualenv env(String path = '.gradle/python', String binary = null) { 115 | new Virtualenv(gradleEnv(ProjectBuilder.builder() 116 | .withProjectDir(testProjectDir).build()), null, binary, path) 117 | } 118 | 119 | Venv venv(String path = '.gradle/python', String binary = null) { 120 | new Venv(gradleEnv(ProjectBuilder.builder() 121 | .withProjectDir(testProjectDir).build()), null, binary, path) 122 | } 123 | 124 | Environment gradleEnv() { 125 | gradleEnv(ProjectBuilder.builder().build()) 126 | } 127 | 128 | Environment gradleEnv(Project project) { 129 | GradleEnvironment.create(project, "gg", project.gradle.sharedServices.registerIfAbsent( 130 | 'pythonEnvironmentService', EnvService, spec -> { 131 | EnvService.Params params = spec.parameters as EnvService.Params 132 | params.printStats.set(false) 133 | params.debug.set(false) 134 | } 135 | ), project.provider { false }) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/test/groovy/ru/vyarus/gradle/plugin/python/AbstractTest.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python 2 | 3 | import org.apache.tools.ant.taskdefs.condition.Os 4 | import org.gradle.api.Project 5 | import org.gradle.testfixtures.ProjectBuilder 6 | import ru.vyarus.gradle.plugin.python.cmd.env.Environment 7 | import ru.vyarus.gradle.plugin.python.cmd.env.GradleEnvironment 8 | import ru.vyarus.gradle.plugin.python.service.EnvService 9 | import spock.lang.Specification 10 | import spock.lang.TempDir 11 | 12 | /** 13 | * Base class for plugin configuration tests. 14 | * 15 | * @author Vyacheslav Rusakov 16 | * @since 11.11.2017 17 | */ 18 | abstract class AbstractTest extends Specification { 19 | 20 | boolean isWin = Os.isFamily(Os.FAMILY_WINDOWS) 21 | 22 | @TempDir 23 | File testProjectDir 24 | 25 | Project project(Closure config = null) { 26 | projectBuilder(config).build() 27 | } 28 | 29 | ExtendedProjectBuilder projectBuilder(Closure root = null) { 30 | new ExtendedProjectBuilder().root(testProjectDir, root) 31 | } 32 | 33 | File file(String path) { 34 | new File(testProjectDir, path) 35 | } 36 | 37 | File fileFromClasspath(String toFile, String source) { 38 | File target = file(toFile) 39 | target.parentFile.mkdirs() 40 | target << getClass().getResourceAsStream(source).text 41 | } 42 | 43 | Environment gradleEnv() { 44 | gradleEnv(project()) 45 | } 46 | 47 | Environment gradleEnv(Project project) { 48 | GradleEnvironment.create(project, "gg", project.gradle.sharedServices.registerIfAbsent( 49 | 'pythonEnvironmentService', EnvService, spec -> { 50 | EnvService.Params params = spec.parameters as EnvService.Params 51 | // only root project value counted for print stats activation 52 | params.printStats.set(false) 53 | params.debug.set(false) 54 | }), project.provider { false }) 55 | } 56 | 57 | protected String unifyString(String input) { 58 | return input 59 | // cleanup win line break for simpler comparisons 60 | .replace("\r", '') 61 | } 62 | 63 | String unifyStats(String text) { 64 | return unifyString(text) 65 | .replaceAll(/\d{2}:\d{2}:\d{2}:\d{3}/, '11:11:11:111') 66 | .replaceAll(/(\d\.?)+(ms|s)\s+/, '11ms ') 67 | .replaceAll(/11ms\s+\(overall\)/, '11ms (overall)') 68 | } 69 | 70 | static class ExtendedProjectBuilder { 71 | Project root 72 | 73 | ExtendedProjectBuilder root(File dir, Closure config = null) { 74 | assert root == null, "Root project already declared" 75 | Project project = ProjectBuilder.builder() 76 | .withProjectDir(dir).build() 77 | if (config) { 78 | project.configure(project, config) 79 | } 80 | root = project 81 | return this 82 | } 83 | 84 | /** 85 | * Direct child of parent project 86 | * 87 | * @param name child project name 88 | * @param config optional configuration closure 89 | * @return builder 90 | */ 91 | ExtendedProjectBuilder child(String name, Closure config = null) { 92 | return childOf(null, name, config) 93 | } 94 | 95 | /** 96 | * Direct child of any registered child project 97 | * 98 | * @param projectRef name of required parent module (gradle project reference format: `:some:deep:module`) 99 | * @param name child project name 100 | * @param config optional configuration closure 101 | * @return builder 102 | */ 103 | ExtendedProjectBuilder childOf(String projectRef, String name, Closure config = null) { 104 | assert root != null, "Root project not declared" 105 | Project parent = projectRef == null ? root : root.project(projectRef) 106 | File folder = parent.file(name) 107 | if (!folder.exists()) { 108 | folder.mkdir() 109 | } 110 | Project project = ProjectBuilder.builder() 111 | .withName(name) 112 | .withProjectDir(folder) 113 | .withParent(parent) 114 | .build() 115 | if (config) { 116 | project.configure(project, config) 117 | } 118 | return this 119 | } 120 | 121 | /** 122 | * Evaluate configuration. 123 | * 124 | * @return root project 125 | */ 126 | Project build() { 127 | if (root.subprojects) { 128 | linkSubprojectsEvaluation(root) 129 | } 130 | root.evaluate() 131 | return root 132 | } 133 | 134 | private void linkSubprojectsEvaluation(Project project) { 135 | project.evaluationDependsOnChildren() 136 | project.subprojects.each { linkSubprojectsEvaluation(it) } 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/test/groovy/ru/vyarus/gradle/plugin/python/GlobalVirtualenvTest.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python 2 | 3 | import org.gradle.testkit.runner.BuildResult 4 | import org.gradle.testkit.runner.TaskOutcome 5 | import ru.vyarus.gradle.plugin.python.cmd.Venv 6 | import ru.vyarus.gradle.plugin.python.cmd.Virtualenv 7 | 8 | /** 9 | * @author Vyacheslav Rusakov 10 | * @since 06.03.2020 11 | */ 12 | class GlobalVirtualenvTest extends AbstractKitTest { 13 | 14 | def "Check venv from virtualenv creation"() { 15 | setup: 16 | // create virtualenv and use it as "global" python 17 | // without extra detection, plugin will try to use --user flag for virtualenv installation and fail 18 | Virtualenv env = env('env') 19 | env.create(false) 20 | 21 | build """ 22 | plugins { 23 | id 'ru.vyarus.use-python' 24 | } 25 | 26 | python { 27 | pythonPath = '${env.pythonPath.replace('\\', '\\\\')}' 28 | scope = VIRTUALENV 29 | pip 'extract-msg:0.34.3' 30 | } 31 | 32 | tasks.register('sample', PythonTask) { 33 | command = '-c print(\\'samplee\\')' 34 | } 35 | 36 | """ 37 | 38 | when: "run task" 39 | BuildResult result = run('sample') 40 | 41 | then: "task successful" 42 | result.task(':sample').outcome == TaskOutcome.SUCCESS 43 | result.output =~ /extract-msg\s+0.34.3/ 44 | result.output.contains('samplee') 45 | } 46 | 47 | def "Check venv from venv creation"() { 48 | setup: 49 | // create virtualenv and use it as "global" python 50 | // without extra detection, plugin will try to use --user flag for virtualenv installation and fail 51 | Venv env = venv('env') 52 | env.create(false) 53 | 54 | build """ 55 | plugins { 56 | id 'ru.vyarus.use-python' 57 | } 58 | 59 | python { 60 | pythonPath = '${env.pythonPath.replace('\\', '\\\\')}' 61 | scope = VIRTUALENV 62 | pip 'extract-msg:0.34.3' 63 | } 64 | 65 | tasks.register('sample', PythonTask) { 66 | command = '-c print(\\'samplee\\')' 67 | } 68 | 69 | """ 70 | 71 | when: "run task" 72 | BuildResult result = run('sample') 73 | 74 | then: "task successful" 75 | result.task(':sample').outcome == TaskOutcome.SUCCESS 76 | result.output =~ /extract-msg\s+0.34.3/ 77 | result.output.contains('samplee') 78 | } 79 | 80 | def "Check virtualenv from venv creation"() { 81 | setup: 82 | // create virtualenv and use it as "global" python 83 | // without extra detection, plugin will try to use --user flag for virtualenv installation and fail 84 | Venv env = venv('env') 85 | env.create(false) 86 | 87 | build """ 88 | plugins { 89 | id 'ru.vyarus.use-python' 90 | } 91 | 92 | python { 93 | pythonPath = '${env.pythonPath.replace('\\', '\\\\')}' 94 | scope = VIRTUALENV 95 | pip 'extract-msg:0.34.3' 96 | useVenv = false 97 | } 98 | 99 | tasks.register('sample', PythonTask) { 100 | command = '-c print(\\'samplee\\')' 101 | } 102 | 103 | """ 104 | 105 | when: "run task" 106 | BuildResult result = run('sample') 107 | 108 | then: "task successful" 109 | result.task(':sample').outcome == TaskOutcome.SUCCESS 110 | result.output =~ /extract-msg\s+0.34.3/ 111 | result.output.contains('samplee') 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/test/groovy/ru/vyarus/gradle/plugin/python/LegacyKitTest.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python 2 | 3 | import org.gradle.testkit.runner.BuildResult 4 | import org.gradle.testkit.runner.TaskOutcome 5 | import spock.lang.IgnoreIf 6 | 7 | /** 8 | * @author Vyacheslav Rusakov 9 | * @since 05.03.2020 10 | */ 11 | @IgnoreIf({jvm.java17Compatible}) // only gradle 7.3 supports java 17 12 | class LegacyKitTest extends AbstractKitTest { 13 | 14 | String GRADLE_VERSION = '7.0' 15 | 16 | def "Check simple plugin execution"() { 17 | setup: 18 | build """ 19 | plugins { 20 | id 'ru.vyarus.use-python' 21 | } 22 | 23 | python { 24 | scope = USER 25 | pip 'extract-msg:0.28.0' 26 | } 27 | 28 | tasks.register('sample', PythonTask) { 29 | command = '-c print(\\'samplee\\')' 30 | } 31 | 32 | """ 33 | 34 | when: "run task" 35 | BuildResult result = runVer(GRADLE_VERSION, 'sample') 36 | 37 | then: "task successful" 38 | result.task(':sample').outcome == TaskOutcome.SUCCESS 39 | result.output =~ /extract-msg\s+0.28.0/ 40 | result.output.contains('samplee') 41 | } 42 | 43 | def "Check env plugin execution"() { 44 | setup: 45 | build """ 46 | plugins { 47 | id 'ru.vyarus.use-python' 48 | } 49 | 50 | python { 51 | scope = VIRTUALENV 52 | pip 'extract-msg:0.28.0' 53 | } 54 | 55 | tasks.register('sample', PythonTask) { 56 | command = '-c print(\\'samplee\\')' 57 | } 58 | 59 | """ 60 | 61 | when: "run task" 62 | BuildResult result = runVer(GRADLE_VERSION, 'sample') 63 | 64 | then: "task successful" 65 | result.task(':sample').outcome == TaskOutcome.SUCCESS 66 | result.output =~ /extract-msg\s+0.28.0/ 67 | result.output.contains('samplee') 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/test/groovy/ru/vyarus/gradle/plugin/python/PipUpgradeTest.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python 2 | 3 | import org.gradle.testkit.runner.BuildResult 4 | import org.gradle.testkit.runner.TaskOutcome 5 | import ru.vyarus.gradle.plugin.python.cmd.Virtualenv 6 | 7 | /** 8 | * @author Vyacheslav Rusakov 9 | * @since 24.05.2018 10 | */ 11 | class PipUpgradeTest extends AbstractKitTest { 12 | 13 | def "Check pip local upgrade"() { 14 | 15 | setup: 16 | Virtualenv env = env() 17 | build """ 18 | plugins { 19 | id 'ru.vyarus.use-python' 20 | } 21 | 22 | python { 23 | pip 'pip:23.3.2' 24 | pip 'extract-msg:0.48.3' 25 | 26 | alwaysInstallModules = true 27 | } 28 | 29 | """ 30 | 31 | when: "run task" 32 | BuildResult result = run('pipInstall') 33 | 34 | then: "pip installed" 35 | result.task(':pipInstall').outcome == TaskOutcome.SUCCESS 36 | result.output.contains('pip==23.3.2') 37 | 38 | when: "run one more time to check used pip" 39 | result = run('pipInstall') 40 | then: "pip 23 used" 41 | result.task(':pipInstall').outcome == TaskOutcome.SUCCESS 42 | result.output.contains('Using pip 23.3.2 from') 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/test/groovy/ru/vyarus/gradle/plugin/python/PythonPluginTest.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python 2 | 3 | import org.gradle.api.GradleException 4 | import org.gradle.api.Project 5 | import org.gradle.api.tasks.Delete 6 | import ru.vyarus.gradle.plugin.python.task.PythonTask 7 | import ru.vyarus.gradle.plugin.python.task.pip.PipInstallTask 8 | import ru.vyarus.gradle.plugin.python.task.pip.PipListTask 9 | import ru.vyarus.gradle.plugin.python.task.pip.PipUpdatesTask 10 | import ru.vyarus.gradle.plugin.python.task.pip.module.VcsPipModule 11 | 12 | /** 13 | * @author Vyacheslav Rusakov 14 | * @since 11.11.2017 15 | */ 16 | class PythonPluginTest extends AbstractTest { 17 | 18 | def "Check extension registration"() { 19 | 20 | when: "plugin applied" 21 | Project project = project() 22 | project.plugins.apply "ru.vyarus.use-python" 23 | 24 | then: "extension registered" 25 | project.extensions.findByType(PythonExtension) 26 | 27 | then: "pip task registered" 28 | project.tasks.getByName('checkPython') 29 | project.tasks.getByName('pipInstall') 30 | project.tasks.getByName('pipUpdates') 31 | project.tasks.getByName('pipList') 32 | project.tasks.getByName('cleanPython') 33 | } 34 | 35 | def "Check extension usage"() { 36 | 37 | when: "plugin configured" 38 | Project project = project { 39 | apply plugin: "ru.vyarus.use-python" 40 | 41 | python { 42 | pythonPath = 'foo/bar' 43 | pythonBinary = 'py' 44 | scope = GLOBAL 45 | pip 'sample:1', 'foo:2' 46 | showInstalledVersions = false 47 | alwaysInstallModules = true 48 | } 49 | 50 | task('pyt', type: PythonTask) {} 51 | } 52 | 53 | then: "pip install task configured" 54 | PipInstallTask pipTask = project.tasks.getByName('pipInstall'); 55 | pipTask.pythonPath.get() == 'foo/bar' 56 | pipTask.pythonBinary.get() == 'py' 57 | !pipTask.userScope.get() 58 | pipTask.modules.get() == ['sample:1', 'foo:2'] 59 | !pipTask.showInstalledVersions.get() 60 | pipTask.alwaysInstallModules.get() 61 | 62 | then: "python task configured" 63 | PythonTask pyTask = project.tasks.getByName('pyt'); 64 | pyTask.pythonPath.get() == 'foo/bar' 65 | pyTask.pythonBinary.get() == 'py' 66 | pyTask.dependsOn.collect {it.name}.contains('pipInstall') 67 | 68 | then: "pip updates task configured" 69 | PipUpdatesTask pipUpdates = project.tasks.getByName('pipUpdates'); 70 | pipUpdates.pythonPath.get() == 'foo/bar' 71 | pipUpdates.pythonBinary.get() == 'py' 72 | !pipUpdates.userScope.get() 73 | pipUpdates.modules.get() == ['sample:1', 'foo:2'] 74 | 75 | then: "pip list task configured" 76 | PipListTask pipList = project.tasks.getByName('pipList'); 77 | pipList.pythonPath.get() == 'foo/bar' 78 | pipList.pythonBinary.get() == 'py' 79 | !pipList.userScope.get() 80 | pipList.modules.get() == ['sample:1', 'foo:2'] 81 | 82 | then: "clean task configured" 83 | Delete clean = project.tasks.getByName('cleanPython') 84 | clean.delete == ['.gradle/python'.replace('/', File.separator)] as Set 85 | } 86 | 87 | 88 | def "Check python task misconfiguration"() { 89 | 90 | when: "plugin configured" 91 | Project project = project { 92 | apply plugin: "ru.vyarus.use-python" 93 | 94 | task('pyt', type: PythonTask) {} 95 | } 96 | project.tasks.getByName('pyt').run() 97 | 98 | then: "validation failed" 99 | def ex = thrown(GradleException) 100 | ex.message == 'Module or command to execute must be defined' 101 | } 102 | 103 | def "Check module declaration util"() { 104 | 105 | when: "plugin configured" 106 | Project project = project { 107 | apply plugin: "ru.vyarus.use-python" 108 | 109 | python.pip 'sample:1', 'foo:2' 110 | } 111 | 112 | then: "modules check correct" 113 | def ext = project.extensions.getByType(PythonExtension) 114 | ext.isModuleDeclared('sample') 115 | ext.isModuleDeclared('foo') 116 | !ext.isModuleDeclared('sampleee') 117 | } 118 | 119 | def "Check modules override"() { 120 | 121 | when: "vcs declaration override normal" 122 | Project project = project { 123 | apply plugin: "ru.vyarus.use-python" 124 | 125 | python.pip 'foo:1', 126 | 'git+https://git.example.com/foo@v2.0#egg=foo-2' 127 | } 128 | def res = project.tasks.getByName('pipInstall').getModulesList() 129 | 130 | then: "one module" 131 | res.size() == 1 132 | res[0] instanceof VcsPipModule 133 | res[0].toPipString() == "foo @ git+https://git.example.com/foo@v2.0" 134 | 135 | 136 | when: "opposite override" 137 | project = super.project { 138 | apply plugin: "ru.vyarus.use-python" 139 | 140 | python.pip 'git+https://git.example.com/foo@v2.0#egg=foo-2', 141 | 'foo:1' 142 | } 143 | res = project.tasks.getByName('pipInstall').getModulesList() 144 | 145 | then: "one module" 146 | res.size() == 1 147 | !(res[0] instanceof VcsPipModule) 148 | res[0].toPipString() == "foo==1" 149 | } 150 | } -------------------------------------------------------------------------------- /src/test/groovy/ru/vyarus/gradle/plugin/python/UpstreamKitTest.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python 2 | 3 | import org.gradle.testkit.runner.BuildResult 4 | import org.gradle.testkit.runner.TaskOutcome 5 | 6 | /** 7 | * @author Vyacheslav Rusakov 8 | * @since 05.03.2020 9 | */ 10 | class UpstreamKitTest extends AbstractKitTest { 11 | 12 | String GRADLE_VERSION = '8.10.2' 13 | 14 | def "Check simple plugin execution"() { 15 | setup: 16 | build """ 17 | plugins { 18 | id 'ru.vyarus.use-python' 19 | } 20 | 21 | python { 22 | scope = USER 23 | pip 'extract-msg:0.28.0' 24 | } 25 | 26 | tasks.register('sample', PythonTask) { 27 | command = '-c print(\\'samplee\\')' 28 | } 29 | 30 | """ 31 | 32 | when: "run task" 33 | BuildResult result = runVer(GRADLE_VERSION, 'sample') 34 | 35 | then: "task successful" 36 | result.task(':sample').outcome == TaskOutcome.SUCCESS 37 | result.output =~ /extract-msg\s+0.28.0/ 38 | result.output.contains('samplee') 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/test/groovy/ru/vyarus/gradle/plugin/python/UseCustomPythonForTaskKitTest.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python 2 | 3 | import org.gradle.testkit.runner.BuildResult 4 | import org.gradle.testkit.runner.TaskOutcome 5 | 6 | /** 7 | * @author Vyacheslav Rusakov 8 | * @since 28.03.2024 9 | */ 10 | class UseCustomPythonForTaskKitTest extends AbstractKitTest { 11 | 12 | def "Check env plugin execution"() { 13 | setup: 14 | build """ 15 | plugins { 16 | id 'ru.vyarus.use-python' 17 | } 18 | 19 | python { 20 | scope = VIRTUALENV 21 | pip 'extract-msg:0.28.0' 22 | } 23 | 24 | tasks.register('sample', PythonTask) { 25 | // force global python usage instead of virtualenv 26 | pythonPath = null 27 | useCustomPython = true 28 | command = '-c print(\\'samplee\\')' 29 | } 30 | 31 | """ 32 | 33 | when: "run task" 34 | BuildResult result = run('sample') 35 | 36 | then: "task successful" 37 | result.task(':sample').outcome == TaskOutcome.SUCCESS 38 | result.output =~ /extract-msg\s+0.28.0/ 39 | result.output.contains('samplee') 40 | result.output =~ /(?m)\[python] python(3)? -c ${isWin ? 'print' : 'exec\\(\"print'}/ 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/groovy/ru/vyarus/gradle/plugin/python/VenvFromVenvCreationTest.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python 2 | 3 | import org.gradle.api.Project 4 | import ru.vyarus.gradle.plugin.python.cmd.Pip 5 | import ru.vyarus.gradle.plugin.python.cmd.Virtualenv 6 | import ru.vyarus.gradle.plugin.python.util.PythonExecutionFailed 7 | 8 | /** 9 | * @author Vyacheslav Rusakov 10 | * @since 11.03.2020 11 | */ 12 | class VenvFromVenvCreationTest extends AbstractTest { 13 | 14 | def "Check venv creation correctness"() { 15 | 16 | setup: 17 | Project project = project() 18 | Virtualenv env = new Virtualenv(gradleEnv(project), 'initial') 19 | env.create(true) 20 | 21 | // second, derived from first one 22 | Virtualenv env2 = new Virtualenv(gradleEnv(project), env.pythonPath, null, "second") 23 | env2.python.extraArgs('-v') // enable logs 24 | Pip pip = new Pip(gradleEnv(project), env.pythonPath, null).userScope(false) 25 | pip.install(env2.name + "==20.24.6") 26 | env2.createPythonOnly() 27 | 28 | when: "validating pip in second environment" 29 | Pip pip2 = new Pip(gradleEnv(project), env2.pythonPath, null).userScope(false) 30 | println pip2.version 31 | 32 | then: "pip not exists" 33 | thrown(PythonExecutionFailed) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/test/groovy/ru/vyarus/gradle/plugin/python/cmd/AbstractCliMockSupport.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.cmd 2 | 3 | import org.apache.tools.ant.taskdefs.condition.Os 4 | import org.gradle.api.Action 5 | import org.gradle.api.Project 6 | import org.gradle.api.internal.file.FileOperations 7 | import org.gradle.api.logging.Logger 8 | import org.gradle.api.model.ObjectFactory 9 | import org.gradle.api.plugins.ExtensionContainer 10 | import org.gradle.api.plugins.ExtraPropertiesExtension 11 | import org.gradle.api.provider.Provider 12 | import org.gradle.api.provider.ProviderFactory 13 | import org.gradle.internal.file.PathToFileResolver 14 | import org.gradle.process.ExecOperations 15 | import org.gradle.process.ExecSpec 16 | import org.gradle.process.internal.DefaultExecSpec 17 | import ru.vyarus.gradle.plugin.python.cmd.env.Environment 18 | import ru.vyarus.gradle.plugin.python.cmd.env.GradleEnvironment 19 | import ru.vyarus.gradle.plugin.python.service.stat.PythonStat 20 | import ru.vyarus.gradle.plugin.python.service.value.CacheValueSource 21 | import ru.vyarus.gradle.plugin.python.util.ExecRes 22 | import ru.vyarus.gradle.plugin.python.util.TestLogger 23 | import spock.lang.Specification 24 | import spock.lang.TempDir 25 | 26 | /** 27 | * @author Vyacheslav Rusakov 28 | * @since 20.11.2017 29 | */ 30 | abstract class AbstractCliMockSupport extends Specification { 31 | 32 | // used to overcome manual file existence check on win 33 | @TempDir 34 | File dir 35 | 36 | Project project 37 | TestLogger logger 38 | private boolean execMocked 39 | Map, String> execCases = [:] 40 | Map extraProps = [:] 41 | 42 | boolean isWin = Os.isFamily(Os.FAMILY_WINDOWS) 43 | 44 | File file(String path) { 45 | new File(dir, path) 46 | } 47 | 48 | void setup() { 49 | project = Stub(Project) 50 | logger = new TestLogger() 51 | project.getLogger() >> { logger } 52 | project.getProjectDir() >> { dir } 53 | project.file(_) >> { new File(dir, it[0]) } 54 | project.getRootProject() >> { project } 55 | project.findProperty(_ as String) >> { args -> extraProps.get(args[0]) } 56 | // required for GradleEnvironment 57 | ObjectFactory objects = Stub(ObjectFactory) 58 | ExecOperations exec = Stub(ExecOperations) 59 | exec.exec(_) >> { project.exec it[0] as Action } 60 | FileOperations fs = Stub(FileOperations) 61 | fs.file(_) >> { project.file(it[0]) } 62 | objects.newInstance(_, _) >> { args -> 63 | List params = [exec, fs] 64 | params.addAll(args[1] as Object[]) 65 | // have to use special class because GradleEnvironment is abstract (assume gradle injection) 66 | GradleEnv.newInstance(params as Object[]) 67 | } 68 | project.getObjects() >> { objects } 69 | 70 | ProviderFactory providers = Stub(ProviderFactory) 71 | providers.of(_, _) >> { args -> 72 | Class cls = args[0] as Class 73 | if (CacheValueSource.isAssignableFrom(cls)) { 74 | return { [:] } as Provider 75 | } else { 76 | return { [] } as Provider 77 | } 78 | } 79 | project.getProviders() >> { providers } 80 | 81 | def ext = Stub(ExtensionContainer) 82 | project.getExtensions() >> { ext } 83 | def props = Stub(ExtraPropertiesExtension) 84 | ext.getExtraProperties() >> { props } 85 | props.set(_ as String, _) >> { args -> extraProps.put(args[0], args[1]) } 86 | props.get(_ as String) >> { args -> extraProps.get(args[0]) } 87 | props.has(_ as String) >> { args -> extraProps.containsKey(args[0]) } 88 | } 89 | 90 | Environment gradleEnv() { 91 | gradleEnv(project) 92 | } 93 | 94 | Environment gradleEnv(Project project) { 95 | GradleEnvironment.create(project, "gg", {} as Provider, { false } as Provider) 96 | } 97 | 98 | // use to provide specialized output for executed commands 99 | // (e.g. under pip tests to cactch python virtualenv detection) 100 | // closure accepts called command line 101 | void execCase(Closure closure, String output) { 102 | execCases.put(closure, output) 103 | } 104 | 105 | void mockExec(Project project, String output, int res) { 106 | assert !execMocked, "Exec can be mocked just once!" 107 | // check execution with logs without actual execution 108 | project.exec(_) >> { Action action -> 109 | ExecSpec spec = new DefaultExecSpec(Stub(PathToFileResolver)) 110 | action.execute(spec) 111 | String cmd = "${spec.executable} ${spec.args.join(' ')}" 112 | println ">> Mocked exec: $cmd" 113 | String out = output 114 | execCases.each { k, v -> 115 | if (k.call(cmd)) { 116 | println ">> Special exec case detected, output become: $v" 117 | out = v 118 | } 119 | } 120 | if (out == output) { 121 | println ">> Default execution, output: $out" 122 | } 123 | 124 | OutputStream os = spec.standardOutput 125 | if (out) { 126 | os.write(out.bytes) 127 | } 128 | return new ExecRes(res) 129 | } 130 | } 131 | 132 | static class GradleEnv extends GradleEnvironment { 133 | ExecOperations exec 134 | FileOperations fs 135 | 136 | GradleEnv(ExecOperations exec, FileOperations fs, Logger logger, File projectDir, File rootDir, String rootName, 137 | String projectPath, String taskName, 138 | Provider> globalCache, 139 | Provider> projectCache, 140 | Provider> stats, 141 | Provider debug) { 142 | super(logger, projectDir, rootDir, rootName, projectPath, taskName, globalCache, projectCache, stats, debug) 143 | this.exec = exec 144 | this.fs = fs 145 | } 146 | 147 | @Override 148 | protected ExecOperations getExec() { 149 | return exec 150 | } 151 | 152 | @Override 153 | protected FileOperations getFs() { 154 | return fs 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/test/groovy/ru/vyarus/gradle/plugin/python/cmd/PipCliTest.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.cmd 2 | 3 | 4 | import ru.vyarus.gradle.plugin.python.AbstractTest 5 | import ru.vyarus.gradle.plugin.python.cmd.env.Environment 6 | 7 | /** 8 | * @author Vyacheslav Rusakov 9 | * @since 20.11.2017 10 | */ 11 | class PipCliTest extends AbstractTest { 12 | 13 | def "Check pip cli usage"() { 14 | 15 | when: "call pip" 16 | Pip pip = new Pip(gradleEnv()) 17 | pip.exec('list') 18 | then: 'ok' 19 | true 20 | 21 | when: "pip install" 22 | pip.install('extract-msg==0.28.0') 23 | then: "ok" 24 | pip.isInstalled('extract-msg') 25 | 26 | when: "pip uninstall" 27 | pip.uninstall('extract-msg') 28 | then: "ok" 29 | !pip.isInstalled('extract-msg') 30 | } 31 | 32 | def "Check pip utils"() { 33 | 34 | when: "call pip" 35 | Pip pip = new Pip(gradleEnv()) 36 | pip.exec('list') 37 | then: "ok" 38 | pip.version =~ /\d+\.\d+(\.\d+)?/ 39 | pip.versionLine =~ /pip \d+\.\d+(\.\d+)? from/ 40 | } 41 | 42 | def "Check version parse fail"() { 43 | 44 | when: "prepare pip" 45 | Pip pip = new FooPip(gradleEnv()) 46 | then: "ok" 47 | pip.version =~ /\d+\.\d+(\.\d+)?/ 48 | 49 | } 50 | 51 | class FooPip extends Pip { 52 | 53 | FooPip(Environment environment) { 54 | super(environment) 55 | } 56 | 57 | @Override 58 | String getVersionLine() { 59 | return 'you will not parse it' 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/test/groovy/ru/vyarus/gradle/plugin/python/cmd/PipExecTest.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.cmd 2 | 3 | /** 4 | * @author Vyacheslav Rusakov 5 | * @since 20.11.2017 6 | */ 7 | class PipExecTest extends AbstractCliMockSupport { 8 | 9 | Pip pip 10 | 11 | @Override 12 | void setup() { 13 | // required for virtualenv detection in pip (which must not detect env now) 14 | // (this can't be done with spies or mocks) 15 | execCase({it.contains('sys.prefix')}, '3.5\n/usr/\n/usr/python3') 16 | pip = new Pip(gradleEnv()) 17 | } 18 | 19 | def "Check execution"() { 20 | setup: 21 | mockExec(project, 'sample output', 0) 22 | 23 | when: "call install module" 24 | pip.install('mod') 25 | then: "ok" 26 | logger.res =~ /\[python] python(3)? -m pip install mod --user\n\t sample output\n/ 27 | 28 | when: "call pip cmd" 29 | logger.reset() 30 | pip.exec('list --format') 31 | then: "ok" 32 | logger.res =~ /\[python] python(3)? -m pip list --format --user\n\t sample output\n/ 33 | 34 | when: "call freeze" 35 | logger.reset() 36 | pip.exec('freeze') 37 | then: "ok" 38 | logger.res =~ /\[python] python(3)? -m pip freeze --user\n\t sample output\n/ 39 | } 40 | 41 | def "Check global scope usage"() { 42 | setup: 43 | mockExec(project, null, 0) 44 | 45 | when: "call in user scope" 46 | pip.install('mod') 47 | then: "ok" 48 | logger.res =~ /\[python] python(3)? -m pip install mod --user/ 49 | 50 | when: "call in global scope" 51 | logger.reset() 52 | pip.inGlobalScope { pip.install('mod') } 53 | then: "ok" 54 | !(logger.res =~ /\[python] python(3)? -m pip install mod --user/) 55 | 56 | when: "call in user scope" 57 | logger.reset() 58 | pip.install('mod') 59 | then: "scope is correct" 60 | logger.res =~ /\[python] python(3)? -m pip install mod --user/ 61 | } 62 | 63 | def "Check pip cache disable for installation"() { 64 | setup: 65 | mockExec(project, null, 0) 66 | pip.useCache = false 67 | 68 | when: "call install without cache" 69 | pip.install('mod') 70 | then: "flag applied" 71 | logger.res =~ /\[python] python(3)? -m pip install mod --user --no-cache-dir/ 72 | 73 | when: "call different command" 74 | pip.exec('list') 75 | then: "no flag applied" 76 | logger.res =~ /\[python] python(3)? -m pip list --user/ 77 | 78 | cleanup: 79 | pip.useCache = true 80 | } 81 | 82 | def "Check pip extraIndexUrls for installation"() { 83 | setup: 84 | mockExec(project, null, 0) 85 | pip.extraIndexUrls = ["http://extra-url.com", "http://another-url.com"] 86 | 87 | when: "call install with extra index urls" 88 | pip.install('mod') 89 | then: "flag applied" 90 | logger.res =~ /\[python] python(3)? -m pip install mod --user --extra-index-url http:\/\/extra-url\.com --extra-index-url http:\/\/another-url\.com/ 91 | 92 | when: "call list with extra index urls" 93 | pip.exec('list') 94 | then: "flag applied" 95 | logger.res =~ /\[python] python(3)? -m pip list --user --extra-index-url http:\/\/extra-url\.com --extra-index-url http:\/\/another-url\.com/ 96 | 97 | when: "call freeze command" 98 | pip.exec('freeze') 99 | then: "no flag applied" 100 | logger.res =~ /\[python] python(3)? -m pip freeze --user/ 101 | 102 | cleanup: 103 | pip.extraIndexUrls = [] 104 | } 105 | 106 | def "Check pip extraIndexUrls with credentials"() { 107 | setup: 108 | mockExec(project, null, 0) 109 | pip.extraIndexUrls = ["http://user:pass@extra-url.com"] 110 | 111 | when: "call install with extra index urls" 112 | pip.install('mod') 113 | then: "flag applied" 114 | logger.res =~ /\[python] python(3)? -m pip install mod --user --extra-index-url http:\/\/user:\*{5}@extra-url\.com/ 115 | 116 | cleanup: 117 | pip.extraIndexUrls = [] 118 | } 119 | 120 | def "Check pip extraIndexUrls with multiple credentials"() { 121 | setup: 122 | mockExec(project, null, 0) 123 | pip.extraIndexUrls = ["http://user:pass@extra-url.com", "https://user22:pass22@another-url.com"] 124 | 125 | when: "call list with extra index urls" 126 | pip.exec('list') 127 | then: "flag applied" 128 | logger.res =~ /\[python] python(3)? -m pip list --user --extra-index-url http:\/\/user:\*{5}@extra-url\.com --extra-index-url https:\/\/user22:\*{5}@another-url\.com/ 129 | 130 | cleanup: 131 | pip.extraIndexUrls = [] 132 | } 133 | 134 | def "Check pip trustedHosts for installation"() { 135 | setup: 136 | mockExec(project, null, 0) 137 | pip.trustedHosts = ["extra-url.com", "another-url.com"] 138 | 139 | when: "call install with extra index urls" 140 | pip.install('mod') 141 | then: "flag applied" 142 | logger.res =~ /\[python] python(3)? -m pip install mod --user --trusted-host extra-url\.com --trusted-host another-url\.com/ 143 | 144 | when: "call different command" 145 | pip.exec('list') 146 | then: "no flag applied" 147 | logger.res =~ /\[python] python(3)? -m pip list --user/ 148 | 149 | cleanup: 150 | pip.trustedHosts = [] 151 | } 152 | 153 | def "Check pip break system packages"() { 154 | setup: 155 | mockExec(project, null, 0) 156 | pip.breakSystemPackages(true) 157 | 158 | when: "new option applied" 159 | pip.install('mod') 160 | then: "flag applied" 161 | logger.res =~ /\[python] python(3)? -m pip install mod --user --break-system-packages/ 162 | 163 | when: "call different command" 164 | pip.exec('list') 165 | then: "no flag applied" 166 | logger.res =~ /\[python] python(3)? -m pip list --user/ 167 | 168 | cleanup: 169 | pip.breakSystemPackages(false) 170 | } 171 | 172 | } 173 | -------------------------------------------------------------------------------- /src/test/groovy/ru/vyarus/gradle/plugin/python/cmd/PipExecUnderVirtualenvTest.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.cmd 2 | 3 | import org.apache.tools.ant.taskdefs.condition.Os 4 | import ru.vyarus.gradle.plugin.python.util.CliUtils 5 | 6 | /** 7 | * @author Vyacheslav Rusakov 8 | * @since 10.03.2020 9 | */ 10 | class PipExecUnderVirtualenvTest extends AbstractCliMockSupport { 11 | 12 | Pip pip 13 | 14 | @Override 15 | void setup() { 16 | String root = dir.absolutePath 17 | String binPath = CliUtils.pythonBinPath(root, Os.isFamily(Os.FAMILY_WINDOWS)) 18 | File bin = new File(binPath, 'activate') 19 | bin.mkdirs() 20 | bin.createNewFile() // force virtualenv detection 21 | assert bin.exists() 22 | execCase({ it.contains('sys.prefix') }, "3.5\n${root}\n${binPath + '/python3'}") 23 | pip = new Pip(gradleEnv()) 24 | } 25 | 26 | def "Check execution"() { 27 | setup: 28 | mockExec(project, 'sample output', 0) 29 | 30 | when: "call install module" 31 | pip.install('mod') 32 | then: "user flag not set under virtualenv" 33 | pip.python.virtualenv 34 | logger.res =~ /\[python] python(3)? -m pip install mod\n\t sample output\n/ 35 | 36 | when: "call pip cmd" 37 | logger.reset() 38 | pip.exec('list --format') 39 | then: "ok" 40 | logger.res =~ /\[python] python(3)? -m pip list --format\n\t sample output\n/ 41 | 42 | when: "call freeze" 43 | logger.reset() 44 | pip.exec('freeze') 45 | then: "ok" 46 | logger.res =~ /\[python] python(3)? -m pip freeze\n\t sample output\n/ 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/test/groovy/ru/vyarus/gradle/plugin/python/cmd/PythonCliTest.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.cmd 2 | 3 | import org.gradle.api.Project 4 | import org.gradle.api.logging.LogLevel 5 | import ru.vyarus.gradle.plugin.python.AbstractTest 6 | import ru.vyarus.gradle.plugin.python.util.PythonExecutionFailed 7 | 8 | /** 9 | * @author Vyacheslav Rusakov 10 | * @since 18.11.2017 11 | */ 12 | class PythonCliTest extends AbstractTest { 13 | 14 | def "Check python execution"() { 15 | 16 | when: "Use default configuration" 17 | Python python = new Python(gradleEnv()) 18 | def res = python.readOutput('-c "print(\'hello\')"') 19 | then: "ok" 20 | res == 'hello' 21 | 22 | when: 'check home dir' 23 | res = python.getHomeDir() 24 | then: "ok" 25 | res 26 | 27 | when: 'check version' 28 | res = python.getVersion() 29 | then: "ok" 30 | res ==~ /\d+\.\d+\.\d+/ 31 | 32 | and: 'check virtualenv detection' 33 | !python.virtualenv 34 | 35 | when: 'check simple exec' 36 | python.exec('-c "print(\'hello\')"') 37 | then: "ok" 38 | true 39 | } 40 | 41 | def "Check error reporting"() { 42 | 43 | when: "call bad command" 44 | Python python = new Python(gradleEnv()) 45 | python.readOutput('-c "import fsdfdsfsd;"') 46 | then: "error" 47 | thrown(PythonExecutionFailed) 48 | 49 | when: "call bad exec" 50 | python.exec('-c "import fsdfdsfsd;"') 51 | then: "error" 52 | thrown(PythonExecutionFailed) 53 | } 54 | 55 | def "Check configuration"() { 56 | 57 | setup: 58 | Python python = new Python(gradleEnv()) 59 | 60 | when: "set output prefix" 61 | python.outputPrefix('[]') 62 | then: 'set' 63 | python.outputPrefix == '[]' 64 | 65 | when: "set null prefix" 66 | python.outputPrefix(null) 67 | then: 'set' 68 | python.outputPrefix == null 69 | 70 | when: "set log level" 71 | python.logLevel(LogLevel.DEBUG) 72 | then: 'set' 73 | python.logLevel == LogLevel.DEBUG 74 | 75 | when: "set null log level" 76 | python.logLevel(null) 77 | then: 'ignored' 78 | python.logLevel == LogLevel.DEBUG 79 | 80 | when: "set work dir" 81 | python.workDir('some/dir') 82 | then: 'set' 83 | python.workDir == 'some/dir' 84 | 85 | when: "set null work dir" 86 | python.workDir(null) 87 | then: 'ignored' 88 | python.workDir == 'some/dir' 89 | 90 | when: "set python args" 91 | python.pythonArgs('--arg') 92 | then: 'set' 93 | python.pythonArgs == ['--arg'] 94 | 95 | when: "append python args" 96 | python.pythonArgs(['--arg2', '--arg3']) 97 | then: 'set' 98 | python.pythonArgs == ['--arg', '--arg2', '--arg3'] 99 | 100 | when: "set args" 101 | python.extraArgs('--arg') 102 | then: 'set' 103 | python.extraArgs == ['--arg'] 104 | 105 | when: "append args" 106 | python.extraArgs(['--arg2', '--arg3']) 107 | then: 'set' 108 | python.extraArgs == ['--arg', '--arg2', '--arg3'] 109 | 110 | when: "append null args" 111 | python.extraArgs(null) 112 | then: 'ignored' 113 | python.extraArgs == ['--arg', '--arg2', '--arg3'] 114 | 115 | when: "clear python args" 116 | python.clearPythonArgs() 117 | then: 'set' 118 | python.pythonArgs.empty 119 | 120 | when: "clear args" 121 | python.clearExtraArgs() 122 | then: 'set' 123 | python.clearEnvironment() 124 | 125 | when: "set vars" 126 | python.environment('foo', 1) 127 | python.environment('bar', 2) 128 | then: 'set' 129 | python.binary.envVars == ['foo': 1, 'bar' : 2] 130 | 131 | when: 'clear vars' 132 | python.clearEnvironment() 133 | then: 'no vars' 134 | python.binary.envVars.size() == 0 135 | 136 | when: 'mass variables set' 137 | python.environment(['foo': 2, 'bar' : 3]) 138 | python.environment(['foo': 1, 'baz' : 4]) 139 | then: 'aggregated' 140 | python.binary.envVars == ['foo': 1, 'bar' : 3, 'baz': 4] 141 | 142 | when: 'additional var' 143 | python.environment('sample': 'sam') 144 | then: 'aggregated' 145 | python.binary.envVars == ['foo': 1, 'bar' : 3, 'baz': 4, 'sample': 'sam'] 146 | 147 | } 148 | 149 | def "Check module detection"() { 150 | 151 | when: "check pip" 152 | Python python = new Python(gradleEnv()) 153 | def res = python.isModuleExists('pip') 154 | then: "ok" 155 | res 156 | 157 | when: 'check not existing module' 158 | res = python.isModuleExists('abababa') 159 | then: "ok" 160 | !res 161 | } 162 | } -------------------------------------------------------------------------------- /src/test/groovy/ru/vyarus/gradle/plugin/python/cmd/VenvCliTest.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.cmd 2 | 3 | import org.gradle.api.Project 4 | import ru.vyarus.gradle.plugin.python.AbstractTest 5 | import ru.vyarus.gradle.plugin.python.util.CliUtils 6 | 7 | /** 8 | * @author Vyacheslav Rusakov 9 | * @since 02.04.2024 10 | */ 11 | class VenvCliTest extends AbstractTest { 12 | 13 | def "Check incorrect virtualenv creation"() { 14 | 15 | when: "create venv cli without path" 16 | new Venv(gradleEnv(), null) 17 | then: "error" 18 | thrown(IllegalArgumentException) 19 | } 20 | 21 | def "Check virtualenv detection"() { 22 | 23 | when: "call check env existence" 24 | Venv env = new Venv(gradleEnv(), 'env') 25 | then: "env not exists" 26 | !env.exists() 27 | 28 | when: "empty env dir exists" 29 | file('env/').mkdir() 30 | then: "still no env" 31 | !env.exists() 32 | 33 | when: "at least one file in env" 34 | file('env/foo.txt').createNewFile() 35 | then: "detected" 36 | env.exists() 37 | } 38 | 39 | def "Check env creation"() { 40 | 41 | when: "create new env" 42 | Venv env = new Venv(gradleEnv(), 'env') 43 | assert !env.exists() 44 | env.createPythonOnly() 45 | then: "env created" 46 | env.exists() 47 | 48 | when: "create one more time" 49 | env.createPythonOnly() 50 | then: "nothing happen" 51 | env.exists() 52 | } 53 | 54 | def "Check util methods"() { 55 | 56 | when: "prepare virtualenv" 57 | Project project = project() 58 | Venv env = new Venv(gradleEnv(project), 'env') 59 | then: "path correct" 60 | env.path == 'env' 61 | env.pythonPath == CliUtils.canonicalPath(project.rootDir.absolutePath, isWin ? 'env/Scripts' : 'env/bin') 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/test/groovy/ru/vyarus/gradle/plugin/python/cmd/VenvExecTest.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.cmd 2 | 3 | /** 4 | * @author Vyacheslav Rusakov 5 | * @since 02.04.2024 6 | */ 7 | class VenvExecTest extends AbstractCliMockSupport { 8 | 9 | Venv env 10 | 11 | @Override 12 | void setup() { 13 | env= new Venv(gradleEnv(), 'env') 14 | } 15 | 16 | def "Check execution"() { 17 | setup: 18 | mockExec(project, null, 0) 19 | 20 | when: "full create" 21 | env.create() 22 | then: "ok" 23 | logger.res =~ /\[python] python(3)? -m venv env/ 24 | 25 | when: "full create with copy" 26 | env.create(true) 27 | then: "ok" 28 | logger.res =~ /\[python] python(3)? -m venv env --copies/ 29 | 30 | when: "python only create" 31 | env.createPythonOnly() 32 | then: "ok" 33 | logger.res =~ /\[python] python(3)? -m venv env --without-pip/ 34 | 35 | when: "python only create with copy" 36 | env.createPythonOnly(true) 37 | then: "ok" 38 | logger.res =~ /\[python] python(3)? -m venv env --copies --without-pip/ 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/test/groovy/ru/vyarus/gradle/plugin/python/cmd/VirtualenvCliTest.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.cmd 2 | 3 | import org.gradle.api.Project 4 | import ru.vyarus.gradle.plugin.python.AbstractTest 5 | import ru.vyarus.gradle.plugin.python.util.CliUtils 6 | 7 | /** 8 | * @author Vyacheslav Rusakov 9 | * @since 14.12.2017 10 | */ 11 | class VirtualenvCliTest extends AbstractTest { 12 | 13 | void setup() { 14 | Pip pip = new Pip(gradleEnv()) 15 | if (!pip.isInstalled(Virtualenv.PIP_NAME)) { 16 | pip.install(Virtualenv.PIP_NAME) 17 | } 18 | } 19 | 20 | def "Check incorrect virtualenv creation"() { 21 | 22 | when: "create virtualenv cli without path" 23 | new Virtualenv(gradleEnv(), null) 24 | then: "error" 25 | thrown(IllegalArgumentException) 26 | } 27 | 28 | def "Check virtualenv detection"() { 29 | 30 | when: "call check env existence" 31 | Virtualenv env = new Virtualenv(gradleEnv(), 'env') 32 | then: "env not exists" 33 | !env.exists() 34 | 35 | when: "empty env dir exists" 36 | file('env/').mkdir() 37 | then: "still no env" 38 | !env.exists() 39 | 40 | when: "at least one file in env" 41 | file('env/foo.txt').createNewFile() 42 | then: "detected" 43 | env.exists() 44 | } 45 | 46 | def "Check env creation"() { 47 | 48 | when: "create new env" 49 | Virtualenv env = new Virtualenv(gradleEnv(), 'env') 50 | assert !env.exists() 51 | env.createPythonOnly() 52 | then: "env created" 53 | env.exists() 54 | 55 | when: "create one more time" 56 | env.createPythonOnly() 57 | then: "nothing happen" 58 | env.exists() 59 | } 60 | 61 | def "Check util methods"() { 62 | 63 | when: "prepare virtualenv" 64 | Project project = project() 65 | Virtualenv env = new Virtualenv(gradleEnv(project), 'env') 66 | then: "path correct" 67 | env.path == 'env' 68 | env.pythonPath == CliUtils.canonicalPath(project.rootDir.absolutePath, isWin ? 'env/Scripts' : 'env/bin') 69 | 70 | then: "version correct" 71 | env.version =~ /\d+\.\d+\.\d+/ 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/test/groovy/ru/vyarus/gradle/plugin/python/cmd/VirtualenvExecTest.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.cmd 2 | 3 | /** 4 | * @author Vyacheslav Rusakov 5 | * @since 15.12.2017 6 | */ 7 | class VirtualenvExecTest extends AbstractCliMockSupport { 8 | 9 | Virtualenv env 10 | 11 | @Override 12 | void setup() { 13 | env= new Virtualenv(gradleEnv(), 'env') 14 | } 15 | 16 | def "Check execution"() { 17 | setup: 18 | mockExec(project, null, 0) 19 | 20 | when: "full create" 21 | env.create() 22 | then: "ok" 23 | logger.res =~ /\[python] python(3)? -m virtualenv env/ 24 | 25 | when: "full create with copy" 26 | env.create(true) 27 | then: "ok" 28 | logger.res =~ /\[python] python(3)? -m virtualenv env --always-copy/ 29 | 30 | when: "python only create" 31 | env.createPythonOnly() 32 | then: "ok" 33 | logger.res =~ /\[python] python(3)? -m virtualenv env --no-setuptools --no-pip --no-wheel/ 34 | 35 | when: "python only create with copy" 36 | env.createPythonOnly(true) 37 | then: "ok" 38 | logger.res =~ /\[python] python(3)? -m virtualenv env --always-copy --no-setuptools --no-pip --no-wheel/ 39 | 40 | when: "create without no pip" 41 | env.create(true, false, true) 42 | then: "ok" 43 | logger.res =~ /\[python] python(3)? -m virtualenv env --always-copy --no-pip --no-wheel/ 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/groovy/ru/vyarus/gradle/plugin/python/docker/DockerAutoRestartKitTest.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.docker 2 | 3 | import org.gradle.testkit.runner.BuildResult 4 | import org.gradle.testkit.runner.TaskOutcome 5 | import ru.vyarus.gradle.plugin.python.AbstractKitTest 6 | import spock.lang.IgnoreIf 7 | 8 | /** 9 | * @author Vyacheslav Rusakov 10 | * @since 04.10.2022 11 | */ 12 | // testcontainers doesn't work on windows server https://github.com/testcontainers/testcontainers-java/issues/2960 13 | @IgnoreIf({ System.getProperty("os.name").toLowerCase().contains("windows") }) 14 | class DockerAutoRestartKitTest extends AbstractKitTest { 15 | 16 | def "Check auto container restart due to changed working dir"() { 17 | setup: 18 | build """ 19 | plugins { 20 | id 'ru.vyarus.use-python' 21 | } 22 | 23 | python { 24 | docker.use = true 25 | } 26 | 27 | tasks.register('sample', PythonTask) { 28 | workDir = 'build' 29 | command = '-c print(\\'samplee\\')' 30 | } 31 | 32 | """ 33 | 34 | when: "run task" 35 | debug() 36 | BuildResult result = run('sample') 37 | 38 | then: "task successful" 39 | result.task(':sample').outcome == TaskOutcome.SUCCESS 40 | result.output.contains('Restarting container due to changed working directory') 41 | result.output.contains('samplee') 42 | } 43 | 44 | def "Check auto container restart due to changed environment config"() { 45 | setup: 46 | build """ 47 | plugins { 48 | id 'ru.vyarus.use-python' 49 | } 50 | 51 | python { 52 | environment 'ONE', 'one' 53 | docker.use = true 54 | } 55 | 56 | tasks.register('sample', PythonTask) { 57 | environment 'ONE', 'two' 58 | command = '-c print(\\'samplee\\')' 59 | } 60 | 61 | """ 62 | 63 | when: "run task" 64 | debug() 65 | BuildResult result = run('sample') 66 | 67 | then: "task successful" 68 | result.task(':sample').outcome == TaskOutcome.SUCCESS 69 | result.output.contains('Restarting container due to changed environment variables') 70 | result.output.contains('samplee') 71 | } 72 | 73 | def "Check no auto container restart due to same environment config"() { 74 | setup: 75 | build """ 76 | plugins { 77 | id 'ru.vyarus.use-python' 78 | } 79 | 80 | python { 81 | environment 'ONE', 'one' 82 | docker.use = true 83 | } 84 | 85 | tasks.register('sample', PythonTask) { 86 | environment 'ONE', 'one' 87 | command = '-c print(\\'samplee\\')' 88 | } 89 | 90 | """ 91 | 92 | when: "run task" 93 | debug() 94 | BuildResult result = run('sample') 95 | 96 | then: "task successful" 97 | result.task(':sample').outcome == TaskOutcome.SUCCESS 98 | !result.output.contains('Restarting container due to changed') 99 | result.output.contains('samplee') 100 | } 101 | 102 | def "Check auto container restart due to added ports"() { 103 | setup: 104 | build """ 105 | plugins { 106 | id 'ru.vyarus.use-python' 107 | } 108 | 109 | python { 110 | docker.use = true 111 | } 112 | 113 | tasks.register('sample', PythonTask) { 114 | docker.ports 9000 115 | command = '-c print(\\'samplee\\')' 116 | } 117 | 118 | """ 119 | 120 | when: "run task" 121 | debug() 122 | BuildResult result = run('sample') 123 | 124 | then: "task successful" 125 | result.task(':sample').outcome == TaskOutcome.SUCCESS 126 | result.output.contains('Restarting container due to changed ports') 127 | result.output.contains('samplee') 128 | } 129 | 130 | def "Check auto container restart due to changed ports"() { 131 | setup: 132 | build """ 133 | plugins { 134 | id 'ru.vyarus.use-python' 135 | } 136 | 137 | python { 138 | docker.use = true 139 | docker.ports 5000 140 | } 141 | 142 | tasks.register('sample', PythonTask) { 143 | docker.ports 5001 144 | command = '-c print(\\'samplee\\')' 145 | } 146 | 147 | """ 148 | 149 | when: "run task" 150 | debug() 151 | BuildResult result = run('sample') 152 | 153 | then: "task successful" 154 | result.task(':sample').outcome == TaskOutcome.SUCCESS 155 | result.output.contains('Restarting container due to changed ports') 156 | result.output.contains('samplee') 157 | } 158 | 159 | def "Check no auto container restart due to same ports"() { 160 | setup: 161 | build """ 162 | plugins { 163 | id 'ru.vyarus.use-python' 164 | } 165 | 166 | python { 167 | docker.ports 5000, 5001 168 | docker.use = true 169 | } 170 | 171 | tasks.register('sample', PythonTask) { 172 | docker.ports 5001 173 | command = '-c print(\\'samplee\\')' 174 | } 175 | 176 | """ 177 | 178 | when: "run task" 179 | debug() 180 | BuildResult result = run('sample') 181 | 182 | then: "task successful" 183 | result.task(':sample').outcome == TaskOutcome.SUCCESS 184 | !result.output.contains('Restarting container due to changed') 185 | result.output.contains('samplee') 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/test/groovy/ru/vyarus/gradle/plugin/python/docker/DockerExclusiveExecutionKitTest.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.docker 2 | 3 | import org.gradle.testkit.runner.BuildResult 4 | import org.gradle.testkit.runner.TaskOutcome 5 | import ru.vyarus.gradle.plugin.python.AbstractKitTest 6 | import spock.lang.IgnoreIf 7 | 8 | /** 9 | * @author Vyacheslav Rusakov 10 | * @since 04.10.2022 11 | */ 12 | // testcontainers doesn't work on windows server https://github.com/testcontainers/testcontainers-java/issues/2960 13 | @IgnoreIf({ System.getProperty("os.name").toLowerCase().contains("windows") }) 14 | class DockerExclusiveExecutionKitTest extends AbstractKitTest { 15 | 16 | def "Check exclusive execution"() { 17 | setup: 18 | build """ 19 | plugins { 20 | id 'ru.vyarus.use-python' 21 | } 22 | 23 | python { 24 | docker { use = true } 25 | } 26 | 27 | tasks.register('sample', PythonTask) { 28 | docker.exclusive = true 29 | command = '-c print(\\'samplee\\')' 30 | } 31 | 32 | """ 33 | 34 | when: "run task" 35 | debug() 36 | BuildResult result = run('sample') 37 | 38 | then: "task successful" 39 | result.task(':sample').outcome == TaskOutcome.SUCCESS 40 | result.output.contains('[docker] exclusive container') 41 | result.output.contains('samplee') 42 | } 43 | 44 | def "Check exclusive execution with closure syntax"() { 45 | setup: 46 | build """ 47 | plugins { 48 | id 'ru.vyarus.use-python' 49 | } 50 | 51 | python { 52 | docker { use = true } 53 | } 54 | 55 | tasks.register('sample', PythonTask) { 56 | docker { 57 | exclusive = true 58 | } 59 | command = '-c print(\\'samplee\\')' 60 | } 61 | 62 | """ 63 | 64 | when: "run task" 65 | debug() 66 | BuildResult result = run('sample') 67 | 68 | then: "task successful" 69 | result.task(':sample').outcome == TaskOutcome.SUCCESS 70 | result.output.contains('[docker] exclusive container') 71 | result.output.contains('samplee') 72 | } 73 | 74 | def "Check exclusive fail"() { 75 | setup: 76 | build """ 77 | plugins { 78 | id 'ru.vyarus.use-python' 79 | } 80 | 81 | python { 82 | docker.use = true 83 | } 84 | 85 | tasks.register('sample', PythonTask) { 86 | docker.exclusive = true 87 | command = '-c printTt(\\'samplee\\')' 88 | } 89 | 90 | """ 91 | 92 | when: "run task" 93 | debug() 94 | BuildResult result = runFailed('sample') 95 | 96 | then: "task successful" 97 | result.task(':sample').outcome == TaskOutcome.FAILED 98 | result.output.contains('\'printTt\' is not defined') 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/test/groovy/ru/vyarus/gradle/plugin/python/docker/DockerMultiModuleKitTest.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.docker 2 | 3 | import org.gradle.testkit.runner.BuildResult 4 | import org.gradle.testkit.runner.TaskOutcome 5 | import ru.vyarus.gradle.plugin.python.AbstractKitTest 6 | import spock.lang.IgnoreIf 7 | 8 | /** 9 | * @author Vyacheslav Rusakov 10 | * @since 07.10.2022 11 | */ 12 | // testcontainers doesn't work on windows server https://github.com/testcontainers/testcontainers-java/issues/2960 13 | @IgnoreIf({ System.getProperty("os.name").toLowerCase().contains("windows") }) 14 | class DockerMultiModuleKitTest extends AbstractKitTest { 15 | 16 | def "Check modules use different python"() { 17 | 18 | setup: 19 | file('settings.gradle') << ' include "sub1", "sub2"' 20 | file('sub1').mkdir() 21 | file('sub2').mkdir() 22 | build """ 23 | plugins { 24 | id 'ru.vyarus.use-python' apply false 25 | } 26 | 27 | subprojects { 28 | apply plugin: 'ru.vyarus.use-python' 29 | python { 30 | docker.use = true 31 | } 32 | 33 | tasks.register('sample', PythonTask) { 34 | command = "-c print('sampl\${project.name}')" 35 | } 36 | } 37 | """ 38 | 39 | when: "run python tasks in all modules" 40 | BuildResult result = run('sample') 41 | 42 | then: "task successful" 43 | result.task(':sub1:sample').outcome == TaskOutcome.SUCCESS 44 | result.task(':sub2:sample').outcome == TaskOutcome.SUCCESS 45 | result.output.contains('samplsub1') 46 | result.output.contains('samplsub2') 47 | } 48 | 49 | def "Check parallel execution"() { 50 | 51 | build(""" 52 | plugins { 53 | id 'ru.vyarus.use-python' 54 | } 55 | 56 | subprojects { 57 | apply plugin: 'ru.vyarus.use-python' 58 | python { 59 | docker.use = true 60 | } 61 | 62 | tasks.register('sample', PythonTask) { 63 | command = "-c print('sampl\${project.name}')" 64 | } 65 | } 66 | """) 67 | 68 | // amount of modules in test project 69 | int cnt = 20 70 | 71 | file('settings.gradle') << ' include ' + (1..cnt).collect { 72 | // work dir MUST exist otherwise process will fail to start! 73 | assert file("mod$it").mkdir() 74 | return "'mod$it'" 75 | }.join(',') 76 | 77 | when: "run python tasks in all modules" 78 | debug() 79 | BuildResult result = run('sample', '--parallel', '--max-workers=5') 80 | 81 | then: "tasks successful" 82 | (1..cnt).collect { 83 | result.task(":mod$it:sample").outcome == TaskOutcome.SUCCESS 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/test/groovy/ru/vyarus/gradle/plugin/python/multimodule/MultiplePythonInstallationsKitTest.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.multimodule 2 | 3 | import org.gradle.testkit.runner.BuildResult 4 | import org.gradle.testkit.runner.TaskOutcome 5 | import ru.vyarus.gradle.plugin.python.AbstractKitTest 6 | 7 | /** 8 | * @author Vyacheslav Rusakov 9 | * @since 28.08.2018 10 | */ 11 | class MultiplePythonInstallationsKitTest extends AbstractKitTest { 12 | 13 | def "Check modules use different python"() { 14 | 15 | setup: 16 | file('settings.gradle') << ' include "sub1", "sub2"' 17 | file('sub1').mkdir() 18 | file('sub2').mkdir() 19 | build """ 20 | plugins { 21 | id 'ru.vyarus.use-python' apply false 22 | } 23 | 24 | subprojects { 25 | apply plugin: 'ru.vyarus.use-python' 26 | python { 27 | envPath = 'python' // relative to module! 28 | 29 | // here different python version could be configured, but for test it's not important 30 | 31 | pip 'extract-msg:0.28.0' 32 | } 33 | 34 | tasks.register('sample', PythonTask) { 35 | command = '-c print(\\'samplee\\')' 36 | } 37 | } 38 | """ 39 | 40 | when: "run module 1 task" 41 | BuildResult result = run(':sub1:sample') 42 | 43 | then: "task successful" 44 | result.task(':sub1:sample').outcome == TaskOutcome.SUCCESS 45 | result.output =~ /extract-msg\s+0.28.0/ 46 | result.output.contains('samplee') 47 | result.output.contains("${projectName()}${File.separator}sub1${File.separator}python") 48 | 49 | 50 | when: "run module 2 task" 51 | result = run(':sub2:sample') 52 | 53 | then: "task successful" 54 | result.task(':sub2:sample').outcome == TaskOutcome.SUCCESS 55 | result.output =~ /extract-msg\s+0.28.0/ 56 | result.output.contains('samplee') 57 | result.output.contains("${projectName()}${File.separator}sub2${File.separator}python") 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/test/groovy/ru/vyarus/gradle/plugin/python/multimodule/RequirementsInSubmoduleKitTest.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.multimodule 2 | 3 | import org.gradle.testkit.runner.BuildResult 4 | import org.gradle.testkit.runner.TaskOutcome 5 | import ru.vyarus.gradle.plugin.python.AbstractKitTest 6 | 7 | /** 8 | * @author Vyacheslav Rusakov 9 | * @since 26.08.2022 10 | */ 11 | class RequirementsInSubmoduleKitTest extends AbstractKitTest { 12 | 13 | def "Check requirements works in submodule"() { 14 | 15 | setup: 16 | file('settings.gradle') << ' include "sub"' 17 | file('sub').mkdir() 18 | build """ 19 | plugins { 20 | id 'ru.vyarus.use-python' apply false 21 | } 22 | 23 | subprojects { 24 | apply plugin: 'ru.vyarus.use-python' 25 | } 26 | 27 | """ 28 | file('sub/').mkdir() 29 | file('sub/requirements.txt') << """ 30 | # comment 31 | extract-msg == 0.34.3 32 | """ 33 | 34 | when: "run task" 35 | BuildResult result = run(':sub:pipInstall') 36 | 37 | then: "task successful" 38 | result.task(':sub:pipInstall').outcome == TaskOutcome.SUCCESS 39 | result.output.contains('-m venv ../.gradle/python'.replace('/', File.separator)) 40 | result.output =~ /extract-msg\s+0.34.3/ 41 | 42 | then: "virtualenv created at the root level" 43 | result.output.contains("${projectName()}${File.separator}.gradle${File.separator}python") 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/groovy/ru/vyarus/gradle/plugin/python/task/CheckTaskKitTest.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.task 2 | 3 | import org.gradle.testkit.runner.BuildResult 4 | import ru.vyarus.gradle.plugin.python.AbstractKitTest 5 | 6 | /** 7 | * @author Vyacheslav Rusakov 8 | * @since 13.12.2017 9 | */ 10 | class CheckTaskKitTest extends AbstractKitTest { 11 | 12 | def "Check no python"() { 13 | setup: 14 | build """ 15 | plugins { 16 | id 'ru.vyarus.use-python' 17 | } 18 | 19 | python { 20 | pythonPath = 'somewhere' 21 | } 22 | 23 | """ 24 | 25 | when: "run task" 26 | BuildResult result = runFailed('checkPython') 27 | 28 | then: "task successful" 29 | result.output.contains("Python not found: somewhere${isWin ? '\\python.exe' : '/python'}") 30 | } 31 | 32 | def "Check python version requirement"() { 33 | setup: 34 | build """ 35 | plugins { 36 | id 'ru.vyarus.use-python' 37 | } 38 | 39 | python { 40 | minPythonVersion = '5' 41 | } 42 | 43 | """ 44 | 45 | when: "run task" 46 | BuildResult result = runFailed('checkPython') 47 | 48 | then: "task successful" 49 | result.output.contains('does not match minimal required version: 5') 50 | } 51 | 52 | 53 | def "Check pip version requirement"() { 54 | setup: 55 | build """ 56 | plugins { 57 | id 'ru.vyarus.use-python' 58 | } 59 | 60 | python { 61 | minPipVersion = '200.1' 62 | scope = USER 63 | 64 | pip 'mod:1' 65 | } 66 | 67 | """ 68 | 69 | when: "run task" 70 | BuildResult result = runFailed('checkPython') 71 | 72 | then: "task successful" 73 | result.output.contains('does not match minimal required version: 200.1') 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/test/groovy/ru/vyarus/gradle/plugin/python/task/PipListTaskKitTest.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.task 2 | 3 | 4 | import org.gradle.testkit.runner.BuildResult 5 | import org.gradle.testkit.runner.TaskOutcome 6 | import ru.vyarus.gradle.plugin.python.AbstractKitTest 7 | import ru.vyarus.gradle.plugin.python.cmd.Pip 8 | 9 | /** 10 | * @author Vyacheslav Rusakov 11 | * @since 15.12.2017 12 | */ 13 | class PipListTaskKitTest extends AbstractKitTest { 14 | 15 | def "Check list task"() { 16 | 17 | setup: 18 | // to show at least something 19 | new Pip(gradleEnv()).install('extract-msg==0.28.0') 20 | 21 | build """ 22 | plugins { 23 | id 'ru.vyarus.use-python' 24 | } 25 | 26 | python.scope = USER 27 | """ 28 | 29 | when: "run task" 30 | BuildResult result = run('pipList') 31 | 32 | then: "extract-msg update detected" 33 | result.task(':pipList').outcome == TaskOutcome.SUCCESS 34 | result.output.contains('pip list --format=columns --user') 35 | result.output =~ /extract-msg\s+0.28.0/ 36 | } 37 | 38 | def "Check list all task"() { 39 | 40 | setup: 41 | // to show at least something 42 | new Pip(gradleEnv()).install('extract-msg==0.28.0') 43 | 44 | build """ 45 | plugins { 46 | id 'ru.vyarus.use-python' 47 | } 48 | 49 | python.scope = USER 50 | 51 | pipList.all = true 52 | """ 53 | 54 | when: "run task" 55 | BuildResult result = run('pipList') 56 | 57 | then: "extract-msg update detected" 58 | result.task(':pipList').outcome == TaskOutcome.SUCCESS 59 | !result.output.contains('pip list --format=columns --user') 60 | result.output =~ /extract-msg\s+0.28.0/ 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/test/groovy/ru/vyarus/gradle/plugin/python/task/PipModulesInstallTest.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.task 2 | 3 | import org.gradle.testkit.runner.BuildResult 4 | import org.gradle.testkit.runner.TaskOutcome 5 | import ru.vyarus.gradle.plugin.python.AbstractKitTest 6 | 7 | /** 8 | * @author Vyacheslav Rusakov 9 | * @since 19.05.2018 10 | */ 11 | class PipModulesInstallTest extends AbstractKitTest { 12 | 13 | def "Check vcs install"() { 14 | 15 | setup: 16 | build """ 17 | plugins { 18 | id 'ru.vyarus.use-python' 19 | } 20 | 21 | python { 22 | pip 'git+https://github.com/ictxiangxin/boson/@b52727f7170acbedc5a1b4e1df03972bd9bb85e3#egg=boson-0.9' 23 | usePipCache = false 24 | useVenv = false 25 | } 26 | 27 | """ 28 | 29 | when: "run task" 30 | BuildResult result = run('pipInstall') 31 | 32 | then: "package install called" 33 | result.task(':checkPython').outcome == TaskOutcome.SUCCESS 34 | result.task(':pipInstall').outcome == TaskOutcome.SUCCESS 35 | result.output.contains('Successfully built boson') 36 | result.output.contains('boson-0.9') 37 | 38 | when: "second install" 39 | result = run('pipInstall') 40 | then: "package not installed" 41 | result.task(':checkPython').outcome == TaskOutcome.SUCCESS 42 | result.task(':pipInstall').outcome == TaskOutcome.SUCCESS // up to date check removed 43 | !result.output.contains('Successfully built boson') 44 | !result.output.contains('boson-0.9') 45 | } 46 | 47 | 48 | def "Check vcs install venv"() { 49 | 50 | setup: 51 | build """ 52 | plugins { 53 | id 'ru.vyarus.use-python' 54 | } 55 | 56 | python { 57 | pip 'git+https://github.com/ictxiangxin/boson/@b52727f7170acbedc5a1b4e1df03972bd9bb85e3#egg=boson-0.9' 58 | usePipCache = false 59 | useVenv = true 60 | } 61 | 62 | """ 63 | 64 | when: "run task" 65 | BuildResult result = run('pipInstall') 66 | 67 | then: "package install called" 68 | result.task(':checkPython').outcome == TaskOutcome.SUCCESS 69 | result.task(':pipInstall').outcome == TaskOutcome.SUCCESS 70 | result.output.replace('MarkupSafe-2.1.5', 'MarkupSafe-3.0.2').contains('Successfully installed MarkupSafe-3.0.2 boson-0.9') 71 | result.output.contains('boson-0.9') 72 | 73 | when: "second install" 74 | result = run('pipInstall') 75 | then: "package not installed" 76 | result.task(':checkPython').outcome == TaskOutcome.SUCCESS 77 | result.task(':pipInstall').outcome == TaskOutcome.SUCCESS // up to date check removed 78 | !result.output.replace('MarkupSafe-2.1.5', 'MarkupSafe-3.0.2').contains('Successfully installed MarkupSafe-3.0.2 boson-0.9') 79 | !result.output.contains('boson-0.9') 80 | } 81 | 82 | def "Check square syntax"() { 83 | 84 | setup: 85 | build """ 86 | plugins { 87 | id 'ru.vyarus.use-python' 88 | } 89 | 90 | python { 91 | pip 'requests[socks,security]:2.18.4' 92 | } 93 | 94 | """ 95 | 96 | when: "run task" 97 | BuildResult result = run('pipInstall') 98 | 99 | then: "package install called" 100 | result.task(':checkPython').outcome == TaskOutcome.SUCCESS 101 | result.task(':pipInstall').outcome == TaskOutcome.SUCCESS 102 | result.output.contains('requests-2.18.4') 103 | 104 | when: "second install" 105 | result = run('pipInstall') 106 | then: "package not installed" 107 | result.task(':checkPython').outcome == TaskOutcome.SUCCESS 108 | result.task(':pipInstall').outcome == TaskOutcome.SUCCESS // up to date check removed 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/test/groovy/ru/vyarus/gradle/plugin/python/task/PipUpdatesTaskKitTest.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.task 2 | 3 | 4 | import org.gradle.testkit.runner.BuildResult 5 | import org.gradle.testkit.runner.TaskOutcome 6 | import ru.vyarus.gradle.plugin.python.AbstractKitTest 7 | import ru.vyarus.gradle.plugin.python.cmd.Pip 8 | import ru.vyarus.gradle.plugin.python.cmd.Python 9 | 10 | /** 11 | * @author Vyacheslav Rusakov 12 | * @since 01.12.2017 13 | */ 14 | class PipUpdatesTaskKitTest extends AbstractKitTest { 15 | 16 | def "Check updates detected"() { 17 | 18 | setup: 19 | // make sure old version installed 20 | new Pip(gradleEnv()).install('extract-msg==0.28.0') 21 | 22 | build """ 23 | plugins { 24 | id 'ru.vyarus.use-python' 25 | } 26 | 27 | python { 28 | scope = USER 29 | pip 'extract-msg:0.28.0' 30 | } 31 | 32 | """ 33 | 34 | when: "run task" 35 | BuildResult result = run('pipUpdates') 36 | 37 | then: "extract-msg update detected" 38 | result.task(':pipUpdates').outcome == TaskOutcome.SUCCESS 39 | result.output.contains('The following modules could be updated:') 40 | result.output =~ /extract-msg\s+0.28.0/ 41 | } 42 | 43 | def "Check updates detected in environment"() { 44 | 45 | setup: 46 | build """ 47 | plugins { 48 | id 'ru.vyarus.use-python' 49 | } 50 | 51 | python { 52 | scope = VIRTUALENV 53 | pip 'extract-msg:0.28.0' 54 | } 55 | 56 | """ 57 | 58 | when: "install old version" 59 | BuildResult result = run('pipInstall') 60 | then: "installed" 61 | result.task(':pipInstall').outcome == TaskOutcome.SUCCESS 62 | result.output.contains('pip install extract-msg') 63 | 64 | 65 | when: "run task" 66 | result = run('pipUpdates') 67 | 68 | then: "extract-msg update detected" 69 | result.task(':pipUpdates').outcome == TaskOutcome.SUCCESS 70 | result.output.contains('The following modules could be updated:') 71 | result.output =~ /extract-msg\s+0.28.0/ 72 | } 73 | 74 | def "Check no modules"() { 75 | 76 | setup: 77 | build """ 78 | plugins { 79 | id 'ru.vyarus.use-python' 80 | } 81 | """ 82 | 83 | when: "run task" 84 | BuildResult result = run('pipUpdates') 85 | 86 | then: "nothing declared" 87 | result.task(':pipUpdates').outcome == TaskOutcome.SUCCESS 88 | result.output.contains('No modules declared') 89 | } 90 | 91 | def "Check no updates detected"() { 92 | 93 | setup: 94 | // use the latest version 95 | new Python(gradleEnv()).callModule('pip', 'install extract-msg --upgrade --user') 96 | 97 | build """ 98 | plugins { 99 | id 'ru.vyarus.use-python' 100 | } 101 | 102 | python { 103 | scope = USER 104 | pip 'extract-msg:0.28.0' // version does not matter here 105 | } 106 | 107 | """ 108 | 109 | when: "run task" 110 | BuildResult result = run('pipUpdates') 111 | 112 | then: "nothing to update" 113 | result.task(':pipUpdates').outcome == TaskOutcome.SUCCESS 114 | result.output.contains('All modules use the most recent versions') 115 | } 116 | 117 | def "Check updates for all"() { 118 | 119 | setup: 120 | // use the latest version 121 | new Pip(gradleEnv()).install('extract-msg==0.28.0') 122 | 123 | build """ 124 | plugins { 125 | id 'ru.vyarus.use-python' 126 | } 127 | 128 | python { 129 | scope = USER 130 | } 131 | 132 | pipUpdates.all = true 133 | 134 | """ 135 | 136 | when: "run task" 137 | BuildResult result = run('pipUpdates') 138 | 139 | then: "nothing to update" 140 | result.task(':pipUpdates').outcome == TaskOutcome.SUCCESS 141 | !result.output.contains('All modules use the most recent versions') 142 | } 143 | 144 | 145 | def "Check no updates"() { 146 | 147 | setup: 148 | // empty environment 149 | env().create(false, true) 150 | 151 | build """ 152 | plugins { 153 | id 'ru.vyarus.use-python' 154 | } 155 | 156 | python { 157 | scope = USER 158 | } 159 | 160 | pipUpdates.all = true 161 | 162 | """ 163 | 164 | when: "run task" 165 | BuildResult result = run('pipUpdates') 166 | 167 | then: "nothing to update" 168 | result.task(':pipUpdates').outcome == TaskOutcome.SUCCESS 169 | !result.output.contains('All modules use the most recent versions') 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/test/groovy/ru/vyarus/gradle/plugin/python/task/PythonTaskEnvironmentKitTest.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.task 2 | 3 | import org.gradle.testkit.runner.BuildResult 4 | import org.gradle.testkit.runner.TaskOutcome 5 | import ru.vyarus.gradle.plugin.python.AbstractKitTest 6 | 7 | /** 8 | * @author Vyacheslav Rusakov 9 | * @since 17.03.2020 10 | */ 11 | class PythonTaskEnvironmentKitTest extends AbstractKitTest { 12 | 13 | def "Check env vars"() { 14 | setup: 15 | build """ 16 | plugins { 17 | id 'ru.vyarus.use-python' 18 | } 19 | 20 | tasks.register('sample', PythonTask) { 21 | command = "-c \\"import os;print('variables: '+os.getenv('some', 'null')+' '+os.getenv('foo', 'null'))\\"" 22 | environment 'some', 1 23 | environment(['foo': 'bar']) 24 | } 25 | """ 26 | 27 | when: "run task" 28 | debug() 29 | BuildResult result = run('sample') 30 | 31 | then: "variables visible" 32 | result.task(':sample').outcome == TaskOutcome.SUCCESS 33 | result.output.contains('variables: 1 bar') 34 | } 35 | 36 | 37 | def "Check python see system variables"() { 38 | setup: 39 | build """ 40 | plugins { 41 | id 'ru.vyarus.use-python' 42 | } 43 | 44 | assert System.getenv('some') == 'foo' 45 | 46 | tasks.register('sample', PythonTask) { 47 | command = "-c \\"import os;print('variables: '+os.getenv('some', 'null'))\\"" 48 | } 49 | """ 50 | 51 | when: "run task" 52 | def env = new HashMap(System.getenv()) 53 | env.put('some', 'foo') 54 | BuildResult result = gradle('sample') 55 | .withEnvironment(env) 56 | .build() 57 | 58 | then: "system variable visible" 59 | result.task(':sample').outcome == TaskOutcome.SUCCESS 60 | result.output.contains('variables: foo') 61 | } 62 | 63 | def "Check python dont see system variables after override"() { 64 | setup: 65 | build """ 66 | plugins { 67 | id 'ru.vyarus.use-python' 68 | } 69 | 70 | assert System.getenv('some') == 'foo' 71 | 72 | tasks.register('sample', PythonTask) { 73 | command = "-c \\"import os;print('variables: '+os.getenv('some', 'null'))\\"" 74 | environment 'bar', 1 75 | } 76 | """ 77 | 78 | when: "run task" 79 | def env = new HashMap(System.getenv()) 80 | env.put('some', 'foo') 81 | BuildResult result = gradle('sample') 82 | .withEnvironment(env) 83 | .build() 84 | 85 | then: "setting variable doesn't hide system vars" 86 | result.task(':sample').outcome == TaskOutcome.SUCCESS 87 | result.output.contains('variables: foo') 88 | } 89 | 90 | def "Check composition with global vars"() { 91 | setup: 92 | build """ 93 | plugins { 94 | id 'ru.vyarus.use-python' 95 | } 96 | 97 | python.environment 'some', 1 98 | 99 | tasks.register('sample', PythonTask) { 100 | command = "-c \\"import os;print('variables: '+os.getenv('some', 'null')+' '+os.getenv('foo', 'null'))\\"" 101 | environment 'foo', 'bar' 102 | } 103 | """ 104 | 105 | when: "run task" 106 | debug() 107 | BuildResult result = run('sample') 108 | 109 | then: "both variables visible" 110 | result.task(':sample').outcome == TaskOutcome.SUCCESS 111 | result.output.contains('variables: 1 bar') 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/test/groovy/ru/vyarus/gradle/plugin/python/task/PythonTaskKitTest.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.task 2 | 3 | import org.gradle.testkit.runner.BuildResult 4 | import org.gradle.testkit.runner.TaskOutcome 5 | import ru.vyarus.gradle.plugin.python.AbstractKitTest 6 | 7 | /** 8 | * @author Vyacheslav Rusakov 9 | * @since 20.11.2017 10 | */ 11 | class PythonTaskKitTest extends AbstractKitTest { 12 | 13 | def "Check work dir"() { 14 | setup: 15 | build """ 16 | plugins { 17 | id 'ru.vyarus.use-python' 18 | } 19 | 20 | tasks.register('sample', PythonTask) { 21 | workDir = 'build/pyth' 22 | command = '-c "open(\\'fl\\', \\'a\\').close()"' 23 | } 24 | """ 25 | 26 | when: "run task" 27 | BuildResult result = run('sample') 28 | 29 | then: "file created in work dir" 30 | result.task(':sample').outcome == TaskOutcome.SUCCESS 31 | file('build/pyth').list({fl, name-> name == 'fl'} as FilenameFilter).size() == 1 32 | } 33 | 34 | def "Check no work dir creation"() { 35 | setup: 36 | build """ 37 | plugins { 38 | id 'ru.vyarus.use-python' 39 | } 40 | 41 | tasks.register('sample', PythonTask) { 42 | workDir = 'build/pyth' 43 | createWorkDir = false 44 | command = '-c "open(\\'fl\\', \\'a\\').close()"' 45 | } 46 | """ 47 | 48 | when: "run task" 49 | BuildResult result = runFailed('sample') 50 | 51 | then: "python failed to start" 52 | result.output =~ /net\.rubygrapefruit\.platform\.NativeException: Could not start 'python(3)?'/ 53 | } 54 | 55 | def "Check array command"() { 56 | setup: 57 | build """ 58 | plugins { 59 | id 'ru.vyarus.use-python' 60 | } 61 | 62 | tasks.register('sample', PythonTask) { 63 | module = 'pip' 64 | command = ['list', '--user'] 65 | } 66 | """ 67 | 68 | when: "run task" 69 | BuildResult result = run('sample') 70 | 71 | then: "executed" 72 | result.task(':sample').outcome == TaskOutcome.SUCCESS 73 | result.output =~ /\[python] python(3)? -m pip list --user/ 74 | } 75 | 76 | 77 | def "Check module command"() { 78 | setup: 79 | build """ 80 | plugins { 81 | id 'ru.vyarus.use-python' 82 | } 83 | 84 | tasks.register('sample', PythonTask) { 85 | module = 'pip' 86 | command = 'list --user' 87 | } 88 | """ 89 | 90 | when: "run task" 91 | BuildResult result = run('sample') 92 | 93 | then: "executed" 94 | result.task(':sample').outcome == TaskOutcome.SUCCESS 95 | result.output =~ /\[python] python(3)? -m pip list --user/ 96 | } 97 | 98 | def "Check log level change"() { 99 | setup: 100 | build """ 101 | plugins { 102 | id 'ru.vyarus.use-python' 103 | } 104 | 105 | tasks.register('sample', PythonTask) { 106 | module = 'pip' 107 | command = 'list' 108 | logLevel = LogLevel.DEBUG 109 | } 110 | """ 111 | 112 | when: "run task" 113 | BuildResult result = run('sample') 114 | 115 | then: "executed" 116 | result.task(':sample').outcome == TaskOutcome.SUCCESS 117 | !result.output.contains('[python] python -m pip list') 118 | } 119 | 120 | def "Check different prefix"() { 121 | setup: 122 | build """ 123 | plugins { 124 | id 'ru.vyarus.use-python' 125 | } 126 | 127 | tasks.register('sample', PythonTask) { 128 | module = 'pip' 129 | command = 'list' 130 | outputPrefix = '---->' 131 | } 132 | """ 133 | 134 | when: "run task" 135 | BuildResult result = run('sample') 136 | 137 | then: "executed" 138 | result.task(':sample').outcome == TaskOutcome.SUCCESS 139 | result.output.contains('---->') 140 | } 141 | 142 | def "Check extra args"() { 143 | setup: 144 | build """ 145 | plugins { 146 | id 'ru.vyarus.use-python' 147 | } 148 | 149 | tasks.register('sample', PythonTask) { 150 | module = 'pip' 151 | command = 'list' 152 | pythonArgs '-s' 153 | extraArgs '--format=columns', '--user' 154 | } 155 | """ 156 | 157 | when: "run task" 158 | BuildResult result = run('sample') 159 | 160 | then: "executed" 161 | result.task(':sample').outcome == TaskOutcome.SUCCESS 162 | result.output =~ /\[python] python(3)? -s -m pip list --format=columns --user/ 163 | } 164 | 165 | def "Check script file call"() { 166 | setup: 167 | build """ 168 | plugins { 169 | id 'ru.vyarus.use-python' 170 | } 171 | 172 | tasks.register('script', PythonTask) { 173 | command = 'sample.py' 174 | } 175 | """ 176 | file('sample.py') << "print('sample')" 177 | 178 | when: "run task" 179 | BuildResult result = run('script') 180 | 181 | then: "executed" 182 | result.task(':script').outcome == TaskOutcome.SUCCESS 183 | result.output =~ /\[python] python(3)? sample.py/ 184 | result.output.contains('\t sample') 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/test/groovy/ru/vyarus/gradle/plugin/python/util/DurationFormatTest.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.util 2 | 3 | import spock.lang.Specification 4 | 5 | /** 6 | * @author Vyacheslav Rusakov 7 | * @since 19.12.2017 8 | */ 9 | class DurationFormatTest extends Specification { 10 | 11 | def "Check time formatting"() { 12 | 13 | expect: 14 | DurationFormatter.format(0) == '0ms' 15 | DurationFormatter.format(100) == '100ms' 16 | DurationFormatter.format(1200) == '1.2s' 17 | DurationFormatter.format(1020) == '1.02s' 18 | DurationFormatter.format(10_000) == '10s' 19 | DurationFormatter.format(1_000_000) == '16m 40s' 20 | DurationFormatter.format(1_000_000_000) == '11d 13h 46m 40s' 21 | DurationFormatter.format(1*24*60*60*1000) == '1d' 22 | DurationFormatter.format(1*25*60*60*1000) == '1d 1h' 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/groovy/ru/vyarus/gradle/plugin/python/util/ExecRes.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.util 2 | 3 | import org.gradle.process.ExecResult 4 | import org.gradle.process.internal.ExecException 5 | 6 | /** 7 | * @author Vyacheslav Rusakov 8 | * @since 20.11.2017 9 | */ 10 | class ExecRes implements ExecResult { 11 | 12 | int returnValue 13 | 14 | ExecRes(int returnValue) { 15 | this.returnValue = returnValue 16 | } 17 | 18 | @Override 19 | int getExitValue() { 20 | return returnValue 21 | } 22 | 23 | @Override 24 | ExecResult assertNormalExitValue() throws ExecException { 25 | return null 26 | } 27 | 28 | @Override 29 | ExecResult rethrowFailure() throws ExecException { 30 | return null 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/groovy/ru/vyarus/gradle/plugin/python/util/OutputLoggerTest.groovy: -------------------------------------------------------------------------------- 1 | package ru.vyarus.gradle.plugin.python.util 2 | 3 | import org.gradle.api.logging.LogLevel 4 | import spock.lang.Specification 5 | 6 | /** 7 | * @author Vyacheslav Rusakov 8 | * @since 17.11.2017 9 | */ 10 | class OutputLoggerTest extends Specification { 11 | 12 | def "Check output with prefix"() { 13 | 14 | when: "configure logger with prefix" 15 | def logger = new TestLogger(appendLevel: true) 16 | new OutputLogger(logger, LogLevel.INFO, '\t').withStream { 17 | it.write('sample'.getBytes()) 18 | } 19 | then: "output prefixed" 20 | logger.res == 'INFO \t sample\n' 21 | } 22 | 23 | def "Check output without prefix"() { 24 | 25 | when: "configure logger without prefix" 26 | def logger = new TestLogger(appendLevel: true) 27 | new OutputLogger(logger, LogLevel.LIFECYCLE, null).withStream { 28 | it.write('sample'.getBytes()) 29 | } 30 | then: "output prefixed" 31 | logger.res == 'LIFECYCLE sample\n' 32 | } 33 | } --------------------------------------------------------------------------------