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.

Support PathResource that comes from egg
I was seeing failures when creating an entry point to a script that was included in an egg. I'm not super familiar with the pex internals, but it seemed to only happen when the pex was actually executing (IE it found the script during the build process but not during execution).

My guess is that the eggs get converted to a PathResource at some point and since the PathResource is treated the same as a WheelResource it fails on paths that came from eggs because the scripts directory is in a different location.

My fix was to try both finding scripts from an egg location and from a wheel location when a PathResource is found. I'm happy to add/update tests if you can point me to the relevant ones.



Discussion:

Resolving patch:
diff --git a/pex/variables.py b/pex/variables.py
--- a/pex/variables.py
+++ b/pex/variables.py
@@ -5,6 +5,7 @@
 # checkstyle: noqa
 
 import os
+import sys
 from contextlib import contextmanager
 
 from .common import die
@@ -32,9 +33,13 @@ def iter_help(cls):
       variable_type, variable_text = cls.process_pydoc(getattr(value, '__doc__'))
       yield variable_name, variable_type, variable_text
 
-  def __init__(self, environ=None, use_defaults=True):
-    self._environ = environ.copy() if environ is not None else os.environ
+  def __init__(self, environ=None, rc='~/.pexrc', use_defaults=True):
     self._use_defaults = use_defaults
+    self._environ = environ.copy() if environ else os.environ
+    if not self.PEX_IGNORE_RCFILES:
+      rc_values = self._from_rc(rc).copy()
+      rc_values.update(self._environ)
+      self._environ = rc_values
 
   def copy(self):
     return self._environ.copy()
@@ -45,6 +50,22 @@ def delete(self, variable):
   def set(self, variable, value):
     self._environ[variable] = str(value)
 
+  def _from_rc(self, rc):
+    ret_vars = {}
+    for filename in [rc, os.path.join(os.path.dirname(sys.argv[0]), '.pexrc')]:
+      try:
+        with open(os.path.expanduser(filename)) as fh:
+          rc_items = map(self._get_kv, fh)
+          ret_vars.update(dict(filter(None, rc_items)))
+      except IOError:
+        continue
+    return ret_vars
+
+  def _get_kv(self, variable):
+    kv = variable.strip().split('=')
+    if len(list(filter(None, kv))) == 2:
+      return kv
+
   def _defaulted(self, default):
     return default if self._use_defaults else None
 
@@ -271,6 +292,14 @@ def PEX_HTTP_RETRIES(self):
     """
     return self._get_int('PEX_HTTP_RETRIES', default=5)
 
+  @property
+  def PEX_IGNORE_RCFILES(self):
+    """Boolean
+
+    Explicitly disable the reading/parsing of pexrc files (~/.pexrc). Default: false.
+    """
+    return self._get_bool('PEX_IGNORE_RCFILES', default=False)
+
 
 # Global singleton environment
 ENV = Variables()


Test patch:
diff --git a/tests/test_variables.py b/tests/test_variables.py
--- a/tests/test_variables.py
+++ b/tests/test_variables.py
@@ -1,6 +1,8 @@
 # Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
 # Licensed under the Apache License, Version 2.0 (see LICENSE).
 
+import tempfile
+
 import pytest
 
 from pex.variables import Variables
@@ -74,6 +76,38 @@ def test_pex_vars_set():
   assert v._get_int('HELLO') is None
 
 
+def test_pex_get_kv():
+  v = Variables(environ={})
+  assert v._get_kv('HELLO') is None
+  assert v._get_kv('=42') is None
+  assert v._get_kv('TOO=MANY=COOKS') is None
+  assert v._get_kv('THIS=WORKS') == ['THIS', 'WORKS']
+
+
+def test_pex_from_rc():
+  with tempfile.NamedTemporaryFile(mode='w') as pexrc:
+    pexrc.write('HELLO=42')
+    pexrc.flush()
+    v = Variables(rc=pexrc.name)
+    assert v._get_int('HELLO') == 42
+
+
+def test_pexrc_precedence():
+  with tempfile.NamedTemporaryFile(mode='w') as pexrc:
+    pexrc.write('HELLO=FORTYTWO')
+    pexrc.flush()
+    v = Variables(environ={'HELLO': 42}, rc=pexrc.name)
+    assert v._get_int('HELLO') == 42
+
+
+def test_rc_ignore():
+  with tempfile.NamedTemporaryFile(mode='w') as pexrc:
+    pexrc.write('HELLO=FORTYTWO')
+    pexrc.flush()
+    v = Variables(environ={'PEX_IGNORE_RC_FILES': True, 'HELLO': 42}, rc=pexrc.name)
+    assert v._get_int('HELLO') == 42
+
+
 def test_pex_vars_defaults_stripped():
   v = Variables(environ={})
   stripped = v.strip_defaults()