Skip to content

Commit 40b1164

Browse files
authored
Drop Python 2 support (kivy#1918)
* Added build failure if run under Python 2 * Made Python 2 check more robust * Style fixes * Changed minimum python3 minor version to 3.4 * Added a deprecation warning log to the Python 2 recipe during build * Updated tox.ini to run py2 tests only on android module * Added tests for Python version checking * Added test for Python 2 running long enough to exit nicely * Pep8 and code style improvements * Added descriptions for flake8 exceptions * Hardcoded python2 tests in tox.ini * Removed E127 and E129 global disable
1 parent c261db5 commit 40b1164

File tree

12 files changed

+179
-34
lines changed

12 files changed

+179
-34
lines changed

pythonforandroid/bdistapk.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def finalize_options(self):
7575
def run(self):
7676
self.prepare_build_dir()
7777

78-
from pythonforandroid.toolchain import main
78+
from pythonforandroid.entrypoints import main
7979
sys.argv[1] = 'apk'
8080
main()
8181

pythonforandroid/entrypoints.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from pythonforandroid.recommendations import check_python_version
2+
from pythonforandroid.util import BuildInterruptingException, handle_build_exception
3+
4+
5+
def main():
6+
"""
7+
Main entrypoint for running python-for-android as a script.
8+
"""
9+
10+
try:
11+
# Check the Python version before importing anything heavier than
12+
# the util functions. This lets us provide a nice message about
13+
# incompatibility rather than having the interpreter crash if it
14+
# reaches unsupported syntax from a newer Python version.
15+
check_python_version()
16+
17+
from pythonforandroid.toolchain import ToolchainCL
18+
ToolchainCL()
19+
except BuildInterruptingException as exc:
20+
handle_build_exception(exc)

pythonforandroid/logger.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from math import log10
77
from collections import defaultdict
88
from colorama import Style as Colo_Style, Fore as Colo_Fore
9+
10+
# six import left for Python 2 compatibility during initial Python version check
911
import six
1012

1113
# This codecs change fixes a bug with log output, but crashes under python3

pythonforandroid/recipes/python2/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from os.path import join, exists
22
from pythonforandroid.recipe import Recipe
33
from pythonforandroid.python import GuestPythonRecipe
4-
from pythonforandroid.logger import shprint
4+
from pythonforandroid.logger import shprint, warning
55
import sh
66

77

@@ -57,6 +57,11 @@ def prebuild_arch(self, arch):
5757
self.apply_patch(join('patches', 'enable-openssl.patch'), arch.arch)
5858
shprint(sh.touch, patch_mark)
5959

60+
def build_arch(self, arch):
61+
warning('DEPRECATION: Support for the Python 2 recipe will be '
62+
'removed in 2020, please upgrade to Python 3.')
63+
super().build_arch(arch)
64+
6065
def set_libs_flags(self, env, arch):
6166
env = super(Python2Recipe, self).set_libs_flags(env, arch)
6267
if 'libffi' in self.ctx.recipe_build_order:

pythonforandroid/recommendations.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
"""Simple functions for checking dependency versions."""
22

3+
import sys
34
from distutils.version import LooseVersion
45
from os.path import join
6+
57
from pythonforandroid.logger import info, warning
68
from pythonforandroid.util import BuildInterruptingException
79

@@ -182,3 +184,37 @@ def check_ndk_api(ndk_api, android_api):
182184

183185
if ndk_api < MIN_NDK_API:
184186
warning(OLD_NDK_API_MESSAGE)
187+
188+
189+
MIN_PYTHON_MAJOR_VERSION = 3
190+
MIN_PYTHON_MINOR_VERSION = 4
191+
MIN_PYTHON_VERSION = LooseVersion('{major}.{minor}'.format(major=MIN_PYTHON_MAJOR_VERSION,
192+
minor=MIN_PYTHON_MINOR_VERSION))
193+
PY2_ERROR_TEXT = (
194+
'python-for-android no longer supports running under Python 2. Either upgrade to '
195+
'Python {min_version} or higher (recommended), or revert to python-for-android 2019.07.08. '
196+
'Note that you *can* still target Python 2 on Android by including python2 in your '
197+
'requirements.').format(
198+
min_version=MIN_PYTHON_VERSION)
199+
200+
PY_VERSION_ERROR_TEXT = (
201+
'Your Python version {user_major}.{user_minor} is not supported by python-for-android, '
202+
'please upgrade to {min_version} or higher.'
203+
).format(
204+
user_major=sys.version_info.major,
205+
user_minor=sys.version_info.minor,
206+
min_version=MIN_PYTHON_VERSION)
207+
208+
209+
def check_python_version():
210+
# Python 2 special cased because it's a major transition. In the
211+
# future the major or minor versions can increment more quietly.
212+
if sys.version_info.major == 2:
213+
raise BuildInterruptingException(PY2_ERROR_TEXT)
214+
215+
if (
216+
sys.version_info.major < MIN_PYTHON_MAJOR_VERSION or
217+
sys.version_info.minor < MIN_PYTHON_MINOR_VERSION
218+
):
219+
220+
raise BuildInterruptingException(PY_VERSION_ERROR_TEXT)

pythonforandroid/toolchain.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
from pythonforandroid.pythonpackage import get_dep_names_of_package
1313
from pythonforandroid.recommendations import (
1414
RECOMMENDED_NDK_API, RECOMMENDED_TARGET_API)
15-
from pythonforandroid.util import BuildInterruptingException, handle_build_exception
15+
from pythonforandroid.util import BuildInterruptingException
16+
from pythonforandroid.entrypoints import main
1617

1718

1819
def check_python_dependencies():
@@ -568,6 +569,7 @@ def add_parser(subparsers, *args, **kwargs):
568569

569570
args, unknown = parser.parse_known_args(sys.argv[1:])
570571
args.unknown_args = unknown
572+
571573
if hasattr(args, "private") and args.private is not None:
572574
# Pass this value on to the internal bootstrap build.py:
573575
args.unknown_args += ["--private", args.private]
@@ -1187,12 +1189,5 @@ def build_status(self, _args):
11871189
print(recipe_str)
11881190

11891191

1190-
def main():
1191-
try:
1192-
ToolchainCL()
1193-
except BuildInterruptingException as exc:
1194-
handle_build_exception(exc)
1195-
1196-
11971192
if __name__ == "__main__":
11981193
main()

pythonforandroid/util.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,17 @@
33
from os import getcwd, chdir, makedirs, walk, uname
44
import sh
55
import shutil
6-
import sys
76
from fnmatch import fnmatch
87
from tempfile import mkdtemp
9-
try:
8+
9+
# This Python version workaround left for compatibility during initial version check
10+
try: # Python 3
1011
from urllib.request import FancyURLopener
11-
except ImportError:
12+
except ImportError: # Python 2
1213
from urllib import FancyURLopener
1314

1415
from pythonforandroid.logger import (logger, Err_Fore, error, info)
1516

16-
IS_PY3 = sys.version_info[0] >= 3
17-
1817

1918
class WgetDownloader(FancyURLopener):
2019
version = ('Wget/1.17.1')

setup.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
data_files = []
1818

1919

20+
2021
# must be a single statement since buildozer is currently parsing it, refs:
2122
# https://github.com/kivy/buildozer/issues/722
2223
install_reqs = [
@@ -94,15 +95,15 @@ def recursively_include(results, directory, patterns):
9495
install_requires=install_reqs,
9596
entry_points={
9697
'console_scripts': [
97-
'python-for-android = pythonforandroid.toolchain:main',
98-
'p4a = pythonforandroid.toolchain:main',
98+
'python-for-android = pythonforandroid.entrypoints:main',
99+
'p4a = pythonforandroid.entrypoints:main',
99100
],
100101
'distutils.commands': [
101102
'apk = pythonforandroid.bdistapk:BdistAPK',
102103
],
103104
},
104105
classifiers = [
105-
'Development Status :: 4 - Beta',
106+
'Development Status :: 5 - Production/Stable',
106107
'Intended Audience :: Developers',
107108
'License :: OSI Approved :: MIT License',
108109
'Operating System :: Microsoft :: Windows',
@@ -111,7 +112,6 @@ def recursively_include(results, directory, patterns):
111112
'Operating System :: MacOS :: MacOS X',
112113
'Operating System :: Android',
113114
'Programming Language :: C',
114-
'Programming Language :: Python :: 2',
115115
'Programming Language :: Python :: 3',
116116
'Topic :: Software Development',
117117
'Topic :: Utilities',

tests/test_androidmodule_ctypes_finder.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11

2-
import mock
3-
from mock import MagicMock
2+
# This test is still expected to support Python 2, as it tests
3+
# on-Android functionality that we still maintain
4+
try: # Python 3+
5+
from unittest import mock
6+
from unittest.mock import MagicMock
7+
except ImportError: # Python 2
8+
import mock
9+
from mock import MagicMock
410
import os
511
import shutil
612
import sys

tests/test_entrypoints_python2.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
2+
# This test is a special case that we expect to run under Python 2, so
3+
# include the necessary compatibility imports:
4+
try: # Python 3
5+
from unittest import mock
6+
except ImportError: # Python 2
7+
import mock
8+
9+
from pythonforandroid.recommendations import PY2_ERROR_TEXT
10+
from pythonforandroid import entrypoints
11+
12+
13+
def test_main_python2():
14+
"""Test that running under Python 2 leads to the build failing, while
15+
running under a suitable version works fine.
16+
17+
Note that this test must be run *using* Python 2 to truly test
18+
that p4a can reach the Python version check before importing some
19+
Python-3-only syntax and crashing.
20+
"""
21+
22+
# Under Python 2, we should get a normal control flow exception
23+
# that is handled properly, not any other type of crash
24+
handle_exception_path = 'pythonforandroid.entrypoints.handle_build_exception'
25+
with mock.patch('sys.version_info') as fake_version_info, \
26+
mock.patch(handle_exception_path) as handle_build_exception: # noqa: E127
27+
28+
fake_version_info.major = 2
29+
fake_version_info.minor = 7
30+
31+
def check_python2_exception(exc):
32+
"""Check that the exception message is Python 2 specific."""
33+
assert exc.message == PY2_ERROR_TEXT
34+
handle_build_exception.side_effect = check_python2_exception
35+
36+
entrypoints.main()
37+
38+
handle_build_exception.assert_called_once()

tests/test_recommendations.py

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,13 @@
22
from os.path import join
33
from sys import version as py_version
44

5-
try:
6-
from unittest import mock
7-
except ImportError:
8-
# `Python 2` or lower than `Python 3.3` does not
9-
# have the `unittest.mock` module built-in
10-
import mock
5+
from unittest import mock
116
from pythonforandroid.recommendations import (
127
check_ndk_api,
138
check_ndk_version,
149
check_target_api,
1510
read_ndk_version,
11+
check_python_version,
1612
MAX_NDK_VERSION,
1713
RECOMMENDED_NDK_VERSION,
1814
RECOMMENDED_TARGET_API,
@@ -33,7 +29,12 @@
3329
OLD_NDK_API_MESSAGE,
3430
NEW_NDK_MESSAGE,
3531
OLD_API_MESSAGE,
32+
MIN_PYTHON_MAJOR_VERSION,
33+
MIN_PYTHON_MINOR_VERSION,
34+
PY2_ERROR_TEXT,
35+
PY_VERSION_ERROR_TEXT,
3636
)
37+
3738
from pythonforandroid.util import BuildInterruptingException
3839

3940
running_in_py2 = int(py_version[0]) < 3
@@ -202,3 +203,37 @@ def test_check_ndk_api_warning_old_ndk(self):
202203
)
203204
],
204205
)
206+
207+
def test_check_python_version(self):
208+
"""With any version info lower than the minimum, we should get a
209+
BuildInterruptingException with an appropriate message.
210+
"""
211+
with mock.patch('sys.version_info') as fake_version_info:
212+
213+
# Major version is Python 2 => exception
214+
fake_version_info.major = MIN_PYTHON_MAJOR_VERSION - 1
215+
fake_version_info.minor = MIN_PYTHON_MINOR_VERSION
216+
with self.assertRaises(BuildInterruptingException) as context:
217+
check_python_version()
218+
assert context.exception.message == PY2_ERROR_TEXT
219+
220+
# Major version too low => exception
221+
# Using a float valued major version just to test the logic and avoid
222+
# clashing with the Python 2 check
223+
fake_version_info.major = MIN_PYTHON_MAJOR_VERSION - 0.1
224+
fake_version_info.minor = MIN_PYTHON_MINOR_VERSION
225+
with self.assertRaises(BuildInterruptingException) as context:
226+
check_python_version()
227+
assert context.exception.message == PY_VERSION_ERROR_TEXT
228+
229+
# Minor version too low => exception
230+
fake_version_info.major = MIN_PYTHON_MAJOR_VERSION
231+
fake_version_info.minor = MIN_PYTHON_MINOR_VERSION - 1
232+
with self.assertRaises(BuildInterruptingException) as context:
233+
check_python_version()
234+
assert context.exception.message == PY_VERSION_ERROR_TEXT
235+
236+
# Version high enough => nothing interesting happens
237+
fake_version_info.major = MIN_PYTHON_MAJOR_VERSION
238+
fake_version_info.minor = MIN_PYTHON_MINOR_VERSION
239+
check_python_version()

