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