Gradle use-python plugin

Additional

Language
Groovy
Version
2.1.0 (Mar 17, 2020)
Created
Dec 11, 2017
Updated
Mar 17, 2020
Owner
Vyacheslav Rusakov (xvik)
Contributors
Vyacheslav Rusakov (xvik)
Greg Brown (yellowsquid)
2
Activity
Badge
Generate
Download
Source code

Show card

Gradle use-python plugin

About

Plugin does not install python and pip itself and use globally installed python (by default). It's easier to prepare python manually because python have good compatibility (from user perspective) and does not need to be updated often.

The only plugin intention is to simplify python usage from gradle. By default, plugin creates python virtualenv inside the project and installs all modules there so each project has its own python (copy) and could not be affected by other projects or system changes.

Features:

  • Install required python modules using pip (per project (virtualenv), os user (--user) or globally)
  • Provides task to call python commands, modules or scripts (PythonTask)
  • Could be used as basement for building plugins for specific python modules (like mkdocs plugin)
Summary
  • Configuration: python
  • Tasks:
    • checkPython - validate python installation (and create virtualenv if required)
    • pipInstall - install declared pip modules
    • pipUpdates - show the latest available versions for the registered modules
    • pipList - show all installed modules (the same as pipInstall shows after installation)
    • type:PythonTask - call python command/script/module
    • type:PipInstallTask - may be used for custom pip modules installation workflow
Possible pip issue warning (linux/macos)

If pip3 list -o fails with: TypeError: '>' not supported between instances of 'Version' and 'Version' Then simply update installed pip version: python3 -m pip install --upgrade pip

This is a known issue related to incorrectly patched pip packages in some distributions.

Setup

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'ru.vyarus:gradle-use-python-plugin:2.1.0'
    }
}
apply plugin: 'ru.vyarus.use-python'

OR

plugins {
    id 'ru.vyarus.use-python' version '2.1.0'
}

Compatibility

Plugin compiled for java 8, compatible with java 11

Gradle Version
5-6 2.1.0
4.x 1.2.0

Snapshots

Snapshots may be used through JitPack
  • Go to JitPack project page
  • Select Commits section and click Get it on commit you want to use or use master-SNAPSHOT to use the most recent snapshot

For gradle before 6.0 use buildscript block with required commit hash as version:

buildscript {
    repositories {
        maven { url 'https://jitpack.io' }
    }
    dependencies {
        classpath 'ru.vyarus:gradle-use-python-plugin:2450c7e881'
    }
}
apply plugin: 'ru.vyarus.use-python'

For gradle 6.0 and above:

  • Add to settings.gradle (top most!) with required commit hash as version:

    pluginManagement {
        resolutionStrategy {
            eachPlugin {
                if (requested.id.namespace == 'ru.vyarus.use-python') {
                    useModule('ru.vyarus:gradle-use-python-plugin:2450c7e881')
                }
            }
        }
        repositories {
            maven { url 'https://jitpack.io' }
            gradlePluginPortal()          
        }
    }    
  • Use plugin without declaring version:

    plugins {
        id 'ru.vyarus.use-python'
    }

Python & Pip

Make sure python and pip are installed:

python --version  
pip --version

On *nix python usually reference python2. For python3:

python3 --version  
pip3 --version
Windows install

Download and install python manually or use chocolately:

choco install python
Linux/Macos install

On most *nix distributions python is already installed, but often without pip.

Install pip if required (ubuntu example):

sudo apt-get install python3-pip

Make sure the latest pip installed (required to overcome some older pip problems):

pip3 install -U pip

To install exact pip version:

pip3 install -U pip==10.0.0

Note that on ubuntu pip installed with python3-pip package is 9.0.1, but it did not(!) downgrade module versions (e.g. pip install click 6.6 when click 6.7 is installed will do nothing). Maybe there are other differences, so it's highly recommended to upgrade pip with pip3 install -U pip.

Automatic pip upgrade

As described above, there are different ways of pip installation in linux and, more important, admin permissions are required to upgrade global pip. So it is impossible to upgrade pip from the plugin (in all cases).

But, it is possible inside virtualenv or user (--user) scope. Note that plugin creates virtualenv by default (per project independent python environment).

So, in order to use newer pip simply put it as first dependency:

python {
    pip 'pip:10.0.1'
    pip 'some_module:1.0'
}

Here project virtualenv will be created with global pip and newer pip version installed inside environment. Packages installation is sequential, so all other packages will be installed with newer pip (each installation is independent pip command).

The same will work for user scope: python.scope = USER

