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