From 5636e7c19c2bf8371638f90492a7d9015a694fbd Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Tue, 24 Oct 2023 17:29:36 +0200 Subject: [PATCH 1/6] import code from requests-file (git commit 675c7a6) --- conda_lock/_vendor/requests_file.LICENSE | 13 +++ conda_lock/_vendor/requests_file.py | 119 +++++++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 conda_lock/_vendor/requests_file.LICENSE create mode 100644 conda_lock/_vendor/requests_file.py diff --git a/conda_lock/_vendor/requests_file.LICENSE b/conda_lock/_vendor/requests_file.LICENSE new file mode 100644 index 000000000..2814b3992 --- /dev/null +++ b/conda_lock/_vendor/requests_file.LICENSE @@ -0,0 +1,13 @@ +Copyright 2015 Red Hat, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/conda_lock/_vendor/requests_file.py b/conda_lock/_vendor/requests_file.py new file mode 100644 index 000000000..d9bf5b862 --- /dev/null +++ b/conda_lock/_vendor/requests_file.py @@ -0,0 +1,119 @@ +from requests.adapters import BaseAdapter +from requests.compat import urlparse, unquote +from requests import Response, codes +import errno +import os +import stat +import locale +import io +try: + from io import BytesIO +except ImportError: + from StringIO import StringIO as BytesIO + + +class FileAdapter(BaseAdapter): + def __init__(self, set_content_length=True): + super(FileAdapter, self).__init__() + self._set_content_length = set_content_length + + def send(self, request, **kwargs): + """Wraps a file, described in request, in a Response object. + + :param request: The PreparedRequest` being "sent". + :returns: a Response object containing the file + """ + + # Check that the method makes sense. Only support GET + if request.method not in ("GET", "HEAD"): + raise ValueError("Invalid request method %s" % request.method) + + # Parse the URL + url_parts = urlparse(request.url) + + # Reject URLs with a hostname component + if url_parts.netloc and url_parts.netloc != "localhost": + raise ValueError("file: URLs with hostname components are not permitted") + + resp = Response() + + # Open the file, translate certain errors into HTTP responses + # Use urllib's unquote to translate percent escapes into whatever + # they actually need to be + try: + # Split the path on / (the URL directory separator) and decode any + # % escapes in the parts + path_parts = [unquote(p) for p in url_parts.path.split("/")] + + # Strip out the leading empty parts created from the leading /'s + while path_parts and not path_parts[0]: + path_parts.pop(0) + + # If os.sep is in any of the parts, someone fed us some shenanigans. + # Treat is like a missing file. + if any(os.sep in p for p in path_parts): + raise IOError(errno.ENOENT, os.strerror(errno.ENOENT)) + + # Look for a drive component. If one is present, store it separately + # so that a directory separator can correctly be added to the real + # path, and remove any empty path parts between the drive and the path. + # Assume that a part ending with : or | (legacy) is a drive. + if path_parts and ( + path_parts[0].endswith("|") or path_parts[0].endswith(":") + ): + path_drive = path_parts.pop(0) + if path_drive.endswith("|"): + path_drive = path_drive[:-1] + ":" + + while path_parts and not path_parts[0]: + path_parts.pop(0) + else: + path_drive = "" + + # Try to put the path back together + # Join the drive back in, and stick os.sep in front of the path to + # make it absolute. + path = path_drive + os.sep + os.path.join(*path_parts) + + # Check if the drive assumptions above were correct. If path_drive + # is set, and os.path.splitdrive does not return a drive, it wasn't + # really a drive. Put the path together again treating path_drive + # as a normal path component. + if path_drive and not os.path.splitdrive(path): + path = os.sep + os.path.join(path_drive, *path_parts) + + # Use io.open since we need to add a release_conn method, and + # methods can't be added to file objects in python 2. + resp.raw = io.open(path, "rb") + resp.raw.release_conn = resp.raw.close + except IOError as e: + if e.errno == errno.EACCES: + resp.status_code = codes.forbidden + elif e.errno == errno.ENOENT: + resp.status_code = codes.not_found + else: + resp.status_code = codes.bad_request + + # Wrap the error message in a file-like object + # The error message will be localized, try to convert the string + # representation of the exception into a byte stream + resp_str = str(e).encode(locale.getpreferredencoding(False)) + resp.raw = BytesIO(resp_str) + if self._set_content_length: + resp.headers["Content-Length"] = len(resp_str) + + # Add release_conn to the BytesIO object + resp.raw.release_conn = resp.raw.close + else: + resp.status_code = codes.ok + resp.url = request.url + + # If it's a regular file, set the Content-Length + resp_stat = os.fstat(resp.raw.fileno()) + if stat.S_ISREG(resp_stat.st_mode) and self._set_content_length: + resp.headers["Content-Length"] = resp_stat.st_size + + return resp + + def close(self): + pass From 4f3c1caf4fb640b5a59a435d208405f6dc279715 Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Tue, 24 Oct 2023 18:14:41 +0200 Subject: [PATCH 2/6] support also "file://" urls for private pip repositories --- conda_lock/_vendor/requests_file.py | 2 + conda_lock/interfaces/vendored_poetry.py | 7 +++ .../interfaces/vendored_requests_file.py | 6 +++ conda_lock/pypi_solver.py | 45 +++++++++++++++---- 4 files changed, 52 insertions(+), 8 deletions(-) create mode 100644 conda_lock/interfaces/vendored_requests_file.py diff --git a/conda_lock/_vendor/requests_file.py b/conda_lock/_vendor/requests_file.py index d9bf5b862..e850ed0c8 100644 --- a/conda_lock/_vendor/requests_file.py +++ b/conda_lock/_vendor/requests_file.py @@ -82,6 +82,8 @@ def send(self, request, **kwargs): if path_drive and not os.path.splitdrive(path): path = os.sep + os.path.join(path_drive, *path_parts) + if os.path.isdir(path): + path = os.path.join(path, "index.html") # Use io.open since we need to add a release_conn method, and # methods can't be added to file objects in python 2. resp.raw = io.open(path, "rb") diff --git a/conda_lock/interfaces/vendored_poetry.py b/conda_lock/interfaces/vendored_poetry.py index ac04c220e..e8e9bd253 100644 --- a/conda_lock/interfaces/vendored_poetry.py +++ b/conda_lock/interfaces/vendored_poetry.py @@ -1,3 +1,4 @@ +from conda_lock._vendor.poetry.config.config import Config from conda_lock._vendor.poetry.core.packages import Dependency as PoetryDependency from conda_lock._vendor.poetry.core.packages import Package as PoetryPackage from conda_lock._vendor.poetry.core.packages import ( @@ -9,16 +10,22 @@ from conda_lock._vendor.poetry.installation.chooser import Chooser from conda_lock._vendor.poetry.installation.operations.uninstall import Uninstall from conda_lock._vendor.poetry.puzzle import Solver as PoetrySolver +from conda_lock._vendor.poetry.repositories.legacy_repository import LegacyRepository from conda_lock._vendor.poetry.repositories.pool import Pool from conda_lock._vendor.poetry.repositories.pypi_repository import PyPiRepository from conda_lock._vendor.poetry.repositories.repository import Repository from conda_lock._vendor.poetry.utils.env import Env +from conda_lock._vendor.poetry.utils.helpers import get_cert, get_client_cert __all__ = [ + "get_cert", + "get_client_cert", "Chooser", + "Config", "Env", "Factory", + "LegacyRepository", "PoetryDependency", "PoetryPackage", "PoetryProjectPackage", diff --git a/conda_lock/interfaces/vendored_requests_file.py b/conda_lock/interfaces/vendored_requests_file.py new file mode 100644 index 000000000..9e0cbdb48 --- /dev/null +++ b/conda_lock/interfaces/vendored_requests_file.py @@ -0,0 +1,6 @@ +from conda_lock._vendor.requests_file import FileAdapter + + +__all__ = [ + "FileAdapter", +] diff --git a/conda_lock/pypi_solver.py b/conda_lock/pypi_solver.py index 9fe7f742e..a432b6e38 100644 --- a/conda_lock/pypi_solver.py +++ b/conda_lock/pypi_solver.py @@ -4,7 +4,9 @@ from pathlib import Path from posixpath import expandvars from typing import TYPE_CHECKING, Dict, List, Optional -from urllib.parse import urldefrag, urlparse, urlsplit, urlunsplit +from urllib.parse import urldefrag, urlsplit, urlunsplit + +import requests from clikit.api.io.flags import VERY_VERBOSE from clikit.io import ConsoleIO, NullIO @@ -12,8 +14,10 @@ from conda_lock.interfaces.vendored_poetry import ( Chooser, + Config, Env, Factory, + LegacyRepository, PoetryDependency, PoetryPackage, PoetryProjectPackage, @@ -24,7 +28,10 @@ PyPiRepository, Repository, Uninstall, + get_cert, + get_client_cert, ) +from conda_lock.interfaces.vendored_requests_file import FileAdapter from conda_lock.lockfile import apply_categories from conda_lock.lockfile.v2prelim.models import ( DependencySource, @@ -384,6 +391,28 @@ def solve_pypi( return {dep.name: dep for dep in requirements} +class CondaLockLegacyRepository(LegacyRepository): + @property + def session(self) -> requests.Session: + _session = super().session + _session.mount("file://", FileAdapter()) + return _session + + +def create_legacy_repository( + source: Dict[str, str], auth_config: Config +) -> LegacyRepository: + name = source["name"] + url = source["url"] + return CondaLockLegacyRepository( + name, + url, + config=auth_config, + cert=get_cert(auth_config, name), + client_cert=get_client_cert(auth_config, name), + ) + + def _prepare_repositories_pool( allow_pypi_requests: bool, pip_repositories: Optional[List[PipRepository]] = None ) -> Pool: @@ -397,13 +426,13 @@ def _prepare_repositories_pool( """ factory = Factory() config = factory.create_config() - repos = [ - factory.create_legacy_repository( - {"name": pip_repository.name, "url": expandvars(pip_repository.url)}, - config, - ) - for pip_repository in pip_repositories or [] - ] + [ + + legacy_repos = [] + for pip_repository in pip_repositories or []: + repo_cfg = {"name": pip_repository.name, "url": expandvars(pip_repository.url)} + legacy_repo = create_legacy_repository(repo_cfg, config) + legacy_repos.append(legacy_repo) + repos = legacy_repos + [ factory.create_legacy_repository( {"name": source[0], "url": source[1]["url"]}, config ) From 19ce3b55a7212a8643a76d07db58b00abad60289 Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Sun, 5 Nov 2023 09:01:29 +0000 Subject: [PATCH 3/6] add tests --- tests/test-local-pip-repository/.gitignore | 1 + .../environment.yaml | 10 ++ tests/test_local_pip_repository.py | 104 ++++++++++++++++++ 3 files changed, 115 insertions(+) create mode 100644 tests/test-local-pip-repository/.gitignore create mode 100644 tests/test-local-pip-repository/environment.yaml create mode 100644 tests/test_local_pip_repository.py diff --git a/tests/test-local-pip-repository/.gitignore b/tests/test-local-pip-repository/.gitignore new file mode 100644 index 000000000..335ec9573 --- /dev/null +++ b/tests/test-local-pip-repository/.gitignore @@ -0,0 +1 @@ +*.tar.gz diff --git a/tests/test-local-pip-repository/environment.yaml b/tests/test-local-pip-repository/environment.yaml new file mode 100644 index 000000000..82b257ece --- /dev/null +++ b/tests/test-local-pip-repository/environment.yaml @@ -0,0 +1,10 @@ +channels: + - conda-forge +pip-repositories: + - file://$PYPI_FILE_URL/ +dependencies: + - python=3.11 + - pip: + - fake-private-package==1.0.0 + - six + diff --git a/tests/test_local_pip_repository.py b/tests/test_local_pip_repository.py new file mode 100644 index 000000000..0588a11ba --- /dev/null +++ b/tests/test_local_pip_repository.py @@ -0,0 +1,104 @@ +import base64 +import os +import re +import shutil +import tarfile + +from pathlib import Path +from urllib.parse import urlparse + +import pytest + +from conda_lock.conda_lock import DEFAULT_LOCKFILE_NAME, run_lock +from conda_lock.lockfile import parse_conda_lock_file +from tests.test_conda_lock import clone_test_dir +from tests.test_pip_repositories import private_package_tar + + +_PRIVATE_REPO_ROOT = """ + + + fake-private-package + + +""" + +_PRIVATE_REPO_PACKAGE = """ + + + fake-private-package-1.0.0.tar.gz + + +""" + +_PRIVATE_PACKAGE_SDIST_PATH = ( + Path(__file__).parent / "test-pip-repositories" / "fake-private-package-1.0.0" +) + + +@pytest.fixture(autouse=True) +def create_local_private_pypi(private_package_tar: Path, tmp_path: Path): # noqa: F811 + repo_dir = tmp_path / "repo" + pkg_dir = repo_dir / "fake-private-package" + pkg_dir.mkdir(parents=True, exist_ok=True) + + tar_path = pkg_dir / private_package_tar.name + shutil.copy(private_package_tar, tar_path) + + with open(repo_dir / "index.html", "w") as repo_file: + repo_file.write(_PRIVATE_REPO_ROOT) + _repo_package_html = _PRIVATE_REPO_PACKAGE.replace("$PYPI_FILE_URL", str(repo_dir)) + with open(pkg_dir / "index.html", "w") as pkg_file: + pkg_file.write(_repo_package_html) + yield + + +def use_local_file_url_in_environment_yaml(environment_file: Path, repo_dir: Path): + with environment_file.open("r+") as env_fp: + templated_env_yml = env_fp.read() + env_yml = templated_env_yml.replace("$PYPI_FILE_URL", str(repo_dir)) + env_fp.seek(0) + env_fp.write(env_yml) + env_fp.truncate() + + +def test_it_installs_packages_from_private_pip_repository_on_local_disk( + monkeypatch: "pytest.MonkeyPatch", + conda_exe: str, + tmp_path: Path, +): + # GIVEN an environment.yaml with custom pip repositories + directory = clone_test_dir("test-local-pip-repository", tmp_path) + monkeypatch.chdir(directory) + repo_dir = tmp_path / "repo" + environment_file = directory / "environment.yaml" + assert environment_file.exists(), list(directory.iterdir()) + use_local_file_url_in_environment_yaml(environment_file, repo_dir) + + # WHEN I create the lockfile + run_lock([directory / "environment.yaml"], conda_exe=conda_exe) + + # THEN the lockfile is generated correctly + lockfile_path = directory / DEFAULT_LOCKFILE_NAME + assert lockfile_path.exists(), list(directory.iterdir()) + lockfile = parse_conda_lock_file(lockfile_path) + lockfile_content = lockfile_path.read_text(encoding="utf-8") + packages = {package.name: package for package in lockfile.package} + + # AND the private package is in the lockfile + private_package = packages.get("fake-private-package") + assert private_package, lockfile_content + + # AND the private package was installed from the local repository + assert private_package.url.startswith("file://") + + # AND the six package is in the lockfile + package = packages.get("six") + assert package, lockfile_content + + package_url = urlparse(package.url) + # AND the package was sourced from pypi + assert package_url.hostname == "files.pythonhosted.org", ( + "Package was fetched from incorrect host. See full lock-file:\n" + + lockfile_content + ) From 6a1eca1e224e2c59bd41fdcbc9b9d6c74f32fa80 Mon Sep 17 00:00:00 2001 From: Ben Mares Date: Fri, 10 Nov 2023 18:10:08 +0100 Subject: [PATCH 4/6] Configure "vendoring" for requests-file --- ...{requests_file.LICENSE => requests-file.LICENSE} | 0 conda_lock/_vendor/requests_file.pyi | 1 + conda_lock/_vendor/vendor.txt | 3 +++ .../vendor_poetry/patches/requests-file.patch | 13 +++++++++++++ 4 files changed, 17 insertions(+) rename conda_lock/_vendor/{requests_file.LICENSE => requests-file.LICENSE} (100%) create mode 100644 conda_lock/_vendor/requests_file.pyi create mode 100644 conda_lock/scripts/vendor_poetry/patches/requests-file.patch diff --git a/conda_lock/_vendor/requests_file.LICENSE b/conda_lock/_vendor/requests-file.LICENSE similarity index 100% rename from conda_lock/_vendor/requests_file.LICENSE rename to conda_lock/_vendor/requests-file.LICENSE diff --git a/conda_lock/_vendor/requests_file.pyi b/conda_lock/_vendor/requests_file.pyi new file mode 100644 index 000000000..156b606f8 --- /dev/null +++ b/conda_lock/_vendor/requests_file.pyi @@ -0,0 +1 @@ +from requests_file import * \ No newline at end of file diff --git a/conda_lock/_vendor/vendor.txt b/conda_lock/_vendor/vendor.txt index 664a8c7a7..7fbac335f 100644 --- a/conda_lock/_vendor/vendor.txt +++ b/conda_lock/_vendor/vendor.txt @@ -5,3 +5,6 @@ poetry-core==1.0.8 # install conda from github git+https://github.com/conda/conda.git@22.9.0 + +# For resolution of file:// URLs +git+https://github.com/dashea/requests-file@675c7a6849bd5b2c271b503a3f51ea65599b34ec diff --git a/conda_lock/scripts/vendor_poetry/patches/requests-file.patch b/conda_lock/scripts/vendor_poetry/patches/requests-file.patch new file mode 100644 index 000000000..ba89bd159 --- /dev/null +++ b/conda_lock/scripts/vendor_poetry/patches/requests-file.patch @@ -0,0 +1,13 @@ +diff --git b/conda_lock/_vendor/requests_file.py a/conda_lock/_vendor/requests_file.py +index d9bf5b8..e850ed0 100644 +--- b/conda_lock/_vendor/requests_file.py ++++ a/conda_lock/_vendor/requests_file.py +@@ -82,6 +82,8 @@ class FileAdapter(BaseAdapter): + if path_drive and not os.path.splitdrive(path): + path = os.sep + os.path.join(path_drive, *path_parts) + ++ if os.path.isdir(path): ++ path = os.path.join(path, "index.html") + # Use io.open since we need to add a release_conn method, and + # methods can't be added to file objects in python 2. + resp.raw = io.open(path, "rb") From 971b5416575027711d36b159f75851ebcac653ff Mon Sep 17 00:00:00 2001 From: Ben Mares Date: Fri, 10 Nov 2023 18:38:45 +0100 Subject: [PATCH 5/6] Ignore type error in vendored_requests_file --- conda_lock/interfaces/vendored_requests_file.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conda_lock/interfaces/vendored_requests_file.py b/conda_lock/interfaces/vendored_requests_file.py index 9e0cbdb48..562b98d1c 100644 --- a/conda_lock/interfaces/vendored_requests_file.py +++ b/conda_lock/interfaces/vendored_requests_file.py @@ -1,4 +1,4 @@ -from conda_lock._vendor.requests_file import FileAdapter +from conda_lock._vendor.requests_file import FileAdapter # type: ignore __all__ = [ From 04109c00f4a5d6aa205bc840153c7e4592ee749f Mon Sep 17 00:00:00 2001 From: Ben Mares Date: Fri, 10 Nov 2023 20:20:35 +0100 Subject: [PATCH 6/6] Suggested cleanups --- tests/test-local-pip-repository/.gitignore | 1 - .../environment.yaml | 5 +- tests/test_local_pip_repository.py | 79 ++++++++++--------- 3 files changed, 45 insertions(+), 40 deletions(-) delete mode 100644 tests/test-local-pip-repository/.gitignore diff --git a/tests/test-local-pip-repository/.gitignore b/tests/test-local-pip-repository/.gitignore deleted file mode 100644 index 335ec9573..000000000 --- a/tests/test-local-pip-repository/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.tar.gz diff --git a/tests/test-local-pip-repository/environment.yaml b/tests/test-local-pip-repository/environment.yaml index 82b257ece..acd19d9f3 100644 --- a/tests/test-local-pip-repository/environment.yaml +++ b/tests/test-local-pip-repository/environment.yaml @@ -3,8 +3,9 @@ channels: pip-repositories: - file://$PYPI_FILE_URL/ dependencies: - - python=3.11 + - python - pip: - fake-private-package==1.0.0 - six - +platforms: + - linux-64 diff --git a/tests/test_local_pip_repository.py b/tests/test_local_pip_repository.py index 0588a11ba..b3760c884 100644 --- a/tests/test_local_pip_repository.py +++ b/tests/test_local_pip_repository.py @@ -1,8 +1,4 @@ -import base64 -import os -import re import shutil -import tarfile from pathlib import Path from urllib.parse import urlparse @@ -12,10 +8,12 @@ from conda_lock.conda_lock import DEFAULT_LOCKFILE_NAME, run_lock from conda_lock.lockfile import parse_conda_lock_file from tests.test_conda_lock import clone_test_dir -from tests.test_pip_repositories import private_package_tar +# This is a fixture, so we need to import it: +from tests.test_pip_repositories import private_package_tar # pyright: ignore -_PRIVATE_REPO_ROOT = """ + +_PRIVATE_REPO_ROOT_INDEX = """ fake-private-package @@ -23,7 +21,7 @@ """ -_PRIVATE_REPO_PACKAGE = """ +_PRIVATE_REPO_PACKAGE_INDEX = """ fake-private-package-1.0.0.tar.gz @@ -31,13 +29,18 @@ """ -_PRIVATE_PACKAGE_SDIST_PATH = ( - Path(__file__).parent / "test-pip-repositories" / "fake-private-package-1.0.0" -) - -@pytest.fixture(autouse=True) -def create_local_private_pypi(private_package_tar: Path, tmp_path: Path): # noqa: F811 +@pytest.fixture +def local_private_pypi(private_package_tar: Path, tmp_path: Path): # noqa: F811 + """ + Create a local package index with the following directory structure: + tmp_path + └── repo + ├── fake-private-package + │ ├── fake-private-package-1.0.0.tar.gz + │ └── index.html + └── index.html + """ repo_dir = tmp_path / "repo" pkg_dir = repo_dir / "fake-private-package" pkg_dir.mkdir(parents=True, exist_ok=True) @@ -45,42 +48,44 @@ def create_local_private_pypi(private_package_tar: Path, tmp_path: Path): # noq tar_path = pkg_dir / private_package_tar.name shutil.copy(private_package_tar, tar_path) - with open(repo_dir / "index.html", "w") as repo_file: - repo_file.write(_PRIVATE_REPO_ROOT) - _repo_package_html = _PRIVATE_REPO_PACKAGE.replace("$PYPI_FILE_URL", str(repo_dir)) - with open(pkg_dir / "index.html", "w") as pkg_file: - pkg_file.write(_repo_package_html) - yield + with open(repo_dir / "index.html", "w") as repo_index: + repo_index.write(_PRIVATE_REPO_ROOT_INDEX) + _repo_package_index_html = _PRIVATE_REPO_PACKAGE_INDEX.replace( + "$PYPI_FILE_URL", str(repo_dir) + ) + with open(pkg_dir / "index.html", "w") as pkg_index: + pkg_index.write(_repo_package_index_html) + return repo_dir -def use_local_file_url_in_environment_yaml(environment_file: Path, repo_dir: Path): - with environment_file.open("r+") as env_fp: - templated_env_yml = env_fp.read() - env_yml = templated_env_yml.replace("$PYPI_FILE_URL", str(repo_dir)) - env_fp.seek(0) - env_fp.write(env_yml) - env_fp.truncate() + +@pytest.fixture +def local_pip_repository_environment_file( + local_private_pypi: Path, tmp_path: Path +) -> Path: + environment_file = ( + clone_test_dir("test-local-pip-repository", tmp_path) / "environment.yaml" + ) + templated_env_yml = environment_file.read_text() + env_yml = templated_env_yml.replace("$PYPI_FILE_URL", str(local_private_pypi)) + environment_file.write_text(env_yml) + return environment_file def test_it_installs_packages_from_private_pip_repository_on_local_disk( monkeypatch: "pytest.MonkeyPatch", conda_exe: str, tmp_path: Path, + local_pip_repository_environment_file: Path, ): - # GIVEN an environment.yaml with custom pip repositories - directory = clone_test_dir("test-local-pip-repository", tmp_path) - monkeypatch.chdir(directory) - repo_dir = tmp_path / "repo" - environment_file = directory / "environment.yaml" - assert environment_file.exists(), list(directory.iterdir()) - use_local_file_url_in_environment_yaml(environment_file, repo_dir) - # WHEN I create the lockfile - run_lock([directory / "environment.yaml"], conda_exe=conda_exe) + monkeypatch.chdir(tmp_path) + environment_file = local_pip_repository_environment_file + run_lock([environment_file], conda_exe=conda_exe) # THEN the lockfile is generated correctly - lockfile_path = directory / DEFAULT_LOCKFILE_NAME - assert lockfile_path.exists(), list(directory.iterdir()) + lockfile_path = tmp_path / DEFAULT_LOCKFILE_NAME + assert lockfile_path.exists(), list(tmp_path.iterdir()) lockfile = parse_conda_lock_file(lockfile_path) lockfile_content = lockfile_path.read_text(encoding="utf-8") packages = {package.name: package for package in lockfile.package}