Force Windows local drive names to uppercase

Windows generally use upper cased drive names, but allow (without
normalizing) lower cased names in cmd.exe, which results in
inconsistencies when hashing the full path (to get the name of the
project's virtualenv). Python does not provide a good solution[*], so we
need to roll our own.

[*]: Python does have os.path.normcase(), but it always converts the
     whole paths to lowercase. This would break virtually *all* existing
     virtualenvs for Windows users.

UNC host names (which Python also treats as drives), on the other hand,
can actually be either cased. I am not sure if they are case-sensitive,
or should be coerced to what case, so this patch keeps with the existing
behaviour, and does not try to coerce them.
This commit is contained in:
Tzu-ping Chung
2017-12-19 21:58:11 +08:00
committed by Dan Ryan
parent 5191a8e6b1
commit e73d2bfa17
3 changed files with 49 additions and 7 deletions
+3 -3
View File
@@ -14,7 +14,7 @@ import toml
from .utils import (
mkdir_p, convert_deps_from_pip, pep423_name, recase_file,
find_requirements, is_file, is_vcs, python_version, cleanup_toml,
is_installable_file, is_valid_url
is_installable_file, is_valid_url, normalize_drive,
)
from .environments import PIPENV_MAX_DEPTH, PIPENV_VENV_IN_PROJECT
from .environments import PIPENV_VIRTUALENV, PIPENV_PIPFILE
@@ -23,7 +23,7 @@ if PIPENV_PIPFILE:
if not os.path.isfile(PIPENV_PIPFILE):
raise RuntimeError('Given PIPENV_PIPFILE is not found!')
else:
PIPENV_PIPFILE = os.path.abspath(PIPENV_PIPFILE)
PIPENV_PIPFILE = normalize_drive(os.path.abspath(PIPENV_PIPFILE))
class Project(object):
@@ -224,7 +224,7 @@ class Project(object):
loc = pipfile.Pipfile.find(max_depth=PIPENV_MAX_DEPTH)
except RuntimeError:
loc = None
self._pipfile_location = loc
self._pipfile_location = normalize_drive(loc)
return self._pipfile_location
+22 -4
View File
@@ -277,7 +277,7 @@ def get_requirement(dep):
remote URIs, and package names, and that we pass only valid requirement strings
to the requirements parser. Performs necessary modifications to requirements
object if the user input was a local relative path.
:param str dep: A requirement line
:returns: :class:`requirements.Requirement` object
"""
@@ -932,11 +932,11 @@ def proper_case(package_name):
def split_section(input_file, section_suffix, test_function):
"""
Split a pipfile or a lockfile section out by section name and test function
:param dict input_file: A dictionary containing either a pipfile or lockfile
:param str section_suffix: A string of the name of the section
:param func test_function: A test function to test against the value in the key/value pair
>>> split_section(my_lockfile, 'vcs', is_vcs)
{
'default': {
@@ -992,7 +992,7 @@ def merge_deps(file_dict, project, dev=False, requirements=False, ignore_hashes=
Given a file_dict, merges dependencies and converts them to pip dependency lists.
:param dict file_dict: The result of calling :func:`pipenv.utils.split_file`
:param :class:`pipenv.project.Project` project: Pipenv project
:param bool dev=False: Flag indicating whether dev dependencies are to be installed
:param bool dev=False: Flag indicating whether dev dependencies are to be installed
:param bool requirements=False: Flag indicating whether to use a requirements file
:param bool ignore_hashes=False:
:param bool blocking=False:
@@ -1174,3 +1174,21 @@ def touch_update_stamp():
except OSError:
with open(p, 'w') as fh:
fh.write('')
def normalize_drive(path):
"""Normalize drive in path so they stay consistent.
This currently only affects local drives on Windows, which can be
identified with either upper or lower cased drive names. The case is
always converted to uppercase because it seems to be preferred.
See: <https://github.com/pypa/pipenv/issues/1218>
"""
if os.name != 'nt':
return path
drive, tail = os.path.splitdrive(path)
# Only match (lower cased) local drives (e.g. 'c:'), not UNC mounts.
if drive.islower() and len(drive) == 2 and drive[1] == ':':
return '{}{}'.format(drive.upper(), tail)
return path
+24
View File
@@ -226,3 +226,27 @@ twine = "*"
new_toml = pipenv.utils.cleanup_toml(toml)
# testing if the end of the generated file contains a newline
assert new_toml[-1] == '\n'
@pytest.mark.parametrize('input_path, expected', [
('c:\\Program Files\\Python36\\python.exe',
'C:\\Program Files\\Python36\\python.exe'),
('C:\\Program Files\\Python36\\python.exe',
'C:\\Program Files\\Python36\\python.exe'),
('\\\\host\\share\\file.zip', '\\\\host\\share\\file.zip'),
('artifacts\\file.zip', 'artifacts\\file.zip'),
('.\\artifacts\\file.zip', '.\\artifacts\\file.zip'),
('..\\otherproject\\file.zip', '..\\otherproject\\file.zip'),
])
@pytest.mark.skipif(os.name != 'nt', reason='Windows file paths tested')
def test_win_normalize_drive(self, input_path, expected):
assert pipenv.utils.normalize_drive(input_path) == expected
@pytest.mark.parametrize('input_path, expected', [
('/usr/local/bin/python', '/usr/local/bin/python'),
('artifacts/file.zip', 'artifacts/file.zip'),
('./artifacts/file.zip', './artifacts/file.zip'),
('../otherproject/file.zip', '../otherproject/file.zip'),
])
@pytest.mark.skipif(os.name == 'nt', reason='*nix file paths tested')
def test_nix_normalize_drive(self, input_path, expected):
assert pipenv.utils.normalize_drive(input_path) == expected