Repository: pypa/pip

Issue:
pip 7 lowercases console-script entry-points if wheel is present
When `pip install`ing an artefact that provides a console-script entry-point, the name of the installed command is converted to all lowercase if _wheel_ is present. E.g. `UppercaseInEntryPoints` becomes `uppercaseinentrypoints`. This is obviously a problem on case-sensitive file systems.

Note that the problem does neither exist in _pip_ < 7.0, nor if `pip install -e` is used or if _wheel_ is not installed. I have posted a simple artefact that can be used to test this at https://github.com/MatthiasBach/uppercase-in-entry-points.



Discussion:
This'll be a problem with the script generation in `pip/wheel.py`.

I think this is either coming from distlib or from setuptools itself.

This "feature" surprised (and annoyed :) me too. I tracked it down and it's because pip/wheel.py is calling `ConfigParser.RawConfigParse().readfp()` to read and parse entry_points.txt (which is eventually calling `optionxform()`; `optionxform` converts the arguments to all lower-case).

Looks like this could be solved with the prescription from the ConfigParser documentation page (override the `optionxform` method before invoking `readfp`):

```
cp = Configparser.RawConfigParser()
cp.optionxform = lambda option: option
cp.readfp(data)
...
```

I think it's unfriendly to change the case on platforms that support mixed case path names, and it doesn't seem like it would matter on others. So unless there are non-obvious reasons not to, it would be nice to see it fixed.

Since entry_points are not specified anywhere, it would make sense to be consistent with setuptools behavior.

> So unless there are non-obvious reasons not to, it would be nice to see it fixed.

PR (with corresponding test) welcome :)


Resolving patch:
diff --git a/pip/wheel.py b/pip/wheel.py
--- a/pip/wheel.py
+++ b/pip/wheel.py
@@ -224,6 +224,7 @@ def get_entrypoints(filename):
         data.seek(0)
 
     cp = configparser.RawConfigParser()
+    cp.optionxform = lambda option: option
     cp.readfp(data)
 
     console = {}


Test patch:
diff --git a/tests/data/packages/console_scripts_uppercase-1.0-py2.py3-none-any.whl b/tests/data/packages/console_scripts_uppercase-1.0-py2.py3-none-any.whl
new file mode 100644
Binary files /dev/null and b/tests/data/packages/console_scripts_uppercase-1.0-py2.py3-none-any.whl differ
diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py
--- a/tests/functional/test_install_wheel.py
+++ b/tests/functional/test_install_wheel.py
@@ -206,6 +206,26 @@ def test_install_from_wheel_gen_entrypoint(script, data):
         assert bool(os.access(script.base_path / wrapper_file, os.X_OK))
 
 
+def test_install_from_wheel_gen_uppercase_entrypoint(script, data):
+    """
+    Test installing scripts with uppercase letters in entry point names
+    """
+    result = script.pip(
+        'install', 'console-scripts-uppercase==1.0', '--no-index',
+        '--find-links=' + data.find_links,
+        expect_error=False,
+    )
+    if os.name == 'nt':
+        # Case probably doesn't make any difference on NT
+        wrapper_file = script.bin / 'cmdName.exe'
+    else:
+        wrapper_file = script.bin / 'cmdName'
+    assert wrapper_file in result.files_created
+
+    if os.name != "nt":
+        assert bool(os.access(script.base_path / wrapper_file, os.X_OK))
+
+
 def test_install_from_wheel_with_legacy(script, data):
     """
     Test installing scripts (legacy scripts are preserved)