Commit 2c668058 authored by Jason R. Coombs's avatar Jason R. Coombs

Backed out changeset: ef949e6e6de1, which was itself a backout of the fix for...

Backed out changeset: ef949e6e6de1, which was itself a backout of the fix for Distribute #323, so this backout restores that fix and also Fixes #141.
parent f5473774
...@@ -504,7 +504,7 @@ class WorkingSet(object): ...@@ -504,7 +504,7 @@ class WorkingSet(object):
seen[key]=1 seen[key]=1
yield self.by_key[key] yield self.by_key[key]
def add(self, dist, entry=None, insert=True): def add(self, dist, entry=None, insert=True, replace=False):
"""Add `dist` to working set, associated with `entry` """Add `dist` to working set, associated with `entry`
If `entry` is unspecified, it defaults to the ``.location`` of `dist`. If `entry` is unspecified, it defaults to the ``.location`` of `dist`.
...@@ -512,8 +512,9 @@ class WorkingSet(object): ...@@ -512,8 +512,9 @@ class WorkingSet(object):
set's ``.entries`` (if it wasn't already present). set's ``.entries`` (if it wasn't already present).
`dist` is only added to the working set if it's for a project that `dist` is only added to the working set if it's for a project that
doesn't already have a distribution in the set. If it's added, any doesn't already have a distribution in the set, unless `replace=True`.
callbacks registered with the ``subscribe()`` method will be called. If it's added, any callbacks registered with the ``subscribe()`` method
will be called.
""" """
if insert: if insert:
dist.insert_on(self.entries, entry) dist.insert_on(self.entries, entry)
...@@ -522,7 +523,7 @@ class WorkingSet(object): ...@@ -522,7 +523,7 @@ class WorkingSet(object):
entry = dist.location entry = dist.location
keys = self.entry_keys.setdefault(entry,[]) keys = self.entry_keys.setdefault(entry,[])
keys2 = self.entry_keys.setdefault(dist.location,[]) keys2 = self.entry_keys.setdefault(dist.location,[])
if dist.key in self.by_key: if not replace and dist.key in self.by_key:
return # ignore hidden distros return # ignore hidden distros
self.by_key[dist.key] = dist self.by_key[dist.key] = dist
...@@ -532,7 +533,8 @@ class WorkingSet(object): ...@@ -532,7 +533,8 @@ class WorkingSet(object):
keys2.append(dist.key) keys2.append(dist.key)
self._added_new(dist) self._added_new(dist)
def resolve(self, requirements, env=None, installer=None): def resolve(self, requirements, env=None, installer=None,
replace_conflicting=False):
"""List all distributions needed to (recursively) meet `requirements` """List all distributions needed to (recursively) meet `requirements`
`requirements` must be a sequence of ``Requirement`` objects. `env`, `requirements` must be a sequence of ``Requirement`` objects. `env`,
...@@ -542,6 +544,12 @@ class WorkingSet(object): ...@@ -542,6 +544,12 @@ class WorkingSet(object):
will be invoked with each requirement that cannot be met by an will be invoked with each requirement that cannot be met by an
already-installed distribution; it should return a ``Distribution`` or already-installed distribution; it should return a ``Distribution`` or
``None``. ``None``.
Unless `replace_conflicting=True`, raises a VersionConflict exception if
any requirements are found on the path that have the correct name but
the wrong version. Otherwise, if an `installer` is supplied it will be
invoked to obtain the correct version of the requirement and activate
it.
""" """
requirements = list(requirements)[::-1] # set up the stack requirements = list(requirements)[::-1] # set up the stack
...@@ -558,10 +566,18 @@ class WorkingSet(object): ...@@ -558,10 +566,18 @@ class WorkingSet(object):
if dist is None: if dist is None:
# Find the best distribution and add it to the map # Find the best distribution and add it to the map
dist = self.by_key.get(req.key) dist = self.by_key.get(req.key)
if dist is None: if dist is None or (dist not in req and replace_conflicting):
ws = self
if env is None: if env is None:
env = Environment(self.entries) if dist is None:
dist = best[req.key] = env.best_match(req, self, installer) env = Environment(self.entries)
else:
# Use an empty environment and workingset to avoid
# any further conflicts with the conflicting
# distribution
env = Environment([])
ws = WorkingSet([])
dist = best[req.key] = env.best_match(req, ws, installer)
if dist is None: if dist is None:
#msg = ("The '%s' distribution was not found on this " #msg = ("The '%s' distribution was not found on this "
# "system, and is required by this application.") # "system, and is required by this application.")
...@@ -1811,6 +1827,7 @@ def register_namespace_handler(importer_type, namespace_handler): ...@@ -1811,6 +1827,7 @@ def register_namespace_handler(importer_type, namespace_handler):
def _handle_ns(packageName, path_item): def _handle_ns(packageName, path_item):
"""Ensure that named package includes a subpath of path_item (if needed)""" """Ensure that named package includes a subpath of path_item (if needed)"""
importer = get_importer(path_item) importer = get_importer(path_item)
if importer is None: if importer is None:
return None return None
...@@ -1825,12 +1842,14 @@ def _handle_ns(packageName, path_item): ...@@ -1825,12 +1842,14 @@ def _handle_ns(packageName, path_item):
elif not hasattr(module,'__path__'): elif not hasattr(module,'__path__'):
raise TypeError("Not a package:", packageName) raise TypeError("Not a package:", packageName)
handler = _find_adapter(_namespace_handlers, importer) handler = _find_adapter(_namespace_handlers, importer)
subpath = handler(importer,path_item,packageName,module) subpath = handler(importer, path_item, packageName, module)
if subpath is not None: if subpath is not None:
path = module.__path__ path = module.__path__
path.append(subpath) path.append(subpath)
loader.load_module(packageName) loader.load_module(packageName)
module.__path__ = path for path_item in path:
if path_item not in module.__path__:
module.__path__.append(path_item)
return subpath return subpath
def declare_namespace(packageName): def declare_namespace(packageName):
......
...@@ -260,9 +260,10 @@ class Distribution(_Distribution): ...@@ -260,9 +260,10 @@ class Distribution(_Distribution):
"""Resolve pre-setup requirements""" """Resolve pre-setup requirements"""
from pkg_resources import working_set, parse_requirements from pkg_resources import working_set, parse_requirements
for dist in working_set.resolve( for dist in working_set.resolve(
parse_requirements(requires), installer=self.fetch_build_egg parse_requirements(requires), installer=self.fetch_build_egg,
replace_conflicting=True
): ):
working_set.add(dist) working_set.add(dist, replace=True)
def finalize_options(self): def finalize_options(self):
_Distribution.finalize_options(self) _Distribution.finalize_options(self)
......
...@@ -19,8 +19,10 @@ from setuptools.command.easy_install import ( ...@@ -19,8 +19,10 @@ from setuptools.command.easy_install import (
from setuptools.command.easy_install import PthDistributions from setuptools.command.easy_install import PthDistributions
from setuptools.command import easy_install as easy_install_pkg from setuptools.command import easy_install as easy_install_pkg
from setuptools.dist import Distribution from setuptools.dist import Distribution
from pkg_resources import working_set, VersionConflict
from pkg_resources import Distribution as PRDistribution from pkg_resources import Distribution as PRDistribution
import setuptools.tests.server import setuptools.tests.server
import pkg_resources
class FakeDist(object): class FakeDist(object):
def get_entry_map(self, group): def get_entry_map(self, group):
...@@ -230,47 +232,15 @@ class TestUserInstallTest(unittest.TestCase): ...@@ -230,47 +232,15 @@ class TestUserInstallTest(unittest.TestCase):
SandboxViolation. SandboxViolation.
""" """
test_setup_attrs = { test_pkg = create_setup_requires_package(self.dir)
'name': 'test_pkg', 'version': '0.0',
'setup_requires': ['foobar'],
'dependency_links': [os.path.abspath(self.dir)]
}
test_pkg = os.path.join(self.dir, 'test_pkg')
test_setup_py = os.path.join(test_pkg, 'setup.py') test_setup_py = os.path.join(test_pkg, 'setup.py')
os.mkdir(test_pkg)
f = open(test_setup_py, 'w')
f.write(textwrap.dedent("""\
import setuptools
setuptools.setup(**%r)
""" % test_setup_attrs))
f.close()
foobar_path = os.path.join(self.dir, 'foobar-0.1.tar.gz')
make_trivial_sdist(
foobar_path,
textwrap.dedent("""\
import setuptools
setuptools.setup(
name='foobar',
version='0.1'
)
"""))
old_stdout = sys.stdout
old_stderr = sys.stderr
sys.stdout = StringIO()
sys.stderr = StringIO()
try: try:
try: with quiet_context():
with reset_setup_stop_context(): with reset_setup_stop_context():
run_setup(test_setup_py, ['install']) run_setup(test_setup_py, ['install'])
except SandboxViolation: except SandboxViolation:
self.fail('Installation caused SandboxViolation') self.fail('Installation caused SandboxViolation')
finally:
sys.stdout = old_stdout
sys.stderr = old_stderr
class TestSetupRequires(unittest.TestCase): class TestSetupRequires(unittest.TestCase):
...@@ -290,8 +260,10 @@ class TestSetupRequires(unittest.TestCase): ...@@ -290,8 +260,10 @@ class TestSetupRequires(unittest.TestCase):
# Some platforms (Jython) don't find a port to which to bind, # Some platforms (Jython) don't find a port to which to bind,
# so skip this test for them. # so skip this test for them.
return return
# create an sdist that has a build-time dependency. with quiet_context():
with TestSetupRequires.create_sdist() as dist_file: # TODO: correct indentation here
# create an sdist that has a build-time dependency.
with TestSetupRequires.create_sdist() as dist_file:
with tempdir_context() as temp_install_dir: with tempdir_context() as temp_install_dir:
with environment_context(PYTHONPATH=temp_install_dir): with environment_context(PYTHONPATH=temp_install_dir):
ei_params = ['--index-url', p_index.url, ei_params = ['--index-url', p_index.url,
...@@ -330,6 +302,81 @@ class TestSetupRequires(unittest.TestCase): ...@@ -330,6 +302,81 @@ class TestSetupRequires(unittest.TestCase):
""").lstrip()) """).lstrip())
yield dist_path yield dist_path
def test_setup_requires_overrides_version_conflict(self):
"""
Regression test for issue #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.
"""
pr_state = pkg_resources.__getstate__()
fake_dist = PRDistribution('does-not-matter', project_name='foobar',
version='0.0')
working_set.add(fake_dist)
def setup_and_run(temp_dir):
test_pkg = create_setup_requires_package(temp_dir)
test_setup_py = os.path.join(test_pkg, 'setup.py')
try:
stdout, stderr = quiet_context(
lambda: reset_setup_stop_context(
# Don't even need to install the package, just running
# the setup.py at all is sufficient
lambda: run_setup(test_setup_py, ['--name'])
))
except VersionConflict:
self.fail('Installing setup.py requirements caused '
'VersionConflict')
lines = stdout.splitlines()
self.assertTrue(len(lines) > 0)
self.assertTrue(lines[-1].strip(), 'test_pkg')
try:
tempdir_context(setup_and_run)
finally:
pkg_resources.__setstate__(pr_state)
def create_setup_requires_package(path):
"""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.
"""
test_setup_attrs = {
'name': 'test_pkg', 'version': '0.0',
'setup_requires': ['foobar==0.1'],
'dependency_links': [os.path.abspath(path)]
}
test_pkg = os.path.join(path, '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)
f = open(test_setup_py, 'w')
f.write(textwrap.dedent("""\
import setuptools
setuptools.setup(**%r)
""" % test_setup_attrs))
f.close()
foobar_path = os.path.join(path, 'foobar-0.1.tar.gz')
make_trivial_sdist(
foobar_path,
textwrap.dedent("""\
import setuptools
setuptools.setup(
name='foobar',
version='0.1'
)
"""))
return test_pkg
def make_trivial_sdist(dist_path, setup_py): def make_trivial_sdist(dist_path, setup_py):
"""Create a simple sdist tarball at dist_path, containing just a """Create a simple sdist tarball at dist_path, containing just a
...@@ -392,3 +439,21 @@ def reset_setup_stop_context(): ...@@ -392,3 +439,21 @@ def reset_setup_stop_context():
distutils.core._setup_stop_after = None distutils.core._setup_stop_after = None
yield yield
distutils.core._setup_stop_after = setup_stop_after distutils.core._setup_stop_after = setup_stop_after
@contextlib.contextmanager
def quiet_context():
"""
Redirect stdout/stderr to StringIO objects to prevent console output from
distutils commands.
"""
old_stdout = sys.stdout
old_stderr = sys.stderr
new_stdout = sys.stdout = StringIO.StringIO()
new_stderr = sys.stderr = StringIO.StringIO()
try:
yield new_stdout, new_stderr
finally:
sys.stdout = old_stdout
sys.stderr = old_stderr
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment