From cd961a41b87f00d1b0923bc8ee87861adb88b468 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Tue, 14 Nov 2023 17:40:32 -0500 Subject: [PATCH] Fix issue-6011 direct file url path (#6012) * Refactor this path logic to file url bug and re-use relative pathing logic. * Handle case where the drive letter is different and so relative path may not be possible * Add news fragment --- news/6011.bugfix.rst | 1 + pipenv/utils/dependencies.py | 72 +++++++++++++----------- tests/integration/conftest.py | 2 +- tests/integration/test_install_twists.py | 4 +- tests/integration/test_install_uri.py | 7 +-- 5 files changed, 45 insertions(+), 41 deletions(-) create mode 100644 news/6011.bugfix.rst diff --git a/news/6011.bugfix.rst b/news/6011.bugfix.rst new file mode 100644 index 0000000000..8723f1db42 --- /dev/null +++ b/news/6011.bugfix.rst @@ -0,0 +1 @@ +Fix Using dependencies from a URL fails on Windows. diff --git a/pipenv/utils/dependencies.py b/pipenv/utils/dependencies.py index 937585f7bc..05cc9cbf9f 100644 --- a/pipenv/utils/dependencies.py +++ b/pipenv/utils/dependencies.py @@ -660,34 +660,43 @@ def find_package_name_from_directory(directory): return None +def ensure_path_is_relative(file_path): + abs_path = Path(file_path).resolve() + current_dir = Path.cwd() + + # Check if the paths are on different drives + if abs_path.drive != current_dir.drive: + # If on different drives, return the absolute path + return abs_path + + try: + # Try to create a relative path + return abs_path.relative_to(current_dir) + except ValueError: + # If the direct relative_to fails, manually compute the relative path + common_parts = 0 + for part_a, part_b in zip(abs_path.parts, current_dir.parts): + if part_a == part_b: + common_parts += 1 + else: + break + + # Number of ".." needed are the extra parts in the current directory + # beyond the common parts + up_levels = [".."] * (len(current_dir.parts) - common_parts) + # The relative path is constructed by going up as needed and then + # appending the non-common parts of the absolute path + rel_parts = up_levels + list(abs_path.parts[common_parts:]) + relative_path = Path(*rel_parts) + return str(relative_path) + + def determine_path_specifier(package: InstallRequirement): if package.link: if package.link.scheme in ["http", "https"]: return package.link.url_without_fragment if package.link.scheme == "file": - abs_path = Path(package.link.file_path).resolve() - current_dir = Path.cwd() - - try: - relative_path = abs_path.relative_to(current_dir) - return relative_path.as_posix() - except ValueError: - # If the direct relative_to fails, manually compute the relative path - common_parts = 0 - for part_a, part_b in zip(abs_path.parts, current_dir.parts): - if part_a == part_b: - common_parts += 1 - else: - break - - # Number of ".." needed are the extra parts in the current directory - # beyond the common parts - up_levels = [".."] * (len(current_dir.parts) - common_parts) - # The relative path is constructed by going up as needed and then - # appending the non-common parts of the absolute path - rel_parts = up_levels + list(abs_path.parts[common_parts:]) - relative_path = Path(*rel_parts) - return relative_path.as_posix() + return ensure_path_is_relative(package.link.file_path) def determine_vcs_specifier(package: InstallRequirement): @@ -1003,19 +1012,16 @@ def expansive_install_req_from_line( return install_req, name -def _file_path_from_pipfile(path_obj, pipfile_entry): +def file_path_from_pipfile(path_str, pipfile_entry): """Creates an installable file path from a pipfile entry. Handles local and remote paths, files and directories; supports extras and editable specification. Outputs a pip installable line. """ - parsed_url = urlparse(str(path_obj)) - if parsed_url.scheme in ["http", "https", "ftp", "file"]: - req_str = str(path_obj) - elif path_obj.is_absolute(): - req_str = str(path_obj.as_posix()) + if path_str.startswith(("http:", "https:", "ftp:")): + req_str = path_str else: - req_str = f"./{str(path_obj.as_posix())}" + req_str = ensure_path_is_relative(path_str) if pipfile_entry.get("extras"): req_str = f"{req_str}[{','.join(pipfile_entry['extras'])}]" @@ -1076,11 +1082,9 @@ def install_req_from_pipfile(name, pipfile): else: req_str = f"{name}{extras_str}@ {req_str}{subdirectory}" elif "path" in _pipfile: - path_obj = Path(_pipfile["path"]) - req_str = _file_path_from_pipfile(path_obj, _pipfile) + req_str = file_path_from_pipfile(_pipfile["path"], _pipfile) elif "file" in _pipfile: - path_obj = Path(_pipfile["file"]) - req_str = _file_path_from_pipfile(path_obj, _pipfile) + req_str = file_path_from_pipfile(_pipfile["file"], _pipfile) else: # We ensure version contains an operator. Default to equals (==) _pipfile["version"] = version = get_version(pipfile) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 3a4c88f29b..171774767d 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -166,7 +166,7 @@ def write(self): @classmethod def get_fixture_path(cls, path, fixtures="test_artifacts"): - return Path(__file__).resolve().parent.parent / fixtures / path + return Path(Path(__file__).resolve().parent.parent / fixtures / path) class _PipenvInstance: diff --git a/tests/integration/test_install_twists.py b/tests/integration/test_install_twists.py index fa692b4a36..4bb250180f 100644 --- a/tests/integration/test_install_twists.py +++ b/tests/integration/test_install_twists.py @@ -182,7 +182,7 @@ def test_local_tar_gz_file(pipenv_instance_private_pypi, testsroot): file_name = "requests-2.19.1.tar.gz" with pipenv_instance_private_pypi() as p: - requests_path = p._pipfile.get_fixture_path(f"{file_name}").as_posix() + requests_path = p._pipfile.get_fixture_path(f"{file_name}") # This tests for a bug when installing a zipfile c = p.pipenv(f"install {requests_path}") @@ -248,7 +248,7 @@ def test_multiple_editable_packages_should_not_race(pipenv_instance_private_pypi """ for pkg_name in pkgs: - source_path = p._pipfile.get_fixture_path(f"git/{pkg_name}/").as_posix() + source_path = p._pipfile.get_fixture_path(f"git/{pkg_name}/") shutil.copytree(source_path, pkg_name) pipfile_string += f'"{pkg_name}" = {{path = "./{pkg_name}", editable = true}}\n' diff --git a/tests/integration/test_install_uri.py b/tests/integration/test_install_uri.py index b07f52a552..71f89f2e26 100644 --- a/tests/integration/test_install_uri.py +++ b/tests/integration/test_install_uri.py @@ -53,9 +53,8 @@ def test_urls_work(pipenv_instance_pypi): @pytest.mark.files def test_file_urls_work(pipenv_instance_pypi): with pipenv_instance_pypi() as p: - whl = Path(__file__).parent.parent.joinpath( - "pypi", "six", "six-1.11.0-py2.py3-none-any.whl" - ) + whl = Path(Path(__file__).resolve().parent.parent / "pypi" / "six" / "six-1.11.0-py2.py3-none-any.whl") + try: whl = whl.resolve() except OSError: @@ -172,7 +171,7 @@ def test_install_specifying_index_url(pipenv_instance_private_pypi): def test_install_local_vcs_not_in_lockfile(pipenv_instance_pypi): with pipenv_instance_pypi() as p: # six_path = os.path.join(p.path, "six") - six_path = p._pipfile.get_fixture_path("git/six/").as_posix() + six_path = p._pipfile.get_fixture_path("git/six/") c = subprocess_run(["git", "clone", six_path, "./six"]) assert c.returncode == 0 c = p.pipenv("install -e ./six")