When applying this trick, consider minimal pip version declared in configuration (python.minPipVersion='9' by default) as minimal pip version required for project setup (instead of minimal version required for work).

Automatic python install

Python is assumed to be used as java: install and forget. It perfectly fits user use case: install python once and plugin will replace all manual work on project environment setup.

It is also easy to configure python on CI (like travis).

If you want automatic python installation, try looking on JetBrain's python-envs plugin. But be careful because it has some caveats (for example, on windows python could be installed automatically just once and requires manual un-installation).

Multi-module projects

When used in multi-module project, plugin will create virtualenv inside the root project directory in order to share the same environment for all modules.

See multi-module setup cases

Travis CI configuration

To make plugin work on travis you'll need to install python3 packages:

language: java  
dist: xenial
jdk: openjdk8

sudo: required
addons:
  apt:
    packages:
    - "python3"
    - "python3-pip"
    - "python3-setuptools" 

before_install:
  - sudo pip3 install -U pip

It will be python 3.5 by default.

NOTE: travis does not require manual sudo support enable anymore (enabled by default)

Appveyour CI configuration

To make plugin work on appveyour you'll need to add python to path:

environment:
    matrix:
        - JAVA_HOME: C:\Program Files\Java\jdk1.8.0
          PYTHON: "C:\\Python35-x64"

install:
  - set PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%

Now plugin would be able to find python binary.

Usage

Declare required modules (optional):

python.pip 'module1:1.0', 'module2:1.0'

or

python {
    pip 'module1:1.0'
    pip 'module2:1.0'
}

Module format is: name:version (will mean name==version in pip notion). Non strict version definition is not allowed (for obvious reasons). Dependencies are installed in declaration order. If duplicate declaration specified then only the latest declaration will be used:

python.pip 'module1:2.0', 'module2:1.0', 'module1:1.0' 

Will install version 1.0 of module1 because it was the latest declaration. "Module overrides" works for all declaration types (see below): the latest declared module version always wins.

Dependencies are installed with pipInstall task which is called before any declared PythonTask.

Note that by default dependencies are installed inside project specific virtualenv (project specific copy of python environment).

Behaviour matrix for possible scope and installVirtualenv configurations:

scope installVirtualenv Behaviour default
GLOBAL ignored packages installed in global scope (pip install name)
USER ignored packages installed in user scope (pip install name --user)
VIRTUALENV_OR_USER true if virtualenv not installed, install it in user scope; create project specific virtualenv and use it default
VIRTUALENV_OR_USER false when virtualenv is not installed install packages in user scope (same as USER); when virtualenv installed create project specific virtualenv and use it
VIRTUALENV true if virtualenv not installed, install it in user scope; create project specific virtualenv and use it
VIRTUALENV false throw error when virtualenv not installed

Note that VIRTUALENV + true and VIRTUALENV_OR_USER + true behaviours are the same. Different scope name here describes behavior for unexpected installVirtualenv=false change (to fail or fallback to user scope).

USER and GLOBAL scopes will ignore local (virtual)environment, even if project-specific environment was created before, with these options global python will be used instead.

Pip module extra features

You can declare modules with extra features in module name to install special version of module (with enabled features):

python.pip 'requests[socks,security]:2.18.4'

IMPORTANT: it is impossible to track if this "variation" of module is installed, so plugin performs up-to-date check for such modules by name only (for example, if 'requests==2.18.4' is already installed). For most cases, this is suitable behaviour because, by default, modules are installed in virtualenv and so you will always have correct module installed. For other cases, you can disable up-to-date checks (delegate all dependencies logic to pip): python.alwaysInstallModules = true

VCS pip modules

You can declare vcs modules: modules installed directly from version control (e.g. git, svn). Format:

vcs+protocol://repo_url/@vcsVersion#egg=pkg-pkgVersion
  • @vcsVersion part is required: prefer using commit version or tag for reproducible builds
  • -pkgVersion is installed module version. Required to be able to compare declared plugin with installed version.

For example:

python.pip 'git+https://github.com/ictxiangxin/boson/@b52727f7170acbedc5a1b4e1df03972bd9bb85e3#egg=boson-0.9'

Declares module boson version 0.9, installed from git commit b52727f7170acbedc5a1b4e1df03972bd9bb85e3 (it may be tag name or branch, but prefer not using branch names).

pipInstall will be considered up-to-date if boson==0.9 is already installed. Note that declared module version is completely free: you can set any version (0.10, 1.2, etc.), it is not checked and used only for up-to-date validation.

Vcs module installation is: source checkout and module build (using setup.py). You may need to specify subdirectory as &subdirectory=pkg_dir (see docs)