tox.ini

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,19 @@ deps =
99
virtualenv
1010
py3: coveralls
1111
backports.tempfile
12-
# makes it possible to override pytest args, e.g.
13-
# tox -- tests/test_graph.py
12+
# posargs will be replaced by the tox args, so you can override pytest
13+
# args e.g. `tox -- tests/test_graph.py`
1414
commands = pytest {posargs:tests/}
1515
passenv = TRAVIS TRAVIS_*
1616
setenv =
1717
PYTHONPATH={toxinidir}
1818

19+
[testenv:py27]
20+
# Note that the set of tests is not posargs-configurable here: we only
21+
# check a minimal set of Python 2 tests for the remaining Python 2
22+
# functionality that we support
23+
commands = pytest tests/test_androidmodule_ctypes_finder.py tests/test_entrypoints_python2.py
24+
1925
[testenv:py3]
2026
# for py3 env we will get code coverage
2127
commands =
@@ -28,8 +34,11 @@ commands = flake8 pythonforandroid/ tests/ ci/
2834

2935
[flake8]
3036
ignore =
31-
E123, E124, E126,
32-
E226,
33-
E402, E501,
34-
W503,
35-
W504
37+
E123, # Closing bracket does not match indentation of opening bracket's line
38+
E124, # Closing bracket does not match visual indentation
39+
E126, # Continuation line over-indented for hanging indent
40+
E226, # Missing whitespace around arithmetic operator
41+
E402, # Module level import not at top of file
42+
E501, # Line too long (82 > 79 characters)
43+
W503, # Line break occurred before a binary operator
44+
W504 # Line break occurred after a binary operator

0 commit comments

Comments
 (0)