├── .travis.yml ├── Dockerfile ├── Dockerfile.centos ├── README.md ├── get-python.sh └── tests.py /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | services: 4 | - docker 5 | 6 | env: 7 | - DOCKERFILE=Dockerfile 8 | - DOCKERFILE=Dockerfile.centos 9 | 10 | before_install: 11 | - docker build -t ikalnitsky/pythonista -f $DOCKERFILE . 12 | 13 | script: 14 | - docker run -v `pwd`:/src ikalnitsky/pythonista python2.7 /src/tests.py 15 | 16 | notifications: 17 | email: false 18 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ####################################################################### 2 | # Dockerfile for building Pythonista docker image. 3 | # 4 | # Copyright (c) 2014, Igor Kalnitsky 5 | # Licensed under 3-clause BSD 6 | # 7 | # See https://github.com/ikalnitsky/pythonista for details. 8 | ####################################################################### 9 | 10 | FROM debian:stretch 11 | MAINTAINER Igor Kalnitsky 12 | 13 | ADD get-python.sh /var/tmp/get-python.sh 14 | 15 | # Install CPython interpreters. 16 | # 17 | # NOTE: We're going to install 2.7 and 3.5 first because we want to 18 | # make more rational defaults for pip and python. 19 | RUN ["/bin/bash", "/var/tmp/get-python.sh", \ 20 | "2.7.12", \ 21 | "3.5.2", \ 22 | "3.3.6", \ 23 | "3.4.5", \ 24 | "pypy-portable-5.4.1", \ 25 | "pypy3-portable-2.4"] 26 | 27 | # Unfortunately, there's no way (at least I don't know one) to 28 | # change PATH variable inside container. The only way I know is 29 | # to setup it directly in Dockerfile, but it's awful when the 30 | # PATH should be changed dynamically based on some third-party 31 | # input. But I have no choice... :( 32 | # 33 | # Also, I use one ENV statement in order to avoid creation of 34 | # unneccessary extra Docker layers. 35 | ENV PATH /opt/python/2.7.12/bin\ 36 | :/opt/python/3.5.2/bin\ 37 | :/opt/python/3.3.6/bin\ 38 | :/opt/python/3.4.5/bin\ 39 | :/opt/python/pypy-portable-5.4.1/bin\ 40 | :/opt/python/pypy3-portable-2.4/bin\ 41 | :$PATH 42 | 43 | # Install first-class tools 44 | RUN pip install tox virtualenv 45 | -------------------------------------------------------------------------------- /Dockerfile.centos: -------------------------------------------------------------------------------- 1 | ####################################################################### 2 | # Dockerfile for building Pythonista docker image based on CentOS. 3 | # 4 | # Copyright (c) 2015, Ruslan Kiyanchuk 5 | # Licensed under 3-clause BSD 6 | # 7 | # See https://github.com/ikalnitsky/pythonista for details. 8 | ####################################################################### 9 | 10 | FROM centos:7 11 | MAINTAINER Ruslan Kiianchuk 12 | 13 | ADD get-python.sh /var/tmp/get-python.sh 14 | 15 | # Install CPython interpreters. 16 | # 17 | # NOTE: We're going to install 2.7 and 3.5 first because we want to 18 | # make more rational defaults for pip and python. 19 | RUN ["/bin/bash", "/var/tmp/get-python.sh", \ 20 | "2.7.12", \ 21 | "3.5.2", \ 22 | "3.3.6", \ 23 | "3.4.5", \ 24 | "pypy-portable-5.4.1", \ 25 | "pypy3-portable-2.4"] 26 | 27 | # Unfortunately, there's no way (at least I don't know one) to 28 | # change PATH variable inside container. The only way I know is 29 | # to setup it directly in Dockerfile, but it's awful when the 30 | # PATH should be changed dynamically based on some third-party 31 | # input. But I have no choice... :( 32 | # 33 | # Also, I use one ENV statement in order to avoid creation of 34 | # unneccessary extra Docker layers. 35 | ENV PATH /opt/python/2.7.12/bin\ 36 | :/opt/python/3.5.2/bin\ 37 | :/opt/python/3.3.6/bin\ 38 | :/opt/python/3.4.5/bin\ 39 | :/opt/python/pypy-portable-5.4.1/bin\ 40 | :/opt/python/pypy3-portable-2.4/bin\ 41 | :$PATH 42 | 43 | # Install first-class tools 44 | RUN pip install tox virtualenv 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Pythonista - the Docker image 2 | ============================= 3 | 4 | Pythonista is a [Docker] image for real Python developers. That means 5 | you can find here most popular Python interpreters and test your 6 | application in all of them. The idea for the image was born when I needed 7 | to test some code in Python 2.6 environment and my workstation couldn't 8 | handle it, since Debian Jessie doesn't have Python 2.6 in its repo. 9 | 10 | [Docker]: https://docker.com/ 11 | 12 | 13 | About Image 14 | ----------- 15 | 16 | * **OS** - Debian 9 Stretch (testing) or CentOS 7 17 | * **CPython** - 2.7.12 / 3.3.6 / 3.4.5 / 3.5.2 18 | * **PyPy** - PyPy 5.4.1 (based on 2.7.10) / PyPy3 2.4.0 (based on 3.2.5) 19 | 20 | 21 | How To Use? 22 | ----------- 23 | 24 | Pythonista image is available on [Docker Hub], so you can easily `pull` 25 | it by means `docker` client: 26 | 27 | $ [sudo] docker pull ikalnitsky/pythonista 28 | 29 | If you want to enter a bash session, just do it how you did it for 30 | another containers: 31 | 32 | $ [sudo] docker run -t -i ikalnitsky/pythonista bash 33 | 34 | and enjoy `bash` session within container. 35 | 36 | [Docker Hub]: https://hub.docker.com/ 37 | 38 | 39 | Unit Tests 40 | ---------- 41 | 42 | It's very convenient to run unit tests inside Pythonista container because 43 | you can run it using different Python interpreters. For example, with [tox] 44 | the command might look like: 45 | 46 | $ [sudo] docker run -v /path/to/src/:/src -w /src ikalnitsky/pythonista tox 47 | 48 | [tox]: https://tox.readthedocs.org/ 49 | 50 | 51 | Build the image locally 52 | ----------------------- 53 | 54 | You are also welcome to build and tweak the testing image locally using 55 | provided Dockerfiles. 56 | 57 | To build Pythonista Docker image (by default based on **Debian**): 58 | 59 | $ [sudo] docker build -t pythonista-deb . 60 | 61 | To build Pythonista Docker image based on **CentOS** use `Dockerfile.centos`: 62 | 63 | $ [sudo] docker build -f Dockerfile.centos -t pythonista-yum . 64 | 65 | 66 | Feedback 67 | -------- 68 | 69 | It's important for me to get user's feedback, so please don't hesitate 70 | to suggest improvements or report bugs via [GitHub Issue]. 71 | 72 | [GitHub Issue]: https://github.com/ikalnitsky/pythonista/issues 73 | -------------------------------------------------------------------------------- /get-python.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # The script is designed to help you download, compile and install 4 | # a given Python interpreter into your system. Sometimes you need 5 | # to test your software in old Python envs and you can't fine it 6 | # in your repo. In that case you have no choice except compile 7 | # Python from the sources. 8 | # 9 | # Usage: 10 | # 11 | # $ bash ./get-python.sh ... 12 | # 13 | # Note: the script has a lot of abstract functions because in the 14 | # future it may be easier to port it to RH-based systems. 15 | # 16 | # Copyright (c) 2014, Igor Kalnitsky 17 | # Licensed under 3-clause BSD. 18 | 19 | 20 | # Get distribution name to distinguish between `debian`, `centos`, etc. 21 | OSNAME=$(cat /etc/*-release | grep ^ID= | cut -d'=' -f 2 | tr -d '"') 22 | 23 | # 24 | # Define distribution specific package requirements. 25 | # 26 | case ${OSNAME} in 27 | 28 | "debian") 29 | REQUIREMENTS=( 30 | "libsqlite3-0" 31 | "libssl1.0.2" 32 | "libexpat1" 33 | "libffi6" 34 | ) 35 | 36 | BUILD_REQUIREMENTS=( 37 | "wget" 38 | "build-essential" 39 | "libsqlite3-dev" 40 | "libreadline-dev" 41 | "libssl-dev" 42 | "zlib1g-dev" 43 | "libbz2-dev" 44 | "libncurses5-dev" 45 | "libffi-dev" 46 | "libexpat-dev" 47 | ) 48 | ;; 49 | 50 | "centos") 51 | REQUIREMENTS=( 52 | "sqlite" 53 | "openssl-libs" 54 | "expat" 55 | "libffi" 56 | "python-pip" 57 | ) 58 | 59 | BUILD_REQUIREMENTS=( 60 | "wget" 61 | "sqlite-devel" 62 | "readline-devel" 63 | "openssl-devel" 64 | "zlib-devel" 65 | "bzip2-devel" 66 | "ncurses-devel" 67 | "libffi-devel" 68 | "expat-devel" 69 | ) 70 | ;; 71 | *) 72 | echo "ERROR: Unsupported OS type: ${OSNAME}" 73 | exit 1 74 | ;; 75 | esac 76 | 77 | # 78 | # A sorf of entry point - download, compile and install given Pythons. 79 | # 80 | function main { 81 | install_packages "${REQUIREMENTS[@]}" || exit 1 82 | install_packages "${BUILD_REQUIREMENTS[@]}" || exit 1 83 | install_pyenv || exit 1 84 | 85 | for pyversion in "${@}"; do 86 | install_python "$pyversion" || exit 1 87 | done 88 | 89 | remove_packages "${BUILD_REQUIREMENTS[@]}" 90 | clean_packages 91 | } 92 | 93 | 94 | # 95 | # Install a given packages to the system. 96 | # 97 | # $@ - array of packages to be installed 98 | # 99 | function install_packages { 100 | case ${OSNAME} in 101 | "debian") 102 | apt-get update 103 | apt-get -y install "${@}" 104 | ;; 105 | "centos") 106 | yum upgrade -y 107 | yum -y install epel-release 108 | yum -y groupinstall development 109 | yum -y install "${@}" 110 | ;; 111 | esac 112 | } 113 | 114 | 115 | # 116 | # Install and configure the PyEnv tool. 117 | # 118 | function install_pyenv { 119 | wget https://github.com/yyuu/pyenv/archive/master.tar.gz 120 | tar -xzf master.tar.gz 121 | bash pyenv-master/plugins/python-build/install.sh 122 | rm -rf pyenv-master/ master.tar.gz 123 | } 124 | 125 | 126 | # 127 | # Install a given Python interpreter version. 128 | # 129 | # $1 - a Python interpreter version 130 | # 131 | function install_python { 132 | echo "export PATH=\"\$PATH:/opt/python/$1/bin/\"" >> /etc/profile.d/pythonista.sh 133 | PYTHON_CONFIGURE_OPTS="--enable-shared" CFLAGS="-g -O2" python-build "$1" "/opt/python/$1" 134 | } 135 | 136 | # 137 | # Remove given packages from the system. 138 | # 139 | # $@ - array of packages to be removed 140 | # 141 | function remove_packages { 142 | case ${OSNAME} in 143 | "debian") 144 | apt-get -y purge --auto-remove "${@}" 145 | apt-get -y autoremove 146 | ;; 147 | "centos") 148 | yum -y erase "${@}" 149 | yum -y autoremove 150 | ;; 151 | esac 152 | } 153 | 154 | # 155 | # Remove unused packages. 156 | # 157 | function clean_packages { 158 | case ${OSNAME} in 159 | "debian") 160 | apt-get autoclean 161 | apt-get clean 162 | rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 163 | ;; 164 | "centos") 165 | yum -y clean all 166 | ;; 167 | esac 168 | } 169 | 170 | 171 | main "${@}" 172 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | """ 5 | Test suite for the Pythonista Docker image 6 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 7 | 8 | Run the following command inside Pythonista container in order to 9 | check everything inside works as expected: 10 | 11 | $ python path/to/tests.py 12 | 13 | Please note that test cases MUST be changed each time the Python 14 | interpreter version is changed. 15 | 16 | :copyright: (c) 2015 Igor Kalnitsky 17 | :license: 3-clause BSD 18 | """ 19 | 20 | from __future__ import unicode_literals 21 | 22 | import re 23 | import unittest 24 | import subprocess 25 | 26 | 27 | class TestPyVersions(unittest.TestCase): 28 | """ 29 | Ensure that system has expected Python interpreters and proper defaults. 30 | 31 | Just run consequently supported Python interpreters, print their version 32 | and check that they are expected. Test fails if one of interpreters is 33 | missed or has unexpected minor version. 34 | """ 35 | 36 | _py_ver = 'import sys; sys.stdout.write("%d.%d.%d" % sys.version_info[:3])' 37 | _pypy_ver = re.compile('PyPy\s*(\d+\.\d+\.\d+)') 38 | 39 | def _get_py_version(self, py_interpreter): 40 | stdout = subprocess.check_output( 41 | "{0} -c '{1}'".format(py_interpreter, self._py_ver), 42 | shell=True) 43 | return stdout.decode('ascii') 44 | 45 | def _get_pypy_version(self, py_interpreter): 46 | # --version prints to stderr, so we need to redirect it to stdout 47 | stdout = subprocess.check_output( 48 | "{0} --version".format(py_interpreter), 49 | stderr=subprocess.STDOUT, 50 | shell=True) 51 | 52 | return self._pypy_ver.search(stdout).group(1) 53 | 54 | def test_py27(self): 55 | self.assertEqual('2.7.12', self._get_py_version('python2.7')) 56 | 57 | def test_py33(self): 58 | self.assertEqual('3.3.6', self._get_py_version('python3.3')) 59 | 60 | def test_py34(self): 61 | self.assertEqual('3.4.5', self._get_py_version('python3.4')) 62 | 63 | def test_py35(self): 64 | self.assertEqual('3.5.2', self._get_py_version('python3.5')) 65 | 66 | def test_pypy(self): 67 | self.assertEqual('5.4.1', self._get_pypy_version('pypy')) 68 | self.assertEqual('2.7.10', self._get_py_version('pypy')) 69 | 70 | def test_pypy3(self): 71 | self.assertEqual('2.4.0', self._get_pypy_version('pypy3')) 72 | self.assertEqual('3.2.5', self._get_py_version('pypy3')) 73 | 74 | def test_py2_default(self): 75 | self.assertEqual('2.7.12', self._get_py_version('python2')) 76 | 77 | def test_py3_default(self): 78 | self.assertEqual('3.5.2', self._get_py_version('python3')) 79 | 80 | 81 | class _TestModuleSupport(object): 82 | """ 83 | A base class for testing module support in Python interpreters. 84 | 85 | In order to test that Python interpreter is built with some module 86 | support just inherit this class and override its properties. 87 | """ 88 | 89 | py_code = None 90 | 91 | def _execute_code(self, py_interpreter): 92 | retcode = subprocess.call( 93 | "{0} -c '{1}'".format(py_interpreter, self.py_code), 94 | shell=True) 95 | return retcode 96 | 97 | def test_py27(self): 98 | self.assertEqual(0, self._execute_code('python2.7')) 99 | 100 | def test_py33(self): 101 | self.assertEqual(0, self._execute_code('python3.3')) 102 | 103 | def test_py34(self): 104 | self.assertEqual(0, self._execute_code('python3.4')) 105 | 106 | def test_py35(self): 107 | self.assertEqual(0, self._execute_code('python3.5')) 108 | 109 | def test_pypy(self): 110 | self.assertEqual(0, self._execute_code('pypy')) 111 | 112 | def test_pypy3(self): 113 | self.assertEqual(0, self._execute_code('pypy3')) 114 | 115 | 116 | class TestSqlite3Support(_TestModuleSupport, unittest.TestCase): 117 | """ 118 | Check that Python interpreters are built with sqlite3 support. 119 | 120 | It requires to have libsqlite3-dev installed to build CPython with 121 | sqlite3 support. 122 | """ 123 | 124 | py_code = 'import sqlite3' 125 | 126 | 127 | class TestZlibSupport(_TestModuleSupport, unittest.TestCase): 128 | """ 129 | Check that Python interpreters are built with zlib1 support. 130 | 131 | It requires to have zlib1g-dev installed to build CPython with 132 | zlib/gzip support. 133 | """ 134 | 135 | py_code = 'import gzip; import zlib' 136 | 137 | 138 | if __name__ == '__main__': 139 | unittest.main() 140 | --------------------------------------------------------------------------------