From e73d2bfa17caa2f92f0bfcf47944e20e32385296 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Tue, 19 Dec 2017 21:58:11 +0800 Subject: [PATCH 1/3] 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. --- pipenv/project.py | 6 +++--- pipenv/utils.py | 26 ++++++++++++++++++++++---- tests/test_utils.py | 24 ++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/pipenv/project.py b/pipenv/project.py index 7b5f1592..1b1b7142 100644 --- a/pipenv/project.py +++ b/pipenv/project.py @@ -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 diff --git a/pipenv/utils.py b/pipenv/utils.py index 90a774e9..f42e1fac 100644 --- a/pipenv/utils.py +++ b/pipenv/utils.py @@ -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: + """ + 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 diff --git a/tests/test_utils.py b/tests/test_utils.py index ffb875ef..f570a631 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -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 From 9ff4ca1c20042b74e24b5e94187e1a2b23a10cf4 Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Fri, 22 Dec 2017 15:59:54 -0500 Subject: [PATCH 2/3] Fix drive normalization to only parse strings - Fixes bug with handling non-existant pipfile locations on windows --- pipenv/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pipenv/utils.py b/pipenv/utils.py index f42e1fac..1a66d47d 100644 --- a/pipenv/utils.py +++ b/pipenv/utils.py @@ -1185,7 +1185,7 @@ def normalize_drive(path): See: """ - if os.name != 'nt': + if os.name != 'nt' or not isinstance(path, six.string_types): return path drive, tail = os.path.splitdrive(path) # Only match (lower cased) local drives (e.g. 'c:'), not UNC mounts. From 9e8d03970a564a08ae1fdff7c9a18e1a5b9c6a47 Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Fri, 22 Dec 2017 16:05:02 -0500 Subject: [PATCH 3/3] Update test assertions for proper path casing - Aligns tests with #1221 on windows --- pipenv/project.py | 2 +- tests/test_pipenv.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pipenv/project.py b/pipenv/project.py index 1b1b7142..100b4376 100644 --- a/pipenv/project.py +++ b/pipenv/project.py @@ -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, normalize_drive, + 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 diff --git a/tests/test_pipenv.py b/tests/test_pipenv.py index c7268f91..df113963 100644 --- a/tests/test_pipenv.py +++ b/tests/test_pipenv.py @@ -8,7 +8,7 @@ import json import pytest from pipenv.cli import activate_virtualenv -from pipenv.utils import temp_environ, get_windows_path, mkdir_p +from pipenv.utils import temp_environ, get_windows_path, mkdir_p, normalize_drive from pipenv.vendor import toml from pipenv.vendor import delegator from pipenv.project import Project @@ -84,7 +84,7 @@ class TestPipenv: @pytest.mark.cli def test_pipenv_where(self): with PipenvInstance() as p: - assert p.path in p.pipenv('--where').out + assert normalize_drive(p.path) in p.pipenv('--where').out @pytest.mark.cli def test_pipenv_venv(self): @@ -652,7 +652,7 @@ requests = {version = "*"} c = p.pipenv('install requests') assert c.return_code == 0 - assert p.path in p.pipenv('--venv').out + assert normalize_drive(p.path) in p.pipenv('--venv').out @pytest.mark.dotvenv @pytest.mark.install @@ -682,7 +682,7 @@ requests = {version = "*"} # Compare pew's virtualenv path to what we expect venv_path = get_windows_path(project.project_directory, '.venv') # os.path.normpath will normalize slashes - assert venv_path == os.path.normpath(c.out.strip()) + assert venv_path == normalize_drive(os.path.normpath(c.out.strip())) # Have pew run 'pip freeze' in the virtualenv # This is functionally the same as spawning a subshell # If we can do this we can theoretically amke a subshell