Skip to content
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

add venv as an option for venv_backend #231

Merged
merged 7 commits into from
Aug 12, 2019
Merged

Conversation

cs01
Copy link
Contributor

@cs01 cs01 commented Aug 5, 2019

Summary

This PR adds support for a different backend, venv. It adds a keyword argument to the VirtualEnv class, which defaults to False and is only true when the nox session has venv_backend=True.

It also updates the resolution of the Python path, since using venv alone does not fix issues when creating a venv from within a virtual environment.

The path resolution is modified to determine if a virtual environment is being used. If so, it tries to find the desired Python interpreter version in the base_prefix rather than anywhere on the system's PATH. Note that Windows was not tested, and probably will require some more changes.

Test Plan

# Create and enter virtual environment, then
cd ~/git/nox
pip install -e .
cd ~/git/pipx
# modify pipx's noxfile.py to have `venv_backend='venv'` for the unittests session
nox -s unittests-3.6  # tests pass

Additional context

prefix: A string giving the site-specific directory prefix where the platform independent Python files are installed

https://docs.python.org/3/library/sys.html#sys.prefix

base_prefix: Set during Python startup, before site.py is run, to the same value as prefix. If not running in a virtual environment, the values will stay the same; if site.py finds that a virtual environment is in use, the values of prefix and exec_prefix will be changed to point to the virtual environment, whereas base_prefix and base_exec_prefix will remain pointing to the base Python installation (the one which the virtual environment was created from).

https://docs.python.org/3/library/sys.html#sys.base_prefix

Tox's implementation for venv support is included as a plugin in an additional package: https://github.com/tox-dev/tox-venv/blob/master/src/tox_venv/hooks.py


fixes #199
cc @pfmoore

Copy link
Contributor

@pfmoore pfmoore left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comments based on desk-checking the code, I'm not currently at a machine where I can test it.

if self.interpreter:
cmd.extend(["-p", self._resolved_interpreter])
cmd = [self._resolved_interpreter]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change means that if virtualenv is used, it will now need to be installed in the resolved interpreter, whereas the current code only requires virtualenv to be installed in the current interpreter. It's not clear to me whether this would be a problem in practice, but it is a change in behaviour and if it's to be made, it should be done deliberately and not "by accident".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops, that was a remnant of my hack to get pipx tests working earlier. I'll add the -p flag back in if virtualenv is being used. I'll fix it.

