diff --git a/README.md b/README.md index 5a98c07..ffb0f01 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Requires recent pip sudo pip3 install -U pip ``` -Install globally so its always avalible +Install globally so its always available ``` sudo pip3 install git+https://github.com/timeoutdigital/timeout-tools diff --git a/requirements.in b/requirements.in index b199528..b16c08d 100644 --- a/requirements.in +++ b/requirements.in @@ -2,3 +2,4 @@ argparse invoke pip-tools pre-commit +pytest diff --git a/requirements.txt b/requirements.txt index a824f16..faa45a1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,10 +2,12 @@ # This file is autogenerated by pip-compile with Python 3.10 # by the following command: # -# pip-compile --resolver=backtracking +# pip-compile requirements.in # argparse==1.4.0 # via -r requirements.in +attrs==22.2.0 + # via pytest build==0.10.0 # via pip-tools cfgv==3.3.1 @@ -14,30 +16,40 @@ click==8.1.3 # via pip-tools distlib==0.3.6 # via virtualenv +exceptiongroup==1.1.1 + # via pytest filelock==3.10.0 # via virtualenv identify==2.5.21 # via pre-commit +iniconfig==2.0.0 + # via pytest invoke==2.0.0 # via -r requirements.in nodeenv==1.7.0 # via pre-commit packaging==23.0 - # via build + # via + # build + # pytest pip-tools==6.12.3 # via -r requirements.in platformdirs==3.1.1 # via virtualenv +pluggy==1.0.0 + # via pytest pre-commit==3.2.0 # via -r requirements.in pyproject-hooks==1.0.0 # via build +pytest==7.2.2 + # via -r requirements.in pyyaml==6.0 # via pre-commit tomli==2.0.1 # via # build - # pyproject-hooks + # pytest virtualenv==20.21.0 # via pre-commit wheel==0.40.0 diff --git a/timeout_tools/cli.py b/timeout_tools/cli.py index 2af9450..681b958 100644 --- a/timeout_tools/cli.py +++ b/timeout_tools/cli.py @@ -7,6 +7,14 @@ import sys +class PyEnvFailure(Exception): + pass + + +class PyEnvPythonNotInstalled(Exception): + pass + + def main(): parser = argparse.ArgumentParser( description='Timeout Tools', @@ -158,9 +166,24 @@ def python_setup_func(args): def python_setup(app, branch, python_version): + try: + check_python_version_installed(python_version) + except PyEnvFailure as e: + print(e.args[0]['message']) + sys.exit(1) + except PyEnvPythonNotInstalled: + try: + check_python_version_available(python_version) + try: + install_python_version(python_version) + except PyEnvFailure as e: + print(e.args[0]['message']) + sys.exit(1) + except PyEnvFailure as e: + print(e.args[0]['message']) + sys.exit(1) pyenv_name = f'{app}-{python_version}' print(f'- Creating virtualenv `{pyenv_name}`', end='', flush=True) - run(f'pyenv install -s {python_version}') ret, out = run(f'pyenv virtualenv {python_version} {pyenv_name}') if ret != 0: if 'already exists' in out: @@ -274,5 +297,65 @@ def load_python_version(ws=None): return False +def check_python_version_installed(python_version): + ### + # Check whether a version of python is already installed via pyenv + ### + py_ver_present = False + print(f'- Checking Python `{python_version}` is installed', end='', flush=True) + (status, result) = run('pyenv versions --bare --skip-aliases') + if status: + print(' ❌') + raise PyEnvFailure({"message": "Failed to run"}) + for py_ver in result.replace(' ', '').split('\n'): + if py_ver == python_version: + py_ver_present = True + break + + if not py_ver_present: + print(' ❌') + raise PyEnvPythonNotInstalled({"message": "python version not installed"}) + else: + print(' ✅') + + +def check_python_version_available(python_version): + ### + # Check whether a version of python is already is available for install by pyenv + # if not exit and tell user to update pyenv installation + ### + py_ver_available = False + print(f'- Python checking `{python_version}` is available for installation', end='', flush=True) + (status, result) = run('pyenv install --list') + if status: + print(' ❌') + raise PyEnvFailure({"message": "Failed to run"}) + for py_ver in result.replace(' ', '').split('\n'): + if py_ver == python_version: + py_ver_available = True + break + if not py_ver_available: + print(' ❌') + raise PyEnvFailure({"message": ''' + Please update pyenv with latest versions of python by running: + cd ~/.pyenv/plugins/python-build/../.. && git pull && cd - + '''}) + else: + print(' ✅') + + +def install_python_version(python_version): + ### + # Use pyenv to install new version of python + ### + print('- Python installing `{python_version}`', end='', flush=True) + (status, _) = run(f'pyenv install {python_version}') + if status: + print(' ❌') + raise PyEnvFailure(f'Failed to install python version {python_version}') + else: + print(' ✅') + + if __name__ == '__main__': main() diff --git a/timeout_tools/tests/__init__.py b/timeout_tools/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/timeout_tools/tests/test_cli.py b/timeout_tools/tests/test_cli.py new file mode 100644 index 0000000..cbce7f7 --- /dev/null +++ b/timeout_tools/tests/test_cli.py @@ -0,0 +1,138 @@ +from unittest import mock + +import pytest + +import timeout_tools +from timeout_tools.cli import (PyEnvFailure, PyEnvPythonNotInstalled, + check_python_version_available, + check_python_version_installed, + install_python_version) + + +def test_check_python_version_installed_success(): + mock_run = mock.Mock() + mock_run.return_value = ( + 0, + ' 3.7.9\n 3.10.10\n 3.10.11' + ) + + timeout_tools.cli.run = mock_run + + python_version = '3.10.10' + try: + check_python_version_installed(python_version) + except Exception: + pytest.fail(f'Error checking version installed {python_version}') + + mock_run.assert_called_with('pyenv versions --bare --skip-aliases') + + +def test_check_python_version_installed_cmd_exception(): + + mock_run = mock.Mock() + mock_run.return_value = (1, "") + + with mock.patch('timeout_tools.cli.run', mock_run): + python_version = '3.10.10' + with pytest.raises(PyEnvFailure) as e: + check_python_version_installed(python_version) + assert e.value.args[0]['message'] == 'Failed to run' + + mock_run.assert_called_with('pyenv versions --bare --skip-aliases') + + +def test_check_python_version_installed_version_missing_exception(): + + mock_run = mock.Mock() + mock_run.return_value = ( + 0, + ' 3.7.9\n 3.10.10\n 3.10.11' + ) + + with mock.patch('timeout_tools.cli.run', mock_run): + python_version = '0.0.0' + with pytest.raises(PyEnvPythonNotInstalled) as e: + check_python_version_installed(python_version) + assert e.value.args[0]['message'] == 'python version not installed' + + mock_run.assert_called_with('pyenv versions --bare --skip-aliases') + + +def test_check_python_version_available_success(): + mock_run = mock.Mock() + mock_run.return_value = ( + 0, + ' 3.7.9\n 3.10.10\n 3.10.11' + ) + + with mock.patch('timeout_tools.cli.run', mock_run): + python_version = '3.10.10' + try: + check_python_version_available(python_version) + except Exception: + pytest.fail(f'Error checking {python_version}') + + mock_run.assert_called_with('pyenv install --list') + + +def test_check_python_version_available_cmd_exception(): + + mock_run = mock.Mock() + mock_run.return_value = (1, "") + + with mock.patch('timeout_tools.cli.run', mock_run): + python_version = '3.10.10' + with pytest.raises(PyEnvFailure) as e: + check_python_version_available(python_version) + assert e.value.args[0]['message'] == 'Failed to run' + + mock_run.assert_called_with('pyenv install --list') + + +def test_check_python_version_available_version_missing_exception(): + + mock_run = mock.Mock() + mock_run.return_value = ( + 0, + ' 3.7.9\n 3.10.10\n 3.10.11' + ) + + with mock.patch('timeout_tools.cli.run', mock_run): + python_version = '0.0.0' + with pytest.raises(PyEnvFailure) as e: + check_python_version_available(python_version) + assert e.value.args[0]['message'] == ''' + Please update pyenv with latest versions of python by running: + cd ~/.pyenv/plugins/python-build/../.. && git pull && cd - + ''' + + mock_run.assert_called_with('pyenv install --list') + + +def test_install_python_version_success(): + + mock_run = mock.Mock() + mock_run.return_value = (0, "") + + with mock.patch('timeout_tools.cli.run', mock_run): + python_version = '3.10.10' + try: + install_python_version(python_version) + except Exception: + pytest.fail(f'Error installing {python_version}') + + mock_run.assert_called_with(f'pyenv install {python_version}') + + +def test_install_python_version_exception(): + + mock_run = mock.Mock() + mock_run.return_value = (1, "") + + with mock.patch('timeout_tools.cli.run', mock_run): + python_version = '0.0.0' + with pytest.raises(BaseException) as e: + install_python_version(python_version) + assert str(e.value) == f'Failed to install python version {python_version}' + + mock_run.assert_called_with(f'pyenv install {python_version}')