Repository: pex-tool/pex

Issue:
It's possible to build a pex with multiple versions of the same package
``` bash

$ git clone https://github.com/sphinx-doc/sphinx && cd sphinx
$ pex . -v
  snowballstemmer 1.2.0
  MarkupSafe 0.23
  Jinja2 2.8
  six 1.9.0
  Babel 2.0
  Sphinx 1.3.1
  pytz 2015.4
  alabaster 0.7.6
  Pygments 2.0.2
  sphinx-rtd-theme 0.1.8
  Sphinx 1.4a0.dev20150811
  docutils 0.12
```

Note the two copies of sphinx pulled into the environment.  This messes with things like PEX_SCRIPT which expects to see just one version of an entry point.



Discussion:

Resolving patch:
diff --git a/pex/resolver.py b/pex/resolver.py
--- a/pex/resolver.py
+++ b/pex/resolver.py
@@ -8,6 +8,8 @@
 import time
 from collections import namedtuple
 
+from pkg_resources import safe_name
+
 from .common import safe_mkdir
 from .fetcher import Fetcher
 from .interpreter import PythonInterpreter
@@ -56,6 +58,10 @@ def merge(self, other):
 
 
 class _ResolvableSet(object):
+  @classmethod
+  def normalize(cls, name):
+    return safe_name(name).lower()
+
   def __init__(self, tuples=None):
     # A list of _ResolvedPackages
     self.__tuples = tuples or []
@@ -71,7 +77,7 @@ def _collapse(self):
     # adversely but could be the source of subtle resolution quirks.
     resolvables = {}
     for resolved_packages in self.__tuples:
-      key = resolved_packages.resolvable.name
+      key = self.normalize(resolved_packages.resolvable.name)
       previous = resolvables.get(key, _ResolvedPackages.empty())
       if previous.resolvable is None:
         resolvables[key] = resolved_packages
@@ -86,7 +92,7 @@ def render_resolvable(resolved_packages):
           '(from: %s)' % resolved_packages.parent if resolved_packages.parent else '')
     return ', '.join(
         render_resolvable(resolved_packages) for resolved_packages in self.__tuples
-        if resolved_packages.resolvable.name == name)
+        if self.normalize(resolved_packages.resolvable.name) == self.normalize(name))
 
   def _check(self):
     # Check whether or not the resolvables in this set are satisfiable, raise an exception if not.
@@ -102,7 +108,8 @@ def merge(self, resolvable, packages, parent=None):
 
   def get(self, name):
     """Get the set of compatible packages given a resolvable name."""
-    resolvable, packages, parent = self._collapse().get(name, _ResolvedPackages.empty())
+    resolvable, packages, parent = self._collapse().get(
+        self.normalize(name), _ResolvedPackages.empty())
     return packages
 
   def packages(self):
@@ -111,7 +118,8 @@ def packages(self):
 
   def extras(self, name):
     return set.union(
-        *[set(tup.resolvable.extras()) for tup in self.__tuples if tup.resolvable.name == name])
+        *[set(tup.resolvable.extras()) for tup in self.__tuples
+          if self.normalize(tup.resolvable.name) == self.normalize(name)])
 
   def replace_built(self, built_packages):
     """Return a copy of this resolvable set but with built packages.
diff --git a/pex/version.py b/pex/version.py
--- a/pex/version.py
+++ b/pex/version.py
@@ -1,7 +1,7 @@
 # Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
 # Licensed under the Apache License, Version 2.0 (see LICENSE).
 
-__version__ = '1.0.3'
+__version__ = '1.1.0.dev0'
 
 SETUPTOOLS_REQUIREMENT = 'setuptools>=2.2,<16'
 WHEEL_REQUIREMENT = 'wheel>=0.24.0,<0.25.0'


Test patch:
diff --git a/tests/test_resolver.py b/tests/test_resolver.py
--- a/tests/test_resolver.py
+++ b/tests/test_resolver.py
@@ -53,18 +53,21 @@ def test_resolvable_set():
   rs = _ResolvableSet()
   rq = ResolvableRequirement.from_string('foo[ext]', builder)
   source_pkg = SourcePackage.from_href('foo-2.3.4.tar.gz')
-  binary_pkg = EggPackage.from_href('foo-2.3.4-py3.4.egg')
+  binary_pkg = EggPackage.from_href('Foo-2.3.4-py3.4.egg')
 
   rs.merge(rq, [source_pkg, binary_pkg])
-  assert rs.get('foo') == set([source_pkg, binary_pkg])
+  assert rs.get(source_pkg.name) == set([source_pkg, binary_pkg])
+  assert rs.get(binary_pkg.name) == set([source_pkg, binary_pkg])
   assert rs.packages() == [(rq, set([source_pkg, binary_pkg]), None)]
 
   # test methods
   assert rs.extras('foo') == set(['ext'])
+  assert rs.extras('Foo') == set(['ext'])
 
   # test filtering
   rs.merge(rq, [source_pkg])
   assert rs.get('foo') == set([source_pkg])
+  assert rs.get('Foo') == set([source_pkg])
 
   with pytest.raises(Unsatisfiable):
     rs.merge(rq, [binary_pkg])
@@ -88,6 +91,3 @@ def test_resolvable_set_built():
   updated_rs.merge(rq, [binary_pkg])
   assert updated_rs.get('foo') == set([binary_pkg])
   assert updated_rs.packages() == [(rq, set([binary_pkg]), None)]
-
-
-# TODO(wickman) Write more than simple resolver test.