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

Support built-in “venv” #173

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,40 @@ matrix:
- pip install -e . --upgrade
- pip install pytest
script: py.test -rws tests/ --ignore=tests/test_install.py
- language: python
sudo: false
python: "3.4"
env: PEW_USE_VIRTUALENV=1
install:
- pip install -e . --upgrade
- pip install pytest
script: py.test -rws tests/ --ignore=tests/test_install.py
- language: python
sudo: false
python: "3.5"
install:
- pip install -e . --upgrade
- pip install pytest
script: py.test -rws tests/ --ignore=tests/test_install.py
- language: python
sudo: false
python: "3.5"
env: PEW_USE_VIRTUALENV=1
install:
- pip install -e . --upgrade
- pip install pytest
script: py.test -rws tests/ --ignore=tests/test_install.py
- language: python
sudo: false
python: "3.6"
install:
- pip install -e .[pythonz] --upgrade
- pip install pytest
script: py.test -rws tests/
- language: python
sudo: false
python: "3.6"
env: PEW_USE_VIRTUALENV=1
install:
- pip install -e .[pythonz] --upgrade
- pip install pytest
Expand All @@ -42,4 +66,4 @@ matrix:
- pip install -e .[pythonz] --upgrade
- pip install pytest
script: py.test -rws tests/
- language: nix
- language: nix
6 changes: 3 additions & 3 deletions appveyor.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
install:
- SET PATH=C:\\Python34;C:\\Python34\\Scripts;%PATH%
- pip install tox
- SET PATH=C:\\Python36;C:\\Python35;C:\\Python34;C:\\Python33;C:\\Python27;C:\\Python26;%PATH%
- C:\\Python36\\Scripts\\pip.exe install tox

build: false

test_script: tox -e "py27,py34"
test_script: C:\\Python36\\Scripts\\tox.exe
3 changes: 3 additions & 0 deletions pew/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
if __name__ == '__main__':
from . import pew
pew.pew()
32 changes: 32 additions & 0 deletions pew/_cfg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import collections
import pathlib


class Cfg(object):
"""Minimal parser for pyvenv.cfg.
"""
def __init__(self, path):
self._path = path
self._data = collections.OrderedDict()
with path.open(encoding='utf-8') as f:
for line in f:
key, _, value = line.partition('=')
self._data[key.strip().lower()] = value.strip()

@property
def bindir(self):
return pathlib.Path(self._data['home'])

@property
def include_system_sitepackages(self):
return self._data['include-system-site-packages'] == 'true'

@include_system_sitepackages.setter
def include_system_sitepackages(self, value):
value = {True: 'true', False: 'false'}[value]
self._data['include-system-site-packages'] = value

def save(self):
with self._path.open('w', encoding='utf-8') as f:
for k, v in self._data.items():
f.write('{} = {}\n'.format(k, v))
15 changes: 14 additions & 1 deletion pew/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,22 @@ def invoke(*args, **kwargs):

env_bin_dir = 'bin' if sys.platform != 'win32' else 'Scripts'

if windows:
default_home = '~/.virtualenvs'
else:
default_home = os.path.join(
os.environ.get('XDG_DATA_HOME', '~/.local/share'), 'virtualenvs')


def expandpath(path):
return Path(os.path.expanduser(os.path.expandvars(path)))


workon_home = expandpath(os.environ.get('WORKON_HOME', default_home))


def own(path):
if sys.platform == 'win32':
if windows:
# Even if run by an administrator, the permissions will be set
# correctly on Windows, no need to check
return True
Expand All @@ -87,3 +96,7 @@ def temp_environ():
finally:
os.environ.clear()
os.environ.update(environ)


def uses_venv():
return sys.version_info >= (3, 4) and not os.environ.get('PEW_USE_VIRTUALENV')
160 changes: 160 additions & 0 deletions pew/_venv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
__all__ = ['choose_backend', 'guess_backend']

import os
import pathlib
import subprocess
import sys
import warnings

from ._cfg import Cfg
from ._utils import env_bin_dir, invoke, uses_venv, windows


class Backend(object):

def __init__(self, root):
super(Backend, self).__init__()
self.root = root.absolute()

def get_python(self):
return self.root.joinpath(env_bin_dir, 'python.exe' if windows else 'python')

