Repository: pdm-project/pdm

Issue:
Allow exclusion of dependencies
## Is your feature request related to a problem? Please describe.

We have the problem that some python libraries have a lot of dependencies that are actually optional but not marked as such and come with the regular installation.

For example RASA which is a cool library for intent detection but they simply added all dependencies as main dependencies
https://github.com/RasaHQ/rasa/blob/main/pyproject.toml#L65

This means when I install this I get besides the actual core libraries dependencies to e.g. a telegram client lib or a mongodb library
This does not only increase the size of your final application but also introduces potential security or licenses alerts

## Describe the solution you'd like

It would be great if there would be similar to the `[tool.pdm.overrides]` feature a `[tool.pdm.excludes]` section where I can exclude certain dependencies
Would be great if this would not only allowing blacklisting that dependency but also then prevents the resolution of the dependency of this depenency if they are not required by other dependencies

e.g.:

```toml
[tool.pdm.excludes]
pyTelegramBotAPI=<not sure if a version makes sense here>

```


Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/pdm-project/pdm/pull/2551?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project) Report
Attention: `2 lines` in your changes are missing coverage. Please review.
> Comparison is base [(`7a29fcb`)](https://app.codecov.io/gh/pdm-project/pdm/commit/7a29fcbc65c7625bae96f9ec5e45315144e86043?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project) 84.69% compared to head [(`9fbab31`)](https://app.codecov.io/gh/pdm-project/pdm/pull/2551?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project) 84.68%.

| [Files](https://app.codecov.io/gh/pdm-project/pdm/pull/2551?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project) | Patch % | Lines |
|---|---|---|
| [src/pdm/resolver/providers.py](https://app.codecov.io/gh/pdm-project/pdm/pull/2551?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project#diff-c3JjL3BkbS9yZXNvbHZlci9wcm92aWRlcnMucHk=) | 77.77% | [1 Missing and 1 partial :warning: ](https://app.codecov.io/gh/pdm-project/pdm/pull/2551?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #2551      +/-   ##
==========================================
- Coverage   84.69%   84.68%   -0.02%     
==========================================
  Files         104      104              
  Lines       10339    10345       +6     
  Branches     2261     2264       +3     
==========================================
+ Hits         8757     8761       +4     
- Misses       1109     1110       +1     
- Partials      473      474       +1     
```

| [Flag](https://app.codecov.io/gh/pdm-project/pdm/pull/2551/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project) | Coverage Δ | |
|---|---|---|
| [unittests](https://app.codecov.io/gh/pdm-project/pdm/pull/2551/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project) | `84.47% <89.47%> (-0.02%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project#carryforward-flags-in-the-pull-request-comment) to find out more.


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/pdm-project/pdm/pull/2551?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project).


Patch:
diff --git a/docs/docs/usage/config.md b/docs/docs/usage/config.md
index 70c9a95a..84d7e1b4 100644
--- a/docs/docs/usage/config.md
+++ b/docs/docs/usage/config.md
@@ -324,6 +324,19 @@ In this example, PDM will resolve the above packages into the given versions no
     By using `[tool.pdm.resolution.overrides]` setting, you are at your own risk of any incompatibilities from that resolution. It can only be used if there is no valid resolution for your requirements and you know the specific version works.
     Most of the time, you can just add any transient constraints to the `dependencies` array.
 
+## Exclude specific packages and their dependencies from the lock file
+
+_New in version 2.12.0_
+
+Sometimes you don't even want to include certain packages in the locked file because you are sure they won't be used by any code. In this case, you can completely skip them and their dependencies during dependency resolution:
+
+```toml
+[tool.pdm.resolution]
+excludes = ["requests"]
+```
+
+With this config, `requests` will be locked in the lockfile, and its dependencies such as `urllib3` and `idna` will also not show up in the resolution result, if not depended on by other packages. The installer will not be able to pick them up either.
+
 ## Passing constant arguments to every pdm invocation
 
 _New in version 2.7.0_
diff --git a/docs/docs/usage/dependency.md b/docs/docs/usage/dependency.md
index 665194b3..948b95c8 100644
--- a/docs/docs/usage/dependency.md
+++ b/docs/docs/usage/dependency.md
@@ -509,7 +509,7 @@ Unable to find a resolution for asgiref because of the following conflicts:
 To fix this, you could loosen the dependency version constraints in pyproject.toml. If that is not possible, you could also override the resolved version in `[tool.pdm.resolution.overrides]` table.
 ```
 
-You can either change to a lower version of `django` or remove the upper bound of `asgiref`. But if it is not eligible for your project, you can try [overriding the resolved package versions](./config.md#override-the-resolved-package-versions) in `pyproject.toml`.
+You can either change to a lower version of `django` or remove the upper bound of `asgiref`. But if it is not eligible for your project, you can try [overriding the resolved package versions](./config.md#override-the-resolved-package-versions) or even [don't lock that specific package](./config.md#exclude-specific-packages-and-their-dependencies-from-the-lock-file) in `pyproject.toml`.
 
 ## Manage global project
 
diff --git a/news/1316.bugfix.md b/news/1316.bugfix.md
new file mode 100644
index 00000000..3708ad79
--- /dev/null
+++ b/news/1316.bugfix.md
@@ -0,0 +1,1 @@
+`tool.pdm.resolution` settings won't be honored when installing dependencies into the build environment.
diff --git a/news/1316.feature.md b/news/1316.feature.md
new file mode 100644
index 00000000..7da631cf
--- /dev/null
+++ b/news/1316.feature.md
@@ -0,0 +1,1 @@
+Allow excluding packages from the lockfile via `tool.pdm.resolution.excludes` setting, the dependencies will also be skipped.
diff --git a/src/pdm/environments/__init__.py b/src/pdm/environments/__init__.py
index 244af850..592faa59 100644
--- a/src/pdm/environments/__init__.py
+++ b/src/pdm/environments/__init__.py
@@ -3,20 +3,15 @@ from typing import Any
 from pdm.environments.base import BareEnvironment, BaseEnvironment
 from pdm.environments.local import PythonLocalEnvironment
 from pdm.environments.python import PythonEnvironment
+from pdm.utils import deprecation_warning
 
 _deprecated = {"Environment": PythonLocalEnvironment, "GlobalEnvironment": PythonEnvironment}
 
 
 def __getattr__(name: str) -> Any:
     if name in _deprecated:
-        import warnings
-
         real = _deprecated[name]
-        warnings.warn(
-            f"{name} is deprecated, please use {real.__name__} instead",
-            DeprecationWarning,
-            stacklevel=2,
-        )
+        deprecation_warning(f"{name} is deprecated, please use {real.__name__} instead", stacklevel=2)
         return real
     raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
 
diff --git a/src/pdm/environments/base.py b/src/pdm/environments/base.py
index d1349e6f..7416d61d 100644
--- a/src/pdm/environments/base.py
+++ b/src/pdm/environments/base.py
@@ -13,12 +13,7 @@ from pathlib import Path
 from typing import TYPE_CHECKING, Generator
 
 from pdm.exceptions import BuildError, PdmUsageError
-from pdm.models.in_process import (
-    get_pep508_environment,
-    get_python_abis,
-    get_uname,
-    sysconfig_get_platform,
-)
+from pdm.models.in_process import get_pep508_environment, get_python_abis, get_uname, sysconfig_get_platform
 from pdm.models.python import PythonInfo
 from pdm.models.working_set import WorkingSet
 from pdm.utils import get_trusted_hosts, is_pip_compatible_with_python
diff --git a/src/pdm/exceptions.py b/src/pdm/exceptions.py
index 0eb87f6a..42a10ef8 100644
--- a/src/pdm/exceptions.py
+++ b/src/pdm/exceptions.py
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+import warnings
 from typing import TYPE_CHECKING
 
 if TYPE_CHECKING:
@@ -41,7 +42,7 @@ class CandidateInfoNotFound(PdmException):
         super().__init__(message)
 
 
-class PDMWarning(UserWarning):
+class PDMWarning(Warning):
     pass
 
 
@@ -49,10 +50,13 @@ class PackageWarning(PDMWarning):
     pass
 
 
-class PDMDeprecationWarning(PDMWarning):
+class PDMDeprecationWarning(PDMWarning, DeprecationWarning):
     pass
 
 
+warnings.simplefilter("default", category=PDMDeprecationWarning)
+
+
 class ExtrasWarning(PDMWarning):
     def __init__(self, project_name: str, extras: list[str]) -> None:
         super().__init__(f"Extras not found for {project_name}: [{','.join(extras)}]")
diff --git a/src/pdm/installers/core.py b/src/pdm/installers/core.py
index 32febcb3..69fbf86d 100644
--- a/src/pdm/installers/core.py
+++ b/src/pdm/installers/core.py
@@ -15,6 +15,9 @@ def install_requirements(
     # Rewrite the python requires to only resolve for the current python version.
     environment.python_requires = PySpecSet(f"=={environment.interpreter.version}")
     provider = project.get_provider(ignore_compatibility=False)
+    # Clear the overrides and excludes
+    provider.overrides = {}
+    provider.excludes = set()
     reporter = project.get_reporter(reqs)
     resolver = project.core.resolver_class(provider, reporter)
     resolve_max_rounds = int(project.config["strategy.resolve_max_rounds"])
diff --git a/src/pdm/models/environment.py b/src/pdm/models/environment.py
index 3a5a184b..e44a85cc 100644
--- a/src/pdm/models/environment.py
+++ b/src/pdm/models/environment.py
@@ -1,12 +1,11 @@
 import sys
-import warnings
 
 from pdm import environments
+from pdm.utils import deprecation_warning
 
-warnings.warn(
+deprecation_warning(
     "pdm.models.environment is deprecated, please use pdm.environments instead. "
     "This module will be removed in the future.",
-    DeprecationWarning,
     stacklevel=1,
 )
 
diff --git a/src/pdm/project/core.py b/src/pdm/project/core.py
index bae163c7..31c63978 100644
--- a/src/pdm/project/core.py
+++ b/src/pdm/project/core.py
@@ -32,7 +32,6 @@ from pdm.utils import (
     find_python_in_path,
     is_conda_base,
     is_conda_base_python,
-    normalize_name,
     path_to_url,
 )
 
@@ -431,7 +430,6 @@ class Project:
 
         repository = self.get_repository(ignore_compatibility=ignore_compatibility)
         allow_prereleases = self.allow_prereleases
-        overrides = {normalize_name(k): v for k, v in self.pyproject.resolution_overrides.items()}
         locked_repository: LockedRepository | None = None
         if strategy != "all" or for_install:
             try:
@@ -442,13 +440,9 @@ class Project:
                 self.core.ui.warn("Unable to reuse the lock file as it is not compatible with PDM")
 
         if locked_repository is None:
-            return BaseProvider(
-                repository, allow_prereleases, overrides, direct_minimal_versions=direct_minimal_versions
-            )
+            return BaseProvider(repository, allow_prereleases, direct_minimal_versions=direct_minimal_versions)
         if for_install:
-            return BaseProvider(
-                locked_repository, allow_prereleases, overrides, direct_minimal_versions=direct_minimal_versions
-            )
+            return BaseProvider(locked_repository, allow_prereleases, direct_minimal_versions=direct_minimal_versions)
         provider_class = get_provider(strategy)
         assert issubclass(provider_class, ReusePinProvider)
         tracked_names = [strip_extras(name)[0] for name in tracked_names or ()]
@@ -457,7 +451,6 @@ class Project:
             tracked_names,
             repository,
             allow_prereleases,
-            overrides,
             direct_minimal_versions=direct_minimal_versions,
         )
 
diff --git a/src/pdm/project/project_file.py b/src/pdm/project/project_file.py
index 5fca5310..b2c397ec 100644
--- a/src/pdm/project/project_file.py
+++ b/src/pdm/project/project_file.py
@@ -2,7 +2,7 @@ from __future__ import annotations
 
 import hashlib
 import json
-from typing import Mapping
+from typing import Any, Mapping
 
 from tomlkit import TOMLDocument, items
 
@@ -60,12 +60,11 @@ class PyProject(TOMLBase):
         return self._data.get("build-system", {})
 
     @property
-    def resolution_overrides(self) -> Mapping[str, str]:
+    def resolution(self) -> Mapping[str, Any]:
         """A compatible getter method for the resolution overrides
         in the pyproject.toml file.
         """
-        settings = self.settings
-        return settings.get("resolution", {}).get("overrides", {})
+        return self.settings.get("resolution", {})
 
     def content_hash(self, algo: str = "sha256") -> str:
         """Generate a hash of the sensible content of the pyproject.toml file.
@@ -77,7 +76,7 @@ class PyProject(TOMLBase):
             "dev-dependencies": self.settings.get("dev-dependencies", {}),
             "optional-dependencies": self.metadata.get("optional-dependencies", {}),
             "requires-python": self.metadata.get("requires-python", ""),
-            "overrides": self.resolution_overrides,
+            "overrides": self.resolution.get("overrides", {}),
         }
         pyproject_content = json.dumps(dump_data, sort_keys=True)
         hasher = hashlib.new(algo)
diff --git a/src/pdm/resolver/providers.py b/src/pdm/resolver/providers.py
index c8728b83..7e408c2c 100644
--- a/src/pdm/resolver/providers.py
+++ b/src/pdm/resolver/providers.py
@@ -11,14 +11,9 @@ from pdm.exceptions import InvalidPyVersion, RequirementError
 from pdm.models.candidates import Candidate
 from pdm.models.repositories import LockedRepository
 from pdm.models.requirements import FileRequirement, parse_requirement, strip_extras
-from pdm.resolver.python import (
-    PythonCandidate,
-    PythonRequirement,
-    find_python_matches,
-    is_python_satisfied_by,
-)
+from pdm.resolver.python import PythonCandidate, PythonRequirement, find_python_matches, is_python_satisfied_by
 from pdm.termui import logger
-from pdm.utils import is_url, url_without_fragments
+from pdm.utils import deprecation_warning, is_url, normalize_name, url_without_fragments
 
 if TYPE_CHECKING:
     from typing import Any, Iterable, Iterator, Mapping, Sequence
@@ -54,10 +49,18 @@ class BaseProvider(AbstractProvider):
         overrides: dict[str, str] | None = None,
         direct_minimal_versions: bool = False,
     ) -> None:
+        if overrides is not None:
+            deprecation_warning(
+                "The `overrides` argument is deprecated and will be removed in the future.", stacklevel=2
+            )
+        project = repository.environment.project
         self.repository = repository
         self.allow_prereleases = allow_prereleases  # Root allow_prereleases value
         self.fetched_dependencies: dict[tuple[str, str | None], list[Requirement]] = {}
-        self.overrides = overrides or {}
+        self.overrides: Mapping[str, str] = {
+            normalize_name(k): v for k, v in project.pyproject.resolution.get("overrides", {}).items()
+        }
+        self.excludes = {normalize_name(k) for k in project.pyproject.resolution.get("excludes", [])}
         self.direct_minimal_versions = direct_minimal_versions
         self._known_depth: dict[str, int] = {}
 
@@ -247,6 +250,8 @@ class BaseProvider(AbstractProvider):
                 & self.repository.environment.python_requires
             ).is_impossible:
                 continue
+            if dep.identify() in self.excludes:
+                continue
             dep.requires_python &= candidate.req.requires_python
             valid_deps.append(dep)
         self.fetched_dependencies[candidate.dep_key] = valid_deps[:]
@@ -364,7 +369,11 @@ class ReuseInstalledProvider(ReusePinProvider):
     """A provider that reuses installed packages if possible."""
 
     def __init__(
-        self, preferred_pins: dict[str, Candidate], tracked_names: Iterable[str], *args: Any, **kwargs: Any
+        self,
+        preferred_pins: dict[str, Candidate],
+        tracked_names: Iterable[str],
+        *args: Any,
+        **kwargs: Any,
     ) -> None:
         super().__init__(preferred_pins, tracked_names, *args, **kwargs)
         self.installed = self.repository.environment.get_working_set()


Test patch:
diff --git a/tests/resolver/test_resolve.py b/tests/resolver/test_resolve.py
index 47923dcf..5ab4fb85 100644
--- a/tests/resolver/test_resolve.py
+++ b/tests/resolver/test_resolve.py
@@ -54,6 +54,17 @@ def test_resolve_named_requirement(resolve):
     assert result["idna"].version == "2.7"
 
 
+def test_resolve_exclude(resolve, project):
+    project.pyproject.settings.setdefault("resolution", {})["excludes"] = ["urllib3"]
+    result = resolve(["requests"])
+
+    assert result["requests"].version == "2.19.1"
+    assert result["chardet"].version == "3.0.4"
+    assert result["certifi"].version == "2018.11.17"
+    assert result["idna"].version == "2.7"
+    assert "urllib3" not in result
+
+
 def test_resolve_requires_python(resolve):
     with pytest.warns(PackageWarning) as records:
         result = resolve(["django"])
@@ -94,6 +105,19 @@ def test_resolve_allow_prereleases(resolve, repository):
 def test_resolve_with_extras(resolve):
     result = resolve(["requests[socks]"])
     assert result["pysocks"].version == "1.5.6"
+    assert result["urllib3"].version == "1.22"
+    assert result["chardet"].version == "3.0.4"
+    assert result["certifi"].version == "2018.11.17"
+    assert result["idna"].version == "2.7"
+    assert result["requests"].version == "2.19.1"
+
+
+def test_resolve_with_extras_and_excludes(resolve, project):
+    project.pyproject.settings.setdefault("resolution", {})["excludes"] = ["requests"]
+    result = resolve(["requests[socks]"])
+    assert result["pysocks"].version == "1.5.6"
+    assert "requests" not in result
+    assert "urllib3" not in result
 
 
 @pytest.mark.parametrize(