diff --git a/poetry/console/config/application_config.py b/poetry/console/config/application_config.py index e072dd18cd9..57706167151 100644 --- a/poetry/console/config/application_config.py +++ b/poetry/console/config/application_config.py @@ -68,23 +68,6 @@ def set_env(self, event, event_name, _): # type: (PreHandleEvent, str, _) -> No poetry = command.poetry env_manager = EnvManager(poetry) - - # Checking compatibility of the current environment with - # the python dependency specified in pyproject.toml - current_env = env_manager.get() - supported_python = poetry.package.python_constraint - current_python = parse_constraint( - ".".join(str(v) for v in current_env.version_info[:3]) - ) - - if not supported_python.allows(current_python): - raise RuntimeError( - "The current Python version ({}) is not supported by the project ({})\n" - "Please activate a compatible Python version.".format( - current_python, poetry.package.python_versions - ) - ) - env = env_manager.create_venv(io) if env.is_venv() and io.is_verbose(): diff --git a/poetry/packages/package.py b/poetry/packages/package.py index db54b504c3e..562788a2de4 100644 --- a/poetry/packages/package.py +++ b/poetry/packages/package.py @@ -27,7 +27,7 @@ class Package(object): - AVAILABLE_PYTHONS = {"2", "2.7", "3", "3.4", "3.5", "3.6", "3.7"} + AVAILABLE_PYTHONS = {"2", "2.7", "3", "3.4", "3.5", "3.6", "3.7", "3.8"} def __init__(self, name, version, pretty_version=None): """ diff --git a/poetry/utils/env.py b/poetry/utils/env.py index bdfac75e6d6..121bbef32c3 100644 --- a/poetry/utils/env.py +++ b/poetry/utils/env.py @@ -22,6 +22,7 @@ from poetry.locations import CACHE_DIR from poetry.poetry import Poetry +from poetry.semver import parse_constraint from poetry.semver.version import Version from poetry.utils._compat import CalledProcessError from poetry.utils._compat import Path @@ -127,6 +128,26 @@ def __init__(self, e, input=None): # type: (CalledProcessError) -> None super(EnvCommandError, self).__init__(message) +class NoCompatiblePythonVersionFound(EnvError): + def __init__(self, expected, given=None): + if given: + message = ( + "The specified Python version ({}) " + "is not supported by the project ({}).\n" + "Please choose a compatible version " + "or loosen the python constraint specified " + "in the pyproject.toml file.".format(given, expected) + ) + else: + message = ( + "Poetry was unable to find a compatible version. " + "If you have one, you can explicitly use it " + 'via the "env use" command.' + ) + + super(NoCompatiblePythonVersionFound, self).__init__(message) + + class EnvManager(object): """ Environments manager @@ -439,6 +460,7 @@ def create_venv( if not name: name = self._poetry.package.name + python_patch = ".".join([str(v) for v in sys.version_info[:3]]) python_minor = ".".join([str(v) for v in sys.version_info[:2]]) if executable: python_minor = decode( @@ -451,9 +473,84 @@ def create_venv( ] ), shell=True, + ).strip() + ) + + supported_python = self._poetry.package.python_constraint + if not supported_python.allows(Version.parse(python_minor)): + # The currently activated or chosen Python version + # is not compatible with the Python constraint specified + # for the project. + # If an executable has been specified, we stop there + # and notify the user of the incompatibility. + # Otherwise, we try to find a compatible Python version. + if executable: + raise NoCompatiblePythonVersionFound( + self._poetry.package.python_versions, python_minor + ) + + io.write_line( + "The currently activated Python version {} " + "is not supported by the project ({}).\n" + "Trying to find and use a compatible version. ".format( + python_patch, self._poetry.package.python_versions ) ) + for python_to_try in reversed( + sorted( + self._poetry.package.AVAILABLE_PYTHONS, + key=lambda v: (v.startswith("3"), -len(v), v), + ) + ): + if len(python_to_try) == 1: + if not parse_constraint("^{}.0".format(python_to_try)).allows_any( + supported_python + ): + continue + elif not supported_python.allows_all( + parse_constraint(python_to_try + ".*") + ): + continue + + python = "python" + python_to_try + + if io.is_debug(): + io.write_line("Trying {}".format(python)) + + try: + python_patch = decode( + subprocess.check_output( + " ".join( + [ + python, + "-c", + "\"import sys; print('.'.join([str(s) for s in sys.version_info[:3]]))\"", + ] + ), + stderr=subprocess.STDOUT, + shell=True, + ).strip() + ) + except CalledProcessError: + continue + + if not python_patch: + continue + + if supported_python.allows(Version.parse(python_patch)): + io.write_line( + "Using {} ({})".format(python, python_patch) + ) + executable = python + python_minor = ".".join(python_patch.split(".")[:2]) + break + + if not executable: + raise NoCompatiblePythonVersionFound( + self._poetry.package.python_versions + ) + if root_venv: venv = venv_path else: diff --git a/poetry/utils/helpers.py b/poetry/utils/helpers.py index 1e7bc660424..ff2e9f68586 100644 --- a/poetry/utils/helpers.py +++ b/poetry/utils/helpers.py @@ -1,10 +1,14 @@ -import collections import os import re import shutil import stat import tempfile +try: + from collections.abc import Mapping +except ImportError: + from collections import Mapping + from contextlib import contextmanager from typing import List from typing import Optional @@ -144,11 +148,7 @@ def safe_rmtree(path): def merge_dicts(d1, d2): for k, v in d2.items(): - if ( - k in d1 - and isinstance(d1[k], dict) - and isinstance(d2[k], collections.Mapping) - ): + if k in d1 and isinstance(d1[k], dict) and isinstance(d2[k], Mapping): merge_dicts(d1[k], d2[k]) else: d1[k] = d2[k] diff --git a/tests/masonry/builders/test_builder.py b/tests/masonry/builders/test_builder.py index b393022c3ab..dc59e254dff 100644 --- a/tests/masonry/builders/test_builder.py +++ b/tests/masonry/builders/test_builder.py @@ -88,6 +88,7 @@ def test_get_metadata_content(): "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries :: Python Modules", ] diff --git a/tests/masonry/builders/test_complete.py b/tests/masonry/builders/test_complete.py index 699ad3e5bd1..e10fa08a6d7 100644 --- a/tests/masonry/builders/test_complete.py +++ b/tests/masonry/builders/test_complete.py @@ -216,6 +216,7 @@ def test_complete(): Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 Classifier: Topic :: Software Development :: Build Tools Classifier: Topic :: Software Development :: Libraries :: Python Modules Provides-Extra: time @@ -318,6 +319,7 @@ def test_complete_no_vcs(): Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 Classifier: Topic :: Software Development :: Build Tools Classifier: Topic :: Software Development :: Libraries :: Python Modules Provides-Extra: time diff --git a/tests/masonry/test_api.py b/tests/masonry/test_api.py index ddbecb0e258..c2d0f48832e 100644 --- a/tests/masonry/test_api.py +++ b/tests/masonry/test_api.py @@ -94,6 +94,7 @@ def test_prepare_metadata_for_build_wheel(): Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 Classifier: Topic :: Software Development :: Build Tools Classifier: Topic :: Software Development :: Libraries :: Python Modules Provides-Extra: time diff --git a/tests/test_factory.py b/tests/test_factory.py index ae63054b08e..9faa360c269 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -107,6 +107,7 @@ def test_create_poetry(): "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries :: Python Modules", ] diff --git a/tests/utils/test_env.py b/tests/utils/test_env.py index 3c25ecaae72..4cc8dcfdc7c 100644 --- a/tests/utils/test_env.py +++ b/tests/utils/test_env.py @@ -10,6 +10,7 @@ from poetry.utils._compat import Path from poetry.utils.env import EnvManager from poetry.utils.env import EnvCommandError +from poetry.utils.env import NoCompatiblePythonVersionFound from poetry.utils.env import VirtualEnv from poetry.utils.toml_file import TomlFile @@ -554,3 +555,106 @@ def test_run_with_input_non_zero_return(tmp_dir, tmp_venv): tmp_venv.run("python", "-", input_=ERRORING_SCRIPT) assert processError.value.e.returncode == 1 + + +def test_create_venv_tries_to_find_a_compatible_python_executable_using_generic_ones_first( + manager, poetry, config, mocker +): + if "VIRTUAL_ENV" in os.environ: + del os.environ["VIRTUAL_ENV"] + + poetry.package.python_versions = "^3.6" + venv_name = manager.generate_env_name("simple-project", str(poetry.file.parent)) + + mocker.patch("sys.version_info", (2, 7, 16)) + mocker.patch( + "poetry.utils._compat.subprocess.check_output", + side_effect=check_output_wrapper(Version.parse("3.7.5")), + ) + m = mocker.patch( + "poetry.utils.env.EnvManager.build_venv", side_effect=lambda *args, **kwargs: "" + ) + + manager.create_venv(NullIO()) + + m.assert_called_with( + "/foo/virtualenvs/{}-py3.7".format(venv_name), executable="python3" + ) + + +def test_create_venv_tries_to_find_a_compatible_python_executable_using_specific_ones( + manager, poetry, config, mocker +): + if "VIRTUAL_ENV" in os.environ: + del os.environ["VIRTUAL_ENV"] + + poetry.package.python_versions = "^3.6" + venv_name = manager.generate_env_name("simple-project", str(poetry.file.parent)) + + mocker.patch("sys.version_info", (2, 7, 16)) + mocker.patch( + "poetry.utils._compat.subprocess.check_output", side_effect=["3.5.3", "3.8.0"] + ) + m = mocker.patch( + "poetry.utils.env.EnvManager.build_venv", side_effect=lambda *args, **kwargs: "" + ) + + manager.create_venv(NullIO()) + + m.assert_called_with( + "/foo/virtualenvs/{}-py3.8".format(venv_name), executable="python3.8" + ) + + +def test_create_venv_fails_if_no_compatible_python_version_could_be_found( + manager, poetry, config, mocker +): + if "VIRTUAL_ENV" in os.environ: + del os.environ["VIRTUAL_ENV"] + + poetry.package.python_versions = "^4.8" + + mocker.patch( + "poetry.utils._compat.subprocess.check_output", side_effect=["", "", "", ""] + ) + m = mocker.patch( + "poetry.utils.env.EnvManager.build_venv", side_effect=lambda *args, **kwargs: "" + ) + + with pytest.raises(NoCompatiblePythonVersionFound) as e: + manager.create_venv(NullIO()) + + expected_message = ( + "Poetry was unable to find a compatible version. " + "If you have one, you can explicitly use it " + 'via the "env use" command.' + ) + + assert expected_message == str(e.value) + assert 0 == m.call_count + + +def test_create_venv_does_not_try_to_find_compatible_versions_with_executable( + manager, poetry, config, mocker +): + if "VIRTUAL_ENV" in os.environ: + del os.environ["VIRTUAL_ENV"] + + poetry.package.python_versions = "^4.8" + + mocker.patch("poetry.utils._compat.subprocess.check_output", side_effect=["3.8.0"]) + m = mocker.patch( + "poetry.utils.env.EnvManager.build_venv", side_effect=lambda *args, **kwargs: "" + ) + + with pytest.raises(NoCompatiblePythonVersionFound) as e: + manager.create_venv(NullIO(), executable="3.8") + + expected_message = ( + "The specified Python version (3.8.0) is not supported by the project (^4.8).\n" + "Please choose a compatible version or loosen the python constraint " + "specified in the pyproject.toml file." + ) + + assert expected_message == str(e.value) + assert 0 == m.call_count