-
-
Notifications
You must be signed in to change notification settings - Fork 146
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Resolve to interpreter 'python' in PATH if '--version' fits #224
Changes from all commits
0dcd1d9
1ce4c7b
a2b37a4
f1e93c6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -91,6 +91,39 @@ def locate_via_py(version): | |
return None | ||
|
||
|
||
def locate_using_path_and_version(version): | ||
"""Check the PATH's python interpreter and return it if the version | ||
matches. | ||
|
||
On systems without version-named interpreters and with missing | ||
launcher (which is on all Windows Anaconda installations), | ||
we search the PATH for a plain "python" interpreter and accept it | ||
if its --version matches the specified interpreter version. | ||
|
||
Args: | ||
version (str): The desired Python version. Of the form ``X.Y``. | ||
|
||
Returns: | ||
Optional[str]: The full executable path for the Python ``version``, | ||
if it is found. | ||
""" | ||
if not version: | ||
return None | ||
|
||
script = "import platform; print(platform.python_version())" | ||
path_python = py.path.local.sysfind("python") | ||
if path_python: | ||
try: | ||
prefix = "{}.".format(version) | ||
version_string = path_python.sysexec("-c", script).strip() | ||
if version_string.startswith(prefix): | ||
return str(path_python) | ||
except py.process.cmdexec.Error: | ||
return None | ||
|
||
return None | ||
|
||
|
||
def _clean_location(self): | ||
"""Deletes any existing path-based environment""" | ||
if os.path.exists(self.location): | ||
|
@@ -256,11 +289,15 @@ def _resolved_interpreter(self): | |
xy_version = cleaned_interpreter | ||
|
||
path_from_launcher = locate_via_py(xy_version) | ||
|
||
if path_from_launcher: | ||
self._resolved = path_from_launcher | ||
return self._resolved | ||
|
||
path_from_version_param = locate_using_path_and_version(xy_version) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any reason not to do this before the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't want to put the python-in-path-check before the Windows check, because, it being a stop-gap solution, then it would precede the python-launcher-check, which is preferable if it exists. (py can find different python versions, if present, while python-in-path can only check whether the python in the path has the correct version. By putting our python-in-path-check above the py-check we would forfeit the possibility of finding different python versions if any python is in the Personally, if something needs to be changed about this, I'd rather just remove the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That may actually be OK, but I'd rather do it in a separate PR. We could do that one in parallel with this one? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good, left it as it is for now in this PR. Before starting another PR, I'll be sorta-kinda waiting first what else might pop up in this PR. 😃 |
||
if path_from_version_param: | ||
self._resolved = path_from_version_param | ||
return self._resolved | ||
|
||
# If we got this far, then we were unable to resolve the interpreter | ||
# to an actual executable; raise an exception. | ||
self._resolved = InterpreterNotFound(self.interpreter) | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -25,6 +25,7 @@ | |||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
IS_WINDOWS = nox.virtualenv._SYSTEM == "Windows" | ||||||||||||||||||||||||||||||||||||||||||||
HAS_CONDA = shutil.which("conda") is not None | ||||||||||||||||||||||||||||||||||||||||||||
RAISE_ERROR = "RAISE_ERROR" | ||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Very srs bznz! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. I'm exceptionally proud of this line. :) Well, I at least hope it's more explicit than some fake path string like |
||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
@pytest.fixture | ||||||||||||||||||||||||||||||||||||||||||||
|
@@ -47,6 +48,47 @@ def factory(*args, **kwargs): | |||||||||||||||||||||||||||||||||||||||||||
return factory | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
@pytest.fixture | ||||||||||||||||||||||||||||||||||||||||||||
def make_mocked_interpreter_path(): | ||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion 1/2 as discussed in #224 (comment)
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||
def factory(path, sysexec_result): | ||||||||||||||||||||||||||||||||||||||||||||
def mock_sysexec(*_): | ||||||||||||||||||||||||||||||||||||||||||||
if sysexec_result == RAISE_ERROR: | ||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. First of all, thank you for re-factoring these tests at my request. I know that can be a bit annoying after you've written some useful unit tests. The core of my re-factoring request was that I was hoping we could ditch There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not annoying at all! It's an interesting challenge, and, yes, there were absolutely too many But thanks to your review, the number of I am aware that each fixture creates a new potential source for bugs, but for me, the EDIT: To mitigate, I added some documentation to the fixtures, see my two suggestions below. WDYT? |
||||||||||||||||||||||||||||||||||||||||||||
raise py.process.cmdexec.Error(1, 1, "", "", "") | ||||||||||||||||||||||||||||||||||||||||||||
else: | ||||||||||||||||||||||||||||||||||||||||||||
return sysexec_result | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
attrs = { | ||||||||||||||||||||||||||||||||||||||||||||
"sysexec.side_effect": mock_sysexec, | ||||||||||||||||||||||||||||||||||||||||||||
"__str__": mock.Mock(return_value=path), | ||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||
mock_python = mock.Mock() | ||||||||||||||||||||||||||||||||||||||||||||
mock_python.configure_mock(**attrs) | ||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ditto here, is there some reason we can't use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I understood that a plain |
||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
return mock_python | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
return factory | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
@pytest.fixture | ||||||||||||||||||||||||||||||||||||||||||||
def patch_sysfind(make_mocked_interpreter_path): | ||||||||||||||||||||||||||||||||||||||||||||
def patcher(sysfind, only_find, sysfind_result, sysexec_result): | ||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion 2/2 as discussed in #224 (comment)
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||
mock_python = make_mocked_interpreter_path(sysfind_result, sysexec_result) | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
def mock_sysfind(arg): | ||||||||||||||||||||||||||||||||||||||||||||
if sysfind_result is None: | ||||||||||||||||||||||||||||||||||||||||||||
return None | ||||||||||||||||||||||||||||||||||||||||||||
elif arg.lower() in only_find: | ||||||||||||||||||||||||||||||||||||||||||||
return mock_python | ||||||||||||||||||||||||||||||||||||||||||||
else: | ||||||||||||||||||||||||||||||||||||||||||||
return None | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
sysfind.side_effect = mock_sysfind | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
return sysfind | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
return patcher | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
def test_process_env_constructor(): | ||||||||||||||||||||||||||||||||||||||||||||
penv = nox.virtualenv.ProcessEnv() | ||||||||||||||||||||||||||||||||||||||||||||
assert not penv.bin | ||||||||||||||||||||||||||||||||||||||||||||
|
@@ -315,7 +357,7 @@ def test__resolved_interpreter_windows_pyexe(sysfind, make_one, input_, expected | |||||||||||||||||||||||||||||||||||||||||||
attrs = {"sysexec.return_value": expected} | ||||||||||||||||||||||||||||||||||||||||||||
mock_py = mock.Mock() | ||||||||||||||||||||||||||||||||||||||||||||
mock_py.configure_mock(**attrs) | ||||||||||||||||||||||||||||||||||||||||||||
sysfind.side_effect = lambda arg: mock_py if arg == "py" else False | ||||||||||||||||||||||||||||||||||||||||||||
sysfind.side_effect = lambda arg: mock_py if arg == "py" else None | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
# Okay now run the test. | ||||||||||||||||||||||||||||||||||||||||||||
assert venv._resolved_interpreter == expected | ||||||||||||||||||||||||||||||||||||||||||||
|
@@ -338,7 +380,7 @@ def test__resolved_interpreter_windows_pyexe_fails(sysfind, make_one): | |||||||||||||||||||||||||||||||||||||||||||
attrs = {"sysexec.side_effect": py.process.cmdexec.Error(1, 1, "", "", "")} | ||||||||||||||||||||||||||||||||||||||||||||
mock_py = mock.Mock() | ||||||||||||||||||||||||||||||||||||||||||||
mock_py.configure_mock(**attrs) | ||||||||||||||||||||||||||||||||||||||||||||
sysfind.side_effect = lambda arg: mock_py if arg == "py" else False | ||||||||||||||||||||||||||||||||||||||||||||
sysfind.side_effect = lambda arg: mock_py if arg == "py" else None | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
# Okay now run the test. | ||||||||||||||||||||||||||||||||||||||||||||
with pytest.raises(nox.virtualenv.InterpreterNotFound): | ||||||||||||||||||||||||||||||||||||||||||||
|
@@ -348,6 +390,55 @@ def test__resolved_interpreter_windows_pyexe_fails(sysfind, make_one): | |||||||||||||||||||||||||||||||||||||||||||
sysfind.assert_any_call("py") | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
@mock.patch("nox.virtualenv._SYSTEM", new="Windows") | ||||||||||||||||||||||||||||||||||||||||||||
@mock.patch.object(py.path.local, "sysfind") | ||||||||||||||||||||||||||||||||||||||||||||
def test__resolved_interpreter_windows_path_and_version( | ||||||||||||||||||||||||||||||||||||||||||||
sysfind, make_one, patch_sysfind | ||||||||||||||||||||||||||||||||||||||||||||
): | ||||||||||||||||||||||||||||||||||||||||||||
# Establish that if we get a standard pythonX.Y path, we look it | ||||||||||||||||||||||||||||||||||||||||||||
# up via the path on Windows. | ||||||||||||||||||||||||||||||||||||||||||||
venv, _ = make_one(interpreter="3.7") | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
# Trick the system into thinking that it cannot find | ||||||||||||||||||||||||||||||||||||||||||||
# pythonX.Y up until the python-in-path check at the end. | ||||||||||||||||||||||||||||||||||||||||||||
# Also, we don't give it a mock py launcher. | ||||||||||||||||||||||||||||||||||||||||||||
# But we give it a mock python interpreter to find | ||||||||||||||||||||||||||||||||||||||||||||
# in the system path. | ||||||||||||||||||||||||||||||||||||||||||||
correct_path = r"c:\python37-x64\python.exe" | ||||||||||||||||||||||||||||||||||||||||||||
patch_sysfind( | ||||||||||||||||||||||||||||||||||||||||||||
sysfind, | ||||||||||||||||||||||||||||||||||||||||||||
only_find=("python", "python.exe"), | ||||||||||||||||||||||||||||||||||||||||||||
sysfind_result=correct_path, | ||||||||||||||||||||||||||||||||||||||||||||
sysexec_result="3.7.3\\n", | ||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
# Okay, now run the test. | ||||||||||||||||||||||||||||||||||||||||||||
assert venv._resolved_interpreter == correct_path | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
@pytest.mark.parametrize("input_", ["2.7", "python3.7", "goofy"]) | ||||||||||||||||||||||||||||||||||||||||||||
@pytest.mark.parametrize("sysfind_result", [r"c:\python37-x64\python.exe", None]) | ||||||||||||||||||||||||||||||||||||||||||||
@pytest.mark.parametrize("sysexec_result", ["3.7.3\\n", RAISE_ERROR]) | ||||||||||||||||||||||||||||||||||||||||||||
@mock.patch("nox.virtualenv._SYSTEM", new="Windows") | ||||||||||||||||||||||||||||||||||||||||||||
@mock.patch.object(py.path.local, "sysfind") | ||||||||||||||||||||||||||||||||||||||||||||
def test__resolved_interpreter_windows_path_and_version_fails( | ||||||||||||||||||||||||||||||||||||||||||||
sysfind, input_, sysfind_result, sysexec_result, make_one, patch_sysfind | ||||||||||||||||||||||||||||||||||||||||||||
): | ||||||||||||||||||||||||||||||||||||||||||||
# Establish that if we get a standard pythonX.Y path, we look it | ||||||||||||||||||||||||||||||||||||||||||||
# up via the path on Windows. | ||||||||||||||||||||||||||||||||||||||||||||
venv, _ = make_one(interpreter=input_) | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
# Trick the system into thinking that it cannot find | ||||||||||||||||||||||||||||||||||||||||||||
# pythonX.Y up until the python-in-path check at the end. | ||||||||||||||||||||||||||||||||||||||||||||
# Also, we don't give it a mock py launcher. | ||||||||||||||||||||||||||||||||||||||||||||
# But we give it a mock python interpreter to find | ||||||||||||||||||||||||||||||||||||||||||||
# in the system path. | ||||||||||||||||||||||||||||||||||||||||||||
patch_sysfind(sysfind, ("python", "python.exe"), sysfind_result, sysexec_result) | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
with pytest.raises(nox.virtualenv.InterpreterNotFound): | ||||||||||||||||||||||||||||||||||||||||||||
venv._resolved_interpreter | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
@mock.patch("nox.virtualenv._SYSTEM", new="Windows") | ||||||||||||||||||||||||||||||||||||||||||||
@mock.patch.object(py._path.local.LocalPath, "check") | ||||||||||||||||||||||||||||||||||||||||||||
@mock.patch.object(py.path.local, "sysfind") | ||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why lift this out of the
if
statement but not out of the function as a module global?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was aiming for style consistency with
locate_via_py
. I can put it inside theif
block if it helps (would prefer not to make it module global, as it is the only time this string is used). Should I?