def get_sitepackages_dir(self):
result = invoke(
str(self.get_python()), '-c',
'import distutils.sysconfig; '
'print(distutils.sysconfig.get_python_lib())',
)
if result.returncode != 0:
raise RuntimeError(
'could not get site-packages location.\n%s' % result.err,
)
return pathlib.Path(result.out)


class VirtualenvBackend(Backend):
"""Functionality provided by virtualenv.
"""
def create_env(self, py, args):
if py:
args = ["--python=%s" % py] + args
subprocess.check_call(
[sys.executable, '-m', 'virtualenv', str(self.root)] + args,
)

def restore_env(self):
subprocess.check_call([
sys.executable, "-m", 'virtualenv', str(self.root),
"--python=%s" % self.get_python().name,
])

def toggle_global_sitepackages(self):
ngsp = self.get_sitepackages_dir().with_name(
'no-global-site-packages.txt',
)
should_enable = not ngsp.exists()
if should_enable:
with ngsp.open('w'):
pass
else:
ngsp.unlink()
return should_enable


class BrokenEnvironmentError(RuntimeError):
pass


def find_real_python():
# We need to run venv on the Python we want to use. sys.executable is not
# enough because it might come from a venv itself. venv can be easily
# confused if it is nested inside another venv.
# https://bugs.python.org/issue30811
python = ('python.exe' if windows else 'python')

# If we're in a venv, excellent! The config file has this information.
pyvenv_cfg = pathlib.Path(sys.prefix, 'pyvenv.cfg')
if pyvenv_cfg.exists():
return Cfg(pyvenv_cfg).bindir.joinpath(python)

# Or we can try looking this up from the build configuration. This is
# usually good enough, unless we're in a virtualenv (i.e. sys.real_prefix
# is set), and sysconfig reports the wrong Python (i.e. in the virtualenv).
import sysconfig
bindir = sysconfig.get_config_var('BINDIR')
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we should make a Path out of bindir straight away

try:
real_prefix = sys.real_prefix
except AttributeError:
return pathlib.Path(bindir, python)
if not os.path.realpath(bindir).startswith(real_prefix):
return pathlib.Path(bindir, python)

# OK, so we're in a virtualenv, and sysconfig lied. At this point there's
# no definitive way to tell where the real Python is, so let's make an
# educated guess. This works if the user isn't using a very exotic build.
for rel in ['', os.path.relpath(str(bindir), real_prefix), env_bin_dir]:
try:
path = pathlib.Path(real_prefix, rel, python).resolve()
except FileNotFoundError:
continue
if path.exists(): # On 3.6+ resolve() doesn't check for existence.
return path

# We've tried everything. Sorry.
raise BrokenEnvironmentError


class VenvBackend(Backend):
"""Functionality provided by venv, built-in since Python 3.4.
"""
module_name = 'venv'

def create_env(self, py, args):
if not py:
py = find_real_python()
subprocess.check_call([str(py), '-m', 'venv', str(self.root)] + args)

def restore_env(self):
cfg = Cfg(self.root.joinpath('pyvenv.cfg'))
py = cfg.bindir.joinpath('python.exe' if windows else 'python')
subprocess.check_call([str(py), "-m", 'venv', str(self.root)])

def toggle_global_sitepackages(self):
cfg = Cfg(self.root.joinpath('pyvenv.cfg'))
should_enable = not cfg.include_system_sitepackages
cfg.include_system_sitepackages = should_enable
cfg.save()
return should_enable


def choose_backend(root):
"""Choose a preferred virtual environment backend to use.
"""
if not uses_venv():
return VirtualenvBackend(root)

# Without ensurepip the venv can can't bootstrap Setuptools and Pip.
# The venv would not be useful for us without them.
for module_name in ['ensurepip', 'venv']:
try:
__import__(module_name)
except ImportError:
warnings.warn(
'Module "%s" unavailable, falling back to virtualenv...\n'
'Set PEW_USE_VIRTUALENV environment variable to '
'suppress this.' % module_name,
FutureWarning,
)
return VirtualenvBackend(root)
return VenvBackend(root)


def guess_backend(root):
"""Guess what backend a virtual environment backend uses.

The built-in venv writes a pyvenv.cfg file; check if it exists.
"""
pyvenv_cfg = root / 'pyvenv.cfg'
if pyvenv_cfg.exists():
return VenvBackend(root)
return VirtualenvBackend(root)
Loading