Skip to content

Commit f94024d

Browse files
committed
more robust testing for the activate this
1 parent 1cdee10 commit f94024d

File tree

8 files changed

+175
-72
lines changed

8 files changed

+175
-72
lines changed

docs/changes.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ Release History
1212
* Fix preserving of original path when using fish and a subshell :issue:`904`
1313
* ``powershell`` now also provides the ``pydoc`` function that uses the virtual environments ``pydoc``
1414
* ``powershell`` activator is no longer signed :issue:`816`
15-
* Set VIRTUAL_ENV enviroment variable in activate_this.py. (:pull:`1057`)
15+
* Set ``VIRTUAL_ENV`` enviroment variable in ``activate_this.py``. (:pull:`1057`)
16+
* Recommend ``exec(open(this_file).read(), {'__file__': this_file})`` to activate via ``activate_this.py`` as it works both
17+
on Python 2 and 3.
18+
* ``pypy`` and ``pypy3`` supports ``activate_this.py``
1619

1720
16.1.0 (2018-10-31)
1821
-------------------

docs/userguide.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ the path is correct. A script is available to correct the path. You
195195
can setup the environment like::
196196

197197
activate_this = '/path/to/env/bin/activate_this.py'
198-
execfile(activate_this, dict(__file__=activate_this))
198+
exec(open(activate_this).read(), {'__file__': activate_this}))
199199

200200
This will change ``sys.path`` and even change ``sys.prefix``, but also allow
201201
you to use an existing interpreter. Items in your environment will show up

src/virtualenv.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1766,7 +1766,7 @@ def relative_script(lines):
17661766
activate = (
17671767
"import os; "
17681768
"activate_this=os.path.join(os.path.dirname(os.path.realpath(__file__)), 'activate_this.py'); "
1769-
"exec(compile(open(activate_this).read(), activate_this, 'exec'), dict(__file__=activate_this)); "
1769+
"exec(compile(open(activate_this).read(), activate_this, 'exec'), { '__file__': activate_this}); "
17701770
"del os, activate_this"
17711771
)
17721772
# Find the last future statement in the script. If we insert the activation
@@ -2248,16 +2248,18 @@ def convert(s):
22482248
# file activate_this.py
22492249
ACTIVATE_THIS = convert(
22502250
"""
2251-
eJyNU21r2zAQ/q5fITRKbZY5Y/0WyIcMCht0pYyuMEIQin1OtNqSkRQn/ve7k6O8dBvMkFjne+65
2252-
u0d3QojPA995bTYcDlDWuoEsbLWXdJrwSpchk9GQcn5y5Dkf7I7vddMwVQbdqwCcvLzXLuxUA6bn
2253-
+NPOmhZMKBh7Jm+pDF8D5oOK77dgIku784E+cXTCQftAxTwNYWsN1yaA6xzg/4QbG1jYwmWOtTbT
2254-
LkKZEIKx4IYZ4/ikmhkcSugCf1Qt3Dtn3eh2SmPChffggrYmerLooUf8TGW5nRn7avQrnBW67VTY
2255-
ToOdpuYlgYpuuH0r2b+ReS5iwpzptrMucOvTyesAp/PgGbNNJa2XxMXnCCyO2hYbCJl4Wjx/ERMu
2256-
RM5QD1lpN4IIXqBlsPcs2Wrt6X2qMM/ZmW85cq0wPjG9T0weOjLOlbC1Qg3/zHSMvOZ9+fr9+cfi
2257-
Qd4/voz0GMt0Te0VXaNCbV3L53Mu9trcfRLjLZEOmKp8VRvwF5l+WW0yYsCmH/SaeifohwRFIaDx
2258-
8P8kzUgyTtKNF/wmFtaD8zgdy9nd6i8pcCx7ibB0LQ2Obhb7QTtnjPCFqip6ox7ZVSE5I6QD1Ujk
2259-
qfUBCWJsNNj5mLR6x7/ZnpYM96SqcH+QrfU82PipRpFxgupoUP4ZM7C/rG65YqhxjMKtelPsqBRe
2260-
R3TjnhHkqr/ZaTkueQvVdWCqjMLyEyLRYnst1nz0pq/L2UcagEsa9hvWn4mj
2251+
eJydVE1v2zAMvetXENqhNpZ5wHoL0EMOBRagC4Kt7VAEgaHYdKzNlgxJSWMU/e8j/dG4Ww/FcohE
2252+
i3x8JJ8kpVxkQR9VQDhqFw6qQnOEwjrIDs6hCaBNQNc4pP+5EHceAU+YRbZBE4VS+7TQFcaJQ5VH
2253+
8QyeLtLuS5pezOHl/DlOhLglCzJlYIdw8JjDY4kGWnuA+uADfwI6xJP2QZs9rNtQWjPNPwNjA4G+
2254+
4rrT5nPTuSZCSil03VgXwPpx53XAl33rhQiunQug38hU4CnDJsBK1XjtnHX9sVOaKC28Rxe0Nd1J
2255+
JB+mfP+rFbGMhfgAVBMF5lwABAvrxe1X6GCA3YBWbbzOsSuYnXLtMAvWtbEgMyUTrqjOpFGhTMgy
2256+
RD8abbXzvEYjhTgWdEQN086ajeRscnuO99gkv6w20WaA3sJHOAckewxRHzQDKePEN5UO0TmY4MVO
2257+
UUf+ZTQAdjUPcwMeHBXo9L4MoHb2+FeFr7jeL7/f3i1u0uvVfUeZ8zCYyvOpGGAIqEm1vpv6p0Zl
2258+
v9Ueub3sWFoaWzPIqhdEjVmpjPa1WP5I1w/rB8IvlVchuIjEQsU2bdOmR3SeJJBqU1iaHvn+XK7I
2259+
lVySplKBLkwNV1cgH7W5/EIiLGDA66XEbNKBjZ/0qGs510OJpow9JcHKYx/do1HG3nwv4I3eyTdw
2260+
OfyM/V6wqgfru/f0LBMuWQVuUjJ0ZzO/3MZvJBSk9CM3C3t3xo4FeyU0Ql5p7tErEnRMjnSRqpSC
2261+
C30ae90Z4rydyOEbq4jHTKD0uBBc7cfJF6QLehOKzuD8M7pcQJPLUBh8JJSKXp0pu2G3mbPiNrp7
2262+
EjUHsTsNRHdvEYNQcXxZ3vQZz8UfOZfTng==
22612263
"""
22622264
)
22632265

