Repository: pypa/pipenv
Issue: Capitalisation of package names in Pipfile is inconsistent.

When I run "pipenv install {package}" all the package names in the Pipfile are lower-cased.

However, when I run "pipenv uninstall {package}" many of the names are changed back to their mixed case form.

This creates a *lot* of noise in change logs.

$ python -m pipenv.help
<details><summary>$ python -m pipenv.help output</summary>

Pipenv version: `'11.9.0'`

Pipenv location: `'/home/curtis/.local/lib/python2.7/site-packages/pipenv'`

Python location: `'/usr/bin/python'`

Other Python installations in `PATH`:

  - `2.7`: `/usr/bin/python2.7`
  - `2.7`: `/usr/bin/python2.7`
  - `3.4`: `/usr/bin/python3.4m`
  - `3.4`: `/usr/bin/python3.4`

  - `2.7.9`: `/usr/bin/python`
  - `2.7.9`: `/usr/bin/python2`
  - `3.4.2`: `/usr/bin/python3`

PEP 508 Information:

```
{'implementation_name': 'cpython',
 'implementation_version': '0',
 'os_name': 'posix',
 'platform_machine': 'x86_64',
 'platform_python_implementation': 'CPython',
 'platform_release': '3.16.0-5-amd64',
 'platform_system': 'Linux',
 'platform_version': '#1 SMP Debian 3.16.51-3+deb8u1 (2018-01-08)',
 'python_full_version': '2.7.9',
 'python_version': '2.7',
 'sys_platform': 'linux2'}
```

System environment variables:

  - `PYTHONDONTWRITEBYTECODE`
  - `PGPASSWORD`
  - `LOGNAME`
  - `USER`
  - `HOME`
  - `PATH`
  - `PYTHONUNBUFFERED`
  - `LANG`
  - `TERM`
  - `SHELL`
  - `LANGUAGE`
  - `SHLVL`
  - `PGUSER`
  - `PYTHONPATH`
  - `PIP_PYTHON_PATH`
  - `_`
  - `SSH_CONNECTION`
  - `SSH_TTY`
  - `OLDPWD`
  - `SSH_CLIENT`
  - `PWD`
  - `MAIL`
  - `LS_COLORS`

Pipenv–specific environment variables:


Debug–specific environment variables:

  - `PATH`: `/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games`
  - `SHELL`: `/bin/bash`
  - `LANG`: `en_AU.UTF-8`
  - `PWD`: `/home/curtis/review_app/workspace/candle-server/src/components/erasmus`


---------------------------

</details>

------------

##### Expected result

Consistent casing for both install / uninstall.

##### Actual result

```diff
 flower = "*"
-flask = "*"
-flask-xml-rpc = "*"
-flask-restful = "*"
+Flask = "*"
+Flask-XML-RPC = "*"
+Flask-RESTful = "*"
 pytest-flask = "*"
```

Changed files:
pipenv/core.py, tests/test_pipenv.py, pipenv/project.py

diff --git a/pipenv/core.py b/pipenv/core.py
index 52132607..955ae461 100644
--- a/pipenv/core.py
+++ b/pipenv/core.py
@@ -290,8 +290,7 @@ def ensure_pipfile(validate=True, skip_requirements=False):
     if validate and project.virtualenv_exists and not PIPENV_SKIP_VALIDATION:
         # Ensure that Pipfile is using proper casing.
         p = project.parsed_pipfile