To avoid installation problems, package version is not used for actual installation (in spite of the fact that its official convention, it doesnt work in some cases). For example, module above will be installed as (no -0.9):

pip install git+https://github.com/ictxiangxin/boson/@b52727f7170acbedc5a1b4e1df03972bd9bb85e3#egg=boson

All pip supported vcs could be used: git, svn, hg, bzr

If up-to-date logic, implemented by pipInstall task, does not suit your needs, you can always disable it with python.alwaysInstallModules = true (pip always called). But this will be slower.

NOTE: since pip 20, compiled vcs module is cached (before it was build on each execution), but it is possible to disable cache (for all modules) with python.usePipCache=false configuration (applies --no-cache-dir pip flag)

Virtualenv

When you declare any pip modules, plugin will try to use virtualenv in order to install required modules locally (for current project only).

If virtualenv is not installed - it will be installed automatically in --user scope. If you don't want automatic installation then disable it:

python.installVirtualenv = false

Plugin installs exact pip version declared in python.virtualenvVersion (by default, 16.7.9). This way, plugin will always install only known to be working version and avoid side effects of "just released" versions (note that pip 20 is a major rewrite and may still contain side effects).

In any case, plugin checks if virtualenv is already installed and use it to create local environment (if not, then fall back to --user scope by default). Virtualenv usage is driven by declared scope, so if you don't want to use it set:

python.scope = USER // or GLOBAL

With USER (or GLOBAL) scope, virtualenv will not be used, even if it's already created in project (plugin will ignore it and use global python).

If you already use virtualenv in your project (have created manually environment), then simply point plugin to use it:

python.envPath = 'path/to/your/env'

It will automatically change pythonPath configuration accordingly.

NOTE: plugin will not create environment if you don't use any modules. If you still want to use project specific environment (without declared pip modules) then create it manually: python3 -m virtualenv .gradle/python (default location). Plugin will recognize existing env and use it.

IMPORTANT: virtualenv creates local python copy (by default in .gradle/python). Copy is created from global python and later used instead of global python. If you want to change used python version in the environment, then manually remove .gradle/python so it could be created again (from global python).

To copy environment instead of symlinking (default) set (--always-copy):

python.envCopy = true

Scope

Pip dependencies could be installed per project, for current user (~/) or globally.

Default behaviour:

  • if virtualenv module installed (or automatically installed, see above): manage pip dependencies per project (env .gradle/python created)
  • if no virtualenv - use user scope (--user pip flag): pip modules are installed only for current user (this avoid permission problems on linux)

To change defaults:

python.scope = VIRTUALENV
  • GLOBAL - install modules globally (this may not work on linux due to permissions)
  • USER - use --user flag to install for current user only
  • VIRTUALENV_OR_USER - default
  • VIRTUALENV - use virtualenv (if module not installed - error thrown)

Note that values may be declared without quotes because it's an enum which values are declared as project ext properties (ext.USER==ru.vyarus.gradle.plugin.python.PythonExtension.Scope.USER).

Complete behaviour matrix see above

Check modules updates

To quick check if new versions are available for the registered pip modules use pipUpdates task:

:pipUpdates
The following modules could be updated:

 package            version latest type 
 ------------------ ------- ------ -----
 click              6.6     6.7    wheel

Note that it will not show versions for transitive modules, only for modules specified directly in python.pip.

To see all available updates (without filtering):

pipUpdates.all = true

NOTE: If you see an error like

TypeError: '>' not supported between instances of 'Version' and 'SetuptoolsVersion'

then update pip:

pip install -U pip

Call python

Call python command:

task cmd(type: PythonTask) {
    command = "-c print('sample')"
}

called: python -c print('sample') on win and python -c exec("print('sample')") on *nix (exec applied automatically for compatibility)

Call multi-line command:

task cmd(type: PythonTask) {
    command = "-c \"import sys; print(sys.prefix)\""
}

called: python -c "import sys; print(sys.prefix)" on win and python -c exec("import sys; print(sys.prefix)") on *nix

NOTE: it is important to wrap script with space in quotes (otherwise parser will incorrectly parse arguments).

Call module:

task mod(type: PythonTask) {
    module = 'sample' 
    command = "mod args"
}

called: python -m sample mod args

Call script:

task script(type: PythonTask) { 
    command = "path/to/script.py 1 2"
}

called: python path/to/script.py 1 2 (arguments are optional, just for demo)

String command is used for simplicity, but it could be array/collection of args:

task script(type: PythonTask) { 
    command = ['path/to/script.py', '1', '2'] 
}
Command parsing