tests/activation/conftest.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from __future__ import absolute_import, unicode_literals
2+
3+
import os
4+
import subprocess
5+
6+
import pytest
7+
8+
import virtualenv
9+
10+
11+
@pytest.fixture(scope="session")
12+
def activation_env(tmp_path_factory):
13+
path = tmp_path_factory.mktemp("activation-test-env")
14+
prev_cwd = os.getcwd()
15+
try:
16+
os.chdir(str(path))
17+
home_dir, _, __, bin_dir = virtualenv.path_locations(str(path / "env"))
18+
virtualenv.create_environment(home_dir, no_pip=True, no_setuptools=True, no_wheel=True)
19+
20+
site_packages = subprocess.check_output(
21+
[
22+
os.path.join(bin_dir, virtualenv.EXPECTED_EXE),
23+
"-c",
24+
"from distutils.sysconfig import get_python_lib; print(get_python_lib())",
25+
],
26+
universal_newlines=True,
27+
).strip()
28+
29+
pydoc_test = path.__class__(site_packages) / "pydoc_test.py"
30+
pydoc_test.write_text('"""This is pydoc_test.py"""')
31+
32+
yield home_dir, bin_dir, pydoc_test
33+
finally:
34+
os.chdir(str(prev_cwd))
Lines changed: 92 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,103 @@
1+
"""Activate this does not mangles with the shell itself to provision the python, but instead mangles
2+
with the caller interpreter, effectively making so that the virtualenv activation constraints are met once
3+
it's loaded.
4+
5+
While initially may feel like a import is all we need, import is executed only once not at every activation. To
6+
work around this we'll use Python 2 execfile, and Python 3 exec(read()).
7+
8+
Virtual env activation constraints that we guarantee:
9+
- the virtualenv site-package will be visible from the activator Python
10+
- virtualenv packages take priority over python 2
11+
- virtualenv bin PATH pre-pended
12+
- VIRTUAL_ENV env var will be set.
13+
- if the user tries to import we'll raise
14+
15+
"""
16+
from __future__ import absolute_import, unicode_literals
17+
118
import os
19+
import re
20+
import subprocess
21+
import sys
22+
import textwrap
23+
224

25+
def test_activate_this(activation_env, tmp_path, monkeypatch):
26+
# to test this, we'll try to use the activation env from this Python
27+
monkeypatch.delenv(str("VIRTUAL_ENV"), raising=False)
28+
monkeypatch.delenv(str("PYTHONPATH"), raising=False)
29+
start_path = os.pathsep.join([str(tmp_path), str(tmp_path / "other")])
30+
monkeypatch.setenv(str("PATH"), start_path)
31+
activator = tmp_path.__class__(activation_env[1]) / "activate_this.py"
32+
assert activator.exists()
333