-        p.clear_pipfile_cache()
-        changed = ensure_proper_casing(pfile=p)
+        changed = project.ensure_proper_casing()
         # Write changes out to disk.
         if changed:
             click.echo(
@@ -635,42 +634,6 @@ def ensure_project(
     ensure_pipfile(validate=validate, skip_requirements=skip_requirements)
 
 
-def ensure_proper_casing(pfile):
-    """Ensures proper casing of Pipfile packages, writes changes to disk."""
-    casing_changed = proper_case_section(pfile.get('packages', {}))
-    casing_changed |= proper_case_section(pfile.get('dev-packages', {}))
-    return casing_changed
-
-
-def proper_case_section(section):
-    """Verify proper casing is retrieved, when available, for each
-    dependency in the section.
-    """
-    # Casing for section.
-    changed_values = False
-    unknown_names = [
-        k for k in section.keys() if k not in set(project.proper_names)
-    ]
-    # Replace each package with proper casing.
-    for dep in unknown_names:
-        try:
-            # Get new casing for package name.
-            new_casing = proper_case(dep)
-        except IOError:
-            # Unable to normalize package name.
-            continue
-
-        if new_casing != dep:
-            changed_values = True
-            project.register_proper_name(new_casing)
-            # Replace old value with new value.
-            old_value = section[dep]
-            section[new_casing] = old_value
-            del section[dep]
-    # Return whether or not values have been changed.
-    return changed_values
-
-
 def shorten_path(location, bold=False):
     """Returns a visually shorter representation of a given system path."""
     original = location
@@ -2075,11 +2038,10 @@ def do_uninstall(
         c = delegator.run(cmd)
         click.echo(crayons.blue(c.out))
         if pipfile_remove:
-            norm_name = pep423_name(package_name)
-            in_dev_packages = (
-                norm_name in project._pipfile.get('dev-packages', {})
-            )
-            in_packages = (norm_name in project._pipfile.get('packages', {}))
+            in_packages = project.get_package_name_in_pipfile(
+                package_name, dev=False)
+            in_dev_packages = project.get_package_name_in_pipfile(
+                package_name, dev=True)
             if not in_dev_packages and not in_packages:
                 click.echo(
                     'No package {0} to remove from Pipfile.'.format(
diff --git a/pipenv/project.py b/pipenv/project.py
index 001652fe..6ce151a1 100644
--- a/pipenv/project.py
+++ b/pipenv/project.py
@@ -3,7 +3,6 @@ import codecs
 import json
 import os
 import re
-import six
 import sys
 import shlex
 import base64
@@ -21,7 +20,7 @@ from .utils import (
     mkdir_p,
     convert_deps_from_pip,
     pep423_name,
-    recase_file,
+    proper_case,
     find_requirements,
     is_editable,
     is_file,
@@ -370,20 +369,6 @@ class Project(object):
             except Exception:
                 return toml.loads(contents)
 
-    @property
-    def _pipfile(self):
-        """Pipfile divided by PyPI and external dependencies."""
-        pfile = self.parsed_pipfile
-        # mutation time!
-        self.clear_pipfile_cache()
-        for section in ('packages', 'dev-packages'):
-            p_section = dict(pfile.get(section, {}))
-            for key in list(p_section.keys()):
-                # Normalize key name to PEP 423.
-                norm_key = pep423_name(key)
-                p_section[norm_key] = p_section.pop(key)
-        return pfile
-
     @property
     def settings(self):
         """A dictionary of the settings added to the Pipfile."""
@@ -416,7 +401,6 @@ class Project(object):
             p['pipenv'] = settings
             # Write the changes to disk.
             self.write_toml(p)
-            self.clear_pipfile_cache()
 
     @property
     def _lockfile(self):
@@ -572,6 +556,8 @@ class Project(object):
         formatted_data = cleanup_toml(formatted_data)
         with open(path, 'w') as f:
             f.write(formatted_data)
+        # pipfile is mutated!
+        self.clear_pipfile_cache()
 
     @property
     def sources(self):
@@ -605,19 +591,28 @@ class Project(object):
         except OSError:
             pass
 
+    def get_package_name_in_pipfile(self, package_name, dev=False):
+        """Get the equivalent package name in pipfile"""
+        key = 'dev-packages' if dev else 'packages'
+        section = self.parsed_pipfile.get(key, {})
+        package_name = pep423_name(package_name)
+        for name in section.keys():
+            if pep423_name(name) == package_name:
+                return name
+        return None
+
     def remove_package_from_pipfile(self, package_name, dev=False):
         # Read and append Pipfile.
-        p = self._pipfile
-        package_name = pep423_name(package_name)
+        name = self.get_package_name_in_pipfile(package_name, dev)
         key = 'dev-packages' if dev else 'packages'
-        if key in p and package_name in p[key]:
-            del p[key][package_name]
-        # Write Pipfile.
-        self.write_toml(recase_file(p))
+        p = self.parsed_pipfile
+        if name:
+            del p[key][name]
+            self.write_toml(p)
 
     def add_package_to_pipfile(self, package_name, dev=False):
         # Read and append Pipfile.
-        p = self._pipfile
+        p = self.parsed_pipfile
         # Don't re-capitalize file URLs or VCSs.
         converted = convert_deps_from_pip(package_name)
         converted = converted[[k for k in converted.keys()][0]]
@@ -631,15 +626,19 @@ class Project(object):
             p[key] = {}
         package = convert_deps_from_pip(package_name)
         package_name = [k for k in package.keys()][0]
+        name = self.get_package_name_in_pipfile(package_name, dev)
+        if name and converted == '*':
+            # Skip for wildcard version
+            return
         # Add the package to the group.
-        p[key][package_name] = package[package_name]
+        p[key][name or package_name] = package[package_name]
         # Write Pipfile.
         self.write_toml(p)
 
     def add_index_to_pipfile(self, index):
         """Adds a given index to the Pipfile."""
         # Read and append Pipfile.
-        p = self._pipfile
+        p = self.parsed_pipfile
         source = {'url': index, 'verify_ssl': True}
         # Add the package to the group.
         if 'source' not in p:
@@ -650,7 +649,8 @@ class Project(object):
         self.write_toml(p)
 
     def recase_pipfile(self):
-        self.write_toml(recase_file(self._pipfile))
+        if self.ensure_proper_casing():
+            self.write_toml(self.parsed_pipfile)
 
     def get_lockfile_hash(self):
         if not os.path.exists(self.lockfile_location):
@@ -664,3 +664,38 @@ class Project(object):
         # Update the lockfile if it is out-of-date.
         p = pipfile.load(self.pipfile_location, inject_env=False)
         return p.hash
+
+    def ensure_proper_casing(self):
+        """Ensures proper casing of Pipfile packages"""
+        pfile = self.parsed_pipfile
+        casing_changed = self.proper_case_section(pfile.get('packages', {}))
+        casing_changed |= self.proper_case_section(pfile.get('dev-packages', {}))
+        return casing_changed
+
+    def proper_case_section(self, section):
+        """Verify proper casing is retrieved, when available, for each
+        dependency in the section.
+        """
+        # Casing for section.
+        changed_values = False
+        unknown_names = [
+            k for k in section.keys() if k not in set(self.proper_names)
+        ]
+        # Replace each package with proper casing.
+        for dep in unknown_names:
+            try:
+                # Get new casing for package name.
+                new_casing = proper_case(dep)
+            except IOError:
+                # Unable to normalize package name.
+                continue
+
+            if new_casing != dep:
+                changed_values = True
+                self.register_proper_name(new_casing)
+                # Replace old value with new value.
+                old_value = section[dep]
+                section[new_casing] = old_value
+                del section[dep]
+        # Return whether or not values have been changed.
+        return changed_values
diff --git a/tests/test_pipenv.py b/tests/test_pipenv.py
index 4d6556b3..51a12d2f 100644
--- a/tests/test_pipenv.py
+++ b/tests/test_pipenv.py
@@ -590,6 +590,58 @@ tpfd = "*"
             c = p.pipenv('run python -c "import requests; import idna; import certifi; import records; import tpfd; import parse;"')
             assert c.return_code == 0
 
+    @pytest.mark.install
+    @pytest.mark.run
+    def test_normalize_name_install(self, pypi):
+        with PipenvInstance(pypi=pypi) as p:
+            with open(p.pipfile_path, 'w') as f:
+                contents = """
+# Pre comment
+[packages]
+Requests = "==2.14.0"   # Inline comment
+"""
+                f.write(contents)
+
+            c = p.pipenv('install')
+            assert c.return_code == 0
+
+            c = p.pipenv('install requests')
+            assert c.return_code == 0
+            assert 'requests' not in p.pipfile['packages']
+            assert p.pipfile['packages']['Requests'] == '==2.14.0'
+            c = p.pipenv('install requests==2.18.4')
+            assert c.return_code == 0
+            assert p.pipfile['packages']['Requests'] == '==2.18.4'
+            c = p.pipenv('install python_DateUtil')
+            assert c.return_code == 0
+            assert 'python-dateutil' in p.pipfile['packages']
+            contents = open(p.pipfile_path).read()
+            assert '# Pre comment' in contents
+            assert '# Inline comment' in contents
+
+    @pytest.mark.uninstall
+    @pytest.mark.run
+    def test_normalize_name_uninstall(self, pypi):
+        with PipenvInstance(pypi=pypi) as p:
+            with open(p.pipfile_path, 'w') as f:
+                contents = """
+# Pre comment
+[packages]
+Requests = "*"
+python_DateUtil = "*"   # Inline comment
+"""
+                f.write(contents)
+
+            c = p.pipenv('install')
+            assert c.return_code == 0
+
+            c = p.pipenv('uninstall python_dateutil')
+            assert 'Requests' in p.pipfile['packages']
+            assert 'python_DateUtil' not in p.pipfile['packages']
+            contents = open(p.pipfile_path).read()
+            assert '# Pre comment' in contents
+            assert '# Inline comment' in contents
+
     @pytest.mark.install
     @pytest.mark.resolver
     @pytest.mark.backup_resolver