Repository: python-poetry/poetry

Issue:
`poetry check` does not consider the package source
<!--
    Hi there! Thank you for submitting a feature request!

    Before you submit your issue, please review and follow the instructions at
    https://python-poetry.org/docs/contributing/#suggesting-enhancements.
-->

<!-- All the below steps should be completed before submitting your issue. Checked checkbox should look like this: [x] -->
- [x] I have searched the [issues](https://github.com/python-poetry/poetry/issues) of this repo and believe that this is not a duplicate.
- [x] I have searched the [FAQ](https://python-poetry.org/docs/faq/) and general [documentation](https://python-poetry.org/docs/) and believe that my question is not already covered.

## Feature Request
<!-- Now feel free to write your issue, and please be as descriptive as possible! -->
<!-- Thanks again 🙌 ❤ -->

It would be excellent if `poetry check` could encompass common errors? when defining package sources.

### Source is undefined
- if there is a package point to not existing source

```yaml
[tool.poetry]
name = "PythonProjects"
version = "2.0.0"
homepage = "https://github.com/roghu/py3_projects"
license = "MIT"
readme = "README.md"
authors = ["Dependabot <support@dependabot.com>"]
description = "Various small python projects."

[tool.poetry.dependencies]
python = "^3.7"
requests = { version = "2.18.0", source = "custom-1" }

[[tool.poetry.source]]
name = "custom"
url = "https://some.internal.registry.com/pypi/"
```

- poetry install will failed directly

```
(pythonprojects-py3.11) ➜  dependabot-source-constraint git:(main) ✗ poetry install
Warning: In a future version of Poetry, PyPI will be disabled automatically if at least one custom primary source is configured. In order to avoid a breaking change and make your pyproject.toml forward compatible, add PyPI explicitly via 'poetry source add pypi'. By the way, this has the advantage that you can set the priority of PyPI as with any other source.
Updating dependencies
Resolving dependencies... (0.0s)

Repository "custom-1" does not exist.
```

### Package's source is not consistence 
- The behavior is undefined when a package is defined with different source configurations.

```yaml
[tool.poetry]
name = "PythonProjects"
version = "2.0.0"
homepage = "https://github.com/roghu/py3_projects"
license = "MIT"
readme = "README.md"
authors = ["Dependabot <support@dependabot.com>"]
description = "Various small python projects."


[tool.poetry.dependencies]
python = "^3.7"

requests = { version = "2.18.0", source = "custom-2" }
[tool.poetry.group.dev]
optional = true

[tool.poetry.group.dev.dependencies]
requests = { version = "2.18.0", extras=["dev"], source = "custom-3" }

[tool.poetry.group.test]
optional = true

[tool.poetry.group.test.dependencies]
requests = { version = "2.18.0", extras=["test"], source = "custom-1" }
 

[[tool.poetry.source]]
name = "custom-1"
url = "https://some.internal.registry-1.com/pypi/"

[[tool.poetry.source]]
name = "custom-2"
url = "https://some.internal.registry-2.com/pypi/"
```

- Currently, Poetry selects only one source.
- It appears that Poetry selects the source that is not in the default group and appears last in the pyproject.toml file.

```
(pythonprojects-py3.11) ➜  dependabot-source-constraint git:(main) ✗ poetry install -vvv
Adding repository custom-1 (https://some.internal.registry-1.com/pypi) and setting it as primary
Adding repository custom-2 (https://some.internal.registry-2.com/pypi) and setting it as primary
Warning: In a future version of Poetry, PyPI will be disabled automatically if at least one custom primary source is configured. In order to avoid a breaking change and make your pyproject.toml forward compatible, add PyPI explicitly via 'poetry source add pypi'. By the way, this has the advantage that you can set the priority of PyPI as with any other source.
Using virtualenv: /Users/davidchen/repo/dependabot-source-constraint/.venv
Updating dependencies
Resolving dependencies...
   1: fact: pythonprojects is 2.0.0
   1: derived: pythonprojects
   1: fact: pythonprojects depends on requests (2.18.0)
   1: fact: pythonprojects depends on requests (2.18.0)
   1: fact: pythonprojects depends on requests (2.18.0)
   1: selecting pythonprojects (2.0.0)
   1: derived: requests[test] (==2.18.0)
   1: derived: requests[dev] (==2.18.0)
   1: derived: requests (==2.18.0)
[keyring.backend] Loading KWallet
[keyring.backend] Loading SecretService
[keyring.backend] Loading Windows
[keyring.backend] Loading chainer
[keyring.backend] Loading libsecret
[keyring.backend] Loading macOS
[keyring.backend] Loading Google Auth
Creating new session for some.internal.registry-1.com
[urllib3.connectionpool] Starting new HTTPS connection (1): some.internal.registry-1.com:443
```


Discussion:
> It would be excellent if poetry check could encompass common errors? when defining package sources.

Are these common errors?  Since the first one in particular will fail, with a sensible error message, as soon as you try to do anything anyway - I don't know that an explicit addition to `poetry check` brings very much value.

Nevertheless if you would find either or both these checks useful: I expect a merge request would have every chance of succeeding.

Resolving patch:
diff --git a/src/poetry/console/commands/check.py b/src/poetry/console/commands/check.py
--- a/src/poetry/console/commands/check.py
+++ b/src/poetry/console/commands/check.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 from typing import TYPE_CHECKING
+from typing import Any
 
 from cleo.helpers import option
 
@@ -88,6 +89,37 @@ 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"""
+        sources = {k["name"] for k in config.get("source", [])}
+
+        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"])
+
+        all_referenced_sources: set[str] = set()
+
+        for dependency_declaration in dependency_declarations:
+            for declaration in dependency_declaration.values():
+                if isinstance(declaration, list):
+                    for item in declaration:
+                        if "source" in item:
+                            all_referenced_sources.add(item["source"])
+                elif isinstance(declaration, dict) and "source" in declaration:
+                    all_referenced_sources.add(declaration["source"])
+
+        return [
+            f'Invalid source "{source}" referenced in dependencies.'
+            for source in sorted(all_referenced_sources - sources)
+        ]
+
     def handle(self) -> int:
         from poetry.factory import Factory
         from poetry.pyproject.toml import PyProjectTOML
@@ -108,6 +140,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."]


Test patch:
diff --git a/tests/console/commands/test_check.py b/tests/console/commands/test_check.py
--- 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
--- 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"