When command passed as string it is manually parsed to arguments array (split by space):

  • Spaces in quotes are ignored: "quoted space" or 'quoted space'
  • Escaped spaces are ignored: with\\ space (argument will be used with simple space then - escape removed).
  • 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.

To view parsed arguments run gradle with -i flag (enable info logs). In case when command can't be parsed properly (bug in parser or unsupported case) use array of arguments instead of string.

Environment variables

By default, executed python can access system environment variables (same as System.getenv()).

To declare custom (process specific) variables:

task sample(type: PythonTask) {
       command = "-c \"import os;print('variables: '+os.getenv('some', 'null')+' '+os.getenv('foo', 'null'))\""
       environment 'some', 1
       environment 'other', 2
       environment(['foo': 'bar', 'baz': 'bag'])
}

Map based declaration (environment(['foo': 'bar', 'baz': 'bag'])) does not remove previously declared variables (just add all vars from map), but direct assignment environment = ['foo': 'bar', 'baz': 'bag'] will reset variables.

System variables will be available even after declaring custom variables (of course, custom variables could override global value).

Configuration

Python location

On linux, plugin will use python3 if available (and fall back to python if not). To use different binary use:

python {
    pythonBinary = 'python'
}

This will force python 2 for linux. Also, this may be handy if python binary is named differently.

To use non global python:

python {
    pythonPath = 'path/to/python/binray/'
}

pythonPath must be set to directory containing python binary (e.g. 'path/to/python/binray/python.exe')

NOTE: pythonPath is ignored when virtualenv used (virtualenv located at python.envPath already exists).

Minimal python and pip versions

To set python version constraint:

python {
    minPythonVersion = '3.2'
}

Python version format is: major.minor.micro. Constraint may include any number of levels: '3', '3.1', '2.7.5'

The same way pip version could be restricted:

python {
    minPipVersion = '9.0.1'
}
Pip

By default, all installed python modules are printed to console after pip installations using pip list (of course, if at least one module were declared for installation). This should simplify problems resolution (show used transitive dependencies versions).

To switch off:

python {
    showInstalledVersions = false
}

You can always see the list of installed modules with pipList task (exactly the same list as after pipInstall).

NOTE: if global python is used with USER scope and some modules were manually installed in global scope then they will not be shown by pipList (and after pip install). To see all modules:

pipList.all = true

Global modules are hidden by default (for USER scope) because on linux there are a lot of system modules pre-installed.

By default, 'pip install' is not called for modules already installed with correct version. In most situations this is preferred behaviour, but if you need to be sure about dependencies then force installation:

python {
    alwaysInstallModules = true
}
Reference

All configuration options with default values:

python {
   // path to python binary (global by default)
   pythonPath
   // python binary name (python or python3 by default)
   pythonBinary
   
   // minimal required python version (m.m.m)
   minPythonVersion
   // minimal required pip version (m.m.m)
   minPipVersion = '9'   
   
   // show all installed modules versions after pip installation
   showInstalledVersions = true
   // always call module install, even if correct version is already installed
   alwaysInstallModules = false
   
    // pip modules installation scope (project local, os user dir, global) 
   scope = VIRTUALENV_OR_USER
   // automatically install virtualenv module (if pip modules declared and scope allows)   
   installVirtualenv = true
   // if virtualenv not installed (in --user scope), plugin will install exactly this version
   // (known to be working version) to avoid side effects
   virtualenvVersion = '16.7.9'
   // used virtualenv path (if virtualenv used, see 'scope')
   envPath = '.gradle/python'
   // copy virtualenv instead of symlink (when created)
   envCopy = false
   // may be used to disable pip cache (--no-cache-dir option)
   usePipCache = true 
}

Note that in case of multi-module project envPath is set to '.gradle/python' inside the root project, even if plugin is activated inside module (see multi-module setup).

PythonTask

PythonTask configuration:

Property Description
pythonPath Path to python binary. By default used path declared in global configuration
pythonBinary Python binary name. By default, python3 on linux and python otherwise.
workDir Working directory (important if called script/module do file operations). By default, it's a project root
createWorkDir Automatically create working directory if does not exist. Enabled by default
module Module name to call command on (if command not set module called directly). Useful for derived tasks.
command Python command to execute (string, array, iterable)
logLevel Logging level for python output. By default is LIFECYCLE (visible in console). To hide output use LogLevel.INFO
pythonArgs Extra python arguments applied just after python binary. Useful for declaring common python options (-I, -S, etc.)
extraArgs Extra arguments applied at the end of declared command (usually module arguments). Useful for derived tasks to declare default options
outputPrefix Prefix, applied for each line of python output. By default is '\t' to identify output for called gradle command
environment Process specific environment variables

