diff --git a/pipenv/core.py b/pipenv/core.py index c16d8d34..930f7f1f 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 2e602eed..0625cc2c 100644 --- a/tests/test_pipenv.py +++ b/tests/test_pipenv.py @@ -579,6 +579,58 @@ pytz = "*" c = p.pipenv('run python -c "import six; import urllib3; import pytz;"') 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