1459 lines
52 KiB
Python
1459 lines
52 KiB
Python
|
"""Easy install Tests"""
|
||
|
|
||
|
import contextlib
|
||
|
import io
|
||
|
import itertools
|
||
|
import logging
|
||
|
import os
|
||
|
import pathlib
|
||
|
import re
|
||
|
import site
|
||
|
import subprocess
|
||
|
import sys
|
||
|
import tarfile
|
||
|
import tempfile
|
||
|
import time
|
||
|
import warnings
|
||
|
import zipfile
|
||
|
from pathlib import Path
|
||
|
from typing import NamedTuple
|
||
|
from unittest import mock
|
||
|
|
||
|
import pytest
|
||
|
from jaraco import path
|
||
|
|
||
|
import pkg_resources
|
||
|
import setuptools.command.easy_install as ei
|
||
|
from pkg_resources import Distribution as PRDistribution, normalize_path, working_set
|
||
|
from setuptools import sandbox
|
||
|
from setuptools.command.easy_install import PthDistributions
|
||
|
from setuptools.dist import Distribution
|
||
|
from setuptools.sandbox import run_setup
|
||
|
from setuptools.tests import fail_on_ascii
|
||
|
from setuptools.tests.server import MockServer, path_to_url
|
||
|
|
||
|
from . import contexts
|
||
|
from .textwrap import DALS
|
||
|
|
||
|
import distutils.errors
|
||
|
|
||
|
|
||
|
@pytest.fixture(autouse=True)
|
||
|
def pip_disable_index(monkeypatch):
|
||
|
"""
|
||
|
Important: Disable the default index for pip to avoid
|
||
|
querying packages in the index and potentially resolving
|
||
|
and installing packages there.
|
||
|
"""
|
||
|
monkeypatch.setenv('PIP_NO_INDEX', 'true')
|
||
|
|
||
|
|
||
|
class FakeDist:
|
||
|
def get_entry_map(self, group):
|
||
|
if group != 'console_scripts':
|
||
|
return {}
|
||
|
return {'name': 'ep'}
|
||
|
|
||
|
def as_requirement(self):
|
||
|
return 'spec'
|
||
|
|
||
|
|
||
|
SETUP_PY = DALS(
|
||
|
"""
|
||
|
from setuptools import setup
|
||
|
|
||
|
setup()
|
||
|
"""
|
||
|
)
|
||
|
|
||
|
|
||
|
class TestEasyInstallTest:
|
||
|
def test_get_script_args(self):
|
||
|
header = ei.CommandSpec.best().from_environment().as_header()
|
||
|
dist = FakeDist()
|
||
|
args = next(ei.ScriptWriter.get_args(dist))
|
||
|
name, script = itertools.islice(args, 2)
|
||
|
assert script.startswith(header)
|
||
|
assert "'spec'" in script
|
||
|
assert "'console_scripts'" in script
|
||
|
assert "'name'" in script
|
||
|
assert re.search('^# EASY-INSTALL-ENTRY-SCRIPT', script, flags=re.MULTILINE)
|
||
|
|
||
|
def test_no_find_links(self):
|
||
|
# new option '--no-find-links', that blocks find-links added at
|
||
|
# the project level
|
||
|
dist = Distribution()
|
||
|
cmd = ei.easy_install(dist)
|
||
|
cmd.check_pth_processing = lambda: True
|
||
|
cmd.no_find_links = True
|
||
|
cmd.find_links = ['link1', 'link2']
|
||
|
cmd.install_dir = os.path.join(tempfile.mkdtemp(), 'ok')
|
||
|
cmd.args = ['ok']
|
||
|
cmd.ensure_finalized()
|
||
|
assert cmd.package_index.scanned_urls == {}
|
||
|
|
||
|
# let's try without it (default behavior)
|
||
|
cmd = ei.easy_install(dist)
|
||
|
cmd.check_pth_processing = lambda: True
|
||
|
cmd.find_links = ['link1', 'link2']
|
||
|
cmd.install_dir = os.path.join(tempfile.mkdtemp(), 'ok')
|
||
|
cmd.args = ['ok']
|
||
|
cmd.ensure_finalized()
|
||
|
keys = sorted(cmd.package_index.scanned_urls.keys())
|
||
|
assert keys == ['link1', 'link2']
|
||
|
|
||
|
def test_write_exception(self):
|
||
|
"""
|
||
|
Test that `cant_write_to_target` is rendered as a DistutilsError.
|
||
|
"""
|
||
|
dist = Distribution()
|
||
|
cmd = ei.easy_install(dist)
|
||
|
cmd.install_dir = os.getcwd()
|
||
|
with pytest.raises(distutils.errors.DistutilsError):
|
||
|
cmd.cant_write_to_target()
|
||
|
|
||
|
def test_all_site_dirs(self, monkeypatch):
|
||
|
"""
|
||
|
get_site_dirs should always return site dirs reported by
|
||
|
site.getsitepackages.
|
||
|
"""
|
||
|
path = normalize_path('/setuptools/test/site-packages')
|
||
|
|
||
|
def mock_gsp():
|
||
|
return [path]
|
||
|
|
||
|
monkeypatch.setattr(site, 'getsitepackages', mock_gsp, raising=False)
|
||
|
assert path in ei.get_site_dirs()
|
||
|
|
||
|
def test_all_site_dirs_works_without_getsitepackages(self, monkeypatch):
|
||
|
monkeypatch.delattr(site, 'getsitepackages', raising=False)
|
||
|
assert ei.get_site_dirs()
|
||
|
|
||
|
@pytest.fixture
|
||
|
def sdist_unicode(self, tmpdir):
|
||
|
files = [
|
||
|
(
|
||
|
'setup.py',
|
||
|
DALS(
|
||
|
"""
|
||
|
import setuptools
|
||
|
setuptools.setup(
|
||
|
name="setuptools-test-unicode",
|
||
|
version="1.0",
|
||
|
packages=["mypkg"],
|
||
|
include_package_data=True,
|
||
|
)
|
||
|
"""
|
||
|
),
|
||
|
),
|
||
|
(
|
||
|
'mypkg/__init__.py',
|
||
|
"",
|
||
|
),
|
||
|
(
|
||
|
'mypkg/☃.txt',
|
||
|
"",
|
||
|
),
|
||
|
]
|
||
|
sdist_name = 'setuptools-test-unicode-1.0.zip'
|
||
|
sdist = tmpdir / sdist_name
|
||
|
# can't use make_sdist, because the issue only occurs
|
||
|
# with zip sdists.
|
||
|
sdist_zip = zipfile.ZipFile(str(sdist), 'w')
|
||
|
for filename, content in files:
|
||
|
sdist_zip.writestr(filename, content)
|
||
|
sdist_zip.close()
|
||
|
return str(sdist)
|
||
|
|
||
|
@fail_on_ascii
|
||
|
def test_unicode_filename_in_sdist(self, sdist_unicode, tmpdir, monkeypatch):
|
||
|
"""
|
||
|
The install command should execute correctly even if
|
||
|
the package has unicode filenames.
|
||
|
"""
|
||
|
dist = Distribution({'script_args': ['easy_install']})
|
||
|
target = (tmpdir / 'target').ensure_dir()
|
||
|
cmd = ei.easy_install(
|
||
|
dist,
|
||
|
install_dir=str(target),
|
||
|
args=['x'],
|
||
|
)
|
||
|
monkeypatch.setitem(os.environ, 'PYTHONPATH', str(target))
|
||
|
cmd.ensure_finalized()
|
||
|
cmd.easy_install(sdist_unicode)
|
||
|
|
||
|
@pytest.fixture
|
||
|
def sdist_unicode_in_script(self, tmpdir):
|
||
|
files = [
|
||
|
(
|
||
|
"setup.py",
|
||
|
DALS(
|
||
|
"""
|
||
|
import setuptools
|
||
|
setuptools.setup(
|
||
|
name="setuptools-test-unicode",
|
||
|
version="1.0",
|
||
|
packages=["mypkg"],
|
||
|
include_package_data=True,
|
||
|
scripts=['mypkg/unicode_in_script'],
|
||
|
)
|
||
|
"""
|
||
|
),
|
||
|
),
|
||
|
("mypkg/__init__.py", ""),
|
||
|
(
|
||
|
"mypkg/unicode_in_script",
|
||
|
DALS(
|
||
|
"""
|
||
|
#!/bin/sh
|
||
|
# á
|
||
|
|
||
|
non_python_fn() {
|
||
|
}
|
||
|
"""
|
||
|
),
|
||
|
),
|
||
|
]
|
||
|
sdist_name = "setuptools-test-unicode-script-1.0.zip"
|
||
|
sdist = tmpdir / sdist_name
|
||
|
# can't use make_sdist, because the issue only occurs
|
||
|
# with zip sdists.
|
||
|
sdist_zip = zipfile.ZipFile(str(sdist), "w")
|
||
|
for filename, content in files:
|
||
|
sdist_zip.writestr(filename, content.encode('utf-8'))
|
||
|
sdist_zip.close()
|
||
|
return str(sdist)
|
||
|
|
||
|
@fail_on_ascii
|
||
|
def test_unicode_content_in_sdist(
|
||
|
self, sdist_unicode_in_script, tmpdir, monkeypatch
|
||
|
):
|
||
|
"""
|
||
|
The install command should execute correctly even if
|
||
|
the package has unicode in scripts.
|
||
|
"""
|
||
|
dist = Distribution({"script_args": ["easy_install"]})
|
||
|
target = (tmpdir / "target").ensure_dir()
|
||
|
cmd = ei.easy_install(dist, install_dir=str(target), args=["x"])
|
||
|
monkeypatch.setitem(os.environ, "PYTHONPATH", str(target))
|
||
|
cmd.ensure_finalized()
|
||
|
cmd.easy_install(sdist_unicode_in_script)
|
||
|
|
||
|
@pytest.fixture
|
||
|
def sdist_script(self, tmpdir):
|
||
|
files = [
|
||
|
(
|
||
|
'setup.py',
|
||
|
DALS(
|
||
|
"""
|
||
|
import setuptools
|
||
|
setuptools.setup(
|
||
|
name="setuptools-test-script",
|
||
|
version="1.0",
|
||
|
scripts=["mypkg_script"],
|
||
|
)
|
||
|
"""
|
||
|
),
|
||
|
),
|
||
|
(
|
||
|
'mypkg_script',
|
||
|
DALS(
|
||
|
"""
|
||
|
#/usr/bin/python
|
||
|
print('mypkg_script')
|
||
|
"""
|
||
|
),
|
||
|
),
|
||
|
]
|
||
|
sdist_name = 'setuptools-test-script-1.0.zip'
|
||
|
sdist = str(tmpdir / sdist_name)
|
||
|
make_sdist(sdist, files)
|
||
|
return sdist
|
||
|
|
||
|
@pytest.mark.skipif(
|
||
|
not sys.platform.startswith('linux'), reason="Test can only be run on Linux"
|
||
|
)
|
||
|
def test_script_install(self, sdist_script, tmpdir, monkeypatch):
|
||
|
"""
|
||
|
Check scripts are installed.
|
||
|
"""
|
||
|
dist = Distribution({'script_args': ['easy_install']})
|
||
|
target = (tmpdir / 'target').ensure_dir()
|
||
|
cmd = ei.easy_install(
|
||
|
dist,
|
||
|
install_dir=str(target),
|
||
|
args=['x'],
|
||
|
)
|
||
|
monkeypatch.setitem(os.environ, 'PYTHONPATH', str(target))
|
||
|
cmd.ensure_finalized()
|
||
|
cmd.easy_install(sdist_script)
|
||
|
assert (target / 'mypkg_script').exists()
|
||
|
|
||
|
|
||
|
@pytest.mark.filterwarnings('ignore:Unbuilt egg')
|
||
|
class TestPTHFileWriter:
|
||
|
def test_add_from_cwd_site_sets_dirty(self):
|
||
|
"""a pth file manager should set dirty
|
||
|
if a distribution is in site but also the cwd
|
||
|
"""
|
||
|
pth = PthDistributions('does-not_exist', [os.getcwd()])
|
||
|
assert not pth.dirty
|
||
|
pth.add(PRDistribution(os.getcwd()))
|
||
|
assert pth.dirty
|
||
|
|
||
|
def test_add_from_site_is_ignored(self):
|
||
|
location = '/test/location/does-not-have-to-exist'
|
||
|
# PthDistributions expects all locations to be normalized
|
||
|
location = pkg_resources.normalize_path(location)
|
||
|
pth = PthDistributions(
|
||
|
'does-not_exist',
|
||
|
[
|
||
|
location,
|
||
|
],
|
||
|
)
|
||
|
assert not pth.dirty
|
||
|
pth.add(PRDistribution(location))
|
||
|
assert not pth.dirty
|
||
|
|
||
|
def test_many_pth_distributions_merge_together(self, tmpdir):
|
||
|
"""
|
||
|
If the pth file is modified under the hood, then PthDistribution
|
||
|
will refresh its content before saving, merging contents when
|
||
|
necessary.
|
||
|
"""
|
||
|
# putting the pth file in a dedicated sub-folder,
|
||
|
pth_subdir = tmpdir.join("pth_subdir")
|
||
|
pth_subdir.mkdir()
|
||
|
pth_path = str(pth_subdir.join("file1.pth"))
|
||
|
pth1 = PthDistributions(pth_path)
|
||
|
pth2 = PthDistributions(pth_path)
|
||
|
assert (
|
||
|
pth1.paths == pth2.paths == []
|
||
|
), "unless there would be some default added at some point"
|
||
|
# and so putting the src_subdir in folder distinct than the pth one,
|
||
|
# so to keep it absolute by PthDistributions
|
||
|
new_src_path = tmpdir.join("src_subdir")
|
||
|
new_src_path.mkdir() # must exist to be accounted
|
||
|
new_src_path_str = str(new_src_path)
|
||
|
pth1.paths.append(new_src_path_str)
|
||
|
pth1.save()
|
||
|
assert (
|
||
|
pth1.paths
|
||
|
), "the new_src_path added must still be present/valid in pth1 after save"
|
||
|
# now,
|
||
|
assert (
|
||
|
new_src_path_str not in pth2.paths
|
||
|
), "right before we save the entry should still not be present"
|
||
|
pth2.save()
|
||
|
assert (
|
||
|
new_src_path_str in pth2.paths
|
||
|
), "the new_src_path entry should have been added by pth2 with its save() call"
|
||
|
assert pth2.paths[-1] == new_src_path, (
|
||
|
"and it should match exactly on the last entry actually "
|
||
|
"given we append to it in save()"
|
||
|
)
|
||
|
# finally,
|
||
|
assert PthDistributions(pth_path).paths == pth2.paths, (
|
||
|
"and we should have the exact same list at the end "
|
||
|
"with a fresh PthDistributions instance"
|
||
|
)
|
||
|
|
||
|
|
||
|
@pytest.fixture
|
||
|
def setup_context(tmpdir):
|
||
|
with (tmpdir / 'setup.py').open('w', encoding="utf-8") as f:
|
||
|
f.write(SETUP_PY)
|
||
|
with tmpdir.as_cwd():
|
||
|
yield tmpdir
|
||
|
|
||
|
|
||
|
@pytest.mark.usefixtures("user_override")
|
||
|
@pytest.mark.usefixtures("setup_context")
|
||
|
class TestUserInstallTest:
|
||
|
# prevent check that site-packages is writable. easy_install
|
||
|
# shouldn't be writing to system site-packages during finalize
|
||
|
# options, but while it does, bypass the behavior.
|
||
|
prev_sp_write = mock.patch(
|
||
|
'setuptools.command.easy_install.easy_install.check_site_dir',
|
||
|
mock.Mock(),
|
||
|
)
|
||
|
|
||
|
# simulate setuptools installed in user site packages
|
||
|
@mock.patch('setuptools.command.easy_install.__file__', site.USER_SITE)
|
||
|
@mock.patch('site.ENABLE_USER_SITE', True)
|
||
|
@prev_sp_write
|
||
|
def test_user_install_not_implied_user_site_enabled(self):
|
||
|
self.assert_not_user_site()
|
||
|
|
||
|
@mock.patch('site.ENABLE_USER_SITE', False)
|
||
|
@prev_sp_write
|
||
|
def test_user_install_not_implied_user_site_disabled(self):
|
||
|
self.assert_not_user_site()
|
||
|
|
||
|
@staticmethod
|
||
|
def assert_not_user_site():
|
||
|
# create a finalized easy_install command
|
||
|
dist = Distribution()
|
||
|
dist.script_name = 'setup.py'
|
||
|
cmd = ei.easy_install(dist)
|
||
|
cmd.args = ['py']
|
||
|
cmd.ensure_finalized()
|
||
|
assert not cmd.user, 'user should not be implied'
|
||
|
|
||
|
def test_multiproc_atexit(self):
|
||
|
pytest.importorskip('multiprocessing')
|
||
|
|
||
|
log = logging.getLogger('test_easy_install')
|
||
|
logging.basicConfig(level=logging.INFO, stream=sys.stderr)
|
||
|
log.info('this should not break')
|
||
|
|
||
|
@pytest.fixture()
|
||
|
def foo_package(self, tmpdir):
|
||
|
egg_file = tmpdir / 'foo-1.0.egg-info'
|
||
|
with egg_file.open('w') as f:
|
||
|
f.write('Name: foo\n')
|
||
|
return str(tmpdir)
|
||
|
|
||
|
@pytest.fixture()
|
||
|
def install_target(self, tmpdir):
|
||
|
target = str(tmpdir)
|
||
|
with mock.patch('sys.path', sys.path + [target]):
|
||
|
python_path = os.path.pathsep.join(sys.path)
|
||
|
with mock.patch.dict(os.environ, PYTHONPATH=python_path):
|
||
|
yield target
|
||
|
|
||
|
def test_local_index(self, foo_package, install_target):
|
||
|
"""
|
||
|
The local index must be used when easy_install locates installed
|
||
|
packages.
|
||
|
"""
|
||
|
dist = Distribution()
|
||
|
dist.script_name = 'setup.py'
|
||
|
cmd = ei.easy_install(dist)
|
||
|
cmd.install_dir = install_target
|
||
|
cmd.args = ['foo']
|
||
|
cmd.ensure_finalized()
|
||
|
cmd.local_index.scan([foo_package])
|
||
|
res = cmd.easy_install('foo')
|
||
|
actual = os.path.normcase(os.path.realpath(res.location))
|
||
|
expected = os.path.normcase(os.path.realpath(foo_package))
|
||
|
assert actual == expected
|
||
|
|
||
|
@contextlib.contextmanager
|
||
|
def user_install_setup_context(self, *args, **kwargs):
|
||
|
"""
|
||
|
Wrap sandbox.setup_context to patch easy_install in that context to
|
||
|
appear as user-installed.
|
||
|
"""
|
||
|
with self.orig_context(*args, **kwargs):
|
||
|
import setuptools.command.easy_install as ei
|
||
|
|
||
|
ei.__file__ = site.USER_SITE
|
||
|
yield
|
||
|
|
||
|
def patched_setup_context(self):
|
||
|
self.orig_context = sandbox.setup_context
|
||
|
|
||
|
return mock.patch(
|
||
|
'setuptools.sandbox.setup_context',
|
||
|
self.user_install_setup_context,
|
||
|
)
|
||
|
|
||
|
|
||
|
@pytest.fixture
|
||
|
def distutils_package():
|
||
|
distutils_setup_py = SETUP_PY.replace(
|
||
|
'from setuptools import setup',
|
||
|
'from distutils.core import setup',
|
||
|
)
|
||
|
with contexts.tempdir(cd=os.chdir):
|
||
|
with open('setup.py', 'w', encoding="utf-8") as f:
|
||
|
f.write(distutils_setup_py)
|
||
|
yield
|
||
|
|
||
|
|
||
|
@pytest.fixture
|
||
|
def mock_index():
|
||
|
# set up a server which will simulate an alternate package index.
|
||
|
p_index = MockServer()
|
||
|
if p_index.server_port == 0:
|
||
|
# Some platforms (Jython) don't find a port to which to bind,
|
||
|
# so skip test for them.
|
||
|
pytest.skip("could not find a valid port")
|
||
|
p_index.start()
|
||
|
return p_index
|
||
|
|
||
|
|
||
|
class TestDistutilsPackage:
|
||
|
def test_bdist_egg_available_on_distutils_pkg(self, distutils_package):
|
||
|
run_setup('setup.py', ['bdist_egg'])
|
||
|
|
||
|
|
||
|
class TestInstallRequires:
|
||
|
def test_setup_install_includes_dependencies(self, tmp_path, mock_index):
|
||
|
"""
|
||
|
When ``python setup.py install`` is called directly, it will use easy_install
|
||
|
to fetch dependencies.
|
||
|
"""
|
||
|
# TODO: Remove these tests once `setup.py install` is completely removed
|
||
|
project_root = tmp_path / "project"
|
||
|
project_root.mkdir(exist_ok=True)
|
||
|
install_root = tmp_path / "install"
|
||
|
install_root.mkdir(exist_ok=True)
|
||
|
|
||
|
self.create_project(project_root)
|
||
|
cmd = [
|
||
|
sys.executable,
|
||
|
'-c',
|
||
|
'__import__("setuptools").setup()',
|
||
|
'install',
|
||
|
'--install-base',
|
||
|
str(install_root),
|
||
|
'--install-lib',
|
||
|
str(install_root),
|
||
|
'--install-headers',
|
||
|
str(install_root),
|
||
|
'--install-scripts',
|
||
|
str(install_root),
|
||
|
'--install-data',
|
||
|
str(install_root),
|
||
|
'--install-purelib',
|
||
|
str(install_root),
|
||
|
'--install-platlib',
|
||
|
str(install_root),
|
||
|
]
|
||
|
env = {**os.environ, "__EASYINSTALL_INDEX": mock_index.url}
|
||
|
cp = subprocess.run(
|
||
|
cmd,
|
||
|
cwd=str(project_root),
|
||
|
env=env,
|
||
|
stdout=subprocess.PIPE,
|
||
|
stderr=subprocess.STDOUT,
|
||
|
text=True,
|
||
|
encoding="utf-8",
|
||
|
)
|
||
|
assert cp.returncode != 0
|
||
|
try:
|
||
|
assert '/does-not-exist/' in {r.path for r in mock_index.requests}
|
||
|
assert next(
|
||
|
line
|
||
|
for line in cp.stdout.splitlines()
|
||
|
if "not find suitable distribution for" in line
|
||
|
and "does-not-exist" in line
|
||
|
)
|
||
|
except Exception:
|
||
|
if "failed to get random numbers" in cp.stdout:
|
||
|
pytest.xfail(f"{sys.platform} failure - {cp.stdout}")
|
||
|
raise
|
||
|
|
||
|
def create_project(self, root):
|
||
|
config = """
|
||
|
[metadata]
|
||
|
name = project
|
||
|
version = 42
|
||
|
|
||
|
[options]
|
||
|
install_requires = does-not-exist
|
||
|
py_modules = mod
|
||
|
"""
|
||
|
(root / 'setup.cfg').write_text(DALS(config), encoding="utf-8")
|
||
|
(root / 'mod.py').touch()
|
||
|
|
||
|
|
||
|
class TestSetupRequires:
|
||
|
def test_setup_requires_honors_fetch_params(self, mock_index, monkeypatch):
|
||
|
"""
|
||
|
When easy_install installs a source distribution which specifies
|
||
|
setup_requires, it should honor the fetch parameters (such as
|
||
|
index-url, and find-links).
|
||
|
"""
|
||
|
monkeypatch.setenv('PIP_RETRIES', '0')
|
||
|
monkeypatch.setenv('PIP_TIMEOUT', '0')
|
||
|
monkeypatch.setenv('PIP_NO_INDEX', 'false')
|
||
|
with contexts.quiet():
|
||
|
# create an sdist that has a build-time dependency.
|
||
|
with TestSetupRequires.create_sdist() as dist_file:
|
||
|
with contexts.tempdir() as temp_install_dir:
|
||
|
with contexts.environment(PYTHONPATH=temp_install_dir):
|
||
|
cmd = [
|
||
|
sys.executable,
|
||
|
'-c',
|
||
|
'__import__("setuptools").setup()',
|
||
|
'easy_install',
|
||
|
'--index-url',
|
||
|
mock_index.url,
|
||
|
'--exclude-scripts',
|
||
|
'--install-dir',
|
||
|
temp_install_dir,
|
||
|
dist_file,
|
||
|
]
|
||
|
subprocess.Popen(cmd).wait()
|
||
|
# there should have been one requests to the server
|
||
|
assert [r.path for r in mock_index.requests] == ['/does-not-exist/']
|
||
|
|
||
|
@staticmethod
|
||
|
@contextlib.contextmanager
|
||
|
def create_sdist():
|
||
|
"""
|
||
|
Return an sdist with a setup_requires dependency (of something that
|
||
|
doesn't exist)
|
||
|
"""
|
||
|
with contexts.tempdir() as dir:
|
||
|
dist_path = os.path.join(dir, 'setuptools-test-fetcher-1.0.tar.gz')
|
||
|
make_sdist(
|
||
|
dist_path,
|
||
|
[
|
||
|
(
|
||
|
'setup.py',
|
||
|
DALS(
|
||
|
"""
|
||
|
import setuptools
|
||
|
setuptools.setup(
|
||
|
name="setuptools-test-fetcher",
|
||
|
version="1.0",
|
||
|
setup_requires = ['does-not-exist'],
|
||
|
)
|
||
|
"""
|
||
|
),
|
||
|
),
|
||
|
('setup.cfg', ''),
|
||
|
],
|
||
|
)
|
||
|
yield dist_path
|
||
|
|
||
|
use_setup_cfg = (
|
||
|
(),
|
||
|
('dependency_links',),
|
||
|
('setup_requires',),
|
||
|
('dependency_links', 'setup_requires'),
|
||
|
)
|
||
|
|
||
|
@pytest.mark.parametrize('use_setup_cfg', use_setup_cfg)
|
||
|
def test_setup_requires_overrides_version_conflict(self, use_setup_cfg):
|
||
|
"""
|
||
|
Regression test for distribution issue 323:
|
||
|
https://bitbucket.org/tarek/distribute/issues/323
|
||
|
|
||
|
Ensures that a distribution's setup_requires requirements can still be
|
||
|
installed and used locally even if a conflicting version of that
|
||
|
requirement is already on the path.
|
||
|
"""
|
||
|
|
||
|
fake_dist = PRDistribution(
|
||
|
'does-not-matter', project_name='foobar', version='0.0'
|
||
|
)
|
||
|
working_set.add(fake_dist)
|
||
|
|
||
|
with contexts.save_pkg_resources_state():
|
||
|
with contexts.tempdir() as temp_dir:
|
||
|
test_pkg = create_setup_requires_package(
|
||
|
temp_dir, use_setup_cfg=use_setup_cfg
|
||
|
)
|
||
|
test_setup_py = os.path.join(test_pkg, 'setup.py')
|
||
|
with contexts.quiet() as (stdout, stderr):
|
||
|
# Don't even need to install the package, just
|
||
|
# running the setup.py at all is sufficient
|
||
|
run_setup(test_setup_py, ['--name'])
|
||
|
|
||
|
lines = stdout.readlines()
|
||
|
assert len(lines) > 0
|
||
|
assert lines[-1].strip() == 'test_pkg'
|
||
|
|
||
|
@pytest.mark.parametrize('use_setup_cfg', use_setup_cfg)
|
||
|
def test_setup_requires_override_nspkg(self, use_setup_cfg):
|
||
|
"""
|
||
|
Like ``test_setup_requires_overrides_version_conflict`` but where the
|
||
|
``setup_requires`` package is part of a namespace package that has
|
||
|
*already* been imported.
|
||
|
"""
|
||
|
|
||
|
with contexts.save_pkg_resources_state():
|
||
|
with contexts.tempdir() as temp_dir:
|
||
|
foobar_1_archive = os.path.join(temp_dir, 'foo.bar-0.1.tar.gz')
|
||
|
make_nspkg_sdist(foobar_1_archive, 'foo.bar', '0.1')
|
||
|
# Now actually go ahead an extract to the temp dir and add the
|
||
|
# extracted path to sys.path so foo.bar v0.1 is importable
|
||
|
foobar_1_dir = os.path.join(temp_dir, 'foo.bar-0.1')
|
||
|
os.mkdir(foobar_1_dir)
|
||
|
with tarfile.open(foobar_1_archive) as tf:
|
||
|
tf.extraction_filter = lambda member, path: member
|
||
|
tf.extractall(foobar_1_dir)
|
||
|
sys.path.insert(1, foobar_1_dir)
|
||
|
|
||
|
dist = PRDistribution(
|
||
|
foobar_1_dir, project_name='foo.bar', version='0.1'
|
||
|
)
|
||
|
working_set.add(dist)
|
||
|
|
||
|
template = DALS(
|
||
|
"""\
|
||
|
import foo # Even with foo imported first the
|
||
|
# setup_requires package should override
|
||
|
import setuptools
|
||
|
setuptools.setup(**%r)
|
||
|
|
||
|
if not (hasattr(foo, '__path__') and
|
||
|
len(foo.__path__) == 2):
|
||
|
print('FAIL')
|
||
|
|
||
|
if 'foo.bar-0.2' not in foo.__path__[0]:
|
||
|
print('FAIL')
|
||
|
"""
|
||
|
)
|
||
|
|
||
|
test_pkg = create_setup_requires_package(
|
||
|
temp_dir,
|
||
|
'foo.bar',
|
||
|
'0.2',
|
||
|
make_nspkg_sdist,
|
||
|
template,
|
||
|
use_setup_cfg=use_setup_cfg,
|
||
|
)
|
||
|
|
||
|
test_setup_py = os.path.join(test_pkg, 'setup.py')
|
||
|
|
||
|
with contexts.quiet() as (stdout, stderr):
|
||
|
try:
|
||
|
# Don't even need to install the package, just
|
||
|
# running the setup.py at all is sufficient
|
||
|
run_setup(test_setup_py, ['--name'])
|
||
|
except pkg_resources.VersionConflict:
|
||
|
self.fail(
|
||
|
'Installing setup.py requirements '
|
||
|
'caused a VersionConflict'
|
||
|
)
|
||
|
|
||
|
assert 'FAIL' not in stdout.getvalue()
|
||
|
lines = stdout.readlines()
|
||
|
assert len(lines) > 0
|
||
|
assert lines[-1].strip() == 'test_pkg'
|
||
|
|
||
|
@pytest.mark.parametrize('use_setup_cfg', use_setup_cfg)
|
||
|
def test_setup_requires_with_attr_version(self, use_setup_cfg):
|
||
|
def make_dependency_sdist(dist_path, distname, version):
|
||
|
files = [
|
||
|
(
|
||
|
'setup.py',
|
||
|
DALS(
|
||
|
"""
|
||
|
import setuptools
|
||
|
setuptools.setup(
|
||
|
name={name!r},
|
||
|
version={version!r},
|
||
|
py_modules=[{name!r}],
|
||
|
)
|
||
|
""".format(name=distname, version=version)
|
||
|
),
|
||
|
),
|
||
|
(
|
||
|
distname + '.py',
|
||
|
DALS(
|
||
|
"""
|
||
|
version = 42
|
||
|
"""
|
||
|
),
|
||
|
),
|
||
|
]
|
||
|
make_sdist(dist_path, files)
|
||
|
|
||
|
with contexts.save_pkg_resources_state():
|
||
|
with contexts.tempdir() as temp_dir:
|
||
|
test_pkg = create_setup_requires_package(
|
||
|
temp_dir,
|
||
|
setup_attrs=dict(version='attr: foobar.version'),
|
||
|
make_package=make_dependency_sdist,
|
||
|
use_setup_cfg=use_setup_cfg + ('version',),
|
||
|
)
|
||
|
test_setup_py = os.path.join(test_pkg, 'setup.py')
|
||
|
with contexts.quiet() as (stdout, stderr):
|
||
|
run_setup(test_setup_py, ['--version'])
|
||
|
lines = stdout.readlines()
|
||
|
assert len(lines) > 0
|
||
|
assert lines[-1].strip() == '42'
|
||
|
|
||
|
def test_setup_requires_honors_pip_env(self, mock_index, monkeypatch):
|
||
|
monkeypatch.setenv('PIP_RETRIES', '0')
|
||
|
monkeypatch.setenv('PIP_TIMEOUT', '0')
|
||
|
monkeypatch.setenv('PIP_NO_INDEX', 'false')
|
||
|
monkeypatch.setenv('PIP_INDEX_URL', mock_index.url)
|
||
|
with contexts.save_pkg_resources_state():
|
||
|
with contexts.tempdir() as temp_dir:
|
||
|
test_pkg = create_setup_requires_package(
|
||
|
temp_dir,
|
||
|
'python-xlib',
|
||
|
'0.19',
|
||
|
setup_attrs=dict(dependency_links=[]),
|
||
|
)
|
||
|
test_setup_cfg = os.path.join(test_pkg, 'setup.cfg')
|
||
|
with open(test_setup_cfg, 'w', encoding="utf-8") as fp:
|
||
|
fp.write(
|
||
|
DALS(
|
||
|
"""
|
||
|
[easy_install]
|
||
|
index_url = https://pypi.org/legacy/
|
||
|
"""
|
||
|
)
|
||
|
)
|
||
|
test_setup_py = os.path.join(test_pkg, 'setup.py')
|
||
|
with pytest.raises(distutils.errors.DistutilsError):
|
||
|
run_setup(test_setup_py, ['--version'])
|
||
|
assert len(mock_index.requests) == 1
|
||
|
assert mock_index.requests[0].path == '/python-xlib/'
|
||
|
|
||
|
def test_setup_requires_with_pep508_url(self, mock_index, monkeypatch):
|
||
|
monkeypatch.setenv('PIP_RETRIES', '0')
|
||
|
monkeypatch.setenv('PIP_TIMEOUT', '0')
|
||
|
monkeypatch.setenv('PIP_INDEX_URL', mock_index.url)
|
||
|
with contexts.save_pkg_resources_state():
|
||
|
with contexts.tempdir() as temp_dir:
|
||
|
dep_sdist = os.path.join(temp_dir, 'dep.tar.gz')
|
||
|
make_trivial_sdist(dep_sdist, 'dependency', '42')
|
||
|
dep_url = path_to_url(dep_sdist, authority='localhost')
|
||
|
test_pkg = create_setup_requires_package(
|
||
|
temp_dir,
|
||
|
# Ignored (overridden by setup_attrs)
|
||
|
'python-xlib',
|
||
|
'0.19',
|
||
|
setup_attrs=dict(setup_requires='dependency @ %s' % dep_url),
|
||
|
)
|
||
|
test_setup_py = os.path.join(test_pkg, 'setup.py')
|
||
|
run_setup(test_setup_py, ['--version'])
|
||
|
assert len(mock_index.requests) == 0
|
||
|
|
||
|
def test_setup_requires_with_allow_hosts(self, mock_index):
|
||
|
"""The `allow-hosts` option in not supported anymore."""
|
||
|
files = {
|
||
|
'test_pkg': {
|
||
|
'setup.py': DALS(
|
||
|
"""
|
||
|
from setuptools import setup
|
||
|
setup(setup_requires='python-xlib')
|
||
|
"""
|
||
|
),
|
||
|
'setup.cfg': DALS(
|
||
|
"""
|
||
|
[easy_install]
|
||
|
allow_hosts = *
|
||
|
"""
|
||
|
),
|
||
|
}
|
||
|
}
|
||
|
with contexts.save_pkg_resources_state():
|
||
|
with contexts.tempdir() as temp_dir:
|
||
|
path.build(files, prefix=temp_dir)
|
||
|
setup_py = str(pathlib.Path(temp_dir, 'test_pkg', 'setup.py'))
|
||
|
with pytest.raises(distutils.errors.DistutilsError):
|
||
|
run_setup(setup_py, ['--version'])
|
||
|
assert len(mock_index.requests) == 0
|
||
|
|
||
|
def test_setup_requires_with_python_requires(self, monkeypatch, tmpdir):
|
||
|
"""Check `python_requires` is honored."""
|
||
|
monkeypatch.setenv('PIP_RETRIES', '0')
|
||
|
monkeypatch.setenv('PIP_TIMEOUT', '0')
|
||
|
monkeypatch.setenv('PIP_NO_INDEX', '1')
|
||
|
monkeypatch.setenv('PIP_VERBOSE', '1')
|
||
|
dep_1_0_sdist = 'dep-1.0.tar.gz'
|
||
|
dep_1_0_url = path_to_url(str(tmpdir / dep_1_0_sdist))
|
||
|
dep_1_0_python_requires = '>=2.7'
|
||
|
make_python_requires_sdist(
|
||
|
str(tmpdir / dep_1_0_sdist), 'dep', '1.0', dep_1_0_python_requires
|
||
|
)
|
||
|
dep_2_0_sdist = 'dep-2.0.tar.gz'
|
||
|
dep_2_0_url = path_to_url(str(tmpdir / dep_2_0_sdist))
|
||
|
dep_2_0_python_requires = '!=' + '.'.join(map(str, sys.version_info[:2])) + '.*'
|
||
|
make_python_requires_sdist(
|
||
|
str(tmpdir / dep_2_0_sdist), 'dep', '2.0', dep_2_0_python_requires
|
||
|
)
|
||
|
index = tmpdir / 'index.html'
|
||
|
index.write_text(
|
||
|
DALS(
|
||
|
"""
|
||
|
<!DOCTYPE html>
|
||
|
<html><head><title>Links for dep</title></head>
|
||
|
<body>
|
||
|
<h1>Links for dep</h1>
|
||
|
<a href="{dep_1_0_url}"\
|
||
|
data-requires-python="{dep_1_0_python_requires}">{dep_1_0_sdist}</a><br/>
|
||
|
<a href="{dep_2_0_url}"\
|
||
|
data-requires-python="{dep_2_0_python_requires}">{dep_2_0_sdist}</a><br/>
|
||
|
</body>
|
||
|
</html>
|
||
|
"""
|
||
|
).format(
|
||
|
dep_1_0_url=dep_1_0_url,
|
||
|
dep_1_0_sdist=dep_1_0_sdist,
|
||
|
dep_1_0_python_requires=dep_1_0_python_requires,
|
||
|
dep_2_0_url=dep_2_0_url,
|
||
|
dep_2_0_sdist=dep_2_0_sdist,
|
||
|
dep_2_0_python_requires=dep_2_0_python_requires,
|
||
|
),
|
||
|
'utf-8',
|
||
|
)
|
||
|
index_url = path_to_url(str(index))
|
||
|
with contexts.save_pkg_resources_state():
|
||
|
test_pkg = create_setup_requires_package(
|
||
|
str(tmpdir),
|
||
|
'python-xlib',
|
||
|
'0.19', # Ignored (overridden by setup_attrs).
|
||
|
setup_attrs=dict(setup_requires='dep', dependency_links=[index_url]),
|
||
|
)
|
||
|
test_setup_py = os.path.join(test_pkg, 'setup.py')
|
||
|
run_setup(test_setup_py, ['--version'])
|
||
|
eggs = list(
|
||
|
map(str, pkg_resources.find_distributions(os.path.join(test_pkg, '.eggs')))
|
||
|
)
|
||
|
assert eggs == ['dep 1.0']
|
||
|
|
||
|
@pytest.mark.parametrize('with_dependency_links_in_setup_py', (False, True))
|
||
|
def test_setup_requires_with_find_links_in_setup_cfg(
|
||
|
self, monkeypatch, with_dependency_links_in_setup_py
|
||
|
):
|
||
|
monkeypatch.setenv('PIP_RETRIES', '0')
|
||
|
monkeypatch.setenv('PIP_TIMEOUT', '0')
|
||
|
with contexts.save_pkg_resources_state():
|
||
|
with contexts.tempdir() as temp_dir:
|
||
|
make_trivial_sdist(
|
||
|
os.path.join(temp_dir, 'python-xlib-42.tar.gz'), 'python-xlib', '42'
|
||
|
)
|
||
|
test_pkg = os.path.join(temp_dir, 'test_pkg')
|
||
|
test_setup_py = os.path.join(test_pkg, 'setup.py')
|
||
|
test_setup_cfg = os.path.join(test_pkg, 'setup.cfg')
|
||
|
os.mkdir(test_pkg)
|
||
|
with open(test_setup_py, 'w', encoding="utf-8") as fp:
|
||
|
if with_dependency_links_in_setup_py:
|
||
|
dependency_links = [os.path.join(temp_dir, 'links')]
|
||
|
else:
|
||
|
dependency_links = []
|
||
|
fp.write(
|
||
|
DALS(
|
||
|
"""
|
||
|
from setuptools import installer, setup
|
||
|
setup(setup_requires='python-xlib==42',
|
||
|
dependency_links={dependency_links!r})
|
||
|
"""
|
||
|
).format(dependency_links=dependency_links)
|
||
|
)
|
||
|
with open(test_setup_cfg, 'w', encoding="utf-8") as fp:
|
||
|
fp.write(
|
||
|
DALS(
|
||
|
"""
|
||
|
[easy_install]
|
||
|
index_url = {index_url}
|
||
|
find_links = {find_links}
|
||
|
"""
|
||
|
).format(
|
||
|
index_url=os.path.join(temp_dir, 'index'),
|
||
|
find_links=temp_dir,
|
||
|
)
|
||
|
)
|
||
|
run_setup(test_setup_py, ['--version'])
|
||
|
|
||
|
def test_setup_requires_with_transitive_extra_dependency(self, monkeypatch):
|
||
|
"""
|
||
|
Use case: installing a package with a build dependency on
|
||
|
an already installed `dep[extra]`, which in turn depends
|
||
|
on `extra_dep` (whose is not already installed).
|
||
|
"""
|
||
|
with contexts.save_pkg_resources_state():
|
||
|
with contexts.tempdir() as temp_dir:
|
||
|
# Create source distribution for `extra_dep`.
|
||
|
make_trivial_sdist(
|
||
|
os.path.join(temp_dir, 'extra_dep-1.0.tar.gz'), 'extra_dep', '1.0'
|
||
|
)
|
||
|
# Create source tree for `dep`.
|
||
|
dep_pkg = os.path.join(temp_dir, 'dep')
|
||
|
os.mkdir(dep_pkg)
|
||
|
path.build(
|
||
|
{
|
||
|
'setup.py': DALS(
|
||
|
"""
|
||
|
import setuptools
|
||
|
setuptools.setup(
|
||
|
name='dep', version='2.0',
|
||
|
extras_require={'extra': ['extra_dep']},
|
||
|
)
|
||
|
"""
|
||
|
),
|
||
|
'setup.cfg': '',
|
||
|
},
|
||
|
prefix=dep_pkg,
|
||
|
)
|
||
|
# "Install" dep.
|
||
|
run_setup(os.path.join(dep_pkg, 'setup.py'), ['dist_info'])
|
||
|
working_set.add_entry(dep_pkg)
|
||
|
# Create source tree for test package.
|
||
|
test_pkg = os.path.join(temp_dir, 'test_pkg')
|
||
|
test_setup_py = os.path.join(test_pkg, 'setup.py')
|
||
|
os.mkdir(test_pkg)
|
||
|
with open(test_setup_py, 'w', encoding="utf-8") as fp:
|
||
|
fp.write(
|
||
|
DALS(
|
||
|
"""
|
||
|
from setuptools import installer, setup
|
||
|
setup(setup_requires='dep[extra]')
|
||
|
"""
|
||
|
)
|
||
|
)
|
||
|
# Check...
|
||
|
monkeypatch.setenv('PIP_FIND_LINKS', str(temp_dir))
|
||
|
monkeypatch.setenv('PIP_NO_INDEX', '1')
|
||
|
monkeypatch.setenv('PIP_RETRIES', '0')
|
||
|
monkeypatch.setenv('PIP_TIMEOUT', '0')
|
||
|
run_setup(test_setup_py, ['--version'])
|
||
|
|
||
|
def test_setup_requires_with_distutils_command_dep(self, monkeypatch):
|
||
|
"""
|
||
|
Use case: ensure build requirements' extras
|
||
|
are properly installed and activated.
|
||
|
"""
|
||
|
with contexts.save_pkg_resources_state():
|
||
|
with contexts.tempdir() as temp_dir:
|
||
|
# Create source distribution for `extra_dep`.
|
||
|
make_sdist(
|
||
|
os.path.join(temp_dir, 'extra_dep-1.0.tar.gz'),
|
||
|
[
|
||
|
(
|
||
|
'setup.py',
|
||
|
DALS(
|
||
|
"""
|
||
|
import setuptools
|
||
|
setuptools.setup(
|
||
|
name='extra_dep',
|
||
|
version='1.0',
|
||
|
py_modules=['extra_dep'],
|
||
|
)
|
||
|
"""
|
||
|
),
|
||
|
),
|
||
|
('setup.cfg', ''),
|
||
|
('extra_dep.py', ''),
|
||
|
],
|
||
|
)
|
||
|
# Create source tree for `epdep`.
|
||
|
dep_pkg = os.path.join(temp_dir, 'epdep')
|
||
|
os.mkdir(dep_pkg)
|
||
|
path.build(
|
||
|
{
|
||
|
'setup.py': DALS(
|
||
|
"""
|
||
|
import setuptools
|
||
|
setuptools.setup(
|
||
|
name='dep', version='2.0',
|
||
|
py_modules=['epcmd'],
|
||
|
extras_require={'extra': ['extra_dep']},
|
||
|
entry_points='''
|
||
|
[distutils.commands]
|
||
|
epcmd = epcmd:epcmd [extra]
|
||
|
''',
|
||
|
)
|
||
|
"""
|
||
|
),
|
||
|
'setup.cfg': '',
|
||
|
'epcmd.py': DALS(
|
||
|
"""
|
||
|
from distutils.command.build_py import build_py
|
||
|
|
||
|
import extra_dep
|
||
|
|
||
|
class epcmd(build_py):
|
||
|
pass
|
||
|
"""
|
||
|
),
|
||
|
},
|
||
|
prefix=dep_pkg,
|
||
|
)
|
||
|
# "Install" dep.
|
||
|
run_setup(os.path.join(dep_pkg, 'setup.py'), ['dist_info'])
|
||
|
working_set.add_entry(dep_pkg)
|
||
|
# Create source tree for test package.
|
||
|
test_pkg = os.path.join(temp_dir, 'test_pkg')
|
||
|
test_setup_py = os.path.join(test_pkg, 'setup.py')
|
||
|
os.mkdir(test_pkg)
|
||
|
with open(test_setup_py, 'w', encoding="utf-8") as fp:
|
||
|
fp.write(
|
||
|
DALS(
|
||
|
"""
|
||
|
from setuptools import installer, setup
|
||
|
setup(setup_requires='dep[extra]')
|
||
|
"""
|
||
|
)
|
||
|
)
|
||
|
# Check...
|
||
|
monkeypatch.setenv('PIP_FIND_LINKS', str(temp_dir))
|
||
|
monkeypatch.setenv('PIP_NO_INDEX', '1')
|
||
|
monkeypatch.setenv('PIP_RETRIES', '0')
|
||
|
monkeypatch.setenv('PIP_TIMEOUT', '0')
|
||
|
run_setup(test_setup_py, ['epcmd'])
|
||
|
|
||
|
|
||
|
def make_trivial_sdist(dist_path, distname, version):
|
||
|
"""
|
||
|
Create a simple sdist tarball at dist_path, containing just a simple
|
||
|
setup.py.
|
||
|
"""
|
||
|
|
||
|
make_sdist(
|
||
|
dist_path,
|
||
|
[
|
||
|
(
|
||
|
'setup.py',
|
||
|
DALS(
|
||
|
"""\
|
||
|
import setuptools
|
||
|
setuptools.setup(
|
||
|
name=%r,
|
||
|
version=%r
|
||
|
)
|
||
|
"""
|
||
|
% (distname, version)
|
||
|
),
|
||
|
),
|
||
|
('setup.cfg', ''),
|
||
|
],
|
||
|
)
|
||
|
|
||
|
|
||
|
def make_nspkg_sdist(dist_path, distname, version):
|
||
|
"""
|
||
|
Make an sdist tarball with distname and version which also contains one
|
||
|
package with the same name as distname. The top-level package is
|
||
|
designated a namespace package).
|
||
|
"""
|
||
|
|
||
|
parts = distname.split('.')
|
||
|
nspackage = parts[0]
|
||
|
|
||
|
packages = ['.'.join(parts[:idx]) for idx in range(1, len(parts) + 1)]
|
||
|
|
||
|
setup_py = DALS(
|
||
|
"""\
|
||
|
import setuptools
|
||
|
setuptools.setup(
|
||
|
name=%r,
|
||
|
version=%r,
|
||
|
packages=%r,
|
||
|
namespace_packages=[%r]
|
||
|
)
|
||
|
"""
|
||
|
% (distname, version, packages, nspackage)
|
||
|
)
|
||
|
|
||
|
init = "__import__('pkg_resources').declare_namespace(__name__)"
|
||
|
|
||
|
files = [('setup.py', setup_py), (os.path.join(nspackage, '__init__.py'), init)]
|
||
|
for package in packages[1:]:
|
||
|
filename = os.path.join(*(package.split('.') + ['__init__.py']))
|
||
|
files.append((filename, ''))
|
||
|
|
||
|
make_sdist(dist_path, files)
|
||
|
|
||
|
|
||
|
def make_python_requires_sdist(dist_path, distname, version, python_requires):
|
||
|
make_sdist(
|
||
|
dist_path,
|
||
|
[
|
||
|
(
|
||
|
'setup.py',
|
||
|
DALS(
|
||
|
"""\
|
||
|
import setuptools
|
||
|
setuptools.setup(
|
||
|
name={name!r},
|
||
|
version={version!r},
|
||
|
python_requires={python_requires!r},
|
||
|
)
|
||
|
"""
|
||
|
).format(
|
||
|
name=distname, version=version, python_requires=python_requires
|
||
|
),
|
||
|
),
|
||
|
('setup.cfg', ''),
|
||
|
],
|
||
|
)
|
||
|
|
||
|
|
||
|
def make_sdist(dist_path, files):
|
||
|
"""
|
||
|
Create a simple sdist tarball at dist_path, containing the files
|
||
|
listed in ``files`` as ``(filename, content)`` tuples.
|
||
|
"""
|
||
|
|
||
|
# Distributions with only one file don't play well with pip.
|
||
|
assert len(files) > 1
|
||
|
with tarfile.open(dist_path, 'w:gz') as dist:
|
||
|
for filename, content in files:
|
||
|
file_bytes = io.BytesIO(content.encode('utf-8'))
|
||
|
file_info = tarfile.TarInfo(name=filename)
|
||
|
file_info.size = len(file_bytes.getvalue())
|
||
|
file_info.mtime = int(time.time())
|
||
|
dist.addfile(file_info, fileobj=file_bytes)
|
||
|
|
||
|
|
||
|
def create_setup_requires_package(
|
||
|
path,
|
||
|
distname='foobar',
|
||
|
version='0.1',
|
||
|
make_package=make_trivial_sdist,
|
||
|
setup_py_template=None,
|
||
|
setup_attrs=None,
|
||
|
use_setup_cfg=(),
|
||
|
):
|
||
|
"""Creates a source tree under path for a trivial test package that has a
|
||
|
single requirement in setup_requires--a tarball for that requirement is
|
||
|
also created and added to the dependency_links argument.
|
||
|
|
||
|
``distname`` and ``version`` refer to the name/version of the package that
|
||
|
the test package requires via ``setup_requires``. The name of the test
|
||
|
package itself is just 'test_pkg'.
|
||
|
"""
|
||
|
|
||
|
test_setup_attrs = {
|
||
|
'name': 'test_pkg',
|
||
|
'version': '0.0',
|
||
|
'setup_requires': ['%s==%s' % (distname, version)],
|
||
|
'dependency_links': [os.path.abspath(path)],
|
||
|
}
|
||
|
if setup_attrs:
|
||
|
test_setup_attrs.update(setup_attrs)
|
||
|
|
||
|
test_pkg = os.path.join(path, 'test_pkg')
|
||
|
os.mkdir(test_pkg)
|
||
|
|
||
|
# setup.cfg
|
||
|
if use_setup_cfg:
|
||
|
options = []
|
||
|
metadata = []
|
||
|
for name in use_setup_cfg:
|
||
|
value = test_setup_attrs.pop(name)
|
||
|
if name in 'name version'.split():
|
||
|
section = metadata
|
||
|
else:
|
||
|
section = options
|
||
|
if isinstance(value, (tuple, list)):
|
||
|
value = ';'.join(value)
|
||
|
section.append('%s: %s' % (name, value))
|
||
|
test_setup_cfg_contents = DALS(
|
||
|
"""
|
||
|
[metadata]
|
||
|
{metadata}
|
||
|
[options]
|
||
|
{options}
|
||
|
"""
|
||
|
).format(
|
||
|
options='\n'.join(options),
|
||
|
metadata='\n'.join(metadata),
|
||
|
)
|
||
|
else:
|
||
|
test_setup_cfg_contents = ''
|
||
|
with open(os.path.join(test_pkg, 'setup.cfg'), 'w', encoding="utf-8") as f:
|
||
|
f.write(test_setup_cfg_contents)
|
||
|
|
||
|
# setup.py
|
||
|
if setup_py_template is None:
|
||
|
setup_py_template = DALS(
|
||
|
"""\
|
||
|
import setuptools
|
||
|
setuptools.setup(**%r)
|
||
|
"""
|
||
|
)
|
||
|
with open(os.path.join(test_pkg, 'setup.py'), 'w', encoding="utf-8") as f:
|
||
|
f.write(setup_py_template % test_setup_attrs)
|
||
|
|
||
|
foobar_path = os.path.join(path, '%s-%s.tar.gz' % (distname, version))
|
||
|
make_package(foobar_path, distname, version)
|
||
|
|
||
|
return test_pkg
|
||
|
|
||
|
|
||
|
@pytest.mark.skipif(
|
||
|
sys.platform.startswith('java') and ei.is_sh(sys.executable),
|
||
|
reason="Test cannot run under java when executable is sh",
|
||
|
)
|
||
|
class TestScriptHeader:
|
||
|
non_ascii_exe = '/Users/José/bin/python'
|
||
|
exe_with_spaces = r'C:\Program Files\Python36\python.exe'
|
||
|
|
||
|
def test_get_script_header(self):
|
||
|
expected = '#!%s\n' % ei.nt_quote_arg(os.path.normpath(sys.executable))
|
||
|
actual = ei.ScriptWriter.get_header('#!/usr/local/bin/python')
|
||
|
assert actual == expected
|
||
|
|
||
|
def test_get_script_header_args(self):
|
||
|
expected = '#!%s -x\n' % ei.nt_quote_arg(os.path.normpath(sys.executable))
|
||
|
actual = ei.ScriptWriter.get_header('#!/usr/bin/python -x')
|
||
|
assert actual == expected
|
||
|
|
||
|
def test_get_script_header_non_ascii_exe(self):
|
||
|
actual = ei.ScriptWriter.get_header(
|
||
|
'#!/usr/bin/python', executable=self.non_ascii_exe
|
||
|
)
|
||
|
expected = '#!%s -x\n' % self.non_ascii_exe
|
||
|
assert actual == expected
|
||
|
|
||
|
def test_get_script_header_exe_with_spaces(self):
|
||
|
actual = ei.ScriptWriter.get_header(
|
||
|
'#!/usr/bin/python', executable='"' + self.exe_with_spaces + '"'
|
||
|
)
|
||
|
expected = '#!"%s"\n' % self.exe_with_spaces
|
||
|
assert actual == expected
|
||
|
|
||
|
|
||
|
class TestCommandSpec:
|
||
|
def test_custom_launch_command(self):
|
||
|
"""
|
||
|
Show how a custom CommandSpec could be used to specify a #! executable
|
||
|
which takes parameters.
|
||
|
"""
|
||
|
cmd = ei.CommandSpec(['/usr/bin/env', 'python3'])
|
||
|
assert cmd.as_header() == '#!/usr/bin/env python3\n'
|
||
|
|
||
|
def test_from_param_for_CommandSpec_is_passthrough(self):
|
||
|
"""
|
||
|
from_param should return an instance of a CommandSpec
|
||
|
"""
|
||
|
cmd = ei.CommandSpec(['python'])
|
||
|
cmd_new = ei.CommandSpec.from_param(cmd)
|
||
|
assert cmd is cmd_new
|
||
|
|
||
|
@mock.patch('sys.executable', TestScriptHeader.exe_with_spaces)
|
||
|
@mock.patch.dict(os.environ)
|
||
|
def test_from_environment_with_spaces_in_executable(self):
|
||
|
os.environ.pop('__PYVENV_LAUNCHER__', None)
|
||
|
cmd = ei.CommandSpec.from_environment()
|
||
|
assert len(cmd) == 1
|
||
|
assert cmd.as_header().startswith('#!"')
|
||
|
|
||
|
def test_from_simple_string_uses_shlex(self):
|
||
|
"""
|
||
|
In order to support `executable = /usr/bin/env my-python`, make sure
|
||
|
from_param invokes shlex on that input.
|
||
|
"""
|
||
|
cmd = ei.CommandSpec.from_param('/usr/bin/env my-python')
|
||
|
assert len(cmd) == 2
|
||
|
assert '"' not in cmd.as_header()
|
||
|
|
||
|
|
||
|
class TestWindowsScriptWriter:
|
||
|
def test_header(self):
|
||
|
hdr = ei.WindowsScriptWriter.get_header('')
|
||
|
assert hdr.startswith('#!')
|
||
|
assert hdr.endswith('\n')
|
||
|
hdr = hdr.lstrip('#!')
|
||
|
hdr = hdr.rstrip('\n')
|
||
|
# header should not start with an escaped quote
|
||
|
assert not hdr.startswith('\\"')
|
||
|
|
||
|
|
||
|
class VersionStub(NamedTuple):
|
||
|
major: int
|
||
|
minor: int
|
||
|
micro: int
|
||
|
releaselevel: str
|
||
|
serial: int
|
||
|
|
||
|
|
||
|
def test_use_correct_python_version_string(tmpdir, tmpdir_cwd, monkeypatch):
|
||
|
# In issue #3001, easy_install wrongly uses the `python3.1` directory
|
||
|
# when the interpreter is `python3.10` and the `--user` option is given.
|
||
|
# See pypa/setuptools#3001.
|
||
|
dist = Distribution()
|
||
|
cmd = dist.get_command_obj('easy_install')
|
||
|
cmd.args = ['ok']
|
||
|
cmd.optimize = 0
|
||
|
cmd.user = True
|
||
|
cmd.install_userbase = str(tmpdir)
|
||
|
cmd.install_usersite = None
|
||
|
install_cmd = dist.get_command_obj('install')
|
||
|
install_cmd.install_userbase = str(tmpdir)
|
||
|
install_cmd.install_usersite = None
|
||
|
|
||
|
with monkeypatch.context() as patch, warnings.catch_warnings():
|
||
|
warnings.simplefilter("ignore")
|
||
|
version = '3.10.1 (main, Dec 21 2021, 09:17:12) [GCC 10.2.1 20210110]'
|
||
|
info = VersionStub(3, 10, 1, "final", 0)
|
||
|
patch.setattr('site.ENABLE_USER_SITE', True)
|
||
|
patch.setattr('sys.version', version)
|
||
|
patch.setattr('sys.version_info', info)
|
||
|
patch.setattr(cmd, 'create_home_path', mock.Mock())
|
||
|
cmd.finalize_options()
|
||
|
|
||
|
name = "pypy" if hasattr(sys, 'pypy_version_info') else "python"
|
||
|
install_dir = cmd.install_dir.lower()
|
||
|
|
||
|
# In some platforms (e.g. Windows), install_dir is mostly determined
|
||
|
# via `sysconfig`, which define constants eagerly at module creation.
|
||
|
# This means that monkeypatching `sys.version` to emulate 3.10 for testing
|
||
|
# may have no effect.
|
||
|
# The safest test here is to rely on the fact that 3.1 is no longer
|
||
|
# supported/tested, and make sure that if 'python3.1' ever appears in the string
|
||
|
# it is followed by another digit (e.g. 'python3.10').
|
||
|
if re.search(name + r'3\.?1', install_dir):
|
||
|
assert re.search(name + r'3\.?1\d', install_dir)
|
||
|
|
||
|
# The following "variables" are used for interpolation in distutils
|
||
|
# installation schemes, so it should be fair to treat them as "semi-public",
|
||
|
# or at least public enough so we can have a test to make sure they are correct
|
||
|
assert cmd.config_vars['py_version'] == '3.10.1'
|
||
|
assert cmd.config_vars['py_version_short'] == '3.10'
|
||
|
assert cmd.config_vars['py_version_nodot'] == '310'
|
||
|
|
||
|
|
||
|
def test_editable_user_and_build_isolation(setup_context, monkeypatch, tmp_path):
|
||
|
"""`setup.py develop` should honor `--user` even under build isolation"""
|
||
|
|
||
|
# == Arrange ==
|
||
|
# Pretend that build isolation was enabled
|
||
|
# e.g pip sets the environment variable PYTHONNOUSERSITE=1
|
||
|
monkeypatch.setattr('site.ENABLE_USER_SITE', False)
|
||
|
|
||
|
# Patching $HOME for 2 reasons:
|
||
|
# 1. setuptools/command/easy_install.py:create_home_path
|
||
|
# tries creating directories in $HOME.
|
||
|
# Given::
|
||
|
# self.config_vars['DESTDIRS'] = (
|
||
|
# "/home/user/.pyenv/versions/3.9.10 "
|
||
|
# "/home/user/.pyenv/versions/3.9.10/lib "
|
||
|
# "/home/user/.pyenv/versions/3.9.10/lib/python3.9 "
|
||
|
# "/home/user/.pyenv/versions/3.9.10/lib/python3.9/lib-dynload")
|
||
|
# `create_home_path` will::
|
||
|
# makedirs(
|
||
|
# "/home/user/.pyenv/versions/3.9.10 "
|
||
|
# "/home/user/.pyenv/versions/3.9.10/lib "
|
||
|
# "/home/user/.pyenv/versions/3.9.10/lib/python3.9 "
|
||
|
# "/home/user/.pyenv/versions/3.9.10/lib/python3.9/lib-dynload")
|
||
|
#
|
||
|
# 2. We are going to force `site` to update site.USER_BASE and site.USER_SITE
|
||
|
# To point inside our new home
|
||
|
monkeypatch.setenv('HOME', str(tmp_path / '.home'))
|
||
|
monkeypatch.setenv('USERPROFILE', str(tmp_path / '.home'))
|
||
|
monkeypatch.setenv('APPDATA', str(tmp_path / '.home'))
|
||
|
monkeypatch.setattr('site.USER_BASE', None)
|
||
|
monkeypatch.setattr('site.USER_SITE', None)
|
||
|
user_site = Path(site.getusersitepackages())
|
||
|
user_site.mkdir(parents=True, exist_ok=True)
|
||
|
|
||
|
sys_prefix = tmp_path / '.sys_prefix'
|
||
|
sys_prefix.mkdir(parents=True, exist_ok=True)
|
||
|
monkeypatch.setattr('sys.prefix', str(sys_prefix))
|
||
|
|
||
|
setup_script = (
|
||
|
"__import__('setuptools').setup(name='aproj', version=42, packages=[])\n"
|
||
|
)
|
||
|
(tmp_path / "setup.py").write_text(setup_script, encoding="utf-8")
|
||
|
|
||
|
# == Sanity check ==
|
||
|
assert list(sys_prefix.glob("*")) == []
|
||
|
assert list(user_site.glob("*")) == []
|
||
|
|
||
|
# == Act ==
|
||
|
run_setup('setup.py', ['develop', '--user'])
|
||
|
|
||
|
# == Assert ==
|
||
|
# Should not install to sys.prefix
|
||
|
assert list(sys_prefix.glob("*")) == []
|
||
|
# Should install to user site
|
||
|
installed = {f.name for f in user_site.glob("*")}
|
||
|
# sometimes easy-install.pth is created and sometimes not
|
||
|
installed = installed - {"easy-install.pth"}
|
||
|
assert installed == {'aproj.egg-link'}
|