4-
def get_base_dir():
5-
return os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
34+
activator_at = str(activator)
35+
script = textwrap.dedent(
36+
"""
37+
import os
38+
import sys
39+
print(os.environ.get("VIRTUAL_ENV"))
40+
print(os.environ.get("PATH"))
41+
try:
42+
import pydoc_test
43+
raise RuntimeError("this should not happen")
44+
except ImportError:
45+
pass
46+
print(os.pathsep.join(sys.path))
47+
file_at = {!r}
48+
exec(open(file_at).read(), {{'__file__': file_at}})
49+
print(os.environ.get("VIRTUAL_ENV"))
50+
print(os.environ.get("PATH"))
51+
print(os.pathsep.join(sys.path))
52+
import pydoc_test
53+
print(pydoc_test.__file__)
54+
""".format(
55+
str(activator_at)
56+
)
57+
)
58+
script_path = tmp_path / "test.py"
59+
script_path.write_text(script)
60+
try:
61+
raw = subprocess.check_output(
62+
[sys.executable, str(script_path)], stderr=subprocess.STDOUT, universal_newlines=True
63+
)
664

65+
out = re.sub(r"pydev debugger: process \d+ is connecting\n\n", "", raw, re.M).strip().split("\n")
766

8-
def get_activate_this_path():
9-
return os.path.abspath(os.path.join(get_base_dir(), "virtualenv_embedded", "activate_this.py"))
67+
assert out[0] == "None"
68+
assert out[1] == start_path
69+
prev_sys_path = out[2].split(os.path.pathsep)
1070

71+
assert out[3] == activation_env[0] # virtualenv set as the activated env
1172

12-
def activate():
13-
activate_this = get_activate_this_path()
14-
exec(compile(open(activate_this).read(), activate_this, "exec"), dict(__file__=activate_this))
73+
# PATH updated with activated
74+
assert out[4].endswith(start_path)
75+
assert out[4][: -len(start_path)].split(os.pathsep) == [activation_env[1], ""]
1576

77+
# sys path contains the site package at its start
78+
new_sys_path = out[5].split(os.path.pathsep)
79+
assert new_sys_path[-len(prev_sys_path) :] == prev_sys_path
80+
extra_start = new_sys_path[0 : -len(prev_sys_path)]
81+
assert len(extra_start) == 1
82+
assert extra_start[0].startswith(activation_env[0])
83+
assert tmp_path.__class__(extra_start[0]).exists()
1684

17-
def test_activate_this():
18-
base_dir = get_base_dir()
19-
old_path = os.environ["PATH"]
85+
# manage to import from activate site package
86+
assert out[6] == str(activation_env[2])
87+
except subprocess.CalledProcessError as exception:
88+
assert not exception.returncode, exception.output
2089

21-
activate()
2290

23-
assert os.environ["PATH"] == os.path.dirname(get_activate_this_path()) + os.pathsep + old_path
24-
assert os.environ["VIRTUAL_ENV"] == base_dir
91+
def test_activate_this_no_file(activation_env, tmp_path, monkeypatch):
92+
activator = tmp_path.__class__(activation_env[1]) / "activate_this.py"
93+
assert activator.exists()
94+
try:
95+
subprocess.check_output(
96+
[sys.executable, "-c", "exec(open({!r}).read())".format(str(activator))],
97+
stderr=subprocess.STDOUT,
98+
universal_newlines=True,
99+
)
100+
raise RuntimeError("this should not happen")
101+
except subprocess.CalledProcessError as exception:
102+
out = re.sub(r"pydev debugger: process \d+ is connecting\n\n", "", exception.output, re.M).strip()
103+
assert "You must use exec(open(this_file).read(), {'__file__': this_file}))" in out, out

tests/test_activation.py renamed to tests/activation/test_activation.py

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -54,19 +54,6 @@ def norm_path(path):
5454
return normcase(result)
5555

5656

57-
@pytest.fixture(scope="session")
58-
def activation_env(tmp_path_factory):
59-
path = tmp_path_factory.mktemp("activation-test-env")
60-
prev_cwd = os.getcwd()
61-
try:
62-
os.chdir(str(path))
63-
home_dir, _, __, bin_dir = virtualenv.path_locations(str(path / "env"))
64-
virtualenv.create_environment(home_dir, no_pip=True, no_setuptools=True, no_wheel=True)
65-
yield home_dir, bin_dir
66-
finally:
67-
os.chdir(str(prev_cwd))
68-
69-
7057
class Activation(object):
7158
cmd = ""
7259
extension = "test"
@@ -102,17 +89,6 @@ def print_os_env_var(self, var):
10289
def __call__(self, monkeypatch):
10390
absolute_activate_script = norm_path(join(self.bin_dir, self.activate_script))
10491

