diff --git a/README.md b/README.md index ea04502d..83fe183d 100644 --- a/README.md +++ b/README.md @@ -154,13 +154,28 @@ The default category is `main`. [environment.yml][envyaml], using a vendored copy of [Poetry's][poetry] dependency solver. ### private pip repositories -Right now `conda-lock` only supports [legacy](https://warehouse.pypa.io/api-reference/legacy.html) pypi repos with basic auth. Most self-hosted repositories like Nexus, Artifactory etc. use this. To use this feature, add your private repo into Poetry's config _including_ the basic auth in the url: +Right now `conda-lock` only supports [legacy](https://warehouse.pypa.io/api-reference/legacy.html) pypi repos with basic auth. Most self-hosted repositories like Nexus, Artifactory etc. use this. You can configure private repositories by setting the `CONDA_LOCK_PYPI_REPOSITORY_` environment variable to the URL of the private repo, _including_ any basic auth. For example: ```bash -poetry config repositories.foo https://username:password@foo.repo/simple/ +export CONDA_LOCK_PYPI_REPOSITORY_EXAMPLE="https://username:password@example.repo/simple" ``` -The private repo will be used in addition to `pypi.org`. For projects using `pyproject.toml`, it is possible to [disable `pypi.org` entirely](#disabling-pypiorg). +Alternatively, you can use the `poetry` configuration file format to configure private PyPi repositories. The configuration file should be named `config.toml` and have the following format: + +```toml +[repositories.example] +url = "https://username:password@example.repo/simple" +``` + +The discoverable location of this file depends on the operating system: + +- **Linux**: `$XDG_CONFIG_HOME/pypoetry-conda-lock` or `~/.config/pypoetry-conda-lock` +- **OSX**: `~/Library/Application Support/pypoetry-conda-lock` +- **Windows**: Check output of `python -c 'from conda_lock._vendor.poetry.locations import CONFIG_DIR;print(CONFIG_DIR)'` + + +Private repositories will be used in addition to `pypi.org`. For projects using `pyproject.toml`, it is possible to [disable `pypi.org` entirely](#disabling-pypiorg). + ### --dev-dependencies/--no-dev-dependencies diff --git a/conda_lock/pypi_solver.py b/conda_lock/pypi_solver.py index 56b85d81..fa26a102 100644 --- a/conda_lock/pypi_solver.py +++ b/conda_lock/pypi_solver.py @@ -1,6 +1,8 @@ +import os import re import sys +from functools import lru_cache from pathlib import Path from typing import TYPE_CHECKING, Dict, List, Optional from urllib.parse import urldefrag, urlsplit, urlunsplit @@ -373,6 +375,9 @@ def _prepare_repositories_pool(allow_pypi_requests: bool) -> Pool: factory = Factory() config = factory.create_config() repos = [ + factory.create_legacy_repository({"name": name, "url": url}, config) + for name, url in _parse_repositories_from_environment().items() + ] + [ factory.create_legacy_repository( {"name": source[0], "url": source[1]["url"]}, config ) @@ -383,6 +388,16 @@ def _prepare_repositories_pool(allow_pypi_requests: bool) -> Pool: return Pool(repositories=[*repos]) +@lru_cache(maxsize=1) +def _parse_repositories_from_environment() -> Dict[str, str]: + env_prefix = "CONDA_LOCK_PYPI_REPOSITORY_" + return { + key[len(env_prefix) :].lower(): value + for key, value in os.environ.items() + if key.startswith(env_prefix) + } + + def _strip_auth(url: str) -> str: """Strip HTTP Basic authentication from a URL.""" parts = urlsplit(url, allow_fragments=True) diff --git a/tests/test_conda_lock.py b/tests/test_conda_lock.py index 764c66ae..9cd9b796 100644 --- a/tests/test_conda_lock.py +++ b/tests/test_conda_lock.py @@ -60,7 +60,13 @@ ) from conda_lock.models.channel import Channel from conda_lock.models.lock_spec import VCSDependency, VersionedDependency -from conda_lock.pypi_solver import _strip_auth, parse_pip_requirement, solve_pypi +from conda_lock.pypi_solver import ( + _parse_repositories_from_environment, + _prepare_repositories_pool, + _strip_auth, + parse_pip_requirement, + solve_pypi, +) from conda_lock.src_parser import ( DEFAULT_PLATFORMS, LockSpecification, @@ -1673,6 +1679,24 @@ def test__extract_domain(line: str, stripped: str): assert _extract_domain(line) == stripped +def test_parse_repositories_from_environment(): + # Given a private repository is configured in the environment + try: + _parse_repositories_from_environment.cache_clear() + repository_url = "https://username:password@example.repo/simple" + os.environ["CONDA_LOCK_PYPI_REPOSITORY_EXAMPLE"] = repository_url + # When I prepare the repositories pool + pool = _prepare_repositories_pool(allow_pypi_requests=False) + # Then the private repository is included in the pool + assert pool.repositories, "No repositories were detected" + assert ( + pool.repositories[0].url == repository_url + ), "Detected repository has incorrect URL" + finally: + _parse_repositories_from_environment.cache_clear() + del os.environ["CONDA_LOCK_PYPI_REPOSITORY_EXAMPLE"] + + def _read_file(filepath: "str | Path") -> str: with open(filepath, mode="r") as file_pointer: return file_pointer.read()