"Re-using existing virtualenv at {}.".format(self.location_name)
"Re-using existing virtual environment at {}.".format(
self.location_name
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a risk here that the existing environment used virtualenv, but the request was to use venv? If so, would this result in the caller getting the wrong type of environment? This is a real edge case, and probably isn't a major consideration, but it's worth noting.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes there is. Agreed this is an edge case. I suppose there could be a call made to identify whether it's a virtualenv or venv and to assert as necessary before continuing.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also have an outstanding bug to make sure the -r checks the interpreter version as well. We might be able to just add a note to that issue that checking the backend should be part of the check as well. #123.

nox/virtualenv.py Outdated Show resolved Hide resolved
if self.interpreter is None:
self._resolved = sys.executable
if currently_in_virtual_environment:
self._resolved = resolve_real_python_outside_venv(self.interpreter)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As it stands, this has no hope of working on Windows. At this point, we're calling resolve_real_python_outside_venv(None) (side note, it might be better to explicitly use None, rather than have the reader need to track back to the if statement to determine that self.interpreter is always None at this point). There's no check for _WINDOWS here (unlike the call below) so the fact that resolve_real_python_outside_venv doesn't (yet) support Windows will cause immediate breakage.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed

@@ -238,7 +268,10 @@ def _resolved_interpreter(self):
cleaned_interpreter = "python{}".format(xy_version)

# If the cleaned interpreter is on the PATH, go ahead and return it.
if py.path.local.sysfind(cleaned_interpreter):
if currently_in_virtual_environment and _SYSTEM != "Windows":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're ignoring cleaned_interpreter here, which seems weird. Why go to the trouble of calculating it and then not use it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, I didn't intentionally ignore it

See also:
https://docs.python.org/3/library/sys.html#sys.prefix
https://docs.python.org/3/library/sys.html#sys.base_prefix
"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This definitely won't work on Windows. There are various implementations of this type of "locate Python" algorithm on the web, including a reusable library version at https://github.com/sarugaku/pythonfinder.

Unfortunately, reusable code for this seems hard to locate (hence the reason so many people write their own) but IMO we should use a library if at all possible (pythonfinder is the one I've heard of, but I have no problem if someone recommends a better one).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pythonfinder looks like it can replace the entire existing nox function unless I'm missing something. Is that correct?

Copy link
Contributor Author

@cs01 cs01 Aug 5, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I replaced all the code with pythonfinder but it isn't able to find any installations. I'll keep trying with it, but won't be able to work on it any more until tonight. If it works it will simplify the code tremendously.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've not used pythonfinder myself, I just knew about it from other projects. So I've no experience with it I'm afraid. I'll try to have a play with it myself, in case I get any ideas.

@theacodes
Copy link
Collaborator

theacodes commented Aug 5, 2019 via email

@pfmoore
Copy link
Contributor

pfmoore commented Aug 5, 2019

I'm completely fine with the pythonfinder thing being a separate discussion. However, getting resolve_real_python_outside_venv to work without it is non-trivial (the existing code doesn't support Windows at all). I'll fish out some known-working code that does the job - both tox or virtualenv have this IIRC (and I have a suspicion I contributed the code in both cases!) Let me see what I can find.

But I do think that in the long term, duplicating this same bit of code in many projects is a bad idea...

@cs01
Copy link
Contributor Author

cs01 commented Aug 5, 2019

@theacodes
Copy link
Collaborator

theacodes commented Aug 5, 2019 via email

@cs01
Copy link
Contributor Author

cs01 commented Aug 5, 2019

@theacodes Assuming pythonfinder does what I think it does, yes.

This PR is really doing two things: adding the venv_backend option of venv, and updating Python resolution to make venv creation from within a virtual environment work. It makes sense to review the two changes separately; I can separate it out into two PRs.

@pfmoore
Copy link
Contributor

pfmoore commented Aug 5, 2019

I'm pretty sure this is tox's

I'm not sure that's all of it. A key part is tox_get_python_executable, which is defined in the tox.interpreters subpackage). To get the available interpreters on Windows you need to look in the registry (PEP 514).

As a simpler workaround, you can let the launcher do the work for you, and call py -X.Y -c "import sys; print(sys.executable)" in a subprocess, but you need to be careful with things like setting PYTHONIOENCODING so that you support full Unicode paths.

The reason I'm in favour of a library solution is that it's really tricky to get all the edge cases correct. Of course, users can always just supply the full executable path, so it's not like it's critical, but it's not a good UX if the software can't find a Python installation you know is there.

updating Python resolution to make venv creation from within a virtual environment work