105-
site_packages = subprocess.check_output(
106-
[
107-
os.path.join(self.bin_dir, virtualenv.EXPECTED_EXE),
108-
"-c",
109-
"from distutils.sysconfig import get_python_lib; print(get_python_lib())",
110-
],
111-
universal_newlines=True,
112-
).strip()
113-
pydoc_test = self.path.__class__(site_packages) / "pydoc_test.py"
114-
pydoc_test.write_text('"""This is pydoc_test.py"""')
115-
11692
commands = [
11793
self.print_python_exe(),
11894
self.print_os_env_var("VIRTUAL_ENV"),

tests/test_virtualenv.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -230,18 +230,19 @@ def test_activate_after_future_statements():
230230
"from __future__ import print_function",
231231
'print("Hello, world!")',
232232
]
233-
assert virtualenv.relative_script(script) == [
233+
out = virtualenv.relative_script(script)
234+
assert out == [
234235
"#!/usr/bin/env python",
235236
"from __future__ import with_statement",
236237
"from __future__ import print_function",
237238
"",
238239
"import os; "
239240
"activate_this=os.path.join(os.path.dirname(os.path.realpath(__file__)), 'activate_this.py'); "
240-
"exec(compile(open(activate_this).read(), activate_this, 'exec'), dict(__file__=activate_this)); "
241+
"exec(compile(open(activate_this).read(), activate_this, 'exec'), { '__file__': activate_this}); "
241242
"del os, activate_this",
242243
"",
243244
'print("Hello, world!")',
244-
]
245+
], out
245246

246247

247248
def test_cop_update_defaults_with_store_false():
Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,43 @@
1-
"""By using execfile(this_file, dict(__file__=this_file)) you will activate this virtualenv environment.
1+
"""Activate virtualenv for current interpreter:
2+
3+
Use exec(open(this_file).read(), {'__file__': this_file}).
24
35
This can be used when you must use an existing Python interpreter, not the virtualenv bin/python.
46
"""
7+
import os
8+
import site
9+
import sys
510

611
try:
712
__file__
813
except NameError:
9-
file = "path/to/activate_this.py"
10-
raise AssertionError("You must run this like execfile({0:!r}, {'__file__': {0:!r}})".format(file))
11-
import os
12-
import site
13-
import sys
14+
raise AssertionError("You must use exec(open(this_file).read(), {'__file__': this_file}))")
1415

15-
old_os_path = os.environ.get("PATH", "")
16+
# prepend bin to PATH (this file is inside the bin directory)
1617
bin_dir = os.path.dirname(os.path.abspath(__file__))
17-
os.environ["PATH"] = bin_dir + os.pathsep + old_os_path
18+
os.environ["PATH"] = os.pathsep.join([bin_dir] + os.environ.get("PATH", "").split(os.pathsep))
19+
1820
base = os.path.dirname(bin_dir)
21+
22+
# virtual env is right above bin directory
1923
os.environ["VIRTUAL_ENV"] = base
20-
if sys.platform == "win32":
21-
site_packages = os.path.join(base, "Lib", "site-packages")
24+
25+
# add the virtual environments site-package to the host python import mechanism
26+
IS_PYPY = hasattr(sys, "pypy_version_info")
27+
IS_WIN = sys.platform == "win32"
28+
if IS_PYPY:
29+
site_packages = os.path.join(base, "site-packages")
2230
else:
23-
site_packages = os.path.join(base, "lib", "python{}".format(sys.version[:3]), "site-packages")
24-
prev_sys_path = list(sys.path)
31+
if IS_WIN:
32+
site_packages = os.path.join(base, "Lib", "site-packages")
33+
else:
34+
site_packages = os.path.join(base, "lib", "python{}".format(sys.version[:3]), "site-packages")
2535

36+
prev = set(sys.path)
2637
site.addsitedir(site_packages)
2738
sys.real_prefix = sys.prefix
2839
sys.prefix = base
29-
# Move the added items to the front of the path:
30-
new_sys_path = []
31-
for item in list(sys.path):
32-
if item not in prev_sys_path:
33-
new_sys_path.append(item)
34-
sys.path.remove(item)
35-
sys.path[:0] = new_sys_path
40+
41+
# Move the added items to the front of the path, in place
42+
new = list(sys.path)
43+
sys.path[:] = [i for i in new if i not in prev] + [i for i in new if i in prev]

0 commit comments

Comments
 (0)