diff --git a/docs/docs/usage/project.md b/docs/docs/usage/project.md index a690b2d53b..0b89ed5c73 100644 --- a/docs/docs/usage/project.md +++ b/docs/docs/usage/project.md @@ -134,6 +134,9 @@ pdm config pypi.extra.username "foo" pdm config pypi.extra.password "password4foo" ``` +The index parameters can be put in different levels of configuration files. That is, you can only put the `url` in `pyproject.toml`, +and store the credentials in `~/.config/pdm/config.toml` under the `[pypi.]` section of which the name must match. + !!! NOTE Configured indexes will be tried **after** the sources in `pyproject.toml`, if you want to completely ignore the locally configured indexes, including the main index, set the config value `pypi.ignore_stored_index` diff --git a/news/1667.feature.md b/news/1667.feature.md new file mode 100644 index 0000000000..3d439fb37f --- /dev/null +++ b/news/1667.feature.md @@ -0,0 +1 @@ +Merge the index parameters from different configuration files. diff --git a/src/pdm/_types.py b/src/pdm/_types.py index e8ed27a40b..0b83eefce2 100644 --- a/src/pdm/_types.py +++ b/src/pdm/_types.py @@ -1,17 +1,54 @@ from __future__ import annotations +import dataclasses from typing import Any, Dict, List, NamedTuple, Tuple, TypeVar, Union -from pdm.compat import Literal, Protocol, TypedDict +from pdm.compat import Protocol + + +@dataclasses.dataclass +class RepositoryConfig: + url: str | None = None + username: str | None = None + password: str | None = None + verify_ssl: bool | None = None + type: str | None = None + ca_certs: str | None = None + + config_prefix: str | None = None + name: str | None = None + + def passive_update( + self, other: RepositoryConfig | None = None, **kwargs: Any + ) -> None: + """An update method that prefers the existing value over the new one.""" + if other is not None: + for k in other.__dataclass_fields__: + v = getattr(other, k) + if getattr(self, k) is None and v is not None: + setattr(self, k, v) + for k, v in kwargs.items(): + if getattr(self, k) is None and v is not None: + setattr(self, k, v) - -class Source(TypedDict, total=False): - url: str - verify_ssl: bool - name: str - type: Literal["index", "find_links"] - username: str - password: str + def __rich__(self) -> str: + config_prefix = ( + f"{self.config_prefix}." if self.config_prefix is not None else "" + ) + lines: list[str] = [] + if self.url: + lines.append(f"[primary]{config_prefix}url[/] = {self.url}") + if self.username: + lines.append(f"[primary]{config_prefix}username[/] = {self.username}") + if self.password: + lines.append(f"[primary]{config_prefix}password[/] = [i][/]") + if self.verify_ssl is not None: + lines.append(f"[primary]{config_prefix}verify_ssl[/] = {self.verify_ssl}") + if self.type: + lines.append(f"[primary]{config_prefix}type[/] = {self.type}") + if self.ca_certs: + lines.append(f"[primary]{config_prefix}ca_certs[/] = {self.ca_certs}") + return "\n".join(lines) RequirementDict = Union[str, Dict[str, Union[str, bool]]] diff --git a/src/pdm/cli/commands/config.py b/src/pdm/cli/commands/config.py index a8b678c37b..8d88306043 100644 --- a/src/pdm/cli/commands/config.py +++ b/src/pdm/cli/commands/config.py @@ -2,15 +2,10 @@ from typing import Any, Mapping from pdm import termui +from pdm._types import RepositoryConfig from pdm.cli.commands.base import BaseCommand from pdm.project import Project -from pdm.project.config import ( - DEFAULT_REPOSITORIES, - REPOSITORY, - Config, - RegistryConfig, - RepositoryConfig, -) +from pdm.project.config import DEFAULT_REPOSITORIES, REPOSITORY, Config class Command(BaseCommand): @@ -93,7 +88,7 @@ def _show_config( style=extra_style, verbosity=termui.Verbosity.DETAIL, ) - self.ui.echo(RegistryConfig(**config[key], config_prefix=key)) + self.ui.echo(RepositoryConfig(**config[key], config_prefix=key)) elif key.startswith(REPOSITORY): for item in config[key]: self.ui.echo( @@ -103,7 +98,7 @@ def _show_config( ) repository = dict(config[key][item]) if "url" not in repository and item in DEFAULT_REPOSITORIES: - repository["url"] = DEFAULT_REPOSITORIES[item].url + repository["url"] = DEFAULT_REPOSITORIES[item] self.ui.echo( RepositoryConfig( **repository, config_prefix=f"{key}.{item}" diff --git a/src/pdm/cli/commands/publish/__init__.py b/src/pdm/cli/commands/publish/__init__.py index 9385834715..7187915057 100644 --- a/src/pdm/cli/commands/publish/__init__.py +++ b/src/pdm/cli/commands/publish/__init__.py @@ -118,6 +118,7 @@ def get_repository(project: Project, options: argparse.Namespace) -> Repository: config = project.global_config.get_repository_config(repository) if config is None: raise PdmUsageError(f"Missing repository config of {repository}") + assert config.url is not None if username is not None: config.username = username if password is not None: diff --git a/src/pdm/cli/commands/publish/repository.py b/src/pdm/cli/commands/publish/repository.py index b2e2dfbd34..2f37365289 100644 --- a/src/pdm/cli/commands/publish/repository.py +++ b/src/pdm/cli/commands/publish/repository.py @@ -85,9 +85,9 @@ def _convert_to_list_of_tuples(data: dict[str, Any]) -> list[tuple[str, Any]]: return result def get_release_urls(self, packages: list[PackageFile]) -> Iterable[str]: - if self.url.startswith(DEFAULT_REPOSITORIES["pypi"].url.rstrip("/")): + if self.url.startswith(DEFAULT_REPOSITORIES["pypi"].rstrip("/")): base = "https://pypi.org/" - elif self.url.startswith(DEFAULT_REPOSITORIES["testpypi"].url.rstrip("/")): + elif self.url.startswith(DEFAULT_REPOSITORIES["testpypi"].rstrip("/")): base = "https://test.pypi.org/" else: return set() diff --git a/src/pdm/formats/poetry.py b/src/pdm/formats/poetry.py index a230ace577..ab039add1c 100644 --- a/src/pdm/formats/poetry.py +++ b/src/pdm/formats/poetry.py @@ -23,7 +23,7 @@ if TYPE_CHECKING: from pdm.project.core import Project from argparse import Namespace - from pdm._types import RequirementDict, Source + from pdm._types import RequirementDict from pdm.utils import cd @@ -193,7 +193,7 @@ def build(self, value: str | dict) -> None: raise Unset() @convert_from("source") - def sources(self, value: list[Source]) -> None: + def sources(self, value: list[dict[str, Any]]) -> None: self.settings["source"] = [ { "name": item.get("name", ""), diff --git a/src/pdm/formats/requirements.py b/src/pdm/formats/requirements.py index 17b381d16e..952234b263 100644 --- a/src/pdm/formats/requirements.py +++ b/src/pdm/formats/requirements.py @@ -7,7 +7,7 @@ import urllib.parse from argparse import Namespace from os import PathLike -from typing import TYPE_CHECKING, Any, Mapping, cast +from typing import Any, Mapping from pdm.formats.base import make_array from pdm.models.candidates import Candidate @@ -15,9 +15,6 @@ from pdm.project import Project from pdm.utils import expand_env_vars_in_auth -if TYPE_CHECKING: - from pdm._types import Source - class RequirementParser: """Reference: @@ -111,7 +108,7 @@ def _is_url_trusted(url: str, trusted_hosts: list[str]) -> bool: def convert_url_to_source( url: str, name: str | None, trusted_hosts: list[str], type: str = "index" -) -> Source: +) -> dict[str, Any]: if not name: name = hashlib.sha1(url.encode("utf-8")).hexdigest()[:6] source = { @@ -121,7 +118,7 @@ def convert_url_to_source( } if type != "index": source["type"] = type - return cast("Source", source) + return source def convert( @@ -150,7 +147,7 @@ def convert( data["optional-dependencies"] = {options.group: deps} else: data["dependencies"] = deps - sources: list[Source] = [] + sources: list[dict[str, Any]] = [] if parser.index_url and not parser.no_index: sources.append( convert_url_to_source(parser.index_url, "pypi", parser.trusted_hosts) diff --git a/src/pdm/models/auth.py b/src/pdm/models/auth.py index 3a02e08e18..df4541c5f0 100644 --- a/src/pdm/models/auth.py +++ b/src/pdm/models/auth.py @@ -5,7 +5,7 @@ from unearth.auth import MaybeAuth, MultiDomainBasicAuth from unearth.utils import split_auth_from_netloc -from pdm._types import Source +from pdm._types import RepositoryConfig from pdm.exceptions import PdmException from pdm.termui import UI @@ -24,7 +24,7 @@ class PdmBasicAuth(MultiDomainBasicAuth): - It shows an error message when credentials are not provided or correct. """ - def __init__(self, sources: list[Source], prompting: bool = True) -> None: + def __init__(self, sources: list[RepositoryConfig], prompting: bool = True) -> None: super().__init__(prompting=True) self._real_prompting = prompting self.sources = sources @@ -33,12 +33,13 @@ def _get_auth_from_index_url(self, netloc: str) -> tuple[MaybeAuth, str | None]: if not self.sources: return None, None for source in self.sources: - parsed = urllib.parse.urlparse(source["url"]) + assert source.url + parsed = urllib.parse.urlparse(source.url) auth, index_netloc = split_auth_from_netloc(parsed.netloc) if index_netloc == netloc: - if "username" in source: - auth = (source["username"], source.get("password")) - return auth, source["url"] + if source.username: + auth = (source.username, source.password) + return auth, source.url return None, None def _prompt_for_password(self, netloc: str) -> tuple[str | None, str | None, bool]: diff --git a/src/pdm/models/environment.py b/src/pdm/models/environment.py index e668d80389..a837f49dc5 100644 --- a/src/pdm/models/environment.py +++ b/src/pdm/models/environment.py @@ -33,7 +33,7 @@ ) if TYPE_CHECKING: - from pdm._types import Source + from pdm._types import RepositoryConfig from pdm.project import Project @@ -145,7 +145,7 @@ def _build_session( @contextmanager def get_finder( self, - sources: list[Source] | None = None, + sources: list[RepositoryConfig] | None = None, ignore_compatibility: bool = False, ) -> Generator[unearth.PackageFinder, None, None]: """Return the package finder of given index sources. diff --git a/src/pdm/models/repositories.py b/src/pdm/models/repositories.py index 6ae8308b0e..1c445db15b 100644 --- a/src/pdm/models/repositories.py +++ b/src/pdm/models/repositories.py @@ -20,7 +20,7 @@ from pdm.utils import cd, normalize_name, url_without_fragments if TYPE_CHECKING: - from pdm._types import CandidateInfo, SearchResult, Source + from pdm._types import CandidateInfo, RepositoryConfig, SearchResult from pdm.models.environment import Environment ALLOW_ALL_PYTHON = PySpecSet() @@ -46,7 +46,7 @@ class BaseRepository: def __init__( self, - sources: list[Source], + sources: list[RepositoryConfig], environment: Environment, ignore_compatibility: bool = True, ) -> None: @@ -62,7 +62,7 @@ def __init__( self._candidate_info_cache = environment.project.make_candidate_info_cache() self._hash_cache = environment.project.make_hash_cache() - def get_filtered_sources(self, req: Requirement) -> list[Source]: + def get_filtered_sources(self, req: Requirement) -> list[RepositoryConfig]: """Get matching sources based on the index attribute.""" return self.sources @@ -295,7 +295,8 @@ def _get_dependencies_from_json(self, candidate: Candidate) -> CandidateInfo: proc_url[:-7] # Strip "/simple". for proc_url in ( raw_url.rstrip("/") - for raw_url in (source.get("url", "") for source in sources) + for raw_url in (source.url for source in sources) + if raw_url ) if proc_url.endswith("/simple") ] @@ -347,7 +348,7 @@ def _find_candidates(self, requirement: Requirement) -> Iterable[Candidate]: return cans def search(self, query: str) -> SearchResult: - pypi_simple = self.sources[0]["url"].rstrip("/") + pypi_simple = self.sources[0].url.rstrip("/") # type: ignore[union-attr] if pypi_simple.endswith("/simple"): search_url = pypi_simple[:-6] + "search" @@ -378,7 +379,7 @@ class LockedRepository(BaseRepository): def __init__( self, lockfile: Mapping[str, Any], - sources: list[Source], + sources: list[RepositoryConfig], environment: Environment, ) -> None: super().__init__(sources, environment, ignore_compatibility=False) diff --git a/src/pdm/project/config.py b/src/pdm/project/config.py index ce5d077235..923a10802e 100644 --- a/src/pdm/project/config.py +++ b/src/pdm/project/config.py @@ -4,76 +4,24 @@ import dataclasses import os from pathlib import Path -from typing import Any, Callable, Iterator, Mapping, MutableMapping, cast +from typing import Any, Callable, Iterator, Mapping, MutableMapping import platformdirs import rich.theme import tomlkit from pdm import termui -from pdm._types import Source +from pdm._types import RepositoryConfig from pdm.exceptions import NoConfigError, PdmUsageError -ui = termui.UI() - REPOSITORY = "repository" - - -@dataclasses.dataclass -class RepositoryConfig: - url: str - username: str | None = None - password: str | None = None - ca_certs: str | None = None - - config_prefix: str | None = None - - def __rich__(self) -> str: - config_prefix = ( - f"{self.config_prefix}." if self.config_prefix is not None else "" - ) - lines = [f"[primary]{config_prefix}url[/] = {self.url}"] - if self.username: - lines.append(f"[primary]{config_prefix}username[/] = {self.username}") - if self.password: - lines.append(f"[primary]{config_prefix}password[/] = [i][/]") - if self.ca_certs: - lines.append(f"[primary]{config_prefix}ca_certs[/] = {self.ca_certs}") - return "\n".join(lines) - - -@dataclasses.dataclass -class RegistryConfig: - - url: str - username: str | None = None - password: str | None = None - verify_ssl: bool | None = None - type: str | None = None - - config_prefix: str | None = None - - def __rich__(self) -> str: - config_prefix = ( - f"{self.config_prefix}." if self.config_prefix is not None else "" - ) - lines = [f"[primary]{config_prefix}url[/] = {self.url}"] - if self.username: - lines.append(f"[primary]{config_prefix}username[/] = {self.username}") - if self.password: - lines.append(f"[primary]{config_prefix}password[/] = [i][/]") - if self.verify_ssl: - lines.append(f"[primary]{config_prefix}verify_ssl[/] = {self.verify_ssl}") - if self.type: - lines.append(f"[primary]{config_prefix}type[/] = {self.type}") - return "\n".join(lines) - - DEFAULT_REPOSITORIES = { - "pypi": RepositoryConfig("https://upload.pypi.org/legacy/"), - "testpypi": RepositoryConfig("https://test.pypi.org/legacy/"), + "pypi": "https://upload.pypi.org/legacy/", + "testpypi": "https://test.pypi.org/legacy/", } +ui = termui.UI() + def load_config(file_path: Path) -> dict[str, Any]: """Load a nested TOML document into key-value pairs @@ -331,10 +279,10 @@ def load_theme(self) -> rich.theme.Theme: def self_data(self) -> dict[str, Any]: return dict(self._file_data) - def iter_sources(self) -> Iterator[tuple[str, Source]]: + def iter_sources(self) -> Iterator[tuple[str, RepositoryConfig]]: for name, data in self._data.items(): if name.startswith("pypi.") and name not in self._config_map: - yield name[5:], cast(Source, dict(data, name=name)) + yield name[5:], RepositoryConfig(**data) def _save_config(self) -> None: """Save the changed to config file.""" @@ -375,7 +323,7 @@ def __getitem__(self, key: str) -> Any: return ( source[parts[2]] if len(parts) >= 3 - else RegistryConfig(**self._data[index_key]) + else RepositoryConfig(**self._data[index_key]) ) elif key == "pypi.password": return "" @@ -489,22 +437,24 @@ def get_repository_config(self, name_or_url: str) -> RepositoryConfig | None: """Get a repository by name or url.""" if not self.is_global: # pragma: no cover raise NoConfigError("repository") - repositories: Mapping[str, Source] = self._data.get(REPOSITORY, {}) - repo: RepositoryConfig | None = None + repositories: Mapping[str, RepositoryConfig] = { + k: RepositoryConfig(**v) for k, v in self._data.get(REPOSITORY, {}).items() + } + config: RepositoryConfig | None = None if "://" in name_or_url: - config: Source = next( - (v for v in repositories.values() if v.get("url") == name_or_url), {} - ) - repo = next( - (r for r in DEFAULT_REPOSITORIES.values() if r.url == name_or_url), + config = next( + (v for v in repositories.values() if v.url == name_or_url), RepositoryConfig(name_or_url), ) else: - config = repositories.get(name_or_url, {}) - if name_or_url in DEFAULT_REPOSITORIES: - repo = DEFAULT_REPOSITORIES[name_or_url] - if repo: - return dataclasses.replace(repo, **config) - if not config: - return None - return RepositoryConfig(**config) # type: ignore + config = repositories.get(name_or_url) + + if name_or_url in DEFAULT_REPOSITORIES: + if config is None: + return RepositoryConfig(DEFAULT_REPOSITORIES[name_or_url]) + config.passive_update(url=DEFAULT_REPOSITORIES[name_or_url]) + if name_or_url in DEFAULT_REPOSITORIES.values(): + if config is None: + return RepositoryConfig(name_or_url) + config.passive_update(url=name_or_url) + return config diff --git a/src/pdm/project/core.py b/src/pdm/project/core.py index 3af82f328c..dfae49605c 100644 --- a/src/pdm/project/core.py +++ b/src/pdm/project/core.py @@ -15,6 +15,7 @@ from unearth import Link from pdm import termui +from pdm._types import RepositoryConfig from pdm.compat import cached_property from pdm.exceptions import NoPythonVersion, PdmUsageError, ProjectError from pdm.models.backends import BuildBackend, get_backend_by_spec @@ -42,7 +43,7 @@ if TYPE_CHECKING: from resolvelib.reporters import BaseReporter - from pdm._types import Source, Spinner + from pdm._types import Spinner from pdm.core import Core from pdm.resolver.providers import BaseProvider @@ -347,38 +348,43 @@ def allow_prereleases(self) -> bool | None: return self.pyproject.settings.get("allow_prereleases") @property - def default_source(self) -> Source: + def default_source(self) -> RepositoryConfig: """Get the default source from the pypi setting""" - return cast( - "Source", - { - "url": self.config["pypi.url"], - "verify_ssl": self.config["pypi.verify_ssl"], - "name": "pypi", - "username": self.config.get("pypi.username"), - "password": self.config.get("pypi.password"), - }, + return RepositoryConfig( + name="pypi", + url=self.config["pypi.url"], + verify_ssl=self.config["pypi.verify_ssl"], + username=self.config.get("pypi.username"), + password=self.config.get("pypi.password"), ) @property - def sources(self) -> list[Source]: - sources = list(self.pyproject.settings.get("source", [])) + def sources(self) -> list[RepositoryConfig]: + result: dict[str, RepositoryConfig] = {} + for source in self.pyproject.settings.get("source", []): + result[source["name"]] = RepositoryConfig(**source) + + def merge_sources( + other_sources: Iterable[tuple[str, RepositoryConfig]] + ) -> None: + for name, source in other_sources: + source.name = name + if name in result: + result[name].passive_update(source) + else: + result[name] = source + if not self.config.get("pypi.ignore_stored_index", False): - if all(source.get("name") != "pypi" for source in sources): - sources.insert(0, self.default_source) - stored_sources = dict(self.project_config.iter_sources()) - stored_sources.update( - (k, v) - for k, v in self.global_config.iter_sources() - if k not in stored_sources - ) - # The order is kept as project sources -> global sources - sources.extend(stored_sources.values()) - expanded_sources = [ - cast("Source", {**source, "url": expand_env_vars_in_auth(source["url"])}) - for source in sources - ] - return expanded_sources + if "pypi" not in result: # put pypi source at the beginning + result = {"pypi": self.default_source, **result} + else: + result["pypi"].passive_update(self.default_source) + merge_sources(self.project_config.iter_sources()) + merge_sources(self.global_config.iter_sources()) + for source in result.values(): + assert source.url, "Source URL must not be empty" + source.url = expand_env_vars_in_auth(source.url) + return list(result.values()) def get_repository( self, cls: type[BaseRepository] | None = None, ignore_compatibility: bool = True diff --git a/src/pdm/pytest.py b/src/pdm/pytest.py index cc84f37345..2893915ed2 100644 --- a/src/pdm/pytest.py +++ b/src/pdm/pytest.py @@ -72,7 +72,7 @@ if TYPE_CHECKING: from _pytest.fixtures import SubRequest - from pdm._types import CandidateInfo, Source + from pdm._types import CandidateInfo, RepositoryConfig class LocalFileAdapter(requests.adapters.BaseAdapter): @@ -156,7 +156,7 @@ class TestRepository(BaseRepository): """ def __init__( - self, sources: list[Source], environment: Environment, pypi_json: Path + self, sources: list[RepositoryConfig], environment: Environment, pypi_json: Path ): super().__init__(sources, environment) self._pypi_data: dict[str, Any] = {} diff --git a/src/pdm/utils.py b/src/pdm/utils.py index e75d8489a9..a8813a98db 100644 --- a/src/pdm/utils.py +++ b/src/pdm/utils.py @@ -22,7 +22,7 @@ from packaging.version import Version -from pdm._types import Source +from pdm._types import RepositoryConfig from pdm.compat import Distribution, importlib_metadata _egg_fragment_re = re.compile(r"(.*)[#&]egg=[^&]*") @@ -48,21 +48,24 @@ def clean_up() -> None: return name -def get_index_urls(sources: list[Source]) -> tuple[list[str], list[str], list[str]]: +def get_index_urls( + sources: list[RepositoryConfig], +) -> tuple[list[str], list[str], list[str]]: """Parse the project sources and return (index_urls, find_link_urls, trusted_hosts) """ index_urls, find_link_urls, trusted_hosts = [], [], [] for source in sources: - url = source["url"] + assert source.url + url = source.url netloc = parse.urlparse(url).netloc host = netloc.rsplit("@", 1)[-1] - if host not in trusted_hosts and not source.get("verify_ssl", True): + if host not in trusted_hosts and source.verify_ssl is False: trusted_hosts.append(host) - if source.get("type", "index") == "index": - index_urls.append(url) - else: + if source.type == "find_links": find_link_urls.append(url) + else: + index_urls.append(url) return index_urls, find_link_urls, trusted_hosts diff --git a/tests/models/test_candidates.py b/tests/models/test_candidates.py index b071a1fdbd..805c710640 100644 --- a/tests/models/test_candidates.py +++ b/tests/models/test_candidates.py @@ -3,6 +3,7 @@ import pytest from unearth import Link +from pdm._types import RepositoryConfig from pdm.exceptions import ExtrasWarning from pdm.models.candidates import Candidate from pdm.models.requirements import parse_requirement @@ -337,11 +338,11 @@ def test_ignore_invalid_py_version(project): def test_find_candidates_from_find_links(project): repo = project.get_repository() repo.sources = [ - { - "url": "http://fixtures.test/index/demo.html", - "verify_ssl": False, - "type": "find_links", - } + RepositoryConfig( + url="http://fixtures.test/index/demo.html", + verify_ssl=False, + type="find_links", + ) ] candidates = list(repo.find_candidates(parse_requirement("demo"))) assert len(candidates) == 2 diff --git a/tests/test_project.py b/tests/test_project.py index 1198c6e876..fbd2817699 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -53,14 +53,14 @@ def test_project_config_set_invalid_key(project): config["foo"] = "bar" -def test_project_sources_overriding(project): +def test_project_sources_overriding_pypi(project): project.project_config["pypi.url"] = "https://test.pypi.org/simple" - assert project.sources[0]["url"] == "https://test.pypi.org/simple" + assert project.sources[0].url == "https://test.pypi.org/simple" project.pyproject.settings["source"] = [ {"url": "https://example.org/simple", "name": "pypi", "verify_ssl": True} ] - assert project.sources[0]["url"] == "https://example.org/simple" + assert project.sources[0].url == "https://example.org/simple" def test_project_sources_env_var_expansion(project, monkeypatch): @@ -70,7 +70,7 @@ def test_project_sources_env_var_expansion(project, monkeypatch): "pypi.url" ] = "https://${PYPI_USER}:${PYPI_PASS}@test.pypi.org/simple" # expanded in sources - assert project.sources[0]["url"] == "https://user:password@test.pypi.org/simple" + assert project.sources[0].url == "https://user:password@test.pypi.org/simple" # not expanded in project config assert ( project.project_config["pypi.url"] @@ -85,7 +85,7 @@ def test_project_sources_env_var_expansion(project, monkeypatch): } ] # expanded in sources - assert project.sources[0]["url"] == "https://user:password@example.org/simple" + assert project.sources[0].url == "https://user:password@example.org/simple" # not expanded in tool settings assert ( project.pyproject.settings["source"][0]["url"] @@ -100,7 +100,7 @@ def test_project_sources_env_var_expansion(project, monkeypatch): } ] # expanded in sources - assert project.sources[1]["url"] == "https://user:password@example2.org/simple" + assert project.sources[1].url == "https://user:password@example2.org/simple" # not expanded in tool settings assert ( project.pyproject.settings["source"][0]["url"] @@ -295,12 +295,12 @@ def test_load_extra_sources(project): project.global_config["pypi.extra.url"] = "https://extra.pypi.org/simple" sources = project.sources assert len(sources) == 3 - assert [item["name"] for item in sources] == ["pypi", "custom", "pypi.extra"] + assert [item.name for item in sources] == ["pypi", "custom", "extra"] project.global_config["pypi.ignore_stored_index"] = True sources = project.sources assert len(sources) == 1 - assert [item["name"] for item in sources] == ["custom"] + assert [item.name for item in sources] == ["custom"] def test_no_index_raise_error(project): @@ -324,3 +324,23 @@ def test_access_index_with_auth(project): session = finder.session resp = session.get(url) assert resp.ok + + +def test_configured_source_overwriting(project): + project.pyproject.settings["source"] = [ + { + "name": "custom", + "url": "https://custom.pypi.org/simple", + } + ] + project.global_config["pypi.custom.url"] = "https://extra.pypi.org/simple" + project.global_config["pypi.custom.verify_ssl"] = False + project.project_config["pypi.custom.username"] = "foo" + project.project_config["pypi.custom.password"] = "bar" + sources = project.sources + assert [source.name for source in sources] == ["pypi", "custom"] + custom_source = sources[1] + assert custom_source.url == "https://custom.pypi.org/simple" + assert custom_source.verify_ssl is False + assert custom_source.username == "foo" + assert custom_source.password == "bar"