Also, task provide extra methods:

  • pythonArgs(String... args) to declare extra python arguments (shortcut to append values to pythonArgs property).
  • extraArgs(String... args) to declare extra arguments (shortcut to append values to extraArgs property).
  • environment(String var, Object value) to set custom environment variable (shortcut to append values to environment property)
  • environment(Map<String, Object> vars) to set multiple custom environment variables at once (shortcut to append values to environment property)

PipInstallTask

Default pip installation task is registered as pipInstall and used to install modules, declared in global configuration. Custom task(s) may be used, if required:

task myPipInst(type: PipInstallTask) {
    pip 'mod:1', 'other:2'
}

Configuration:

Property Description
pythonPath Path to python binary. By default used path declared in global configuration
pythonBinary Python binary name. By default, python3 on linux and python otherwise.
modules Modules to install. In most cases configured indirectly with pip(..) task methods. By default, modules from global configuration.
userScope Use current user scope (--user flag). Enabled by default to avoid permission problems on *nix (global configuration).
showInstalledVersions Perform pip list after installation. By default use global configuration value
alwaysInstallModules Call pip install module for all declared modules, even if it is already installed with correct version. By default use global configuration value
useCache Can be used to disable pip cache (--no-cache-dir)

And, as shown above, custom methods: pip(String... modules) and pip(Iterable<String> modules)

Use as base for specific module plugin

Plugin supposed to be used as base for plugins for specific python modules. With it you don't need to implement modules installation and could use provided abstractions to call python.

Example usage: gradle-mkdocs-plugin.

In your plugin, add plugin as dependency:

dependencies {
    implementation 'ru.vyarus:gradle-use-python-plugin:2.0.0'
}

And apply plugin: project.plugins.apply(PythonPlugin) (required to register python extension and declare default pipInstall task).

Extended task

The simplest way is to extend PythonTask:

class SomeModuleTask extends PythonTask {
    
    @Override
    String getModule() {
        // always call specified commands on module
        return 'somemodule'
    }
    
    @Override
    List<String> getExtraArgs() {
        // example of module options configuration with custom extension 
        def res = []
        SomeModuleExtension ext = project.extensions.getByType(SomeModuleExtension)
        if (ext.somOption) {
            res << '--option'
        }
        return res
    }
    
    // optionally apply extra behaviour
    @Override
    void run() {
        // before python call               
        super.run()
        // after python call
    }
}

Usage:

pyton.pip 'sommemodule:1'

task modCmd(type: SomeModuleTask) {
    command = 'module args'
}

called: python -m somemodule module arfs --option

In some cases, you can use BasePythonTask which is a super class of PythonTask and provides only automatic pythonPath and pythonBinary properties set from global configuration.

Completely custom task

Plugin provides ru.vyarus.gradle.plugin.python.cmd.Python utility class, which could be used directly in custom task (PythonTask is a wrapper above the utility).

Example usage:

Python python = new Python(project, getPythonPath(), getPythonBinary())
            .logLevel(getLogLevel())
            .outputPrefix(getOutputPrefix())
            .workDir(getWorkDir())
            .extraArgs(getExtraArgs())

// execute and get command output
String out = python.readOutput(cmd)

// call module (the same as exec() but applies '-m mod' before command)
python.callModule('mod', cmd)

// direct python call
python.exec(cmd)

This could be used directly in the completely custom task.

Specific utility for target module could be defined, see ru.vyarus.gradle.plugin.python.cmd.Pip util as an example (simplified):

class Pip {

    private final Python python

    Pip(Project project, String pythonPath, String binary) {
        // configure custom python execution util 
        python = new Python(project, pythonPath, binary)
                .logLevel(LogLevel.LIFECYCLE)
    }
    
    // declare module specific commands
    
    void install(String module) {
        python.callModule('pip', "install $module")
    }
}

Apply default modules

In your plugin you could apply default modules like this:

afterEvaluate {
    PythonExtension ext = project.extensions.getByType(PythonExtension)
    // delayed default module(s) declaration based on user configuration
    if (!ext.isModuleDeclared('somemodule')) {
        ext.pip 'sommemodule:1'
    }
}

Or always declare default modules (before configuration):

PythonExtension ext = project.extensions.getByType(PythonExtension)
ext.pip 'sommeodule:1', 'othermodule:2'

User will be able to override default versions by direct module declaration (even downgrade version):

python.pip 'sommodule:0.9'

NOTE: all pip declarations are supported so direct module version could be overridden with VCS declaration and vice-versa (only the declaration order is important).

Might also like