├── .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 | [](http://www.opensource.org/licenses/MIT)
3 | [](https://github.com/xvik/gradle-use-python-plugin/actions/workflows/CI.yml)
4 | [](https://ci.appveyor.com/project/xvik/gradle-use-python-plugin)
5 | [](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 | [](https://maven-badges.herokuapp.com/maven-central/ru.vyarus/gradle-use-python-plugin)
48 | [](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 | [](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 | *
44 | * direct module call: {@code '-m mod cmd'}
45 | * code execution: {@code '-c import sys;\nsys...'}
46 | * file execution: {@code 'path/to/file.py} (relative to workDir)
47 | *
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 | }
--------------------------------------------------------------------------------