Commits

Erik Bray committed 98a9f9d

Fixes and adds a regression test for #323; required adding some new keyword arguments to existing pkg_resources methods. Also had to update how __path__ is handled for namespace packages to ensure that when a new egg distribution containing a namespace package is placed on sys.path, the entries in __path__ are in the same order they would have been in had that egg been on the path when pkg_resources was first imported

Comments (0)

Files changed (3)

                     seen[key]=1
                     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`
 
         If `entry` is unspecified, it defaults to the ``.location`` of `dist`.
         set's ``.entries`` (if it wasn't already present).
 
         `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
-        callbacks registered with the ``subscribe()`` method will be called.
+        doesn't already have a distribution in the set, unless `replace=True`.
+        If it's added, any callbacks registered with the ``subscribe()`` method
+        will be called.
         """
         if insert:
             dist.insert_on(self.entries, entry)
             entry = dist.location
         keys = self.entry_keys.setdefault(entry,[])
         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
 
         self.by_key[dist.key] = dist
             keys2.append(dist.key)
         self._added_new(dist)
 
-    def resolve(self, requirements, env=None, installer=None, replacement=True):
+    def resolve(self, requirements, env=None, installer=None,
+                replacement=True, replace_conflicting=False):
         """List all distributions needed to (recursively) meet `requirements`
 
         `requirements` must be a sequence of ``Requirement`` objects.  `env`,
         will be invoked with each requirement that cannot be met by an
         already-installed distribution; it should return a ``Distribution`` or
         ``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
             if dist is None:
                 # Find the best distribution and add it to the map
                 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:
-                        env = Environment(self.entries)
-                    dist = best[req.key] = env.best_match(req, self, installer)
+                        if dist is None:
+                            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:
                         #msg = ("The '%s' distribution was not found on this "
                         #       "system, and is required by this application.")
 
 def _handle_ns(packageName, path_item):
     """Ensure that named package includes a subpath of path_item (if needed)"""
+
     importer = get_importer(path_item)
     if importer is None:
         return None
     module = sys.modules.get(packageName)
     if module is None:
         module = sys.modules[packageName] = types.ModuleType(packageName)
-        module.__path__ = []; _set_parent_ns(packageName)
+        module.__path__ = []
+        _set_parent_ns(packageName)
     elif not hasattr(module,'__path__'):
         raise TypeError("Not a package:", packageName)
     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:
-        path = module.__path__; path.append(subpath)
-        loader.load_module(packageName); module.__path__ = path
+        path = module.__path__
+        path.append(subpath)
+        loader.load_module(packageName)
+        for path_item in path:
+            if path_item not in module.__path__:
+                module.__path__.append(path_item)
     return subpath
 
 def declare_namespace(packageName):
 class Distribution(object):
     """Wrap an actual or potential sys.path entry w/metadata"""
     PKG_INFO = 'PKG-INFO'
-    
+
     def __init__(self,
         location=None, metadata=None, project_name=None, version=None,
         py_version=PY_MAJOR, platform=None, precedence = EGG_DIST
             from email.parser import Parser
             self._pkg_info = Parser().parsestr(self.get_metadata(self.PKG_INFO))
             return self._pkg_info
-    
+
     @property
     def _dep_map(self):
         try:
 
     def _preparse_requirement(self, requires_dist):
         """Convert 'Foobar (1); baz' to ('Foobar ==1', 'baz')
-        Split environment marker, add == prefix to version specifiers as 
+        Split environment marker, add == prefix to version specifiers as
         necessary, and remove parenthesis.
         """
         parts = requires_dist.split(';', 1) + ['']
         distvers = re.sub(self.EQEQ, r"\1==\2\3", distvers)
         distvers = distvers.replace('(', '').replace(')', '')
         return (distvers, mark)
-            
+
     def _compute_dependencies(self):
         """Recompute this distribution's dependencies."""
         def dummy_marker(marker):
             parsed = parse_requirements(distvers).next()
             parsed.marker_fn = compile_marker(mark)
             reqs.append(parsed)
-            
+
         def reqs_for_extra(extra):
             for req in reqs:
                 if req.marker_fn(override={'extra':extra}):
 
         common = frozenset(reqs_for_extra(None))
         dm[None].extend(common)
-            
+
         for extra in self._parsed_pkg_info.get_all('Provides-Extra') or []:
             extra = safe_extra(extra.strip())
             dm[extra] = list(frozenset(reqs_for_extra(extra)) - common)
 
         return dm
-    
+
 
 _distributionImpl = {'.egg': Distribution,
                      '.egg-info': Distribution,

setuptools/dist.py

         """Resolve pre-setup requirements"""
         from pkg_resources import working_set, parse_requirements
         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):
         _Distribution.finalize_options(self)

setuptools/tests/test_easy_install.py

 from setuptools.command.easy_install import  PthDistributions
 from setuptools.command import easy_install as easy_install_pkg
 from setuptools.dist import Distribution
+from pkg_resources import working_set, VersionConflict
 from pkg_resources import Distribution as PRDistribution
 import setuptools.tests.server
+import pkg_resources
 
 try:
     # import multiprocessing solely for the purpose of testing its existence
         SandboxViolation.
         """
 
-        test_setup_attrs = {
-            'name': 'test_pkg', 'version': '0.0',
-            'setup_requires': ['foobar'],
-            'dependency_links': [os.path.abspath(self.dir)]
-        }
+        test_pkg = create_setup_requires_package(self.dir)
+        test_setup_py = os.path.join(test_pkg, 'setup.py')
 
-        test_pkg = os.path.join(self.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)
-
-        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.StringIO()
-        sys.stderr = StringIO.StringIO()
         try:
-            reset_setup_stop_context(
-                lambda: run_setup(test_setup_py, ['install'])
-            )
+            quiet_context(
+                lambda: reset_setup_stop_context(
+                    lambda: run_setup(test_setup_py, ['install'])
+            ))
         except SandboxViolation:
             self.fail('Installation caused SandboxViolation')
-        finally:
-            sys.stdout = old_stdout
-            sys.stderr = old_stderr
 
 
 class TestSetupRequires(unittest.TestCase):
             tempdir_context(install_at)
 
         # create an sdist that has a build-time dependency.
-        self.create_sdist(install)
+        quiet_context(lambda: self.create_sdist(install))
 
         # there should have been two or three requests to the server
         #  (three happens on Python 3.3a)
             installer(dist_path)
         tempdir_context(build_sdist)
 
+    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.assertGreater(len(lines), 0)
+            self.assert_(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):
     """Create a simple sdist tarball at dist_path, containing just a
         cd(orig_dir)
         shutil.rmtree(temp_dir)
 
+
 def environment_context(f, **updates):
     """
     Invoke f in the context
             del os.environ[key]
         os.environ.update(old_env)
 
+
 def argv_context(f, repl):
     """
     Invoke f in the context
     finally:
         sys.argv[:] = old_argv
 
+
 def reset_setup_stop_context(f):
     """
     When the distribute tests are run using setup.py test, and then
         f()
     finally:
         distutils.core._setup_stop_after = setup_stop_after
+
+
+def quiet_context(f):
+    """
+    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:
+        f()
+    finally:
+        sys.stdout = old_stdout
+        sys.stderr = old_stderr
+    return new_stdout.getvalue(), new_stderr.getvalue()