Yeah, mixing virtualenvs and venvs is really messy. At this point I think you can safely create a virtualenv from within an active venv (see the discussions around pypa/virtualenv#1345 for the ugly details) but I don't think creating a venv from within an active virtual environment (either venv or virtualenv) is even supported. So getting this fixed would be good (for general reliability, even if there's no specific issue it's causing right now).

@pfmoore
Copy link
Contributor

pfmoore commented Aug 5, 2019

Something like the following should work for Windows, for what it's worth. Lightly tested, it works on my PC but I only have one Python version installed, so I couldn't test much...

import os
import subprocess

def get_python(major, minor=None):
    env = os.environ.copy()
    env['PYTHONIOENCODING'] = "utf-8"
    version = "-" + major
    if minor:
        version += "." + minor
    try:
        proc = subprocess.run(
            ["py", version, "-c", "import sys; print(sys.executable)"],
            env=env,
            capture_output=True
        )
    except Exception:
        # Just say we couldn't find anything if we get any sort of error
        return None
    if proc.returncode != 0:
        return None
    return proc.stdout.decode("utf-8").strip()

if __name__ == '__main__':
    import sys
    print(get_python(*sys.argv[1:]))

@pfmoore
Copy link
Contributor

pfmoore commented Aug 5, 2019

Whoa! I just looked at the list of dependencies for pythonfinder:

install_requires =
    attrs
    cached-property
    click
    crayons
    six
    packaging
    vistir[spinner]>=0.4

That's way too much for the job it's doing. Installing it pulled in 15 other packages, for a total of about 14M of data.

At this point, I'd say let's not use pythonfinder. That's just silly :-(


if self.interpreter:
if self.interpreter and self.venv_or_virtualenv == "virtualenv":
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if they request a different interpreter version than the one Nox is installed on?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I think I see a bug.

In the case of venv we should call

[self._resolved_interpreter, 'venv', '-m', 'self.location']

rather than use sys.executable since you can't specify the python interpreter to use in venv (it just uses the Python you create the venv with).

In the case of virtualenv it will remain unchanged

[sys.executable, 'virtualenv', '-m', 'self.location', '-p', self._resolved_interpreter]

@cs01
Copy link
Contributor Author

cs01 commented Aug 6, 2019

Whoa! I just looked at the list of dependencies for pythonfinder:

install_requires =
    attrs
    cached-property
    click
    crayons
    six
    packaging
    vistir[spinner]>=0.4

That's way too much for the job it's doing. Installing it pulled in 15 other packages, for a total of about 14M of data.

At this point, I'd say let's not use pythonfinder. That's just silly :-(

Just FYI I filed an issue in pythonfinder on this: sarugaku/pythonfinder#78

@cs01
Copy link
Contributor Author

cs01 commented Aug 11, 2019

Got the tests passing 🎉 .

So to summarize, this PR adds support for venv as an alternate backend in a nox session. It does not modify the logic when resolving the interpreter, which is required to create a new venv from within a venv. That can be done in a different PR, possibly with pythonfinder, depending on how sarugaku/pythonfinder#78 evolves.

@theacodes theacodes merged commit 9477a91 into wntrblm:master Aug 12, 2019
@theacodes
Copy link
Collaborator

Thank you, @cs01 and @pfmoore!

Does this need a follow-up PR for documentation as well, or do we think it's sufficiently advanced enough that most users won't need it?

@cs01 cs01 deleted the cs01/use-venv branch August 12, 2019 02:19
@cs01
Copy link
Contributor Author

cs01 commented Aug 12, 2019

Thank you, @cs01 and @pfmoore!

Does this need a follow-up PR for documentation as well, or do we think it's sufficiently advanced enough that most users won't need it?

I don't really have a feeling for how many other people want to use this. AFAIK the only reason you'd want it is to test a program that creates virtual environments. So that would include pipx, pipenv, poetry, virtualenv, venv, and maybe some others. Because of this, I don't know if there is much value added to using the venv backend without the improved Python resolution, so I was planning on waiting until the improved Python resolution was added before documenting it.

@theacodes
Copy link
Collaborator

theacodes commented Aug 12, 2019 via email

@gaborbernat
Copy link

Hmmm, I've been working on making virtualenv more like venv, and add python discovery part of it - see https://github.com/gaborbernat/virtualenv/blob/rewrite/src/virtualenv/interpreters/discovery/__init__.py 🤔 probably will be released in a month or so 🤔 so this entire ticket should self solve itself then 🤔

@cs01
Copy link
Contributor Author

cs01 commented Oct 20, 2019

venv is now a valid backend for nox, so nox can create venvs, but interpreter resolution in nox still needs to be improved since it still defaults to the python in the venv.

Are you saying that in the future, nox can use virtualenv but still find the base interpreter from there (I think currently it can only do this from venv), or that the default interpreter in virtualenv will be the base interpreter and therefore able to create a new venv/virtualenv from there? (This is the PR, open issue is at #233)

@gaborbernat
Copy link

gaborbernat commented Oct 20, 2019

virtualenv's future discovery mechanism will always let you know the base interpreter too (for venv/old virtualenv/host python) 👍 furthermore new virtualenv environments will be equivalent with current venvs.

@cs01
Copy link
Contributor Author

cs01 commented Oct 20, 2019

Right now, nox has a @property called _resolved_interpreter. Just want to confirm I am understanding the new workflow: In the future we will be importing virtualenv into nox's virtualenv.py module and using a new method in _resolved_interpreter that allows us to get the base interpreter?

@gaborbernat
Copy link

Not sure how this will integrate into nox; but virtualenv will contain the logic of interpreter discovery (with option to also access the host interpreter, not just the immediate discovered one). tox will definitely use this and no longer implement its own version of this functionality, nox might or might not choose to do so, 🤷‍♂ it's up to @theacodes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

Allow use of venv rather than virtualenv
4 participants