1from __future__ import annotations
2
3import contextlib
4import functools
5import os
6import sys
7from typing import TYPE_CHECKING, Literal, Protocol, cast
8
9from pip._internal.utils.deprecation import deprecated
10from pip._internal.utils.misc import strtobool
11
12from .base import BaseDistribution, BaseEnvironment, FilesystemWheel, MemoryWheel, Wheel
13
14if TYPE_CHECKING:
15 from pip._vendor.packaging.utils import NormalizedName
16
17__all__ = [
18 "BaseDistribution",
19 "BaseEnvironment",
20 "FilesystemWheel",
21 "MemoryWheel",
22 "Wheel",
23 "get_default_environment",
24 "get_environment",
25 "get_wheel_distribution",
26 "select_backend",
27]
28
29
30def _should_use_importlib_metadata() -> bool:
31 """Whether to use the ``importlib.metadata`` or ``pkg_resources`` backend.
32
33 By default, pip uses ``importlib.metadata`` on Python 3.11+, and
34 ``pkg_resources`` otherwise. Up to Python 3.13, This can be
35 overridden by a couple of ways:
36
37 * If environment variable ``_PIP_USE_IMPORTLIB_METADATA`` is set, it
38 dictates whether ``importlib.metadata`` is used, for Python <3.14.
39 * On Python 3.11, 3.12 and 3.13, Python distributors can patch
40 ``importlib.metadata`` to add a global constant
41 ``_PIP_USE_IMPORTLIB_METADATA = False``. This makes pip use
42 ``pkg_resources`` (unless the user set the aforementioned environment
43 variable to *True*).
44
45 On Python 3.14+, the ``pkg_resources`` backend cannot be used.
46 """
47 if sys.version_info >= (3, 14):
48 # On Python >=3.14 we only support importlib.metadata.
49 return True
50 with contextlib.suppress(KeyError, ValueError):
51 # On Python <3.14, if the environment variable is set, we obey what it says.
52 return bool(strtobool(os.environ["_PIP_USE_IMPORTLIB_METADATA"]))
53 if sys.version_info < (3, 11):
54 # On Python <3.11, we always use pkg_resources, unless the environment
55 # variable was set.
56 return False
57 # On Python 3.11, 3.12 and 3.13, we check if the global constant is set.
58 import importlib.metadata
59
60 return bool(getattr(importlib.metadata, "_PIP_USE_IMPORTLIB_METADATA", True))
61
62
63def _emit_pkg_resources_deprecation_if_needed() -> None:
64 if sys.version_info < (3, 11):
65 # All pip versions supporting Python<=3.11 will support pkg_resources,
66 # and pkg_resources is the default for these, so let's not bother users.
67 return
68
69 import importlib.metadata
70
71 if hasattr(importlib.metadata, "_PIP_USE_IMPORTLIB_METADATA"):
72 # The Python distributor has set the global constant, so we don't
73 # warn, since it is not a user decision.
74 return
75
76 # The user has decided to use pkg_resources, so we warn.
77 deprecated(
78 reason="Using the pkg_resources metadata backend is deprecated.",
79 replacement=(
80 "to use the default importlib.metadata backend, "
81 "by unsetting the _PIP_USE_IMPORTLIB_METADATA environment variable"
82 ),
83 gone_in="26.3",
84 issue=13317,
85 )
86
87
88class Backend(Protocol):
89 NAME: Literal["importlib", "pkg_resources"]
90 Distribution: type[BaseDistribution]
91 Environment: type[BaseEnvironment]
92
93
94@functools.cache
95def select_backend() -> Backend:
96 if _should_use_importlib_metadata():
97 from . import importlib
98
99 return cast(Backend, importlib)
100
101 _emit_pkg_resources_deprecation_if_needed()
102
103 from . import pkg_resources
104
105 return cast(Backend, pkg_resources)
106
107
108def get_default_environment() -> BaseEnvironment:
109 """Get the default representation for the current environment.
110
111 This returns an Environment instance from the chosen backend. The default
112 Environment instance should be built from ``sys.path`` and may use caching
113 to share instance state across calls.
114 """
115 return select_backend().Environment.default()
116
117
118def get_environment(paths: list[str] | None) -> BaseEnvironment:
119 """Get a representation of the environment specified by ``paths``.
120
121 This returns an Environment instance from the chosen backend based on the
122 given import paths. The backend must build a fresh instance representing
123 the state of installed distributions when this function is called.
124 """
125 return select_backend().Environment.from_paths(paths)
126
127
128def get_directory_distribution(directory: str) -> BaseDistribution:
129 """Get the distribution metadata representation in the specified directory.
130
131 This returns a Distribution instance from the chosen backend based on
132 the given on-disk ``.dist-info`` directory.
133 """
134 return select_backend().Distribution.from_directory(directory)
135
136
137def get_wheel_distribution(
138 wheel: Wheel, canonical_name: NormalizedName
139) -> BaseDistribution:
140 """Get the representation of the specified wheel's distribution metadata.
141
142 This returns a Distribution instance from the chosen backend based on
143 the given wheel's ``.dist-info`` directory.
144
145 :param canonical_name: Normalized project name of the given wheel.
146 """
147 return select_backend().Distribution.from_wheel(wheel, canonical_name)
148
149
150def get_metadata_distribution(
151 metadata_contents: bytes,
152 filename: str,
153 canonical_name: str,
154) -> BaseDistribution:
155 """Get the dist representation of the specified METADATA file contents.
156
157 This returns a Distribution instance from the chosen backend sourced from the data
158 in `metadata_contents`.
159
160 :param metadata_contents: Contents of a METADATA file within a dist, or one served
161 via PEP 658.
162 :param filename: Filename for the dist this metadata represents.
163 :param canonical_name: Normalized project name of the given dist.
164 """
165 return select_backend().Distribution.from_metadata_file_contents(
166 metadata_contents,
167 filename,
168 canonical_name,
169 )