diff --git a/src/poetry/console/commands/check.py b/src/poetry/console/commands/check.py index 570a7f4e768..44db49ea4c2 100644 --- a/src/poetry/console/commands/check.py +++ b/src/poetry/console/commands/check.py @@ -1,6 +1,9 @@ from __future__ import annotations +from collections import defaultdict +from functools import reduce from typing import TYPE_CHECKING +from typing import Any from cleo.helpers import option @@ -88,6 +91,44 @@ def _validate_readme(self, readme: str | list[str], poetry_file: Path) -> list[s errors.append(f"Declared README file does not exist: {name}") return errors + def _validate_dependencies_source(self, config: dict[str, Any]) -> list[str]: + """Check dependencies's source are valid""" + dependency_sources: dict[str, set[str]] = defaultdict(set) + sources = {k["name"] for k in config.get("source", [])} + errors = [] + + dependency_declarations: list[ + dict[str, str | dict[str, str] | list[dict[str, str]]] + ] = [] + # scan dependencies and group dependencies settings in pyproject.toml + if "dependencies" in config: + dependency_declarations.append(config["dependencies"]) + + for group in config.get("group", {}).values(): + if "dependencies" in group: + dependency_declarations.append(group["dependencies"]) + + for dependency_declaration in dependency_declarations: + for dependency, declaration in dependency_declaration.items(): + if isinstance(declaration, list): + for item in declaration: + if "source" in item: + dependency_sources[dependency].add(item["source"]) + elif isinstance(declaration, dict) and "source" in declaration: + dependency_sources[dependency].add(declaration["source"]) + + all_referenced_sources: set[str] = reduce( + lambda i, j: i | j, dependency_sources.values(), set() + ) + if all_referenced_sources not in sources: + errors.extend([ + f'Invalid source "{source}" referenced in dependencies.' + for source in all_referenced_sources - sources + ]) + + errors.sort() + return errors + def handle(self) -> int: from poetry.factory import Factory from poetry.pyproject.toml import PyProjectTOML @@ -108,6 +149,8 @@ def handle(self) -> int: errors = self._validate_readme(config["readme"], poetry_file) check_result["errors"].extend(errors) + check_result["errors"] += self._validate_dependencies_source(config) + # Verify that lock file is consistent if self.option("lock") and not self.poetry.locker.is_locked(): check_result["errors"] += ["poetry.lock was not found."] diff --git a/tests/console/commands/test_check.py b/tests/console/commands/test_check.py index 421c50dcb54..96836120450 100644 --- a/tests/console/commands/test_check.py +++ b/tests/console/commands/test_check.py @@ -83,6 +83,8 @@ def test_check_invalid( Error: Project name (invalid) is same as one of its dependencies Error: Unrecognized classifiers: ['Intended Audience :: Clowns']. Error: Declared README file does not exist: never/exists.md +Error: Invalid source "not-exists" referenced in dependencies. +Error: Invalid source "not-exists2" referenced in dependencies. Error: poetry.lock was not found. Warning: A wildcard Python dependency is ambiguous.\ Consider specifying a more explicit one. diff --git a/tests/fixtures/invalid_pyproject/pyproject.toml b/tests/fixtures/invalid_pyproject/pyproject.toml index bafa0936489..94c7d9fb4d5 100644 --- a/tests/fixtures/invalid_pyproject/pyproject.toml +++ b/tests/fixtures/invalid_pyproject/pyproject.toml @@ -17,3 +17,12 @@ classifiers = [ python = "*" pendulum = {"version" = "^2.0.5", allows-prereleases = true} invalid = "1.0" +invalid_source = { "version" = "*", source = "not-exists" } +invalid_source_multi = [ + { "version" = "*", platform = "linux", source = "exists" }, + { "version" = "*", platform = "win32", source = "not-exists2" }, +] + +[[tool.poetry.source]] +name = "exists" +priority = "explicit"