diff --git a/docs/html/topics/authentication.md b/docs/html/topics/authentication.md
index 981aab5abd7..5fa7bbe1c34 100644
--- a/docs/html/topics/authentication.md
+++ b/docs/html/topics/authentication.md
@@ -66,18 +66,39 @@ man pages][netrc-docs].
## Keyring Support
pip supports loading credentials stored in your keyring using the
-{pypi}`keyring` library.
+{pypi}`keyring-subprocess` library.
+
+```{note}
+In the previous versions Pip would import {pypi}`keyring` from its environment.
+Now Pip vendors `keyring` and `keyring-subprocess`. Pip will query a
+`keyring-subprocess` executable if it can be found on the PATH.
+
+This should make bootstrapping virtualenvs easier. The `virtualenv` library has
+a mechanism to seed (pre install) packages into new virtualenvs, but Python's
+venv module does not.
+
+The upside of vendoring keyring this way is that is will work for both flavours
+of virtualenvs.
+
+The downside is that this is a breaking change for existing `keyring` users,
+they now need to opt in by installing `keyring-subprocess[landmark]`.
+```
```bash
-$ pip install keyring # install keyring from PyPI
+$ # install keyring-subprocess[landmark] and make it available on the PATH
+$ # and possibly additional keyring backends such
+$ # - artifacts-keyring for Azure DevOps
+$ # - keyrings.google-artifactregistry-auth for Google Artifact Registry
$ echo "your-password" | keyring set pypi.company.com your-username
$ pip install your-package --index-url https://pypi.company.com/
```
-Note that `keyring` (the Python package) needs to be installed separately from
-pip. This can create a bootstrapping issue if you need the credentials stored in
-the keyring to download and install keyring.
+Note that `keyring-subprocess[landmark]` (the Python package) needs to be
+installed separately from pip. This can create a bootstrapping issue if you
+need the credentials stored in the keyring to download and install keyring.
It is, thus, expected that users that wish to use pip's keyring support have
-some mechanism for downloading and installing {pypi}`keyring` in their Python
-environment.
+some mechanism for downloading and installing `keyring-subprocess[landmark]`.
+There is a powershell script for Windows on the {pypi}`keyring-subprocess`
+project page to install pipx and then install it with pipx. It should be able
+to serve as a handy cheat sheet for other platforms.
diff --git a/news/11399.feature.rst b/news/11399.feature.rst
new file mode 100644
index 00000000000..06f84f75f7a
--- /dev/null
+++ b/news/11399.feature.rst
@@ -0,0 +1,3 @@
+Breaking change: ``keyring`` support is now opt-in. A ``keyring-subprocess``
+executable needs to exists on the PATH. See the ``Authentication`` page in the
+documentation for more information.
diff --git a/pyproject.toml b/pyproject.toml
index a02457eeffd..6a1ba183405 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -56,6 +56,10 @@ drop = [
'^pygments/lexers/(?!python|__init__|_mapping).*\.py$',
# trim rich's markdown support
"rich/markdown.py",
+ # unneeded parts of keyring-subprocess
+ "keyring-subprocess.pth",
+ "keyring_subprocess/_vendor/",
+ "keyring_subprocess/_internal/wheels/",
]
[tool.vendoring.typing-stubs]
diff --git a/src/pip/_internal/network/auth.py b/src/pip/_internal/network/auth.py
index ca42798bd95..8a2a977750e 100644
--- a/src/pip/_internal/network/auth.py
+++ b/src/pip/_internal/network/auth.py
@@ -3,7 +3,6 @@
Contains interface (MultiDomainBasicAuth) and associated glue code for
providing credentials in the context of network requests.
"""
-
import urllib.parse
from typing import Any, Dict, List, Optional, Tuple
@@ -26,7 +25,10 @@
Credentials = Tuple[str, str, str]
try:
- import keyring
+ import pip._vendor.keyring as keyring
+ from pip._vendor.keyring_subprocess.backend import SubprocessBackend
+
+ keyring.set_keyring(SubprocessBackend())
except ImportError:
keyring = None # type: ignore[assignment]
except Exception as exc:
diff --git a/src/pip/_vendor/__init__.py b/src/pip/_vendor/__init__.py
index b22f7abb93b..7013779e039 100644
--- a/src/pip/_vendor/__init__.py
+++ b/src/pip/_vendor/__init__.py
@@ -118,3 +118,12 @@ def vendored(modulename):
vendored("tenacity")
vendored("tomli")
vendored("urllib3")
+ vendored("keyring")
+ vendored("keyring.backends")
+ vendored("keyring.util")
+ vendored("keyring_subprocess")
+ vendored("keyring_subprocess._internal")
+ vendored("keyring_subprocess.backend")
+ vendored("importlib_metadata")
+ vendored("zipp")
+ vendored("typing_extensions")
diff --git a/src/pip/_vendor/importlib_metadata/LICENSE b/src/pip/_vendor/importlib_metadata/LICENSE
new file mode 100644
index 00000000000..d6456956733
--- /dev/null
+++ b/src/pip/_vendor/importlib_metadata/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/src/pip/_vendor/importlib_metadata/__init__.py b/src/pip/_vendor/importlib_metadata/__init__.py
new file mode 100644
index 00000000000..7d15de9e8dd
--- /dev/null
+++ b/src/pip/_vendor/importlib_metadata/__init__.py
@@ -0,0 +1,1095 @@
+import os
+import re
+import abc
+import csv
+import sys
+from pip._vendor import zipp
+import email
+import pathlib
+import operator
+import textwrap
+import warnings
+import functools
+import itertools
+import posixpath
+import collections
+
+from . import _adapters, _meta
+from ._collections import FreezableDefaultDict, Pair
+from ._compat import (
+ NullFinder,
+ install,
+ pypy_partial,
+)
+from ._functools import method_cache, pass_none
+from ._itertools import always_iterable, unique_everseen
+from ._meta import PackageMetadata, SimplePath
+
+from contextlib import suppress
+from importlib import import_module
+from importlib.abc import MetaPathFinder
+from itertools import starmap
+from typing import List, Mapping, Optional, Union
+
+
+__all__ = [
+ 'Distribution',
+ 'DistributionFinder',
+ 'PackageMetadata',
+ 'PackageNotFoundError',
+ 'distribution',
+ 'distributions',
+ 'entry_points',
+ 'files',
+ 'metadata',
+ 'packages_distributions',
+ 'requires',
+ 'version',
+]
+
+
+class PackageNotFoundError(ModuleNotFoundError):
+ """The package was not found."""
+
+ def __str__(self):
+ return f"No package metadata was found for {self.name}"
+
+ @property
+ def name(self):
+ (name,) = self.args
+ return name
+
+
+class Sectioned:
+ """
+ A simple entry point config parser for performance
+
+ >>> for item in Sectioned.read(Sectioned._sample):
+ ... print(item)
+ Pair(name='sec1', value='# comments ignored')
+ Pair(name='sec1', value='a = 1')
+ Pair(name='sec1', value='b = 2')
+ Pair(name='sec2', value='a = 2')
+
+ >>> res = Sectioned.section_pairs(Sectioned._sample)
+ >>> item = next(res)
+ >>> item.name
+ 'sec1'
+ >>> item.value
+ Pair(name='a', value='1')
+ >>> item = next(res)
+ >>> item.value
+ Pair(name='b', value='2')
+ >>> item = next(res)
+ >>> item.name
+ 'sec2'
+ >>> item.value
+ Pair(name='a', value='2')
+ >>> list(res)
+ []
+ """
+
+ _sample = textwrap.dedent(
+ """
+ [sec1]
+ # comments ignored
+ a = 1
+ b = 2
+
+ [sec2]
+ a = 2
+ """
+ ).lstrip()
+
+ @classmethod
+ def section_pairs(cls, text):
+ return (
+ section._replace(value=Pair.parse(section.value))
+ for section in cls.read(text, filter_=cls.valid)
+ if section.name is not None
+ )
+
+ @staticmethod
+ def read(text, filter_=None):
+ lines = filter(filter_, map(str.strip, text.splitlines()))
+ name = None
+ for value in lines:
+ section_match = value.startswith('[') and value.endswith(']')
+ if section_match:
+ name = value.strip('[]')
+ continue
+ yield Pair(name, value)
+
+ @staticmethod
+ def valid(line):
+ return line and not line.startswith('#')
+
+
+class DeprecatedTuple:
+ """
+ Provide subscript item access for backward compatibility.
+
+ >>> recwarn = getfixture('recwarn')
+ >>> ep = EntryPoint(name='name', value='value', group='group')
+ >>> ep[:]
+ ('name', 'value', 'group')
+ >>> ep[0]
+ 'name'
+ >>> len(recwarn)
+ 1
+ """
+
+ _warn = functools.partial(
+ warnings.warn,
+ "EntryPoint tuple interface is deprecated. Access members by name.",
+ DeprecationWarning,
+ stacklevel=pypy_partial(2),
+ )
+
+ def __getitem__(self, item):
+ self._warn()
+ return self._key()[item]
+
+
+class EntryPoint(DeprecatedTuple):
+ """An entry point as defined by Python packaging conventions.
+
+ See `the packaging docs on entry points
+ `_
+ for more information.
+
+ >>> ep = EntryPoint(
+ ... name=None, group=None, value='package.module:attr [extra1, extra2]')
+ >>> ep.module
+ 'package.module'
+ >>> ep.attr
+ 'attr'
+ >>> ep.extras
+ ['extra1', 'extra2']
+ """
+
+ pattern = re.compile(
+ r'(?P[\w.]+)\s*'
+ r'(:\s*(?P[\w.]+)\s*)?'
+ r'((?P\[.*\])\s*)?$'
+ )
+ """
+ A regular expression describing the syntax for an entry point,
+ which might look like:
+
+ - module
+ - package.module
+ - package.module:attribute
+ - package.module:object.attribute
+ - package.module:attr [extra1, extra2]
+
+ Other combinations are possible as well.
+
+ The expression is lenient about whitespace around the ':',
+ following the attr, and following any extras.
+ """
+
+ dist: Optional['Distribution'] = None
+
+ def __init__(self, name, value, group):
+ vars(self).update(name=name, value=value, group=group)
+
+ def load(self):
+ """Load the entry point from its definition. If only a module
+ is indicated by the value, return that module. Otherwise,
+ return the named object.
+ """
+ match = self.pattern.match(self.value)
+ module = import_module(match.group('module'))
+ attrs = filter(None, (match.group('attr') or '').split('.'))
+ return functools.reduce(getattr, attrs, module)
+
+ @property
+ def module(self):
+ match = self.pattern.match(self.value)
+ return match.group('module')
+
+ @property
+ def attr(self):
+ match = self.pattern.match(self.value)
+ return match.group('attr')
+
+ @property
+ def extras(self):
+ match = self.pattern.match(self.value)
+ return re.findall(r'\w+', match.group('extras') or '')
+
+ def _for(self, dist):
+ vars(self).update(dist=dist)
+ return self
+
+ def __iter__(self):
+ """
+ Supply iter so one may construct dicts of EntryPoints by name.
+ """
+ msg = (
+ "Construction of dict of EntryPoints is deprecated in "
+ "favor of EntryPoints."
+ )
+ warnings.warn(msg, DeprecationWarning)
+ return iter((self.name, self))
+
+ def matches(self, **params):
+ """
+ EntryPoint matches the given parameters.
+
+ >>> ep = EntryPoint(group='foo', name='bar', value='bing:bong [extra1, extra2]')
+ >>> ep.matches(group='foo')
+ True
+ >>> ep.matches(name='bar', value='bing:bong [extra1, extra2]')
+ True
+ >>> ep.matches(group='foo', name='other')
+ False
+ >>> ep.matches()
+ True
+ >>> ep.matches(extras=['extra1', 'extra2'])
+ True
+ >>> ep.matches(module='bing')
+ True
+ >>> ep.matches(attr='bong')
+ True
+ """
+ attrs = (getattr(self, param) for param in params)
+ return all(map(operator.eq, params.values(), attrs))
+
+ def _key(self):
+ return self.name, self.value, self.group
+
+ def __lt__(self, other):
+ return self._key() < other._key()
+
+ def __eq__(self, other):
+ return self._key() == other._key()
+
+ def __setattr__(self, name, value):
+ raise AttributeError("EntryPoint objects are immutable.")
+
+ def __repr__(self):
+ return (
+ f'EntryPoint(name={self.name!r}, value={self.value!r}, '
+ f'group={self.group!r})'
+ )
+
+ def __hash__(self):
+ return hash(self._key())
+
+
+class DeprecatedList(list):
+ """
+ Allow an otherwise immutable object to implement mutability
+ for compatibility.
+
+ >>> recwarn = getfixture('recwarn')
+ >>> dl = DeprecatedList(range(3))
+ >>> dl[0] = 1
+ >>> dl.append(3)
+ >>> del dl[3]
+ >>> dl.reverse()
+ >>> dl.sort()
+ >>> dl.extend([4])
+ >>> dl.pop(-1)
+ 4
+ >>> dl.remove(1)
+ >>> dl += [5]
+ >>> dl + [6]
+ [1, 2, 5, 6]
+ >>> dl + (6,)
+ [1, 2, 5, 6]
+ >>> dl.insert(0, 0)
+ >>> dl
+ [0, 1, 2, 5]
+ >>> dl == [0, 1, 2, 5]
+ True
+ >>> dl == (0, 1, 2, 5)
+ True
+ >>> len(recwarn)
+ 1
+ """
+
+ __slots__ = ()
+
+ _warn = functools.partial(
+ warnings.warn,
+ "EntryPoints list interface is deprecated. Cast to list if needed.",
+ DeprecationWarning,
+ stacklevel=pypy_partial(2),
+ )
+
+ def _wrap_deprecated_method(method_name: str): # type: ignore
+ def wrapped(self, *args, **kwargs):
+ self._warn()
+ return getattr(super(), method_name)(*args, **kwargs)
+
+ return method_name, wrapped
+
+ locals().update(
+ map(
+ _wrap_deprecated_method,
+ '__setitem__ __delitem__ append reverse extend pop remove '
+ '__iadd__ insert sort'.split(),
+ )
+ )
+
+ def __add__(self, other):
+ if not isinstance(other, tuple):
+ self._warn()
+ other = tuple(other)
+ return self.__class__(tuple(self) + other)
+
+ def __eq__(self, other):
+ if not isinstance(other, tuple):
+ self._warn()
+ other = tuple(other)
+
+ return tuple(self).__eq__(other)
+
+
+class EntryPoints(DeprecatedList):
+ """
+ An immutable collection of selectable EntryPoint objects.
+ """
+
+ __slots__ = ()
+
+ def __getitem__(self, name): # -> EntryPoint:
+ """
+ Get the EntryPoint in self matching name.
+ """
+ if isinstance(name, int):
+ warnings.warn(
+ "Accessing entry points by index is deprecated. "
+ "Cast to tuple if needed.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return super().__getitem__(name)
+ try:
+ return next(iter(self.select(name=name)))
+ except StopIteration:
+ raise KeyError(name)
+
+ def select(self, **params):
+ """
+ Select entry points from self that match the
+ given parameters (typically group and/or name).
+ """
+ return EntryPoints(ep for ep in self if ep.matches(**params))
+
+ @property
+ def names(self):
+ """
+ Return the set of all names of all entry points.
+ """
+ return {ep.name for ep in self}
+
+ @property
+ def groups(self):
+ """
+ Return the set of all groups of all entry points.
+
+ For coverage while SelectableGroups is present.
+ >>> EntryPoints().groups
+ set()
+ """
+ return {ep.group for ep in self}
+
+ @classmethod
+ def _from_text_for(cls, text, dist):
+ return cls(ep._for(dist) for ep in cls._from_text(text))
+
+ @staticmethod
+ def _from_text(text):
+ return (
+ EntryPoint(name=item.value.name, value=item.value.value, group=item.name)
+ for item in Sectioned.section_pairs(text or '')
+ )
+
+
+class Deprecated:
+ """
+ Compatibility add-in for mapping to indicate that
+ mapping behavior is deprecated.
+
+ >>> recwarn = getfixture('recwarn')
+ >>> class DeprecatedDict(Deprecated, dict): pass
+ >>> dd = DeprecatedDict(foo='bar')
+ >>> dd.get('baz', None)
+ >>> dd['foo']
+ 'bar'
+ >>> list(dd)
+ ['foo']
+ >>> list(dd.keys())
+ ['foo']
+ >>> 'foo' in dd
+ True
+ >>> list(dd.values())
+ ['bar']
+ >>> len(recwarn)
+ 1
+ """
+
+ _warn = functools.partial(
+ warnings.warn,
+ "SelectableGroups dict interface is deprecated. Use select.",
+ DeprecationWarning,
+ stacklevel=pypy_partial(2),
+ )
+
+ def __getitem__(self, name):
+ self._warn()
+ return super().__getitem__(name)
+
+ def get(self, name, default=None):
+ self._warn()
+ return super().get(name, default)
+
+ def __iter__(self):
+ self._warn()
+ return super().__iter__()
+
+ def __contains__(self, *args):
+ self._warn()
+ return super().__contains__(*args)
+
+ def keys(self):
+ self._warn()
+ return super().keys()
+
+ def values(self):
+ self._warn()
+ return super().values()
+
+
+class SelectableGroups(Deprecated, dict):
+ """
+ A backward- and forward-compatible result from
+ entry_points that fully implements the dict interface.
+ """
+
+ @classmethod
+ def load(cls, eps):
+ by_group = operator.attrgetter('group')
+ ordered = sorted(eps, key=by_group)
+ grouped = itertools.groupby(ordered, by_group)
+ return cls((group, EntryPoints(eps)) for group, eps in grouped)
+
+ @property
+ def _all(self):
+ """
+ Reconstruct a list of all entrypoints from the groups.
+ """
+ groups = super(Deprecated, self).values()
+ return EntryPoints(itertools.chain.from_iterable(groups))
+
+ @property
+ def groups(self):
+ return self._all.groups
+
+ @property
+ def names(self):
+ """
+ for coverage:
+ >>> SelectableGroups().names
+ set()
+ """
+ return self._all.names
+
+ def select(self, **params):
+ if not params:
+ return self
+ return self._all.select(**params)
+
+
+class PackagePath(pathlib.PurePosixPath):
+ """A reference to a path in a package"""
+
+ def read_text(self, encoding='utf-8'):
+ with self.locate().open(encoding=encoding) as stream:
+ return stream.read()
+
+ def read_binary(self):
+ with self.locate().open('rb') as stream:
+ return stream.read()
+
+ def locate(self):
+ """Return a path-like object for this path"""
+ return self.dist.locate_file(self)
+
+
+class FileHash:
+ def __init__(self, spec):
+ self.mode, _, self.value = spec.partition('=')
+
+ def __repr__(self):
+ return f''
+
+
+class Distribution:
+ """A Python distribution package."""
+
+ @abc.abstractmethod
+ def read_text(self, filename):
+ """Attempt to load metadata file given by the name.
+
+ :param filename: The name of the file in the distribution info.
+ :return: The text if found, otherwise None.
+ """
+
+ @abc.abstractmethod
+ def locate_file(self, path):
+ """
+ Given a path to a file in this distribution, return a path
+ to it.
+ """
+
+ @classmethod
+ def from_name(cls, name: str):
+ """Return the Distribution for the given package name.
+
+ :param name: The name of the distribution package to search for.
+ :return: The Distribution instance (or subclass thereof) for the named
+ package, if found.
+ :raises PackageNotFoundError: When the named package's distribution
+ metadata cannot be found.
+ :raises ValueError: When an invalid value is supplied for name.
+ """
+ if not name:
+ raise ValueError("A distribution name is required.")
+ try:
+ return next(cls.discover(name=name))
+ except StopIteration:
+ raise PackageNotFoundError(name)
+
+ @classmethod
+ def discover(cls, **kwargs):
+ """Return an iterable of Distribution objects for all packages.
+
+ Pass a ``context`` or pass keyword arguments for constructing
+ a context.
+
+ :context: A ``DistributionFinder.Context`` object.
+ :return: Iterable of Distribution objects for all packages.
+ """
+ context = kwargs.pop('context', None)
+ if context and kwargs:
+ raise ValueError("cannot accept context and kwargs")
+ context = context or DistributionFinder.Context(**kwargs)
+ return itertools.chain.from_iterable(
+ resolver(context) for resolver in cls._discover_resolvers()
+ )
+
+ @staticmethod
+ def at(path):
+ """Return a Distribution for the indicated metadata path
+
+ :param path: a string or path-like object
+ :return: a concrete Distribution instance for the path
+ """
+ return PathDistribution(pathlib.Path(path))
+
+ @staticmethod
+ def _discover_resolvers():
+ """Search the meta_path for resolvers."""
+ declared = (
+ getattr(finder, 'find_distributions', None) for finder in sys.meta_path
+ )
+ return filter(None, declared)
+
+ @property
+ def metadata(self) -> _meta.PackageMetadata:
+ """Return the parsed metadata for this Distribution.
+
+ The returned object will have keys that name the various bits of
+ metadata. See PEP 566 for details.
+ """
+ text = (
+ self.read_text('METADATA')
+ or self.read_text('PKG-INFO')
+ # This last clause is here to support old egg-info files. Its
+ # effect is to just end up using the PathDistribution's self._path
+ # (which points to the egg-info file) attribute unchanged.
+ or self.read_text('')
+ )
+ return _adapters.Message(email.message_from_string(text))
+
+ @property
+ def name(self):
+ """Return the 'Name' metadata for the distribution package."""
+ return self.metadata['Name']
+
+ @property
+ def _normalized_name(self):
+ """Return a normalized version of the name."""
+ return Prepared.normalize(self.name)
+
+ @property
+ def version(self):
+ """Return the 'Version' metadata for the distribution package."""
+ return self.metadata['Version']
+
+ @property
+ def entry_points(self):
+ return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self)
+
+ @property
+ def files(self):
+ """Files in this distribution.
+
+ :return: List of PackagePath for this distribution or None
+
+ Result is `None` if the metadata file that enumerates files
+ (i.e. RECORD for dist-info or SOURCES.txt for egg-info) is
+ missing.
+ Result may be empty if the metadata exists but is empty.
+ """
+
+ def make_file(name, hash=None, size_str=None):
+ result = PackagePath(name)
+ result.hash = FileHash(hash) if hash else None
+ result.size = int(size_str) if size_str else None
+ result.dist = self
+ return result
+
+ @pass_none
+ def make_files(lines):
+ return list(starmap(make_file, csv.reader(lines)))
+
+ return make_files(self._read_files_distinfo() or self._read_files_egginfo())
+
+ def _read_files_distinfo(self):
+ """
+ Read the lines of RECORD
+ """
+ text = self.read_text('RECORD')
+ return text and text.splitlines()
+
+ def _read_files_egginfo(self):
+ """
+ SOURCES.txt might contain literal commas, so wrap each line
+ in quotes.
+ """
+ text = self.read_text('SOURCES.txt')
+ return text and map('"{}"'.format, text.splitlines())
+
+ @property
+ def requires(self):
+ """Generated requirements specified for this Distribution"""
+ reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs()
+ return reqs and list(reqs)
+
+ def _read_dist_info_reqs(self):
+ return self.metadata.get_all('Requires-Dist')
+
+ def _read_egg_info_reqs(self):
+ source = self.read_text('requires.txt')
+ return pass_none(self._deps_from_requires_text)(source)
+
+ @classmethod
+ def _deps_from_requires_text(cls, source):
+ return cls._convert_egg_info_reqs_to_simple_reqs(Sectioned.read(source))
+
+ @staticmethod
+ def _convert_egg_info_reqs_to_simple_reqs(sections):
+ """
+ Historically, setuptools would solicit and store 'extra'
+ requirements, including those with environment markers,
+ in separate sections. More modern tools expect each
+ dependency to be defined separately, with any relevant
+ extras and environment markers attached directly to that
+ requirement. This method converts the former to the
+ latter. See _test_deps_from_requires_text for an example.
+ """
+
+ def make_condition(name):
+ return name and f'extra == "{name}"'
+
+ def quoted_marker(section):
+ section = section or ''
+ extra, sep, markers = section.partition(':')
+ if extra and markers:
+ markers = f'({markers})'
+ conditions = list(filter(None, [markers, make_condition(extra)]))
+ return '; ' + ' and '.join(conditions) if conditions else ''
+
+ def url_req_space(req):
+ """
+ PEP 508 requires a space between the url_spec and the quoted_marker.
+ Ref python/importlib_metadata#357.
+ """
+ # '@' is uniquely indicative of a url_req.
+ return ' ' * ('@' in req)
+
+ for section in sections:
+ space = url_req_space(section.value)
+ yield section.value + space + quoted_marker(section.name)
+
+
+class DistributionFinder(MetaPathFinder):
+ """
+ A MetaPathFinder capable of discovering installed distributions.
+ """
+
+ class Context:
+ """
+ Keyword arguments presented by the caller to
+ ``distributions()`` or ``Distribution.discover()``
+ to narrow the scope of a search for distributions
+ in all DistributionFinders.
+
+ Each DistributionFinder may expect any parameters
+ and should attempt to honor the canonical
+ parameters defined below when appropriate.
+ """
+
+ name = None
+ """
+ Specific name for which a distribution finder should match.
+ A name of ``None`` matches all distributions.
+ """
+
+ def __init__(self, **kwargs):
+ vars(self).update(kwargs)
+
+ @property
+ def path(self):
+ """
+ The sequence of directory path that a distribution finder
+ should search.
+
+ Typically refers to Python installed package paths such as
+ "site-packages" directories and defaults to ``sys.path``.
+ """
+ return vars(self).get('path', sys.path)
+
+ @abc.abstractmethod
+ def find_distributions(self, context=Context()):
+ """
+ Find distributions.
+
+ Return an iterable of all Distribution instances capable of
+ loading the metadata for packages matching the ``context``,
+ a DistributionFinder.Context instance.
+ """
+
+
+class FastPath:
+ """
+ Micro-optimized class for searching a path for
+ children.
+
+ >>> FastPath('').children()
+ ['...']
+ """
+
+ @functools.lru_cache() # type: ignore
+ def __new__(cls, root):
+ return super().__new__(cls)
+
+ def __init__(self, root):
+ self.root = root
+
+ def joinpath(self, child):
+ return pathlib.Path(self.root, child)
+
+ def children(self):
+ with suppress(Exception):
+ return os.listdir(self.root or '.')
+ with suppress(Exception):
+ return self.zip_children()
+ return []
+
+ def zip_children(self):
+ zip_path = zipp.Path(self.root)
+ names = zip_path.root.namelist()
+ self.joinpath = zip_path.joinpath
+
+ return dict.fromkeys(child.split(posixpath.sep, 1)[0] for child in names)
+
+ def search(self, name):
+ return self.lookup(self.mtime).search(name)
+
+ @property
+ def mtime(self):
+ with suppress(OSError):
+ return os.stat(self.root).st_mtime
+ self.lookup.cache_clear()
+
+ @method_cache
+ def lookup(self, mtime):
+ return Lookup(self)
+
+
+class Lookup:
+ def __init__(self, path: FastPath):
+ base = os.path.basename(path.root).lower()
+ base_is_egg = base.endswith(".egg")
+ self.infos = FreezableDefaultDict(list)
+ self.eggs = FreezableDefaultDict(list)
+
+ for child in path.children():
+ low = child.lower()
+ if low.endswith((".dist-info", ".egg-info")):
+ # rpartition is faster than splitext and suitable for this purpose.
+ name = low.rpartition(".")[0].partition("-")[0]
+ normalized = Prepared.normalize(name)
+ self.infos[normalized].append(path.joinpath(child))
+ elif base_is_egg and low == "egg-info":
+ name = base.rpartition(".")[0].partition("-")[0]
+ legacy_normalized = Prepared.legacy_normalize(name)
+ self.eggs[legacy_normalized].append(path.joinpath(child))
+
+ self.infos.freeze()
+ self.eggs.freeze()
+
+ def search(self, prepared):
+ infos = (
+ self.infos[prepared.normalized]
+ if prepared
+ else itertools.chain.from_iterable(self.infos.values())
+ )
+ eggs = (
+ self.eggs[prepared.legacy_normalized]
+ if prepared
+ else itertools.chain.from_iterable(self.eggs.values())
+ )
+ return itertools.chain(infos, eggs)
+
+
+class Prepared:
+ """
+ A prepared search for metadata on a possibly-named package.
+ """
+
+ normalized = None
+ legacy_normalized = None
+
+ def __init__(self, name):
+ self.name = name
+ if name is None:
+ return
+ self.normalized = self.normalize(name)
+ self.legacy_normalized = self.legacy_normalize(name)
+
+ @staticmethod
+ def normalize(name):
+ """
+ PEP 503 normalization plus dashes as underscores.
+ """
+ return re.sub(r"[-_.]+", "-", name).lower().replace('-', '_')
+
+ @staticmethod
+ def legacy_normalize(name):
+ """
+ Normalize the package name as found in the convention in
+ older packaging tools versions and specs.
+ """
+ return name.lower().replace('-', '_')
+
+ def __bool__(self):
+ return bool(self.name)
+
+
+@install
+class MetadataPathFinder(NullFinder, DistributionFinder):
+ """A degenerate finder for distribution packages on the file system.
+
+ This finder supplies only a find_distributions() method for versions
+ of Python that do not have a PathFinder find_distributions().
+ """
+
+ def find_distributions(self, context=DistributionFinder.Context()):
+ """
+ Find distributions.
+
+ Return an iterable of all Distribution instances capable of
+ loading the metadata for packages matching ``context.name``
+ (or all names if ``None`` indicated) along the paths in the list
+ of directories ``context.path``.
+ """
+ found = self._search_paths(context.name, context.path)
+ return map(PathDistribution, found)
+
+ @classmethod
+ def _search_paths(cls, name, paths):
+ """Find metadata directories in paths heuristically."""
+ prepared = Prepared(name)
+ return itertools.chain.from_iterable(
+ path.search(prepared) for path in map(FastPath, paths)
+ )
+
+ def invalidate_caches(cls):
+ FastPath.__new__.cache_clear()
+
+
+class PathDistribution(Distribution):
+ def __init__(self, path: SimplePath):
+ """Construct a distribution.
+
+ :param path: SimplePath indicating the metadata directory.
+ """
+ self._path = path
+
+ def read_text(self, filename):
+ with suppress(
+ FileNotFoundError,
+ IsADirectoryError,
+ KeyError,
+ NotADirectoryError,
+ PermissionError,
+ ):
+ return self._path.joinpath(filename).read_text(encoding='utf-8')
+
+ read_text.__doc__ = Distribution.read_text.__doc__
+
+ def locate_file(self, path):
+ return self._path.parent / path
+
+ @property
+ def _normalized_name(self):
+ """
+ Performance optimization: where possible, resolve the
+ normalized name from the file system path.
+ """
+ stem = os.path.basename(str(self._path))
+ return (
+ pass_none(Prepared.normalize)(self._name_from_stem(stem))
+ or super()._normalized_name
+ )
+
+ @staticmethod
+ def _name_from_stem(stem):
+ """
+ >>> PathDistribution._name_from_stem('foo-3.0.egg-info')
+ 'foo'
+ >>> PathDistribution._name_from_stem('CherryPy-3.0.dist-info')
+ 'CherryPy'
+ >>> PathDistribution._name_from_stem('face.egg-info')
+ 'face'
+ >>> PathDistribution._name_from_stem('foo.bar')
+ """
+ filename, ext = os.path.splitext(stem)
+ if ext not in ('.dist-info', '.egg-info'):
+ return
+ name, sep, rest = filename.partition('-')
+ return name
+
+
+def distribution(distribution_name):
+ """Get the ``Distribution`` instance for the named package.
+
+ :param distribution_name: The name of the distribution package as a string.
+ :return: A ``Distribution`` instance (or subclass thereof).
+ """
+ return Distribution.from_name(distribution_name)
+
+
+def distributions(**kwargs):
+ """Get all ``Distribution`` instances in the current environment.
+
+ :return: An iterable of ``Distribution`` instances.
+ """
+ return Distribution.discover(**kwargs)
+
+
+def metadata(distribution_name) -> _meta.PackageMetadata:
+ """Get the metadata for the named package.
+
+ :param distribution_name: The name of the distribution package to query.
+ :return: A PackageMetadata containing the parsed metadata.
+ """
+ return Distribution.from_name(distribution_name).metadata
+
+
+def version(distribution_name):
+ """Get the version string for the named package.
+
+ :param distribution_name: The name of the distribution package to query.
+ :return: The version string for the package as defined in the package's
+ "Version" metadata key.
+ """
+ return distribution(distribution_name).version
+
+
+_unique = functools.partial(
+ unique_everseen,
+ key=operator.attrgetter('_normalized_name'),
+)
+"""
+Wrapper for ``distributions`` to return unique distributions by name.
+"""
+
+
+def entry_points(**params) -> Union[EntryPoints, SelectableGroups]:
+ """Return EntryPoint objects for all installed packages.
+
+ Pass selection parameters (group or name) to filter the
+ result to entry points matching those properties (see
+ EntryPoints.select()).
+
+ For compatibility, returns ``SelectableGroups`` object unless
+ selection parameters are supplied. In the future, this function
+ will return ``EntryPoints`` instead of ``SelectableGroups``
+ even when no selection parameters are supplied.
+
+ For maximum future compatibility, pass selection parameters
+ or invoke ``.select`` with parameters on the result.
+
+ :return: EntryPoints or SelectableGroups for all installed packages.
+ """
+ eps = itertools.chain.from_iterable(
+ dist.entry_points for dist in _unique(distributions())
+ )
+ return SelectableGroups.load(eps).select(**params)
+
+
+def files(distribution_name):
+ """Return a list of files for the named package.
+
+ :param distribution_name: The name of the distribution package to query.
+ :return: List of files composing the distribution.
+ """
+ return distribution(distribution_name).files
+
+
+def requires(distribution_name):
+ """
+ Return a list of requirements for the named package.
+
+ :return: An iterator of requirements, suitable for
+ packaging.requirement.Requirement.
+ """
+ return distribution(distribution_name).requires
+
+
+def packages_distributions() -> Mapping[str, List[str]]:
+ """
+ Return a mapping of top-level packages to their
+ distributions.
+
+ >>> import collections.abc
+ >>> pkgs = packages_distributions()
+ >>> all(isinstance(dist, collections.abc.Sequence) for dist in pkgs.values())
+ True
+ """
+ pkg_to_dist = collections.defaultdict(list)
+ for dist in distributions():
+ for pkg in _top_level_declared(dist) or _top_level_inferred(dist):
+ pkg_to_dist[pkg].append(dist.metadata['Name'])
+ return dict(pkg_to_dist)
+
+
+def _top_level_declared(dist):
+ return (dist.read_text('top_level.txt') or '').split()
+
+
+def _top_level_inferred(dist):
+ return {
+ f.parts[0] if len(f.parts) > 1 else f.with_suffix('').name
+ for f in always_iterable(dist.files)
+ if f.suffix == ".py"
+ }
diff --git a/src/pip/_vendor/importlib_metadata/_adapters.py b/src/pip/_vendor/importlib_metadata/_adapters.py
new file mode 100644
index 00000000000..aa460d3eda5
--- /dev/null
+++ b/src/pip/_vendor/importlib_metadata/_adapters.py
@@ -0,0 +1,68 @@
+import re
+import textwrap
+import email.message
+
+from ._text import FoldedCase
+
+
+class Message(email.message.Message):
+ multiple_use_keys = set(
+ map(
+ FoldedCase,
+ [
+ 'Classifier',
+ 'Obsoletes-Dist',
+ 'Platform',
+ 'Project-URL',
+ 'Provides-Dist',
+ 'Provides-Extra',
+ 'Requires-Dist',
+ 'Requires-External',
+ 'Supported-Platform',
+ 'Dynamic',
+ ],
+ )
+ )
+ """
+ Keys that may be indicated multiple times per PEP 566.
+ """
+
+ def __new__(cls, orig: email.message.Message):
+ res = super().__new__(cls)
+ vars(res).update(vars(orig))
+ return res
+
+ def __init__(self, *args, **kwargs):
+ self._headers = self._repair_headers()
+
+ # suppress spurious error from mypy
+ def __iter__(self):
+ return super().__iter__()
+
+ def _repair_headers(self):
+ def redent(value):
+ "Correct for RFC822 indentation"
+ if not value or '\n' not in value:
+ return value
+ return textwrap.dedent(' ' * 8 + value)
+
+ headers = [(key, redent(value)) for key, value in vars(self)['_headers']]
+ if self._payload:
+ headers.append(('Description', self.get_payload()))
+ return headers
+
+ @property
+ def json(self):
+ """
+ Convert PackageMetadata to a JSON-compatible format
+ per PEP 0566.
+ """
+
+ def transform(key):
+ value = self.get_all(key) if key in self.multiple_use_keys else self[key]
+ if key == 'Keywords':
+ value = re.split(r'\s+', value)
+ tk = key.lower().replace('-', '_')
+ return tk, value
+
+ return dict(map(transform, map(FoldedCase, self)))
diff --git a/src/pip/_vendor/importlib_metadata/_collections.py b/src/pip/_vendor/importlib_metadata/_collections.py
new file mode 100644
index 00000000000..cf0954e1a30
--- /dev/null
+++ b/src/pip/_vendor/importlib_metadata/_collections.py
@@ -0,0 +1,30 @@
+import collections
+
+
+# from jaraco.collections 3.3
+class FreezableDefaultDict(collections.defaultdict):
+ """
+ Often it is desirable to prevent the mutation of
+ a default dict after its initial construction, such
+ as to prevent mutation during iteration.
+
+ >>> dd = FreezableDefaultDict(list)
+ >>> dd[0].append('1')
+ >>> dd.freeze()
+ >>> dd[1]
+ []
+ >>> len(dd)
+ 1
+ """
+
+ def __missing__(self, key):
+ return getattr(self, '_frozen', super().__missing__)(key)
+
+ def freeze(self):
+ self._frozen = lambda key: self.default_factory()
+
+
+class Pair(collections.namedtuple('Pair', 'name value')):
+ @classmethod
+ def parse(cls, text):
+ return cls(*map(str.strip, text.split("=", 1)))
diff --git a/src/pip/_vendor/importlib_metadata/_compat.py b/src/pip/_vendor/importlib_metadata/_compat.py
new file mode 100644
index 00000000000..6861d9fe4c4
--- /dev/null
+++ b/src/pip/_vendor/importlib_metadata/_compat.py
@@ -0,0 +1,72 @@
+import sys
+import platform
+
+
+__all__ = ['install', 'NullFinder', 'Protocol']
+
+
+try:
+ from typing import Protocol
+except ImportError: # pragma: no cover
+ # Python 3.7 compatibility
+ from pip._vendor.typing_extensions import Protocol # type: ignore
+
+
+def install(cls):
+ """
+ Class decorator for installation on sys.meta_path.
+
+ Adds the backport DistributionFinder to sys.meta_path and
+ attempts to disable the finder functionality of the stdlib
+ DistributionFinder.
+ """
+ sys.meta_path.append(cls())
+ disable_stdlib_finder()
+ return cls
+
+
+def disable_stdlib_finder():
+ """
+ Give the backport primacy for discovering path-based distributions
+ by monkey-patching the stdlib O_O.
+
+ See #91 for more background for rationale on this sketchy
+ behavior.
+ """
+
+ def matches(finder):
+ return getattr(
+ finder, '__module__', None
+ ) == '_frozen_importlib_external' and hasattr(finder, 'find_distributions')
+
+ for finder in filter(matches, sys.meta_path): # pragma: nocover
+ del finder.find_distributions
+
+
+class NullFinder:
+ """
+ A "Finder" (aka "MetaClassFinder") that never finds any modules,
+ but may find distributions.
+ """
+
+ @staticmethod
+ def find_spec(*args, **kwargs):
+ return None
+
+ # In Python 2, the import system requires finders
+ # to have a find_module() method, but this usage
+ # is deprecated in Python 3 in favor of find_spec().
+ # For the purposes of this finder (i.e. being present
+ # on sys.meta_path but having no other import
+ # system functionality), the two methods are identical.
+ find_module = find_spec
+
+
+def pypy_partial(val):
+ """
+ Adjust for variable stacklevel on partial under PyPy.
+
+ Workaround for #327.
+ """
+ is_pypy = platform.python_implementation() == 'PyPy'
+ return val + is_pypy
diff --git a/src/pip/_vendor/importlib_metadata/_functools.py b/src/pip/_vendor/importlib_metadata/_functools.py
new file mode 100644
index 00000000000..71f66bd03cb
--- /dev/null
+++ b/src/pip/_vendor/importlib_metadata/_functools.py
@@ -0,0 +1,104 @@
+import types
+import functools
+
+
+# from jaraco.functools 3.3
+def method_cache(method, cache_wrapper=None):
+ """
+ Wrap lru_cache to support storing the cache data in the object instances.
+
+ Abstracts the common paradigm where the method explicitly saves an
+ underscore-prefixed protected property on first call and returns that
+ subsequently.
+
+ >>> class MyClass:
+ ... calls = 0
+ ...
+ ... @method_cache
+ ... def method(self, value):
+ ... self.calls += 1
+ ... return value
+
+ >>> a = MyClass()
+ >>> a.method(3)
+ 3
+ >>> for x in range(75):
+ ... res = a.method(x)
+ >>> a.calls
+ 75
+
+ Note that the apparent behavior will be exactly like that of lru_cache
+ except that the cache is stored on each instance, so values in one
+ instance will not flush values from another, and when an instance is
+ deleted, so are the cached values for that instance.
+
+ >>> b = MyClass()
+ >>> for x in range(35):
+ ... res = b.method(x)
+ >>> b.calls
+ 35
+ >>> a.method(0)
+ 0
+ >>> a.calls
+ 75
+
+ Note that if method had been decorated with ``functools.lru_cache()``,
+ a.calls would have been 76 (due to the cached value of 0 having been
+ flushed by the 'b' instance).
+
+ Clear the cache with ``.cache_clear()``
+
+ >>> a.method.cache_clear()
+
+ Same for a method that hasn't yet been called.
+
+ >>> c = MyClass()
+ >>> c.method.cache_clear()
+
+ Another cache wrapper may be supplied:
+
+ >>> cache = functools.lru_cache(maxsize=2)
+ >>> MyClass.method2 = method_cache(lambda self: 3, cache_wrapper=cache)
+ >>> a = MyClass()
+ >>> a.method2()
+ 3
+
+ Caution - do not subsequently wrap the method with another decorator, such
+ as ``@property``, which changes the semantics of the function.
+
+ See also
+ http://code.activestate.com/recipes/577452-a-memoize-decorator-for-instance-methods/
+ for another implementation and additional justification.
+ """
+ cache_wrapper = cache_wrapper or functools.lru_cache()
+
+ def wrapper(self, *args, **kwargs):
+ # it's the first call, replace the method with a cached, bound method
+ bound_method = types.MethodType(method, self)
+ cached_method = cache_wrapper(bound_method)
+ setattr(self, method.__name__, cached_method)
+ return cached_method(*args, **kwargs)
+
+ # Support cache clear even before cache has been created.
+ wrapper.cache_clear = lambda: None
+
+ return wrapper
+
+
+# From jaraco.functools 3.3
+def pass_none(func):
+ """
+ Wrap func so it's not called if its first param is None
+
+ >>> print_text = pass_none(print)
+ >>> print_text('text')
+ text
+ >>> print_text(None)
+ """
+
+ @functools.wraps(func)
+ def wrapper(param, *args, **kwargs):
+ if param is not None:
+ return func(param, *args, **kwargs)
+
+ return wrapper
diff --git a/src/pip/_vendor/importlib_metadata/_itertools.py b/src/pip/_vendor/importlib_metadata/_itertools.py
new file mode 100644
index 00000000000..d4ca9b9140e
--- /dev/null
+++ b/src/pip/_vendor/importlib_metadata/_itertools.py
@@ -0,0 +1,73 @@
+from itertools import filterfalse
+
+
+def unique_everseen(iterable, key=None):
+ "List unique elements, preserving order. Remember all elements ever seen."
+ # unique_everseen('AAAABBBCCDAABBB') --> A B C D
+ # unique_everseen('ABBCcAD', str.lower) --> A B C D
+ seen = set()
+ seen_add = seen.add
+ if key is None:
+ for element in filterfalse(seen.__contains__, iterable):
+ seen_add(element)
+ yield element
+ else:
+ for element in iterable:
+ k = key(element)
+ if k not in seen:
+ seen_add(k)
+ yield element
+
+
+# copied from more_itertools 8.8
+def always_iterable(obj, base_type=(str, bytes)):
+ """If *obj* is iterable, return an iterator over its items::
+
+ >>> obj = (1, 2, 3)
+ >>> list(always_iterable(obj))
+ [1, 2, 3]
+
+ If *obj* is not iterable, return a one-item iterable containing *obj*::
+
+ >>> obj = 1
+ >>> list(always_iterable(obj))
+ [1]
+
+ If *obj* is ``None``, return an empty iterable:
+
+ >>> obj = None
+ >>> list(always_iterable(None))
+ []
+
+ By default, binary and text strings are not considered iterable::
+
+ >>> obj = 'foo'
+ >>> list(always_iterable(obj))
+ ['foo']
+
+ If *base_type* is set, objects for which ``isinstance(obj, base_type)``
+ returns ``True`` won't be considered iterable.
+
+ >>> obj = {'a': 1}
+ >>> list(always_iterable(obj)) # Iterate over the dict's keys
+ ['a']
+ >>> list(always_iterable(obj, base_type=dict)) # Treat dicts as a unit
+ [{'a': 1}]
+
+ Set *base_type* to ``None`` to avoid any special handling and treat objects
+ Python considers iterable as iterable:
+
+ >>> obj = 'foo'
+ >>> list(always_iterable(obj, base_type=None))
+ ['f', 'o', 'o']
+ """
+ if obj is None:
+ return iter(())
+
+ if (base_type is not None) and isinstance(obj, base_type):
+ return iter((obj,))
+
+ try:
+ return iter(obj)
+ except TypeError:
+ return iter((obj,))
diff --git a/src/pip/_vendor/importlib_metadata/_meta.py b/src/pip/_vendor/importlib_metadata/_meta.py
new file mode 100644
index 00000000000..37ee43e6ef4
--- /dev/null
+++ b/src/pip/_vendor/importlib_metadata/_meta.py
@@ -0,0 +1,48 @@
+from ._compat import Protocol
+from typing import Any, Dict, Iterator, List, TypeVar, Union
+
+
+_T = TypeVar("_T")
+
+
+class PackageMetadata(Protocol):
+ def __len__(self) -> int:
+ ... # pragma: no cover
+
+ def __contains__(self, item: str) -> bool:
+ ... # pragma: no cover
+
+ def __getitem__(self, key: str) -> str:
+ ... # pragma: no cover
+
+ def __iter__(self) -> Iterator[str]:
+ ... # pragma: no cover
+
+ def get_all(self, name: str, failobj: _T = ...) -> Union[List[Any], _T]:
+ """
+ Return all values associated with a possibly multi-valued key.
+ """
+
+ @property
+ def json(self) -> Dict[str, Union[str, List[str]]]:
+ """
+ A JSON-compatible form of the metadata.
+ """
+
+
+class SimplePath(Protocol):
+ """
+ A minimal subset of pathlib.Path required by PathDistribution.
+ """
+
+ def joinpath(self) -> 'SimplePath':
+ ... # pragma: no cover
+
+ def __truediv__(self) -> 'SimplePath':
+ ... # pragma: no cover
+
+ def parent(self) -> 'SimplePath':
+ ... # pragma: no cover
+
+ def read_text(self) -> str:
+ ... # pragma: no cover
diff --git a/src/pip/_vendor/importlib_metadata/_text.py b/src/pip/_vendor/importlib_metadata/_text.py
new file mode 100644
index 00000000000..c88cfbb2349
--- /dev/null
+++ b/src/pip/_vendor/importlib_metadata/_text.py
@@ -0,0 +1,99 @@
+import re
+
+from ._functools import method_cache
+
+
+# from jaraco.text 3.5
+class FoldedCase(str):
+ """
+ A case insensitive string class; behaves just like str
+ except compares equal when the only variation is case.
+
+ >>> s = FoldedCase('hello world')
+
+ >>> s == 'Hello World'
+ True
+
+ >>> 'Hello World' == s
+ True
+
+ >>> s != 'Hello World'
+ False
+
+ >>> s.index('O')
+ 4
+
+ >>> s.split('O')
+ ['hell', ' w', 'rld']
+
+ >>> sorted(map(FoldedCase, ['GAMMA', 'alpha', 'Beta']))
+ ['alpha', 'Beta', 'GAMMA']
+
+ Sequence membership is straightforward.
+
+ >>> "Hello World" in [s]
+ True
+ >>> s in ["Hello World"]
+ True
+
+ You may test for set inclusion, but candidate and elements
+ must both be folded.
+
+ >>> FoldedCase("Hello World") in {s}
+ True
+ >>> s in {FoldedCase("Hello World")}
+ True
+
+ String inclusion works as long as the FoldedCase object
+ is on the right.
+
+ >>> "hello" in FoldedCase("Hello World")
+ True
+
+ But not if the FoldedCase object is on the left:
+
+ >>> FoldedCase('hello') in 'Hello World'
+ False
+
+ In that case, use in_:
+
+ >>> FoldedCase('hello').in_('Hello World')
+ True
+
+ >>> FoldedCase('hello') > FoldedCase('Hello')
+ False
+ """
+
+ def __lt__(self, other):
+ return self.lower() < other.lower()
+
+ def __gt__(self, other):
+ return self.lower() > other.lower()
+
+ def __eq__(self, other):
+ return self.lower() == other.lower()
+
+ def __ne__(self, other):
+ return self.lower() != other.lower()
+
+ def __hash__(self):
+ return hash(self.lower())
+
+ def __contains__(self, other):
+ return super().lower().__contains__(other.lower())
+
+ def in_(self, other):
+ "Does self appear in other?"
+ return self in FoldedCase(other)
+
+ # cache lower since it's likely to be called frequently.
+ @method_cache
+ def lower(self):
+ return super().lower()
+
+ def index(self, sub):
+ return self.lower().index(sub.lower())
+
+ def split(self, splitter=' ', maxsplit=0):
+ pattern = re.compile(re.escape(splitter), re.I)
+ return pattern.split(self, maxsplit)
diff --git a/src/pip/_vendor/importlib_metadata/py.typed b/src/pip/_vendor/importlib_metadata/py.typed
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/src/pip/_vendor/keyring/LICENSE b/src/pip/_vendor/keyring/LICENSE
new file mode 100644
index 00000000000..353924be0e5
--- /dev/null
+++ b/src/pip/_vendor/keyring/LICENSE
@@ -0,0 +1,19 @@
+Copyright Jason R. Coombs
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to
+deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+sell copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+IN THE SOFTWARE.
diff --git a/src/pip/_vendor/keyring/__init__.py b/src/pip/_vendor/keyring/__init__.py
new file mode 100644
index 00000000000..712b1b418dc
--- /dev/null
+++ b/src/pip/_vendor/keyring/__init__.py
@@ -0,0 +1,17 @@
+from .core import (
+ set_keyring,
+ get_keyring,
+ set_password,
+ get_password,
+ delete_password,
+ get_credential,
+)
+
+__all__ = (
+ 'set_keyring',
+ 'get_keyring',
+ 'set_password',
+ 'get_password',
+ 'delete_password',
+ 'get_credential',
+)
diff --git a/src/pip/_vendor/keyring/__main__.py b/src/pip/_vendor/keyring/__main__.py
new file mode 100644
index 00000000000..c0a67abdcfd
--- /dev/null
+++ b/src/pip/_vendor/keyring/__main__.py
@@ -0,0 +1,4 @@
+if __name__ == '__main__':
+ from pip._vendor.keyring import cli
+
+ cli.main()
diff --git a/src/pip/_vendor/keyring/backend.py b/src/pip/_vendor/keyring/backend.py
new file mode 100644
index 00000000000..731b5073b80
--- /dev/null
+++ b/src/pip/_vendor/keyring/backend.py
@@ -0,0 +1,258 @@
+"""
+Keyring implementation support
+"""
+
+import os
+import abc
+import logging
+import operator
+import copy
+
+from typing import Optional
+
+from .py310compat import metadata
+from . import credentials, errors, util
+from .util import properties
+
+log = logging.getLogger(__name__)
+
+
+by_priority = operator.attrgetter('priority')
+_limit = None
+
+
+class KeyringBackendMeta(abc.ABCMeta):
+ """
+ A metaclass that's both an ABCMeta and a type that keeps a registry of
+ all (non-abstract) types.
+ """
+
+ def __init__(cls, name, bases, dict):
+ super().__init__(name, bases, dict)
+ if not hasattr(cls, '_classes'):
+ cls._classes = set()
+ classes = cls._classes
+ if not cls.__abstractmethods__:
+ classes.add(cls)
+
+
+class KeyringBackend(metaclass=KeyringBackendMeta):
+ """The abstract base class of the keyring, every backend must implement
+ this interface.
+ """
+
+ def __init__(self):
+ self.set_properties_from_env()
+
+ # @abc.abstractproperty
+ def priority(cls):
+ """
+ Each backend class must supply a priority, a number (float or integer)
+ indicating the priority of the backend relative to all other backends.
+ The priority need not be static -- it may (and should) vary based
+ attributes of the environment in which is runs (platform, available
+ packages, etc.).
+
+ A higher number indicates a higher priority. The priority should raise
+ a RuntimeError with a message indicating the underlying cause if the
+ backend is not suitable for the current environment.
+
+ As a rule of thumb, a priority between zero but less than one is
+ suitable, but a priority of one or greater is recommended.
+ """
+
+ @properties.ClassProperty
+ @classmethod
+ def viable(cls):
+ with errors.ExceptionRaisedContext() as exc:
+ cls.priority
+ return not exc
+
+ @classmethod
+ def get_viable_backends(cls):
+ """
+ Return all subclasses deemed viable.
+ """
+ return filter(operator.attrgetter('viable'), cls._classes)
+
+ @properties.ClassProperty
+ @classmethod
+ def name(cls):
+ """
+ The keyring name, suitable for display.
+
+ The name is derived from module and class name.
+ """
+ parent, sep, mod_name = cls.__module__.rpartition('.')
+ mod_name = mod_name.replace('_', ' ')
+ return ' '.join([mod_name, cls.__name__])
+
+ def __str__(self):
+ keyring_class = type(self)
+ return "{}.{} (priority: {:g})".format(
+ keyring_class.__module__, keyring_class.__name__, keyring_class.priority
+ )
+
+ @abc.abstractmethod
+ def get_password(self, service: str, username: str) -> Optional[str]:
+ """Get password of the username for the service"""
+ return None
+
+ @abc.abstractmethod
+ def set_password(self, service: str, username: str, password: str) -> None:
+ """Set password for the username of the service.
+
+ If the backend cannot store passwords, raise
+ PasswordSetError.
+ """
+ raise errors.PasswordSetError("reason")
+
+ # for backward-compatibility, don't require a backend to implement
+ # delete_password
+ # @abc.abstractmethod
+ def delete_password(self, service: str, username: str) -> None:
+ """Delete the password for the username of the service.
+
+ If the backend cannot delete passwords, raise
+ PasswordDeleteError.
+ """
+ raise errors.PasswordDeleteError("reason")
+
+ # for backward-compatibility, don't require a backend to implement
+ # get_credential
+ # @abc.abstractmethod
+ def get_credential(
+ self,
+ service: str,
+ username: Optional[str],
+ ) -> Optional[credentials.Credential]:
+ """Gets the username and password for the service.
+ Returns a Credential instance.
+
+ The *username* argument is optional and may be omitted by
+ the caller or ignored by the backend. Callers must use the
+ returned username.
+ """
+ # The default implementation requires a username here.
+ if username is not None:
+ password = self.get_password(service, username)
+ if password is not None:
+ return credentials.SimpleCredential(username, password)
+ return None
+
+ def set_properties_from_env(self):
+ """For all KEYRING_PROPERTY_* env var, set that property."""
+
+ def parse(item):
+ key, value = item
+ pre, sep, name = key.partition('KEYRING_PROPERTY_')
+ return sep and (name.lower(), value)
+
+ props = filter(None, map(parse, os.environ.items()))
+ for name, value in props:
+ setattr(self, name, value)
+
+ def with_properties(self, **kwargs):
+ alt = copy.copy(self)
+ vars(alt).update(kwargs)
+ return alt
+
+
+class Crypter:
+ """Base class providing encryption and decryption"""
+
+ @abc.abstractmethod
+ def encrypt(self, value):
+ """Encrypt the value."""
+ pass
+
+ @abc.abstractmethod
+ def decrypt(self, value):
+ """Decrypt the value."""
+ pass
+
+
+class NullCrypter(Crypter):
+ """A crypter that does nothing"""
+
+ def encrypt(self, value):
+ return value
+
+ def decrypt(self, value):
+ return value
+
+
+def _load_plugins():
+ """
+ Locate all setuptools entry points by the name 'keyring backends'
+ and initialize them.
+ Any third-party library may register an entry point by adding the
+ following to their setup.cfg::
+
+ [options.entry_points]
+ keyring.backends =
+ plugin_name = mylib.mymodule:initialize_func
+
+ `plugin_name` can be anything, and is only used to display the name
+ of the plugin at initialization time.
+
+ `initialize_func` is optional, but will be invoked if callable.
+ """
+ for ep in metadata.entry_points(group='keyring.backends'):
+ try:
+ log.debug('Loading %s', ep.name)
+ init_func = ep.load()
+ if callable(init_func):
+ init_func()
+ except Exception:
+ log.exception(f"Error initializing plugin {ep}.")
+
+
+@util.once
+def get_all_keyring():
+ """
+ Return a list of all implemented keyrings that can be constructed without
+ parameters.
+ """
+ _load_plugins()
+ viable_classes = KeyringBackend.get_viable_backends()
+ rings = util.suppress_exceptions(viable_classes, exceptions=TypeError)
+ return list(rings)
+
+
+class SchemeSelectable:
+ """
+ Allow a backend to select different "schemes" for the
+ username and service.
+
+ >>> backend = SchemeSelectable()
+ >>> backend._query('contoso', 'alice')
+ {'username': 'alice', 'service': 'contoso'}
+ >>> backend._query('contoso')
+ {'service': 'contoso'}
+ >>> backend.scheme = 'KeePassXC'
+ >>> backend._query('contoso', 'alice')
+ {'UserName': 'alice', 'Title': 'contoso'}
+ >>> backend._query('contoso', 'alice', foo='bar')
+ {'UserName': 'alice', 'Title': 'contoso', 'foo': 'bar'}
+ """
+
+ scheme = 'default'
+ schemes = dict(
+ default=dict(username='username', service='service'),
+ KeePassXC=dict(username='UserName', service='Title'),
+ )
+
+ def _query(self, service, username=None, **base):
+ scheme = self.schemes[self.scheme]
+ return dict(
+ {
+ scheme['username']: username,
+ scheme['service']: service,
+ }
+ if username is not None
+ else {
+ scheme['service']: service,
+ },
+ **base,
+ )
diff --git a/src/pip/_vendor/keyring/backends/OS_X.py b/src/pip/_vendor/keyring/backends/OS_X.py
new file mode 100644
index 00000000000..c226564c1b7
--- /dev/null
+++ b/src/pip/_vendor/keyring/backends/OS_X.py
@@ -0,0 +1,13 @@
+"""
+Backward-compatibility shim for users referencing the module
+by name. Ref #487.
+"""
+
+import warnings
+
+from .macOS import Keyring
+
+__all__ = ['Keyring']
+
+
+warnings.warn("OS_X module is deprecated.", DeprecationWarning)
diff --git a/src/pip/_vendor/keyring/backends/SecretService.py b/src/pip/_vendor/keyring/backends/SecretService.py
new file mode 100644
index 00000000000..33e69a7871f
--- /dev/null
+++ b/src/pip/_vendor/keyring/backends/SecretService.py
@@ -0,0 +1,120 @@
+from contextlib import closing
+import logging
+
+from .. import backend
+from ..util import properties
+from ..backend import KeyringBackend
+from ..credentials import SimpleCredential
+from ..errors import (
+ InitError,
+ PasswordDeleteError,
+ ExceptionRaisedContext,
+ KeyringLocked,
+)
+
+try:
+ import secretstorage
+ import secretstorage.exceptions as exceptions
+except ImportError:
+ pass
+except AttributeError:
+ # See https://github.com/jaraco/keyring/issues/296
+ pass
+
+log = logging.getLogger(__name__)
+
+
+class Keyring(backend.SchemeSelectable, KeyringBackend):
+ """Secret Service Keyring"""
+
+ appid = 'Python keyring library'
+
+ @properties.ClassProperty
+ @classmethod
+ def priority(cls):
+ with ExceptionRaisedContext() as exc:
+ secretstorage.__name__
+ if exc:
+ raise RuntimeError("SecretStorage required")
+ if secretstorage.__version_tuple__ < (3, 2):
+ raise RuntimeError("SecretStorage 3.2 or newer required")
+ try:
+ with closing(secretstorage.dbus_init()) as connection:
+ if not secretstorage.check_service_availability(connection):
+ raise RuntimeError(
+ "The Secret Service daemon is neither running nor "
+ "activatable through D-Bus"
+ )
+ except exceptions.SecretStorageException as e:
+ raise RuntimeError("Unable to initialize SecretService: %s" % e)
+ return 5
+
+ def get_preferred_collection(self):
+ """If self.preferred_collection contains a D-Bus path,
+ the collection at that address is returned. Otherwise,
+ the default collection is returned.
+ """
+ bus = secretstorage.dbus_init()
+ try:
+ if hasattr(self, 'preferred_collection'):
+ collection = secretstorage.Collection(bus, self.preferred_collection)
+ else:
+ collection = secretstorage.get_default_collection(bus)
+ except exceptions.SecretStorageException as e:
+ raise InitError("Failed to create the collection: %s." % e)
+ if collection.is_locked():
+ collection.unlock()
+ if collection.is_locked(): # User dismissed the prompt
+ raise KeyringLocked("Failed to unlock the collection!")
+ return collection
+
+ def unlock(self, item):
+ if hasattr(item, 'unlock'):
+ item.unlock()
+ if item.is_locked(): # User dismissed the prompt
+ raise KeyringLocked('Failed to unlock the item!')
+
+ def get_password(self, service, username):
+ """Get password of the username for the service"""
+ collection = self.get_preferred_collection()
+ with closing(collection.connection):
+ items = collection.search_items(self._query(service, username))
+ for item in items:
+ self.unlock(item)
+ return item.get_secret().decode('utf-8')
+
+ def set_password(self, service, username, password):
+ """Set password for the username of the service"""
+ collection = self.get_preferred_collection()
+ attributes = self._query(service, username, application=self.appid)
+ label = "Password for '{}' on '{}'".format(username, service)
+ with closing(collection.connection):
+ collection.create_item(label, attributes, password, replace=True)
+
+ def delete_password(self, service, username):
+ """Delete the stored password (only the first one)"""
+ collection = self.get_preferred_collection()
+ with closing(collection.connection):
+ items = collection.search_items(self._query(service, username))
+ for item in items:
+ return item.delete()
+ raise PasswordDeleteError("No such password!")
+
+ def get_credential(self, service, username):
+ """Gets the first username and password for a service.
+ Returns a Credential instance
+
+ The username can be omitted, but if there is one, it will use get_password
+ and return a SimpleCredential containing the username and password
+ Otherwise, it will return the first username and password combo that it finds.
+ """
+ scheme = self.schemes[self.scheme]
+ query = self._query(service, username)
+ collection = self.get_preferred_collection()
+
+ with closing(collection.connection):
+ items = collection.search_items(query)
+ for item in items:
+ self.unlock(item)
+ username = item.get_attributes().get(scheme['username'])
+ return SimpleCredential(username, item.get_secret().decode('utf-8'))
diff --git a/src/pip/_vendor/keyring/backends/Windows.py b/src/pip/_vendor/keyring/backends/Windows.py
new file mode 100644
index 00000000000..9b6a189fb6a
--- /dev/null
+++ b/src/pip/_vendor/keyring/backends/Windows.py
@@ -0,0 +1,171 @@
+import logging
+
+from ..util import properties
+from ..backend import KeyringBackend
+from ..credentials import SimpleCredential
+from ..errors import PasswordDeleteError, ExceptionRaisedContext
+
+
+with ExceptionRaisedContext() as missing_deps:
+ try:
+ # prefer pywin32-ctypes
+ from win32ctypes.pywin32 import pywintypes
+ from win32ctypes.pywin32 import win32cred
+
+ # force demand import to raise ImportError
+ win32cred.__name__
+ except ImportError:
+ # fallback to pywin32
+ import pywintypes
+ import win32cred
+
+ # force demand import to raise ImportError
+ win32cred.__name__
+
+log = logging.getLogger(__name__)
+
+
+class Persistence:
+ def __get__(self, keyring, type=None):
+ return getattr(keyring, '_persist', win32cred.CRED_PERSIST_ENTERPRISE)
+
+ def __set__(self, keyring, value):
+ """
+ Set the persistence value on the Keyring. Value may be
+ one of the win32cred.CRED_PERSIST_* constants or a
+ string representing one of those constants. For example,
+ 'local machine' or 'session'.
+ """
+ if isinstance(value, str):
+ attr = 'CRED_PERSIST_' + value.replace(' ', '_').upper()
+ value = getattr(win32cred, attr)
+ setattr(keyring, '_persist', value)
+
+
+class DecodingCredential(dict):
+ @property
+ def value(self):
+ """
+ Attempt to decode the credential blob as UTF-16 then UTF-8.
+ """
+ cred = self['CredentialBlob']
+ try:
+ return cred.decode('utf-16')
+ except UnicodeDecodeError:
+ decoded_cred_utf8 = cred.decode('utf-8')
+ log.warning(
+ "Retrieved an UTF-8 encoded credential. Please be aware that "
+ "this library only writes credentials in UTF-16."
+ )
+ return decoded_cred_utf8
+
+
+class WinVaultKeyring(KeyringBackend):
+ """
+ WinVaultKeyring stores encrypted passwords using the Windows Credential
+ Manager.
+
+ Requires pywin32
+
+ This backend does some gymnastics to simulate multi-user support,
+ which WinVault doesn't support natively. See
+ https://github.com/jaraco/keyring/issues/47#issuecomment-75763152
+ for details on the implementation, but here's the gist:
+
+ Passwords are stored under the service name unless there is a collision
+ (another password with the same service name but different user name),
+ in which case the previous password is moved into a compound name:
+ {username}@{service}
+ """
+
+ persist = Persistence()
+
+ @properties.ClassProperty
+ @classmethod
+ def priority(cls):
+ """
+ If available, the preferred backend on Windows.
+ """
+ if missing_deps:
+ raise RuntimeError("Requires Windows and pywin32")
+ return 5
+
+ @staticmethod
+ def _compound_name(username, service):
+ return f'{username}@{service}'
+
+ def get_password(self, service, username):
+ # first attempt to get the password under the service name
+ res = self._get_password(service)
+ if not res or res['UserName'] != username:
+ # It wasn't found so attempt to get it with the compound name
+ res = self._get_password(self._compound_name(username, service))
+ if not res:
+ return None
+ return res.value
+
+ def _get_password(self, target):
+ try:
+ res = win32cred.CredRead(
+ Type=win32cred.CRED_TYPE_GENERIC, TargetName=target
+ )
+ except pywintypes.error as e:
+ if e.winerror == 1168 and e.funcname == 'CredRead': # not found
+ return None
+ raise
+ return DecodingCredential(res)
+
+ def set_password(self, service, username, password):
+ existing_pw = self._get_password(service)
+ if existing_pw:
+ # resave the existing password using a compound target
+ existing_username = existing_pw['UserName']
+ target = self._compound_name(existing_username, service)
+ self._set_password(
+ target,
+ existing_username,
+ existing_pw.value,
+ )
+ self._set_password(service, username, str(password))
+
+ def _set_password(self, target, username, password):
+ credential = dict(
+ Type=win32cred.CRED_TYPE_GENERIC,
+ TargetName=target,
+ UserName=username,
+ CredentialBlob=password,
+ Comment="Stored using python-keyring",
+ Persist=self.persist,
+ )
+ win32cred.CredWrite(credential, 0)
+
+ def delete_password(self, service, username):
+ compound = self._compound_name(username, service)
+ deleted = False
+ for target in service, compound:
+ existing_pw = self._get_password(target)
+ if existing_pw and existing_pw['UserName'] == username:
+ deleted = True
+ self._delete_password(target)
+ if not deleted:
+ raise PasswordDeleteError(service)
+
+ def _delete_password(self, target):
+ try:
+ win32cred.CredDelete(Type=win32cred.CRED_TYPE_GENERIC, TargetName=target)
+ except pywintypes.error as e:
+ if e.winerror == 1168 and e.funcname == 'CredDelete': # not found
+ return
+ raise
+
+ def get_credential(self, service, username):
+ res = None
+ # get the credentials associated with the provided username
+ if username:
+ res = self._get_password(self._compound_name(username, service))
+ # get any first password under the service name
+ if not res:
+ res = self._get_password(service)
+ if not res:
+ return None
+ return SimpleCredential(res['UserName'], res.value)
diff --git a/src/pip/_vendor/keyring/backends/__init__.py b/src/pip/_vendor/keyring/backends/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/src/pip/_vendor/keyring/backends/chainer.py b/src/pip/_vendor/keyring/backends/chainer.py
new file mode 100644
index 00000000000..e21da24f8e4
--- /dev/null
+++ b/src/pip/_vendor/keyring/backends/chainer.py
@@ -0,0 +1,73 @@
+"""
+Keyring Chainer - iterates over other viable backends to
+discover passwords in each.
+"""
+
+from .. import backend
+from ..util import properties
+from . import fail
+
+
+class ChainerBackend(backend.KeyringBackend):
+ """
+ >>> ChainerBackend()
+
+ """
+
+ # override viability as 'priority' cannot be determined
+ # until other backends have been constructed
+ viable = True
+
+ @properties.ClassProperty
+ @classmethod
+ def priority(cls):
+ """
+ If there are backends to chain, high priority
+ Otherwise very low priority since our operation when empty
+ is the same as null.
+ """
+ return 10 if len(cls.backends) > 1 else (fail.Keyring.priority - 1)
+
+ @properties.ClassProperty
+ @classmethod
+ def backends(cls):
+ """
+ Discover all keyrings for chaining.
+ """
+
+ def allow(keyring):
+ limit = backend._limit or bool
+ return (
+ not isinstance(keyring, ChainerBackend)
+ and limit(keyring)
+ and keyring.priority > 0
+ )
+
+ allowed = filter(allow, backend.get_all_keyring())
+ return sorted(allowed, key=backend.by_priority, reverse=True)
+
+ def get_password(self, service, username):
+ for keyring in self.backends:
+ password = keyring.get_password(service, username)
+ if password is not None:
+ return password
+
+ def set_password(self, service, username, password):
+ for keyring in self.backends:
+ try:
+ return keyring.set_password(service, username, password)
+ except NotImplementedError:
+ pass
+
+ def delete_password(self, service, username):
+ for keyring in self.backends:
+ try:
+ return keyring.delete_password(service, username)
+ except NotImplementedError:
+ pass
+
+ def get_credential(self, service, username):
+ for keyring in self.backends:
+ credential = keyring.get_credential(service, username)
+ if credential is not None:
+ return credential
diff --git a/src/pip/_vendor/keyring/backends/fail.py b/src/pip/_vendor/keyring/backends/fail.py
new file mode 100644
index 00000000000..179717a94cb
--- /dev/null
+++ b/src/pip/_vendor/keyring/backends/fail.py
@@ -0,0 +1,27 @@
+from ..backend import KeyringBackend
+from ..errors import NoKeyringError
+
+
+class Keyring(KeyringBackend):
+ """
+ Keyring that raises error on every operation.
+
+ >>> kr = Keyring()
+ >>> kr.get_password('svc', 'user')
+ Traceback (most recent call last):
+ ...
+ keyring.errors.NoKeyringError: ...No recommended backend...
+ """
+
+ priority = 0
+
+ def get_password(self, service, username, password=None):
+ msg = (
+ "No recommended backend was available. Install a recommended 3rd "
+ "party backend package; or, install the keyrings.alt package if "
+ "you want to use the non-recommended backends. See "
+ "https://pypi.org/project/keyring for details."
+ )
+ raise NoKeyringError(msg)
+
+ set_password = delete_password = get_password # type: ignore
diff --git a/src/pip/_vendor/keyring/backends/kwallet.py b/src/pip/_vendor/keyring/backends/kwallet.py
new file mode 100644
index 00000000000..bd4e448a507
--- /dev/null
+++ b/src/pip/_vendor/keyring/backends/kwallet.py
@@ -0,0 +1,167 @@
+import sys
+import os
+import contextlib
+
+from ..backend import KeyringBackend
+from ..credentials import SimpleCredential
+from ..errors import PasswordDeleteError
+from ..errors import PasswordSetError, InitError, KeyringLocked
+from ..util import properties
+
+try:
+ import dbus
+ from dbus.mainloop.glib import DBusGMainLoop
+except ImportError:
+ pass
+except AttributeError:
+ # See https://github.com/jaraco/keyring/issues/296
+ pass
+
+
+def _id_from_argv():
+ """
+ Safely infer an app id from sys.argv.
+ """
+ allowed = AttributeError, IndexError, TypeError
+ with contextlib.suppress(allowed):
+ return sys.argv[0]
+
+
+class DBusKeyring(KeyringBackend):
+ """
+ KDE KWallet 5 via D-Bus
+ """
+
+ appid = _id_from_argv() or 'Python keyring library'
+ wallet = None
+ bus_name = 'org.kde.kwalletd5'
+ object_path = '/modules/kwalletd5'
+
+ @properties.ClassProperty
+ @classmethod
+ def priority(cls):
+ if 'dbus' not in globals():
+ raise RuntimeError('python-dbus not installed')
+ try:
+ bus = dbus.SessionBus(mainloop=DBusGMainLoop())
+ except dbus.DBusException as exc:
+ raise RuntimeError(exc.get_dbus_message())
+ if not (
+ bus.name_has_owner(cls.bus_name)
+ or cls.bus_name in bus.list_activatable_names()
+ ):
+ raise RuntimeError(
+ "The KWallet daemon is neither running nor activatable through D-Bus"
+ )
+ if "KDE" in os.getenv("XDG_CURRENT_DESKTOP", "").split(":"):
+ return 5.1
+ return 4.9
+
+ def __init__(self, *arg, **kw):
+ super().__init__(*arg, **kw)
+ self.handle = -1
+
+ def _migrate(self, service):
+ old_folder = 'Python'
+ entry_list = []
+ if self.iface.hasFolder(self.handle, old_folder, self.appid):
+ entry_list = self.iface.readPasswordList(
+ self.handle, old_folder, '*@*', self.appid
+ )
+
+ for entry in entry_list.items():
+ key = entry[0]
+ password = entry[1]
+
+ username, service = key.rsplit('@', 1)
+ ret = self.iface.writePassword(
+ self.handle, service, username, password, self.appid
+ )
+ if ret == 0:
+ self.iface.removeEntry(self.handle, old_folder, key, self.appid)
+
+ entry_list = self.iface.readPasswordList(
+ self.handle, old_folder, '*', self.appid
+ )
+ if not entry_list:
+ self.iface.removeFolder(self.handle, old_folder, self.appid)
+
+ def connected(self, service):
+ if self.handle >= 0:
+ if self.iface.isOpen(self.handle):
+ return True
+
+ bus = dbus.SessionBus(mainloop=DBusGMainLoop())
+ wId = 0
+ try:
+ remote_obj = bus.get_object(self.bus_name, self.object_path)
+ self.iface = dbus.Interface(remote_obj, 'org.kde.KWallet')
+ self.handle = self.iface.open(self.iface.networkWallet(), wId, self.appid)
+ except dbus.DBusException as e:
+ raise InitError('Failed to open keyring: %s.' % e)
+
+ if self.handle < 0:
+ return False
+ self._migrate(service)
+ return True
+
+ def get_password(self, service, username):
+ """Get password of the username for the service"""
+ if not self.connected(service):
+ # the user pressed "cancel" when prompted to unlock their keyring.
+ raise KeyringLocked("Failed to unlock the keyring!")
+ if not self.iface.hasEntry(self.handle, service, username, self.appid):
+ return None
+ password = self.iface.readPassword(self.handle, service, username, self.appid)
+ return str(password)
+
+ def get_credential(self, service, username):
+ """Gets the first username and password for a service.
+ Returns a Credential instance
+
+ The username can be omitted, but if there is one, it will forward to
+ get_password.
+ Otherwise, it will return the first username and password combo that it finds.
+ """
+ if username is not None:
+ return super().get_credential(service, username)
+
+ if not self.connected(service):
+ # the user pressed "cancel" when prompted to unlock their keyring.
+ raise KeyringLocked("Failed to unlock the keyring!")
+
+ for username in self.iface.entryList(self.handle, service, self.appid):
+ password = self.iface.readPassword(
+ self.handle, service, username, self.appid
+ )
+ return SimpleCredential(str(username), str(password))
+
+ def set_password(self, service, username, password):
+ """Set password for the username of the service"""
+ if not self.connected(service):
+ # the user pressed "cancel" when prompted to unlock their keyring.
+ raise PasswordSetError("Cancelled by user")
+ self.iface.writePassword(self.handle, service, username, password, self.appid)
+
+ def delete_password(self, service, username):
+ """Delete the password for the username of the service."""
+ if not self.connected(service):
+ # the user pressed "cancel" when prompted to unlock their keyring.
+ raise PasswordDeleteError("Cancelled by user")
+ if not self.iface.hasEntry(self.handle, service, username, self.appid):
+ raise PasswordDeleteError("Password not found")
+ self.iface.removeEntry(self.handle, service, username, self.appid)
+
+
+class DBusKeyringKWallet4(DBusKeyring):
+ """
+ KDE KWallet 4 via D-Bus
+ """
+
+ bus_name = 'org.kde.kwalletd'
+ object_path = '/modules/kwalletd'
+
+ @properties.ClassProperty
+ @classmethod
+ def priority(cls):
+ return super().priority - 1
diff --git a/src/pip/_vendor/keyring/backends/libsecret.py b/src/pip/_vendor/keyring/backends/libsecret.py
new file mode 100644
index 00000000000..5581352cc11
--- /dev/null
+++ b/src/pip/_vendor/keyring/backends/libsecret.py
@@ -0,0 +1,153 @@
+import logging
+
+from .. import backend
+from ..util import properties
+from ..backend import KeyringBackend
+from ..credentials import SimpleCredential
+from ..errors import (
+ PasswordDeleteError,
+ PasswordSetError,
+ ExceptionRaisedContext,
+ KeyringLocked,
+)
+
+available = False
+try:
+ import gi
+ from gi.repository import Gio
+ from gi.repository import GLib
+
+ gi.require_version('Secret', '1')
+ from gi.repository import Secret
+
+ available = True
+except (AttributeError, ImportError, ValueError):
+ pass
+
+log = logging.getLogger(__name__)
+
+
+class Keyring(backend.SchemeSelectable, KeyringBackend):
+ """libsecret Keyring"""
+
+ appid = 'Python keyring library'
+
+ @property
+ def schema(self):
+ return Secret.Schema.new(
+ "org.freedesktop.Secret.Generic",
+ Secret.SchemaFlags.NONE,
+ self._query(
+ Secret.SchemaAttributeType.STRING,
+ Secret.SchemaAttributeType.STRING,
+ application=Secret.SchemaAttributeType.STRING,
+ ),
+ )
+
+ @properties.NonDataProperty
+ def collection(self):
+ return Secret.COLLECTION_DEFAULT
+
+ @properties.ClassProperty
+ @classmethod
+ def priority(cls):
+ with ExceptionRaisedContext() as exc:
+ Secret.__name__
+ if exc:
+ raise RuntimeError("libsecret required")
+ return 4.8
+
+ def get_password(self, service, username):
+ """Get password of the username for the service"""
+ attributes = self._query(service, username, application=self.appid)
+ try:
+ items = Secret.password_search_sync(
+ self.schema, attributes, Secret.SearchFlags.UNLOCK, None
+ )
+ except GLib.Error as error:
+ quark = GLib.quark_try_string('g-io-error-quark')
+ if error.matches(quark, Gio.IOErrorEnum.FAILED):
+ raise KeyringLocked('Failed to unlock the item!') from error
+ raise
+ for item in items:
+ try:
+ return item.retrieve_secret_sync().get_text()
+ except GLib.Error as error:
+ quark = GLib.quark_try_string('secret-error')
+ if error.matches(quark, Secret.Error.IS_LOCKED):
+ raise KeyringLocked('Failed to unlock the item!') from error
+ raise
+
+ def set_password(self, service, username, password):
+ """Set password for the username of the service"""
+ attributes = self._query(service, username, application=self.appid)
+ label = "Password for '{}' on '{}'".format(username, service)
+ try:
+ stored = Secret.password_store_sync(
+ self.schema, attributes, self.collection, label, password, None
+ )
+ except GLib.Error as error:
+ quark = GLib.quark_try_string('secret-error')
+ if error.matches(quark, Secret.Error.IS_LOCKED):
+ raise KeyringLocked("Failed to unlock the collection!") from error
+ quark = GLib.quark_try_string('g-io-error-quark')
+ if error.matches(quark, Gio.IOErrorEnum.FAILED):
+ raise KeyringLocked("Failed to unlock the collection!") from error
+ raise
+ if not stored:
+ raise PasswordSetError("Failed to store password!")
+
+ def delete_password(self, service, username):
+ """Delete the stored password (only the first one)"""
+ attributes = self._query(service, username, application=self.appid)
+ try:
+ items = Secret.password_search_sync(
+ self.schema, attributes, Secret.SearchFlags.UNLOCK, None
+ )
+ except GLib.Error as error:
+ quark = GLib.quark_try_string('g-io-error-quark')
+ if error.matches(quark, Gio.IOErrorEnum.FAILED):
+ raise KeyringLocked('Failed to unlock the item!') from error
+ raise
+ for item in items:
+ try:
+ removed = Secret.password_clear_sync(
+ self.schema, item.get_attributes(), None
+ )
+ except GLib.Error as error:
+ quark = GLib.quark_try_string('secret-error')
+ if error.matches(quark, Secret.Error.IS_LOCKED):
+ raise KeyringLocked('Failed to unlock the item!') from error
+ raise
+ return removed
+ raise PasswordDeleteError("No such password!")
+
+ def get_credential(self, service, username):
+ """Get the first username and password for a service.
+ Return a Credential instance
+
+ The username can be omitted, but if there is one, it will use get_password
+ and return a SimpleCredential containing the username and password
+ Otherwise, it will return the first username and password combo that it finds.
+ """
+ query = self._query(service, username)
+ try:
+ items = Secret.password_search_sync(
+ self.schema, query, Secret.SearchFlags.UNLOCK, None
+ )
+ except GLib.Error as error:
+ quark = GLib.quark_try_string('g-io-error-quark')
+ if error.matches(quark, Gio.IOErrorEnum.FAILED):
+ raise KeyringLocked('Failed to unlock the item!') from error
+ raise
+ for item in items:
+ username = item.get_attributes().get("username")
+ try:
+ return SimpleCredential(
+ username, item.retrieve_secret_sync().get_text()
+ )
+ except GLib.Error as error:
+ quark = GLib.quark_try_string('secret-error')
+ if error.matches(quark, Secret.Error.IS_LOCKED):
+ raise KeyringLocked('Failed to unlock the item!') from error
+ raise
diff --git a/src/pip/_vendor/keyring/backends/macOS/__init__.py b/src/pip/_vendor/keyring/backends/macOS/__init__.py
new file mode 100644
index 00000000000..aeea63216ff
--- /dev/null
+++ b/src/pip/_vendor/keyring/backends/macOS/__init__.py
@@ -0,0 +1,77 @@
+import platform
+import os
+import warnings
+
+from ...backend import KeyringBackend
+from ...errors import PasswordSetError
+from ...errors import PasswordDeleteError
+from ...errors import KeyringLocked
+from ...errors import KeyringError
+from ...util import properties
+
+try:
+ from . import api
+except Exception:
+ pass
+
+
+class Keyring(KeyringBackend):
+ """macOS Keychain"""
+
+ keychain = os.environ.get('KEYCHAIN_PATH')
+ "Path to keychain file, overriding default"
+
+ @properties.ClassProperty
+ @classmethod
+ def priority(cls):
+ """
+ Preferred for all macOS environments.
+ """
+ if platform.system() != 'Darwin':
+ raise RuntimeError("macOS required")
+ if 'api' not in globals():
+ raise RuntimeError("Security API unavailable")
+ return 5
+
+ def set_password(self, service, username, password):
+ if username is None:
+ username = ''
+
+ try:
+ api.set_generic_password(self.keychain, service, username, password)
+ except api.KeychainDenied as e:
+ raise KeyringLocked("Can't store password on keychain: " "{}".format(e))
+ except api.Error as e:
+ raise PasswordSetError("Can't store password on keychain: " "{}".format(e))
+
+ def get_password(self, service, username):
+ if username is None:
+ username = ''
+
+ try:
+ return api.find_generic_password(self.keychain, service, username)
+ except api.NotFound:
+ pass
+ except api.KeychainDenied as e:
+ raise KeyringLocked("Can't get password from keychain: " "{}".format(e))
+ except api.Error as e:
+ raise KeyringError("Can't get password from keychain: " "{}".format(e))
+
+ def delete_password(self, service, username):
+ if username is None:
+ username = ''
+
+ try:
+ return api.delete_generic_password(self.keychain, service, username)
+ except api.Error as e:
+ raise PasswordDeleteError(
+ "Can't delete password in keychain: " "{}".format(e)
+ )
+
+ def with_keychain(self, keychain):
+ warnings.warn(
+ "macOS.Keyring.with_keychain is deprecated. Use with_properties instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return self.with_properties(keychain=keychain)
diff --git a/src/pip/_vendor/keyring/backends/macOS/api.py b/src/pip/_vendor/keyring/backends/macOS/api.py
new file mode 100644
index 00000000000..7dd89c17bbf
--- /dev/null
+++ b/src/pip/_vendor/keyring/backends/macOS/api.py
@@ -0,0 +1,172 @@
+import ctypes
+from ctypes import (
+ c_void_p,
+ c_uint32,
+ c_int32,
+ byref,
+)
+from ctypes.util import find_library
+
+
+OS_status = c_int32
+
+
+class error:
+ item_not_found = -25300
+ keychain_denied = -128
+ sec_auth_failed = -25293
+ plist_missing = -67030
+ sec_interaction_not_allowed = -25308
+
+
+_sec = ctypes.CDLL(find_library('Security'))
+_core = ctypes.CDLL(find_library('CoreServices'))
+_found = ctypes.CDLL(find_library('Foundation'))
+
+CFDictionaryCreate = _found.CFDictionaryCreate
+CFDictionaryCreate.restype = c_void_p
+CFDictionaryCreate.argtypes = (
+ c_void_p,
+ c_void_p,
+ c_void_p,
+ c_int32,
+ c_void_p,
+ c_void_p,
+)
+
+CFStringCreateWithCString = _found.CFStringCreateWithCString
+CFStringCreateWithCString.restype = c_void_p
+CFStringCreateWithCString.argtypes = [c_void_p, c_void_p, c_uint32]
+
+CFNumberCreate = _found.CFNumberCreate
+CFNumberCreate.restype = c_void_p
+CFNumberCreate.argtypes = [c_void_p, c_uint32, ctypes.c_void_p]
+
+SecItemAdd = _sec.SecItemAdd
+SecItemAdd.restype = OS_status
+SecItemAdd.argtypes = (c_void_p, c_void_p)
+
+SecItemCopyMatching = _sec.SecItemCopyMatching
+SecItemCopyMatching.restype = OS_status
+SecItemCopyMatching.argtypes = (c_void_p, c_void_p)
+
+SecItemDelete = _sec.SecItemDelete
+SecItemDelete.restype = OS_status
+SecItemDelete.argtypes = (c_void_p,)
+
+CFDataGetBytePtr = _found.CFDataGetBytePtr
+CFDataGetBytePtr.restype = c_void_p
+CFDataGetBytePtr.argtypes = (c_void_p,)
+
+CFDataGetLength = _found.CFDataGetLength
+CFDataGetLength.restype = c_int32
+CFDataGetLength.argtypes = (c_void_p,)
+
+
+def k_(s):
+ return c_void_p.in_dll(_sec, s)
+
+
+def create_cfbool(b):
+ return CFNumberCreate(None, 0x9, ctypes.byref(c_int32(1 if b else 0))) # int32
+
+
+def create_cfstr(s):
+ return CFStringCreateWithCString(
+ None, s.encode('utf8'), 0x08000100
+ ) # kCFStringEncodingUTF8
+
+
+def create_query(**kwargs):
+ return CFDictionaryCreate(
+ None,
+ (c_void_p * len(kwargs))(*[k_(k) for k in kwargs.keys()]),
+ (c_void_p * len(kwargs))(
+ *[create_cfstr(v) if isinstance(v, str) else v for v in kwargs.values()]
+ ),
+ len(kwargs),
+ _found.kCFTypeDictionaryKeyCallBacks,
+ _found.kCFTypeDictionaryValueCallBacks,
+ )
+
+
+def cfstr_to_str(data):
+ return ctypes.string_at(CFDataGetBytePtr(data), CFDataGetLength(data)).decode(
+ 'utf-8'
+ )
+
+
+class Error(Exception):
+ @classmethod
+ def raise_for_status(cls, status):
+ if status == 0:
+ return
+ if status == error.item_not_found:
+ raise NotFound(status, "Item not found")
+ if status == error.keychain_denied:
+ raise KeychainDenied(status, "Keychain Access Denied")
+ if status == error.sec_auth_failed or status == error.plist_missing:
+ raise SecAuthFailure(
+ status,
+ "Security Auth Failure: make sure "
+ "python is signed with codesign util",
+ )
+ raise cls(status, "Unknown Error")
+
+
+class NotFound(Error):
+ pass
+
+
+class KeychainDenied(Error):
+ pass
+
+
+class SecAuthFailure(Error):
+ pass
+
+
+def find_generic_password(kc_name, service, username, not_found_ok=False):
+ q = create_query(
+ kSecClass=k_('kSecClassGenericPassword'),
+ kSecMatchLimit=k_('kSecMatchLimitOne'),
+ kSecAttrService=service,
+ kSecAttrAccount=username,
+ kSecReturnData=create_cfbool(True),
+ )
+
+ data = c_void_p()
+ status = SecItemCopyMatching(q, byref(data))
+
+ if status == error.item_not_found and not_found_ok:
+ return
+
+ Error.raise_for_status(status)
+
+ return cfstr_to_str(data)
+
+
+def set_generic_password(name, service, username, password):
+ if find_generic_password(name, service, username, not_found_ok=True):
+ delete_generic_password(name, service, username)
+
+ q = create_query(
+ kSecClass=k_('kSecClassGenericPassword'),
+ kSecAttrService=service,
+ kSecAttrAccount=username,
+ kSecValueData=password,
+ )
+
+ status = SecItemAdd(q, None)
+ Error.raise_for_status(status)
+
+
+def delete_generic_password(name, service, username):
+ q = create_query(
+ kSecClass=k_('kSecClassGenericPassword'),
+ kSecAttrService=service,
+ kSecAttrAccount=username,
+ )
+
+ status = SecItemDelete(q)
+ Error.raise_for_status(status)
diff --git a/src/pip/_vendor/keyring/backends/null.py b/src/pip/_vendor/keyring/backends/null.py
new file mode 100644
index 00000000000..6525c0ff854
--- /dev/null
+++ b/src/pip/_vendor/keyring/backends/null.py
@@ -0,0 +1,17 @@
+from ..backend import KeyringBackend
+
+
+class Keyring(KeyringBackend):
+ """
+ Keyring that return None on every operation.
+
+ >>> kr = Keyring()
+ >>> kr.get_password('svc', 'user')
+ """
+
+ priority = -1
+
+ def get_password(self, service, username, password=None):
+ pass
+
+ set_password = delete_password = get_password # type: ignore
diff --git a/src/pip/_vendor/keyring/cli.py b/src/pip/_vendor/keyring/cli.py
new file mode 100644
index 00000000000..57b23b538e2
--- /dev/null
+++ b/src/pip/_vendor/keyring/cli.py
@@ -0,0 +1,136 @@
+#!/usr/bin/env python
+"""Simple command line interface to get/set password from a keyring"""
+
+import getpass
+import argparse
+import sys
+
+from . import core
+from . import backend
+from . import set_keyring, get_password, set_password, delete_password
+
+
+class CommandLineTool:
+ def __init__(self):
+ self.parser = argparse.ArgumentParser()
+ self.parser.add_argument(
+ "-p",
+ "--keyring-path",
+ dest="keyring_path",
+ default=None,
+ help="Path to the keyring backend",
+ )
+ self.parser.add_argument(
+ "-b",
+ "--keyring-backend",
+ dest="keyring_backend",
+ default=None,
+ help="Name of the keyring backend",
+ )
+ self.parser.add_argument(
+ "--list-backends",
+ action="store_true",
+ help="List keyring backends and exit",
+ )
+ self.parser.add_argument(
+ "--disable", action="store_true", help="Disable keyring and exit"
+ )
+ self.parser.add_argument(
+ 'operation',
+ help="get|set|del",
+ nargs="?",
+ )
+ self.parser.add_argument(
+ 'service',
+ nargs="?",
+ )
+ self.parser.add_argument(
+ 'username',
+ nargs="?",
+ )
+
+ def run(self, argv):
+ args = self.parser.parse_args(argv)
+ vars(self).update(vars(args))
+
+ if args.list_backends:
+ for k in backend.get_all_keyring():
+ print(k)
+ return
+
+ if args.disable:
+ core.disable()
+ return
+
+ self._check_args()
+ self._load_spec_backend()
+ method = getattr(self, f'do_{self.operation}', self.invalid_op)
+ return method()
+
+ def _check_args(self):
+ if self.operation:
+ if self.service is None or self.username is None:
+ self.parser.error(f"{self.operation} requires service and username")
+
+ def do_get(self):
+ password = get_password(self.service, self.username)
+ if password is None:
+ raise SystemExit(1)
+ print(password)
+
+ def do_set(self):
+ password = self.input_password(
+ f"Password for '{self.username}' in '{self.service}': "
+ )
+ set_password(self.service, self.username, password)
+
+ def do_del(self):
+ delete_password(self.service, self.username)
+
+ def invalid_op(self):
+ self.parser.error("Specify operation 'get', 'del', or 'set'.")
+
+ def _load_spec_backend(self):
+ if self.keyring_backend is None:
+ return
+
+ try:
+ if self.keyring_path:
+ sys.path.insert(0, self.keyring_path)
+ set_keyring(core.load_keyring(self.keyring_backend))
+ except (Exception,) as exc:
+ # Tons of things can go wrong here:
+ # ImportError when using "fjkljfljkl"
+ # AttributeError when using "os.path.bar"
+ # TypeError when using "__builtins__.str"
+ # So, we play on the safe side, and catch everything.
+ self.parser.error(f"Unable to load specified keyring: {exc}")
+
+ def input_password(self, prompt):
+ """Retrieve password from input."""
+ return self.pass_from_pipe() or getpass.getpass(prompt)
+
+ @classmethod
+ def pass_from_pipe(cls):
+ """Return password from pipe if not on TTY, else False."""
+ is_pipe = not sys.stdin.isatty()
+ return is_pipe and cls.strip_last_newline(sys.stdin.read())
+
+ @staticmethod
+ def strip_last_newline(str):
+ """Strip one last newline, if present."""
+ return str[: -str.endswith('\n')]
+
+
+def main(argv=None):
+ """Main command line interface."""
+
+ if argv is None:
+ argv = sys.argv[1:]
+
+ cli = CommandLineTool()
+ return cli.run(argv)
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/src/pip/_vendor/keyring/core.py b/src/pip/_vendor/keyring/core.py
new file mode 100644
index 00000000000..38ca1f081c9
--- /dev/null
+++ b/src/pip/_vendor/keyring/core.py
@@ -0,0 +1,186 @@
+"""
+Core API functions and initialization routines.
+"""
+
+import configparser
+import os
+import sys
+import logging
+import typing
+
+from . import backend, credentials
+from .util import platform_ as platform
+from .backends import fail
+
+
+log = logging.getLogger(__name__)
+
+_keyring_backend = None
+
+
+def set_keyring(keyring):
+ """Set current keyring backend."""
+ global _keyring_backend
+ if not isinstance(keyring, backend.KeyringBackend):
+ raise TypeError("The keyring must be an instance of KeyringBackend")
+ _keyring_backend = keyring
+
+
+def get_keyring() -> backend.KeyringBackend:
+ """Get current keyring backend."""
+ if _keyring_backend is None:
+ init_backend()
+ return typing.cast(backend.KeyringBackend, _keyring_backend)
+
+
+def disable():
+ """
+ Configure the null keyring as the default.
+ """
+ root = platform.config_root()
+ try:
+ os.makedirs(root)
+ except OSError:
+ pass
+ filename = os.path.join(root, 'keyringrc.cfg')
+ if os.path.exists(filename):
+ msg = f"Refusing to overwrite {filename}"
+ raise RuntimeError(msg)
+ with open(filename, 'w') as file:
+ file.write('[backend]\ndefault-keyring=keyring.backends.null.Keyring')
+
+
+def get_password(service_name: str, username: str) -> typing.Optional[str]:
+ """Get password from the specified service."""
+ return get_keyring().get_password(service_name, username)
+
+
+def set_password(service_name: str, username: str, password: str) -> None:
+ """Set password for the user in the specified service."""
+ get_keyring().set_password(service_name, username, password)
+
+
+def delete_password(service_name: str, username: str) -> None:
+ """Delete the password for the user in the specified service."""
+ get_keyring().delete_password(service_name, username)
+
+
+def get_credential(
+ service_name: str, username: typing.Optional[str]
+) -> typing.Optional[credentials.Credential]:
+ """Get a Credential for the specified service."""
+ return get_keyring().get_credential(service_name, username)
+
+
+def recommended(backend):
+ return backend.priority >= 1
+
+
+def init_backend(limit=None):
+ """
+ Load a detected backend.
+ """
+ set_keyring(_detect_backend(limit))
+
+
+def _detect_backend(limit=None):
+ """
+ Return a keyring specified in the config file or infer the best available.
+
+ Limit, if supplied, should be a callable taking a backend and returning
+ True if that backend should be included for consideration.
+ """
+
+ # save the limit for the chainer to honor
+ backend._limit = limit
+ return (
+ load_env()
+ or load_config()
+ or max(
+ # all keyrings passing the limit filter
+ filter(limit, backend.get_all_keyring()),
+ default=fail.Keyring(),
+ key=backend.by_priority,
+ )
+ )
+
+
+def _load_keyring_class(keyring_name):
+ """
+ Load the keyring class indicated by name.
+
+ These popular names are tested to ensure their presence.
+
+ >>> popular_names = [
+ ... 'keyring.backends.Windows.WinVaultKeyring',
+ ... 'keyring.backends.macOS.Keyring',
+ ... 'keyring.backends.kwallet.DBusKeyring',
+ ... 'keyring.backends.SecretService.Keyring',
+ ... ]
+ >>> list(map(_load_keyring_class, popular_names))
+ [...]
+ """
+ module_name, sep, class_name = keyring_name.rpartition('.')
+ __import__(module_name)
+ module = sys.modules[module_name]
+ return getattr(module, class_name)
+
+
+def load_keyring(keyring_name):
+ """
+ Load the specified keyring by name (a fully-qualified name to the
+ keyring, such as 'keyring.backends.file.PlaintextKeyring')
+ """
+ class_ = _load_keyring_class(keyring_name)
+ # invoke the priority to ensure it is viable, or raise a RuntimeError
+ class_.priority
+ return class_()
+
+
+def load_env():
+ """Load a keyring configured in the environment variable."""
+ try:
+ return load_keyring(os.environ['PYTHON_KEYRING_BACKEND'])
+ except KeyError:
+ pass
+
+
+def load_config():
+ """Load a keyring using the config file in the config root."""
+
+ filename = 'keyringrc.cfg'
+
+ keyring_cfg = os.path.join(platform.config_root(), filename)
+
+ if not os.path.exists(keyring_cfg):
+ return
+
+ config = configparser.RawConfigParser()
+ config.read(keyring_cfg)
+ _load_keyring_path(config)
+
+ # load the keyring class name, and then load this keyring
+ try:
+ if config.has_section("backend"):
+ keyring_name = config.get("backend", "default-keyring").strip()
+ else:
+ raise configparser.NoOptionError('backend', 'default-keyring')
+
+ except (configparser.NoOptionError, ImportError):
+ logger = logging.getLogger('keyring')
+ logger.warning(
+ "Keyring config file contains incorrect values.\n"
+ + "Config file: %s" % keyring_cfg
+ )
+ return
+
+ return load_keyring(keyring_name)
+
+
+def _load_keyring_path(config):
+ "load the keyring-path option (if present)"
+ try:
+ path = config.get("backend", "keyring-path").strip()
+ sys.path.insert(0, path)
+ except (configparser.NoOptionError, configparser.NoSectionError):
+ pass
diff --git a/src/pip/_vendor/keyring/credentials.py b/src/pip/_vendor/keyring/credentials.py
new file mode 100644
index 00000000000..933b9d4dd86
--- /dev/null
+++ b/src/pip/_vendor/keyring/credentials.py
@@ -0,0 +1,70 @@
+import os
+import abc
+
+
+class Credential(metaclass=abc.ABCMeta):
+ """Abstract class to manage credentials"""
+
+ @abc.abstractproperty
+ def username(self):
+ return None
+
+ @abc.abstractproperty
+ def password(self):
+ return None
+
+
+class SimpleCredential(Credential):
+ """Simple credentials implementation"""
+
+ def __init__(self, username, password):
+ self._username = username
+ self._password = password
+
+ @property
+ def username(self):
+ return self._username
+
+ @property
+ def password(self):
+ return self._password
+
+
+class EnvironCredential(Credential):
+ """
+ Source credentials from environment variables.
+
+ Actual sourcing is deferred until requested.
+
+ Supports comparison by equality.
+
+ >>> e1 = EnvironCredential('a', 'b')
+ >>> e2 = EnvironCredential('a', 'b')
+ >>> e3 = EnvironCredential('a', 'c')
+ >>> e1 == e2
+ True
+ >>> e2 == e3
+ False
+ """
+
+ def __init__(self, user_env_var, pwd_env_var):
+ self.user_env_var = user_env_var
+ self.pwd_env_var = pwd_env_var
+
+ def __eq__(self, other: object) -> bool:
+ return vars(self) == vars(other)
+
+ def _get_env(self, env_var):
+ """Helper to read an environment variable"""
+ value = os.environ.get(env_var)
+ if not value:
+ raise ValueError('Missing environment variable:%s' % env_var)
+ return value
+
+ @property
+ def username(self):
+ return self._get_env(self.user_env_var)
+
+ @property
+ def password(self):
+ return self._get_env(self.pwd_env_var)
diff --git a/src/pip/_vendor/keyring/devpi_client.py b/src/pip/_vendor/keyring/devpi_client.py
new file mode 100644
index 00000000000..68937edeeb5
--- /dev/null
+++ b/src/pip/_vendor/keyring/devpi_client.py
@@ -0,0 +1,19 @@
+import contextlib
+
+from pluggy import HookimplMarker
+
+from pip._vendor import keyring
+from pip._vendor.keyring.errors import KeyringError
+
+
+hookimpl = HookimplMarker("devpiclient")
+
+
+# https://github.com/jaraco/jaraco.context/blob/c3a9b739/jaraco/context.py#L205
+suppress = type('suppress', (contextlib.suppress, contextlib.ContextDecorator), {})
+
+
+@hookimpl()
+@suppress(KeyringError)
+def devpiclient_get_password(url, username):
+ return keyring.get_password(url, username)
diff --git a/src/pip/_vendor/keyring/errors.py b/src/pip/_vendor/keyring/errors.py
new file mode 100644
index 00000000000..a793c0d3980
--- /dev/null
+++ b/src/pip/_vendor/keyring/errors.py
@@ -0,0 +1,61 @@
+import sys
+
+
+class KeyringError(Exception):
+ """Base class for exceptions in keyring"""
+
+
+class PasswordSetError(KeyringError):
+ """Raised when the password can't be set."""
+
+
+class PasswordDeleteError(KeyringError):
+ """Raised when the password can't be deleted."""
+
+
+class InitError(KeyringError):
+ """Raised when the keyring could not be initialised"""
+
+
+class KeyringLocked(KeyringError):
+ """Raised when the keyring failed unlocking"""
+
+
+class NoKeyringError(KeyringError, RuntimeError):
+ """Raised when there is no keyring backend"""
+
+
+class ExceptionRaisedContext:
+ """
+ An exception-trapping context that indicates whether an exception was
+ raised.
+ """
+
+ def __init__(self, ExpectedException=Exception):
+ self.ExpectedException = ExpectedException
+ self.exc_info = None
+
+ def __enter__(self):
+ self.exc_info = object.__new__(ExceptionInfo)
+ return self.exc_info
+
+ def __exit__(self, *exc_info):
+ self.exc_info.__init__(*exc_info)
+ return self.exc_info.type and issubclass(
+ self.exc_info.type, self.ExpectedException
+ )
+
+
+class ExceptionInfo:
+ def __init__(self, *info):
+ if not info:
+ info = sys.exc_info()
+ self.type, self.value, _ = info
+
+ def __bool__(self):
+ """
+ Return True if an exception occurred
+ """
+ return bool(self.type)
+
+ __nonzero__ = __bool__
diff --git a/src/pip/_vendor/keyring/http.py b/src/pip/_vendor/keyring/http.py
new file mode 100644
index 00000000000..c08fc0d6c41
--- /dev/null
+++ b/src/pip/_vendor/keyring/http.py
@@ -0,0 +1,39 @@
+"""
+urllib2.HTTPPasswordMgr object using the keyring, for use with the
+urllib2.HTTPBasicAuthHandler.
+
+usage:
+ import urllib2
+ handlers = [urllib2.HTTPBasicAuthHandler(PasswordMgr())]
+ urllib2.install_opener(handlers)
+ urllib2.urlopen(...)
+
+This will prompt for a password if one is required and isn't already
+in the keyring. Then, it adds it to the keyring for subsequent use.
+"""
+
+import getpass
+
+from . import get_password, delete_password, set_password
+
+
+class PasswordMgr:
+ def get_username(self, realm, authuri):
+ return getpass.getuser()
+
+ def add_password(self, realm, authuri, password):
+ user = self.get_username(realm, authuri)
+ set_password(realm, user, password)
+
+ def find_user_password(self, realm, authuri):
+ user = self.get_username(realm, authuri)
+ password = get_password(realm, user)
+ if password is None:
+ prompt = 'password for %(user)s@%(realm)s for ' '%(authuri)s: ' % vars()
+ password = getpass.getpass(prompt)
+ set_password(realm, user, password)
+ return user, password
+
+ def clear_password(self, realm, authuri):
+ user = self.get_username(realm, authuri)
+ delete_password(realm, user)
diff --git a/src/pip/_vendor/keyring/py.typed b/src/pip/_vendor/keyring/py.typed
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/src/pip/_vendor/keyring/py310compat.py b/src/pip/_vendor/keyring/py310compat.py
new file mode 100644
index 00000000000..99e4bc5cb62
--- /dev/null
+++ b/src/pip/_vendor/keyring/py310compat.py
@@ -0,0 +1,10 @@
+import sys
+
+
+__all__ = ['metadata']
+
+
+if sys.version_info > (3, 10):
+ import importlib.metadata as metadata
+else:
+ from pip._vendor import importlib_metadata as metadata # type: ignore
diff --git a/src/pip/_vendor/keyring/testing/__init__.py b/src/pip/_vendor/keyring/testing/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/src/pip/_vendor/keyring/testing/backend.py b/src/pip/_vendor/keyring/testing/backend.py
new file mode 100644
index 00000000000..74aeac3ecd3
--- /dev/null
+++ b/src/pip/_vendor/keyring/testing/backend.py
@@ -0,0 +1,172 @@
+# coding: utf-8
+
+"""
+Common test functionality for backends.
+"""
+
+import os
+import string
+
+import pytest
+
+from .util import random_string
+from pip._vendor.keyring import errors
+
+# unicode only characters
+# Sourced from The Quick Brown Fox... Pangrams
+# http://www.columbia.edu/~fdc/utf8/
+UNICODE_CHARS = (
+ "זהכיףסתםלשמועאיךתנצחקרפדעץטובבגן"
+ "ξεσκεπάζωτηνψυχοφθόραβδελυγμία"
+ "Съешьжеещёэтихмягкихфранцузскихбулокдавыпейчаю"
+ "Жълтатадюлябешещастливачепухъткойтоцъфназамръзнакатогьон"
+)
+
+# ensure no-ascii chars slip by - watch your editor!
+assert min(ord(char) for char in UNICODE_CHARS) > 127
+
+
+def is_ascii_printable(s):
+ return all(32 <= ord(c) < 127 for c in s)
+
+
+class BackendBasicTests:
+ """Test for the keyring's basic functions. password_set and password_get"""
+
+ DIFFICULT_CHARS = string.whitespace + string.punctuation
+
+ @pytest.fixture(autouse=True)
+ def _init_properties(self, request):
+ self.keyring = self.init_keyring()
+ self.credentials_created = set()
+ request.addfinalizer(self.cleanup)
+
+ def cleanup(self):
+ for item in self.credentials_created:
+ self.keyring.delete_password(*item)
+
+ def set_password(self, service, username, password):
+ # set the password and save the result so the test runner can clean
+ # up after if necessary.
+ self.keyring.set_password(service, username, password)
+ self.credentials_created.add((service, username))
+
+ def check_set_get(self, service, username, password):
+ keyring = self.keyring
+
+ # for the non-existent password
+ assert keyring.get_password(service, username) is None
+
+ # common usage
+ self.set_password(service, username, password)
+ assert keyring.get_password(service, username) == password
+
+ # for the empty password
+ self.set_password(service, username, "")
+ assert keyring.get_password(service, username) == ""
+
+ def test_password_set_get(self):
+ password = random_string(20)
+ username = random_string(20)
+ service = random_string(20)
+ self.check_set_get(service, username, password)
+
+ def test_difficult_chars(self):
+ password = random_string(20, self.DIFFICULT_CHARS)
+ username = random_string(20, self.DIFFICULT_CHARS)
+ service = random_string(20, self.DIFFICULT_CHARS)
+ self.check_set_get(service, username, password)
+
+ def test_delete_present(self):
+ password = random_string(20, self.DIFFICULT_CHARS)
+ username = random_string(20, self.DIFFICULT_CHARS)
+ service = random_string(20, self.DIFFICULT_CHARS)
+ self.keyring.set_password(service, username, password)
+ self.keyring.delete_password(service, username)
+ assert self.keyring.get_password(service, username) is None
+
+ def test_delete_not_present(self):
+ username = random_string(20, self.DIFFICULT_CHARS)
+ service = random_string(20, self.DIFFICULT_CHARS)
+ with pytest.raises(errors.PasswordDeleteError):
+ self.keyring.delete_password(service, username)
+
+ def test_delete_one_in_group(self):
+ username1 = random_string(20, self.DIFFICULT_CHARS)
+ username2 = random_string(20, self.DIFFICULT_CHARS)
+ password = random_string(20, self.DIFFICULT_CHARS)
+ service = random_string(20, self.DIFFICULT_CHARS)
+ self.keyring.set_password(service, username1, password)
+ self.set_password(service, username2, password)
+ self.keyring.delete_password(service, username1)
+ assert self.keyring.get_password(service, username2) == password
+
+ def test_name_property(self):
+ assert is_ascii_printable(self.keyring.name)
+
+ def test_unicode_chars(self):
+ password = random_string(20, UNICODE_CHARS)
+ username = random_string(20, UNICODE_CHARS)
+ service = random_string(20, UNICODE_CHARS)
+ self.check_set_get(service, username, password)
+
+ def test_unicode_and_ascii_chars(self):
+ source = (
+ random_string(10, UNICODE_CHARS)
+ + random_string(10)
+ + random_string(10, self.DIFFICULT_CHARS)
+ )
+ password = random_string(20, source)
+ username = random_string(20, source)
+ service = random_string(20, source)
+ self.check_set_get(service, username, password)
+
+ def test_different_user(self):
+ """
+ Issue #47 reports that WinVault isn't storing passwords for
+ multiple users. This test exercises that test for each of the
+ backends.
+ """
+
+ keyring = self.keyring
+ self.set_password('service1', 'user1', 'password1')
+ self.set_password('service1', 'user2', 'password2')
+ assert keyring.get_password('service1', 'user1') == 'password1'
+ assert keyring.get_password('service1', 'user2') == 'password2'
+ self.set_password('service2', 'user3', 'password3')
+ assert keyring.get_password('service1', 'user1') == 'password1'
+
+ def test_credential(self):
+ keyring = self.keyring
+
+ cred = keyring.get_credential('service', None)
+ assert cred is None
+
+ self.set_password('service1', 'user1', 'password1')
+ self.set_password('service1', 'user2', 'password2')
+
+ cred = keyring.get_credential('service1', None)
+ assert cred is None or (cred.username, cred.password) in (
+ ('user1', 'password1'),
+ ('user2', 'password2'),
+ )
+
+ cred = keyring.get_credential('service1', 'user2')
+ assert cred is not None
+ assert (cred.username, cred.password) in (
+ ('user1', 'password1'),
+ ('user2', 'password2'),
+ )
+
+ def test_set_properties(self, monkeypatch):
+ env = dict(KEYRING_PROPERTY_FOO_BAR='fizz buzz', OTHER_SETTING='ignore me')
+ monkeypatch.setattr(os, 'environ', env)
+ self.keyring.set_properties_from_env()
+ assert self.keyring.foo_bar == 'fizz buzz'
+
+ def test_new_with_properties(self):
+ alt = self.keyring.with_properties(foo='bar')
+ assert alt is not self.keyring
+ assert alt.foo == 'bar'
+ with pytest.raises(AttributeError):
+ self.keyring.foo
diff --git a/src/pip/_vendor/keyring/testing/util.py b/src/pip/_vendor/keyring/testing/util.py
new file mode 100644
index 00000000000..6a75465d492
--- /dev/null
+++ b/src/pip/_vendor/keyring/testing/util.py
@@ -0,0 +1,71 @@
+import contextlib
+import os
+import sys
+import random
+import string
+
+
+class ImportKiller:
+ "Context manager to make an import of a given name or names fail."
+
+ def __init__(self, *names):
+ self.names = names
+
+ def find_module(self, fullname, path=None):
+ if fullname in self.names:
+ return self
+
+ def load_module(self, fullname):
+ assert fullname in self.names
+ raise ImportError(fullname)
+
+ def __enter__(self):
+ self.original = {}
+ for name in self.names:
+ self.original[name] = sys.modules.pop(name, None)
+ sys.meta_path.insert(0, self)
+
+ def __exit__(self, *args):
+ sys.meta_path.remove(self)
+ for key, value in self.original.items():
+ if value is not None:
+ sys.modules[key] = value
+
+
+@contextlib.contextmanager
+def NoNoneDictMutator(destination, **changes):
+ """Helper context manager to make and unmake changes to a dict.
+
+ A None is not a valid value for the destination, and so means that the
+ associated name should be removed."""
+ original = {}
+ for key, value in changes.items():
+ original[key] = destination.get(key)
+ if value is None:
+ if key in destination:
+ del destination[key]
+ else:
+ destination[key] = value
+ yield
+ for key, value in original.items():
+ if value is None:
+ if key in destination:
+ del destination[key]
+ else:
+ destination[key] = value
+
+
+def Environ(**changes):
+ """A context manager to temporarily change the os.environ"""
+ return NoNoneDictMutator(os.environ, **changes)
+
+
+ALPHABET = string.ascii_letters + string.digits
+
+
+def random_string(k, source=ALPHABET):
+ """Generate a random string with length k"""
+ result = ''
+ for i in range(0, k):
+ result += random.choice(source)
+ return result
diff --git a/src/pip/_vendor/keyring/util/__init__.py b/src/pip/_vendor/keyring/util/__init__.py
new file mode 100644
index 00000000000..14d797de764
--- /dev/null
+++ b/src/pip/_vendor/keyring/util/__init__.py
@@ -0,0 +1,37 @@
+import functools
+
+
+def once(func):
+ """
+ Decorate func so it's only ever called the first time.
+
+ This decorator can ensure that an expensive or non-idempotent function
+ will not be expensive on subsequent calls and is idempotent.
+
+ >>> func = once(lambda a: a+3)
+ >>> func(3)
+ 6
+ >>> func(9)
+ 6
+ >>> func('12')
+ 6
+ """
+
+ def wrapper(*args, **kwargs):
+ if not hasattr(func, 'always_returns'):
+ func.always_returns = func(*args, **kwargs)
+ return func.always_returns
+
+ return functools.wraps(func)(wrapper)
+
+
+def suppress_exceptions(callables, exceptions=Exception):
+ """
+ yield the results of calling each element of callables, suppressing
+ any indicated exceptions.
+ """
+ for callable in callables:
+ try:
+ yield callable()
+ except exceptions:
+ pass
diff --git a/src/pip/_vendor/keyring/util/platform_.py b/src/pip/_vendor/keyring/util/platform_.py
new file mode 100644
index 00000000000..ad103b1b5a0
--- /dev/null
+++ b/src/pip/_vendor/keyring/util/platform_.py
@@ -0,0 +1,68 @@
+import os
+import platform
+import pathlib
+
+
+def _settings_root_XP():
+ return os.path.join(os.environ['USERPROFILE'], 'Local Settings')
+
+
+def _settings_root_Vista():
+ return os.environ.get('LOCALAPPDATA', os.environ.get('ProgramData', '.'))
+
+
+def _data_root_Windows():
+ release, version, csd, ptype = platform.win32_ver()
+ root = _settings_root_XP() if release == 'XP' else _settings_root_Vista()
+ return os.path.join(root, 'Python Keyring')
+
+
+def _data_root_Linux():
+ """
+ Use freedesktop.org Base Dir Specification to determine storage
+ location.
+ """
+ fallback = pathlib.Path.home() / '.local/share'
+ root = os.environ.get('XDG_DATA_HOME', None) or fallback
+ return os.path.join(root, 'python_keyring')
+
+
+_config_root_Windows = _data_root_Windows
+
+
+def _check_old_config_root():
+ """
+ Prior versions of keyring would search for the config
+ in XDG_DATA_HOME, but should probably have been
+ searching for config in XDG_CONFIG_HOME. If the
+ config exists in the former but not in the latter,
+ raise a RuntimeError to force the change.
+ """
+ # disable the check - once is enough and avoids infinite loop
+ globals()['_check_old_config_root'] = lambda: None
+ config_file_new = os.path.join(_config_root_Linux(), 'keyringrc.cfg')
+ config_file_old = os.path.join(_data_root_Linux(), 'keyringrc.cfg')
+ if os.path.isfile(config_file_old) and not os.path.isfile(config_file_new):
+ msg = (
+ "Keyring config exists only in the old location "
+ f"{config_file_old} and should be moved to {config_file_new} "
+ "to work with this version of keyring."
+ )
+ raise RuntimeError(msg)
+
+
+def _config_root_Linux():
+ """
+ Use freedesktop.org Base Dir Specification to determine config
+ location.
+ """
+ _check_old_config_root()
+ fallback = pathlib.Path.home() / '.config'
+ key = 'XDG_CONFIG_HOME'
+ root = os.environ.get(key, None) or fallback
+ return os.path.join(root, 'python_keyring')
+
+
+# by default, use Unix convention
+data_root = globals().get('_data_root_' + platform.system(), _data_root_Linux)
+config_root = globals().get('_config_root_' + platform.system(), _config_root_Linux)
diff --git a/src/pip/_vendor/keyring/util/properties.py b/src/pip/_vendor/keyring/util/properties.py
new file mode 100644
index 00000000000..947edb70f62
--- /dev/null
+++ b/src/pip/_vendor/keyring/util/properties.py
@@ -0,0 +1,57 @@
+from collections import abc
+
+
+class ClassProperty(property):
+ """
+ An implementation of a property callable on a class. Used to decorate a
+ classmethod but to then treat it like a property.
+
+ Example:
+
+ >>> class MyClass:
+ ... @ClassProperty
+ ... @classmethod
+ ... def skillz(cls):
+ ... return cls.__name__.startswith('My')
+ >>> MyClass.skillz
+ True
+ >>> class YourClass(MyClass): pass
+ >>> YourClass.skillz
+ False
+ """
+
+ def __get__(self, cls, owner):
+ return self.fget.__get__(None, owner)()
+
+
+# borrowed from jaraco.util.dictlib
+
+
+class NonDataProperty:
+ """Much like the property builtin, but only implements __get__,
+ making it a non-data property, and can be subsequently reset.
+
+ See http://users.rcn.com/python/download/Descriptor.htm for more
+ information.
+
+ >>> class X:
+ ... @NonDataProperty
+ ... def foo(self):
+ ... return 3
+ >>> x = X()
+ >>> x.foo
+ 3
+ >>> x.foo = 4
+ >>> x.foo
+ 4
+ """
+
+ def __init__(self, fget):
+ assert fget is not None, "fget cannot be none"
+ assert isinstance(fget, abc.Callable), "fget must be callable"
+ self.fget = fget
+
+ def __get__(self, obj, objtype=None):
+ if obj is None:
+ return self
+ return self.fget(obj)
diff --git a/src/pip/_vendor/keyring_subprocess.LICENSE b/src/pip/_vendor/keyring_subprocess.LICENSE
new file mode 100644
index 00000000000..9c60d5a6e43
--- /dev/null
+++ b/src/pip/_vendor/keyring_subprocess.LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022 Dos Moonen
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/src/pip/_vendor/keyring_subprocess.pyi b/src/pip/_vendor/keyring_subprocess.pyi
new file mode 100644
index 00000000000..9c32fdfa186
--- /dev/null
+++ b/src/pip/_vendor/keyring_subprocess.pyi
@@ -0,0 +1 @@
+from keyring_subprocess import *
\ No newline at end of file
diff --git a/src/pip/_vendor/keyring_subprocess/LICENSE b/src/pip/_vendor/keyring_subprocess/LICENSE
new file mode 100644
index 00000000000..b3984aa983e
--- /dev/null
+++ b/src/pip/_vendor/keyring_subprocess/LICENSE
@@ -0,0 +1,19 @@
+Copyright Jason R. Coombs
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to
+deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+sell copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+IN THE SOFTWARE.
diff --git a/src/pip/_vendor/keyring_subprocess/__init__.py b/src/pip/_vendor/keyring_subprocess/__init__.py
new file mode 100644
index 00000000000..af126228ad5
--- /dev/null
+++ b/src/pip/_vendor/keyring_subprocess/__init__.py
@@ -0,0 +1,5 @@
+__version__ = "0.11.0" # poetry-dynamic-versioning substitutes this
+
+from ._internal import *
+
+__all__ = ["backend"]
diff --git a/src/pip/_vendor/keyring_subprocess/_internal/__init__.py b/src/pip/_vendor/keyring_subprocess/_internal/__init__.py
new file mode 100644
index 00000000000..65afeb12efe
--- /dev/null
+++ b/src/pip/_vendor/keyring_subprocess/_internal/__init__.py
@@ -0,0 +1,17 @@
+def sitecustomize() -> None:
+ import sys
+ from ._loader import KeyringSubprocessFinder
+
+ sys.meta_path.append(KeyringSubprocessFinder())
+
+ try:
+ from importlib import import_module
+
+ # if keyring-subprocess is vendored try to import vendored virtualenv
+ vendor_prefix_parts = list(__name__.split(".")[:-2])
+ vendored_virtualenv = vendor_prefix_parts + ["virtualenv"]
+ vendored_virtualenv = ".".join(vendored_virtualenv)
+ import_module(vendored_virtualenv)
+ from ._seeder import KeyringSubprocessFromAppData
+ except ImportError:
+ pass
diff --git a/src/pip/_vendor/keyring_subprocess/_internal/_loader.py b/src/pip/_vendor/keyring_subprocess/_internal/_loader.py
new file mode 100644
index 00000000000..2340cd4ef8d
--- /dev/null
+++ b/src/pip/_vendor/keyring_subprocess/_internal/_loader.py
@@ -0,0 +1,79 @@
+import importlib.abc
+import os
+import sys
+import types
+from importlib.machinery import ModuleSpec
+from pathlib import Path
+from importlib.abc import Loader
+from importlib.util import spec_from_file_location
+from typing import Sequence, Union, Optional, List
+
+
+class KeyringSubprocessFinder(importlib.abc.MetaPathFinder):
+ @staticmethod
+ def path():
+ return Path(__file__).parent.parent / "_vendor"
+
+ def __init__(self, *args, **kwargs):
+ self._load_vendored_deps = False
+ super().__init__(*args, **kwargs)
+
+ def find_spec(
+ self,
+ fullname: str,
+ paths: Optional[Sequence[Union[bytes, str]]],
+ target: Optional[types.ModuleType] = ...,
+ ) -> Optional[ModuleSpec]:
+ location = self.location(fullname.split("."))
+
+ if not location:
+ return None
+
+ spec = spec_from_file_location(fullname, location)
+ spec.loader = KeyringSubprocessLoader(spec.loader)
+
+ return spec
+
+ def location(self, segments: List[str]) -> Optional[Path]:
+ if segments[0] == "keyring":
+ self._load_vendored_deps = sys.version_info < (3, 10)
+ elif self._load_vendored_deps:
+ pass
+ else:
+ return None
+
+ segments, files = (
+ segments[:-1],
+ [
+ f"{segments[-1]}{os.sep}__init__.py",
+ f"{segments[-1]}.py",
+ ],
+ )
+ location = self.path()
+
+ for segment in segments:
+ location = location / segment
+ if not location.exists():
+ return None
+
+ for file in files:
+ file = location / file
+ if file.exists():
+ return file
+
+ return None
+
+
+class KeyringSubprocessLoader(Loader):
+ def __init__(self, loader):
+ self.loader = loader
+
+ def __getattr__(self, item):
+ return getattr(self.loader, item)
+
+ def exec_module(self, module: types.ModuleType) -> None:
+ self.loader.exec_module(module)
+ if module.__name__ == "keyring":
+ from pip._vendor.keyring.backends.chainer import ChainerBackend
+
+ ChainerBackend()
diff --git a/src/pip/_vendor/keyring_subprocess/_internal/_seeder.py b/src/pip/_vendor/keyring_subprocess/_internal/_seeder.py
new file mode 100644
index 00000000000..1a0ca057be6
--- /dev/null
+++ b/src/pip/_vendor/keyring_subprocess/_internal/_seeder.py
@@ -0,0 +1,149 @@
+"""Extensions for virtualenv Seeders to pre-install keyring-subprocess."""
+import functools
+import re
+import abc
+from functools import update_wrapper
+from pathlib import Path
+
+import virtualenv.seed.wheels.embed
+import virtualenv.seed.wheels.bundle
+from virtualenv.seed.wheels import Version, Wheel
+from virtualenv.seed.embed.via_app_data.via_app_data import FromAppData
+
+BUNDLE_SUPPORT = {
+ "3.11": {
+ "keyring-subprocess": "keyring_subprocess-0.9.0-py3-none-any.whl",
+ },
+ "3.10": {
+ "keyring-subprocess": "keyring_subprocess-0.9.0-py3-none-any.whl",
+ },
+ "3.9": {
+ "keyring-subprocess": "keyring_subprocess-0.9.0-py3-none-any.whl",
+ },
+ "3.8": {
+ "keyring-subprocess": "keyring_subprocess-0.9.0-py3-none-any.whl",
+ },
+ "3.7": {
+ "keyring-subprocess": "keyring_subprocess-0.9.0-py3-none-any.whl",
+ },
+}
+MAX = "3.11"
+
+
+def pep503(name):
+ return re.sub(r"[-_.]+", "-", name).lower()
+
+
+def normalize(name):
+ return pep503(name).replace("-", "_")
+
+
+def _get_embed_wheel(wrapped, distribution: str, for_py_version: str, *args, **kwargs):
+ if normalize(distribution) == normalize("keyring-subprocess"):
+ wheel = (
+ virtualenv.seed.wheels.embed.BUNDLE_SUPPORT.get(for_py_version, {})
+ or virtualenv.seed.wheels.embed.BUNDLE_SUPPORT[MAX]
+ ).get("keyring-subprocess")
+
+ wheel = None if wheel is None else Path(__file__).parent / "wheels" / wheel
+ wheel = None if wheel is None or not wheel.exists() else wheel
+
+ return Wheel.from_path(wheel)
+ else:
+ return wrapped(distribution, for_py_version, *args, **kwargs)
+
+
+_get_embed_wheel = functools.partial(
+ _get_embed_wheel, virtualenv.seed.wheels.embed.get_embed_wheel
+)
+update_wrapper(_get_embed_wheel, virtualenv.seed.wheels.embed.get_embed_wheel)
+virtualenv.seed.wheels.bundle.get_embed_wheel = _get_embed_wheel
+
+for (
+ for_py_version,
+ distribution_to_package,
+) in virtualenv.seed.wheels.embed.BUNDLE_SUPPORT.items():
+ version = tuple(map(int, for_py_version.split(".")))
+ if version >= (3, 7):
+ distribution_to_package["keyring-subprocess"] = (
+ BUNDLE_SUPPORT.get(for_py_version, {}) or BUNDLE_SUPPORT[MAX]
+ ).get("keyring-subprocess")
+
+
+class ParserWrapper:
+ def __init__(self, parser):
+ self.parser = parser
+
+ def __getattr__(self, item):
+ return getattr(self.parser, item)
+
+ def add_argument(self, *args, **kwargs):
+ if "dest" in kwargs and (
+ ("metavar" in kwargs and kwargs["metavar"] == "version")
+ or any(
+ arg for arg in args if isinstance(arg, str) and arg.startswith("--no-")
+ )
+ ):
+ kwargs["dest"] = normalize(kwargs["dest"])
+
+ self.parser.add_argument(*args, **kwargs)
+
+
+class Normalize:
+ def __enter__(self):
+ KeyringSubprocessFromAppData.normalize = True
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ KeyringSubprocessFromAppData.normalize = False
+
+
+class MetaClass(abc.ABCMeta):
+ def __init__(cls, name, bases, namespace):
+ super().__init__(name, bases, namespace)
+ if not hasattr(cls, "normalize"):
+ cls.normalize = False
+
+
+class KeyringSubprocessFromAppData(FromAppData, metaclass=MetaClass):
+ """Mixed in keyring-subprocess into seed packages for app-data seeder."""
+
+ def __init__(self, options):
+ """Add the extra attributes for the extensions."""
+ self.keyring_subprocess_version = options.keyring_subprocess
+ self.no_keyring_subprocess = options.no_keyring_subprocess
+
+ super(KeyringSubprocessFromAppData, self).__init__(options)
+
+ @classmethod
+ def add_parser_arguments(cls, parser, interpreter, app_data):
+ parser = ParserWrapper(parser)
+
+ super(KeyringSubprocessFromAppData, cls).add_parser_arguments(
+ parser, interpreter, app_data
+ )
+
+ @classmethod
+ def distributions(cls):
+ """Return the dictionary of distributions."""
+ distributions = super(KeyringSubprocessFromAppData, cls).distributions()
+ distributions["keyring-subprocess"] = Version.bundle
+
+ if cls.normalize:
+ distributions = {
+ normalize(distribution): version
+ for distribution, version in distributions.items()
+ }
+
+ return distributions
+
+ def distribution_to_versions(self):
+ with Normalize():
+ return super().distribution_to_versions()
+
+ def __str__(self):
+ with Normalize():
+ return super().__str__()
+
+ def __repr__(self):
+ with Normalize():
+ return super().__repr__()
diff --git a/src/pip/_vendor/keyring_subprocess/backend/__init__.py b/src/pip/_vendor/keyring_subprocess/backend/__init__.py
new file mode 100644
index 00000000000..73b2f303f9e
--- /dev/null
+++ b/src/pip/_vendor/keyring_subprocess/backend/__init__.py
@@ -0,0 +1 @@
+from ._subprocess import SubprocessBackend
diff --git a/src/pip/_vendor/keyring_subprocess/backend/_subprocess.py b/src/pip/_vendor/keyring_subprocess/backend/_subprocess.py
new file mode 100644
index 00000000000..d051411b900
--- /dev/null
+++ b/src/pip/_vendor/keyring_subprocess/backend/_subprocess.py
@@ -0,0 +1,186 @@
+import base64
+import json
+import os
+import shutil
+import subprocess
+from typing import Optional
+
+from pip._vendor.keyring import credentials, errors
+from pip._vendor.keyring.util import properties
+from pip._vendor.keyring.backend import KeyringBackend
+from pip._vendor.keyring.backends.chainer import ChainerBackend
+
+EXECUTABLE = "keyring-subprocess"
+SERVICE_NAME = "keyring-subprocess"
+ENV_VAR_RECURSIVE = "KEYRING_SUBPROCESS_RECURSIVE"
+
+
+class SubprocessBackend(KeyringBackend):
+ recursive = False
+
+ @properties.ClassProperty
+ @classmethod
+ def priority(cls):
+ if not shutil.which(EXECUTABLE):
+ raise RuntimeError(f"No {EXECUTABLE} executable found")
+
+ return 9
+
+ @properties.ClassProperty
+ @classmethod
+ def recursive(cls):
+ return bool(os.getenv(ENV_VAR_RECURSIVE))
+
+ def _env(self):
+ env = os.environ.copy()
+ env[ENV_VAR_RECURSIVE] = "1"
+ env[
+ "PYTHON_KEYRING_BACKEND"
+ ] = f"{self.__class__.__module__}.{self.__class__.__name__}"
+
+ return env
+
+ def get_password(self, service: str, username: str) -> Optional[str]:
+ if self.recursive:
+ return self._recursive_get_password(service, username)
+
+ executable = shutil.which(EXECUTABLE)
+ if executable is None:
+ return None
+
+ payload = {
+ "method": "get_password",
+ "service": service,
+ "username": username,
+ }
+ result = self._run_subprocess(executable, "get", payload)
+
+ if result.returncode:
+ return None
+
+ password = result.stdout.splitlines()[-1]
+ return password
+
+ def get_credential(
+ self,
+ service: str,
+ username: Optional[str],
+ ) -> Optional[credentials.Credential]:
+ if self.recursive:
+ return None
+
+ executable = shutil.which(EXECUTABLE)
+ if not self.recursive and executable is None:
+ return None
+
+ payload = {
+ "method": "get_credential",
+ "service": service,
+ "username": username,
+ }
+ result = self._run_subprocess(executable, "get", payload)
+
+ if result.returncode:
+ return None
+
+ credential = json.loads(base64.b64decode(result.stdout.splitlines()[-1]))
+
+ return credentials.SimpleCredential(**credential)
+
+ def _recursive_get_password(self, service: str, username: str) -> Optional[str]:
+ if not self.recursive or service != SERVICE_NAME:
+ return None
+
+ params = json.loads(base64.b64decode(username))
+
+ if params["method"] == "get_credential":
+ return self._recursive_get_credential(params["service"], params["username"])
+
+ return ChainerBackend().get_password(params["service"], params["username"])
+
+ def _run_subprocess(self, executable, operation: str, payload: any):
+ payload = json.dumps(payload)
+ payload = base64.b64encode(payload.encode(encoding="utf-8")).decode(
+ encoding="utf-8"
+ )
+ result = subprocess.run(
+ [executable, operation, SERVICE_NAME, payload],
+ env=self._env(),
+ stdout=subprocess.PIPE,
+ encoding="utf-8",
+ )
+ return result
+
+ def _recursive_get_credential(self, service: str, username: str) -> Optional[str]:
+ if not self.recursive:
+ return None
+
+ credential = ChainerBackend().get_credential(service, username)
+ if not credential:
+ return None
+
+ credential = {
+ "username": credential.username,
+ "password": credential.password,
+ }
+
+ return base64.b64encode(json.dumps(credential).encode(encoding="utf-8")).decode(
+ encoding="utf-8"
+ )
+
+ def set_password(self, service: str, username: str, password: str) -> None:
+ if self.recursive:
+ return self._recursive_set_password(service, username, password)
+
+ executable = shutil.which(EXECUTABLE)
+ if not self.recursive and executable is None:
+ return None
+
+ payload = {
+ "service": service,
+ "username": username,
+ "username": password,
+ }
+ result = self._run_subprocess(executable, "set", payload)
+
+ if result.returncode:
+ raise errors.PasswordSetError(
+ f"Subprocess returned with code {result.returncode}"
+ )
+
+ def _recursive_set_password(self, service: str, username: str) -> Optional[str]:
+ if not self.recursive or service != SERVICE_NAME:
+ return None
+
+ params = json.loads(base64.b64decode(username))
+
+ return ChainerBackend().set_password(
+ params["service"], params["username"], params["password"]
+ )
+
+ def delete_password(self, service: str, username: str) -> None:
+ if self.recursive:
+ return self._recursive_delete_password(service, username)
+
+ executable = shutil.which(EXECUTABLE)
+ if not self.recursive and executable is None:
+ return None
+
+ payload = {
+ "service": service,
+ "username": username,
+ }
+ result = self._run_subprocess(executable, "del", payload)
+
+ if result.returncode:
+ raise errors.PasswordDeleteError(
+ f"Subprocess returned with code {result.returncode}"
+ )
+
+ def _recursive_delete_password(self, service: str, username: str) -> Optional[str]:
+ if not self.recursive or service != SERVICE_NAME:
+ return None
+
+ params = json.loads(base64.b64decode(username))
+
+ return ChainerBackend().delete_password(params["service"], params["username"])
diff --git a/src/pip/_vendor/keyring_subprocess/importlib_metadata.LICENSE b/src/pip/_vendor/keyring_subprocess/importlib_metadata.LICENSE
new file mode 100644
index 00000000000..75b52484ea4
--- /dev/null
+++ b/src/pip/_vendor/keyring_subprocess/importlib_metadata.LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/src/pip/_vendor/keyring_subprocess/keyring_subprocess.LICENSE b/src/pip/_vendor/keyring_subprocess/keyring_subprocess.LICENSE
new file mode 100644
index 00000000000..9c60d5a6e43
--- /dev/null
+++ b/src/pip/_vendor/keyring_subprocess/keyring_subprocess.LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022 Dos Moonen
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/src/pip/_vendor/keyring_subprocess/typing_extensions.LICENSE b/src/pip/_vendor/keyring_subprocess/typing_extensions.LICENSE
new file mode 100644
index 00000000000..583f9f6e617
--- /dev/null
+++ b/src/pip/_vendor/keyring_subprocess/typing_extensions.LICENSE
@@ -0,0 +1,254 @@
+A. HISTORY OF THE SOFTWARE
+==========================
+
+Python was created in the early 1990s by Guido van Rossum at Stichting
+Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands
+as a successor of a language called ABC. Guido remains Python's
+principal author, although it includes many contributions from others.
+
+In 1995, Guido continued his work on Python at the Corporation for
+National Research Initiatives (CNRI, see http://www.cnri.reston.va.us)
+in Reston, Virginia where he released several versions of the
+software.
+
+In May 2000, Guido and the Python core development team moved to
+BeOpen.com to form the BeOpen PythonLabs team. In October of the same
+year, the PythonLabs team moved to Digital Creations (now Zope
+Corporation, see http://www.zope.com). In 2001, the Python Software
+Foundation (PSF, see http://www.python.org/psf/) was formed, a
+non-profit organization created specifically to own Python-related
+Intellectual Property. Zope Corporation is a sponsoring member of
+the PSF.
+
+All Python releases are Open Source (see http://www.opensource.org for
+the Open Source Definition). Historically, most, but not all, Python
+releases have also been GPL-compatible; the table below summarizes
+the various releases.
+
+ Release Derived Year Owner GPL-
+ from compatible? (1)
+
+ 0.9.0 thru 1.2 1991-1995 CWI yes
+ 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes
+ 1.6 1.5.2 2000 CNRI no
+ 2.0 1.6 2000 BeOpen.com no
+ 1.6.1 1.6 2001 CNRI yes (2)
+ 2.1 2.0+1.6.1 2001 PSF no
+ 2.0.1 2.0+1.6.1 2001 PSF yes
+ 2.1.1 2.1+2.0.1 2001 PSF yes
+ 2.1.2 2.1.1 2002 PSF yes
+ 2.1.3 2.1.2 2002 PSF yes
+ 2.2 and above 2.1.1 2001-now PSF yes
+
+Footnotes:
+
+(1) GPL-compatible doesn't mean that we're distributing Python under
+ the GPL. All Python licenses, unlike the GPL, let you distribute
+ a modified version without making your changes open source. The
+ GPL-compatible licenses make it possible to combine Python with
+ other software that is released under the GPL; the others don't.
+
+(2) According to Richard Stallman, 1.6.1 is not GPL-compatible,
+ because its license has a choice of law clause. According to
+ CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1
+ is "not incompatible" with the GPL.
+
+Thanks to the many outside volunteers who have worked under Guido's
+direction to make these releases possible.
+
+
+B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON
+===============================================================
+
+PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
+--------------------------------------------
+
+1. This LICENSE AGREEMENT is between the Python Software Foundation
+("PSF"), and the Individual or Organization ("Licensee") accessing and
+otherwise using this software ("Python") in source or binary form and
+its associated documentation.
+
+2. Subject to the terms and conditions of this License Agreement, PSF hereby
+grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
+analyze, test, perform and/or display publicly, prepare derivative works,
+distribute, and otherwise use Python alone or in any derivative version,
+provided, however, that PSF's License Agreement and PSF's notice of copyright,
+i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
+2011, 2012, 2013, 2014 Python Software Foundation; All Rights Reserved" are
+retained in Python alone or in any derivative version prepared by Licensee.
+
+3. In the event Licensee prepares a derivative work that is based on
+or incorporates Python or any part thereof, and wants to make
+the derivative work available to others as provided herein, then
+Licensee hereby agrees to include in any such work a brief summary of
+the changes made to Python.
+
+4. PSF is making Python available to Licensee on an "AS IS"
+basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
+IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
+DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
+FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
+INFRINGE ANY THIRD PARTY RIGHTS.
+
+5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
+FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
+A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
+OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
+
+6. This License Agreement will automatically terminate upon a material
+breach of its terms and conditions.
+
+7. Nothing in this License Agreement shall be deemed to create any
+relationship of agency, partnership, or joint venture between PSF and
+Licensee. This License Agreement does not grant permission to use PSF
+trademarks or trade name in a trademark sense to endorse or promote
+products or services of Licensee, or any third party.
+
+8. By copying, installing or otherwise using Python, Licensee
+agrees to be bound by the terms and conditions of this License
+Agreement.
+
+
+BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0
+-------------------------------------------
+
+BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1
+
+1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an
+office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the
+Individual or Organization ("Licensee") accessing and otherwise using
+this software in source or binary form and its associated
+documentation ("the Software").
+
+2. Subject to the terms and conditions of this BeOpen Python License
+Agreement, BeOpen hereby grants Licensee a non-exclusive,
+royalty-free, world-wide license to reproduce, analyze, test, perform
+and/or display publicly, prepare derivative works, distribute, and
+otherwise use the Software alone or in any derivative version,
+provided, however, that the BeOpen Python License is retained in the
+Software, alone or in any derivative version prepared by Licensee.
+
+3. BeOpen is making the Software available to Licensee on an "AS IS"
+basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
+IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND
+DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
+FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT
+INFRINGE ANY THIRD PARTY RIGHTS.
+
+4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE
+SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS
+AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY
+DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
+
+5. This License Agreement will automatically terminate upon a material
+breach of its terms and conditions.
+
+6. This License Agreement shall be governed by and interpreted in all
+respects by the law of the State of California, excluding conflict of
+law provisions. Nothing in this License Agreement shall be deemed to
+create any relationship of agency, partnership, or joint venture
+between BeOpen and Licensee. This License Agreement does not grant
+permission to use BeOpen trademarks or trade names in a trademark
+sense to endorse or promote products or services of Licensee, or any
+third party. As an exception, the "BeOpen Python" logos available at
+http://www.pythonlabs.com/logos.html may be used according to the
+permissions granted on that web page.
+
+7. By copying, installing or otherwise using the software, Licensee
+agrees to be bound by the terms and conditions of this License
+Agreement.
+
+
+CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1
+---------------------------------------
+
+1. This LICENSE AGREEMENT is between the Corporation for National
+Research Initiatives, having an office at 1895 Preston White Drive,
+Reston, VA 20191 ("CNRI"), and the Individual or Organization
+("Licensee") accessing and otherwise using Python 1.6.1 software in
+source or binary form and its associated documentation.
+
+2. Subject to the terms and conditions of this License Agreement, CNRI
+hereby grants Licensee a nonexclusive, royalty-free, world-wide
+license to reproduce, analyze, test, perform and/or display publicly,
+prepare derivative works, distribute, and otherwise use Python 1.6.1
+alone or in any derivative version, provided, however, that CNRI's
+License Agreement and CNRI's notice of copyright, i.e., "Copyright (c)
+1995-2001 Corporation for National Research Initiatives; All Rights
+Reserved" are retained in Python 1.6.1 alone or in any derivative
+version prepared by Licensee. Alternately, in lieu of CNRI's License
+Agreement, Licensee may substitute the following text (omitting the
+quotes): "Python 1.6.1 is made available subject to the terms and
+conditions in CNRI's License Agreement. This Agreement together with
+Python 1.6.1 may be located on the Internet using the following
+unique, persistent identifier (known as a handle): 1895.22/1013. This
+Agreement may also be obtained from a proxy server on the Internet
+using the following URL: http://hdl.handle.net/1895.22/1013".
+
+3. In the event Licensee prepares a derivative work that is based on
+or incorporates Python 1.6.1 or any part thereof, and wants to make
+the derivative work available to others as provided herein, then
+Licensee hereby agrees to include in any such work a brief summary of
+the changes made to Python 1.6.1.
+
+4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS"
+basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
+IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND
+DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
+FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT
+INFRINGE ANY THIRD PARTY RIGHTS.
+
+5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
+1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
+A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1,
+OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
+
+6. This License Agreement will automatically terminate upon a material
+breach of its terms and conditions.
+
+7. This License Agreement shall be governed by the federal
+intellectual property law of the United States, including without
+limitation the federal copyright law, and, to the extent such
+U.S. federal law does not apply, by the law of the Commonwealth of
+Virginia, excluding Virginia's conflict of law provisions.
+Notwithstanding the foregoing, with regard to derivative works based
+on Python 1.6.1 that incorporate non-separable material that was
+previously distributed under the GNU General Public License (GPL), the
+law of the Commonwealth of Virginia shall govern this License
+Agreement only as to issues arising under or with respect to
+Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this
+License Agreement shall be deemed to create any relationship of
+agency, partnership, or joint venture between CNRI and Licensee. This
+License Agreement does not grant permission to use CNRI trademarks or
+trade name in a trademark sense to endorse or promote products or
+services of Licensee, or any third party.
+
+8. By clicking on the "ACCEPT" button where indicated, or by copying,
+installing or otherwise using Python 1.6.1, Licensee agrees to be
+bound by the terms and conditions of this License Agreement.
+
+ ACCEPT
+
+
+CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2
+--------------------------------------------------
+
+Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,
+The Netherlands. All rights reserved.
+
+Permission to use, copy, modify, and distribute this software and its
+documentation for any purpose and without fee is hereby granted,
+provided that the above copyright notice appear in all copies and that
+both that copyright notice and this permission notice appear in
+supporting documentation, and that the name of Stichting Mathematisch
+Centrum or CWI not be used in advertising or publicity pertaining to
+distribution of the software without specific, written prior
+permission.
+
+STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO
+THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE
+FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/src/pip/_vendor/keyring_subprocess/zipp.LICENSE b/src/pip/_vendor/keyring_subprocess/zipp.LICENSE
new file mode 100644
index 00000000000..b3984aa983e
--- /dev/null
+++ b/src/pip/_vendor/keyring_subprocess/zipp.LICENSE
@@ -0,0 +1,19 @@
+Copyright Jason R. Coombs
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to
+deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+sell copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+IN THE SOFTWARE.
diff --git a/src/pip/_vendor/pep517/meta.py b/src/pip/_vendor/pep517/meta.py
index d525de5c6c8..93d4066e97f 100644
--- a/src/pip/_vendor/pep517/meta.py
+++ b/src/pip/_vendor/pep517/meta.py
@@ -9,12 +9,12 @@
try:
import importlib.metadata as imp_meta
except ImportError:
- import importlib_metadata as imp_meta
+ from pip._vendor import importlib_metadata as imp_meta
try:
from zipfile import Path
except ImportError:
- from zipp import Path
+ from pip._vendor.zipp import Path
from .envbuild import BuildEnvironment
from .wrappers import Pep517HookCaller, quiet_subprocess_runner
diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt
index 375b6411af6..d728ed1331c 100644
--- a/src/pip/_vendor/vendor.txt
+++ b/src/pip/_vendor/vendor.txt
@@ -21,3 +21,8 @@ six==1.16.0
tenacity==8.0.1
tomli==2.0.1
webencodings==0.5.1
+keyring_subprocess==0.11.0
+ keyring==23.8.2
+ importlib_metadata==4.12
+ zipp==3.8.1
+ typing_extensions==4.3.0
diff --git a/src/pip/_vendor/zipp.LICENSE b/src/pip/_vendor/zipp.LICENSE
new file mode 100644
index 00000000000..353924be0e5
--- /dev/null
+++ b/src/pip/_vendor/zipp.LICENSE
@@ -0,0 +1,19 @@
+Copyright Jason R. Coombs
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to
+deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+sell copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+IN THE SOFTWARE.
diff --git a/src/pip/_vendor/zipp.py b/src/pip/_vendor/zipp.py
new file mode 100644
index 00000000000..52c82a0e044
--- /dev/null
+++ b/src/pip/_vendor/zipp.py
@@ -0,0 +1,312 @@
+import io
+import posixpath
+import zipfile
+import itertools
+import contextlib
+import pathlib
+
+
+__all__ = ['Path']
+
+
+def _parents(path):
+ """
+ Given a path with elements separated by
+ posixpath.sep, generate all parents of that path.
+
+ >>> list(_parents('b/d'))
+ ['b']
+ >>> list(_parents('/b/d/'))
+ ['/b']
+ >>> list(_parents('b/d/f/'))
+ ['b/d', 'b']
+ >>> list(_parents('b'))
+ []
+ >>> list(_parents(''))
+ []
+ """
+ return itertools.islice(_ancestry(path), 1, None)
+
+
+def _ancestry(path):
+ """
+ Given a path with elements separated by
+ posixpath.sep, generate all elements of that path
+
+ >>> list(_ancestry('b/d'))
+ ['b/d', 'b']
+ >>> list(_ancestry('/b/d/'))
+ ['/b/d', '/b']
+ >>> list(_ancestry('b/d/f/'))
+ ['b/d/f', 'b/d', 'b']
+ >>> list(_ancestry('b'))
+ ['b']
+ >>> list(_ancestry(''))
+ []
+ """
+ path = path.rstrip(posixpath.sep)
+ while path and path != posixpath.sep:
+ yield path
+ path, tail = posixpath.split(path)
+
+
+_dedupe = dict.fromkeys
+"""Deduplicate an iterable in original order"""
+
+
+def _difference(minuend, subtrahend):
+ """
+ Return items in minuend not in subtrahend, retaining order
+ with O(1) lookup.
+ """
+ return itertools.filterfalse(set(subtrahend).__contains__, minuend)
+
+
+class CompleteDirs(zipfile.ZipFile):
+ """
+ A ZipFile subclass that ensures that implied directories
+ are always included in the namelist.
+ """
+
+ @staticmethod
+ def _implied_dirs(names):
+ parents = itertools.chain.from_iterable(map(_parents, names))
+ as_dirs = (p + posixpath.sep for p in parents)
+ return _dedupe(_difference(as_dirs, names))
+
+ def namelist(self):
+ names = super(CompleteDirs, self).namelist()
+ return names + list(self._implied_dirs(names))
+
+ def _name_set(self):
+ return set(self.namelist())
+
+ def resolve_dir(self, name):
+ """
+ If the name represents a directory, return that name
+ as a directory (with the trailing slash).
+ """
+ names = self._name_set()
+ dirname = name + '/'
+ dir_match = name not in names and dirname in names
+ return dirname if dir_match else name
+
+ @classmethod
+ def make(cls, source):
+ """
+ Given a source (filename or zipfile), return an
+ appropriate CompleteDirs subclass.
+ """
+ if isinstance(source, CompleteDirs):
+ return source
+
+ if not isinstance(source, zipfile.ZipFile):
+ return cls(source)
+
+ # Only allow for FastLookup when supplied zipfile is read-only
+ if 'r' not in source.mode:
+ cls = CompleteDirs
+
+ source.__class__ = cls
+ return source
+
+
+class FastLookup(CompleteDirs):
+ """
+ ZipFile subclass to ensure implicit
+ dirs exist and are resolved rapidly.
+ """
+
+ def namelist(self):
+ with contextlib.suppress(AttributeError):
+ return self.__names
+ self.__names = super(FastLookup, self).namelist()
+ return self.__names
+
+ def _name_set(self):
+ with contextlib.suppress(AttributeError):
+ return self.__lookup
+ self.__lookup = super(FastLookup, self)._name_set()
+ return self.__lookup
+
+
+class Path:
+ """
+ A pathlib-compatible interface for zip files.
+
+ Consider a zip file with this structure::
+
+ .
+ ├── a.txt
+ └── b
+ ├── c.txt
+ └── d
+ └── e.txt
+
+ >>> data = io.BytesIO()
+ >>> zf = zipfile.ZipFile(data, 'w')
+ >>> zf.writestr('a.txt', 'content of a')
+ >>> zf.writestr('b/c.txt', 'content of c')
+ >>> zf.writestr('b/d/e.txt', 'content of e')
+ >>> zf.filename = 'mem/abcde.zip'
+
+ Path accepts the zipfile object itself or a filename
+
+ >>> root = Path(zf)
+
+ From there, several path operations are available.
+
+ Directory iteration (including the zip file itself):
+
+ >>> a, b = root.iterdir()
+ >>> a
+ Path('mem/abcde.zip', 'a.txt')
+ >>> b
+ Path('mem/abcde.zip', 'b/')
+
+ name property:
+
+ >>> b.name
+ 'b'
+
+ join with divide operator:
+
+ >>> c = b / 'c.txt'
+ >>> c
+ Path('mem/abcde.zip', 'b/c.txt')
+ >>> c.name
+ 'c.txt'
+
+ Read text:
+
+ >>> c.read_text()
+ 'content of c'
+
+ existence:
+
+ >>> c.exists()
+ True
+ >>> (b / 'missing.txt').exists()
+ False
+
+ Coercion to string:
+
+ >>> import os
+ >>> str(c).replace(os.sep, posixpath.sep)
+ 'mem/abcde.zip/b/c.txt'
+
+ At the root, ``name``, ``filename``, and ``parent``
+ resolve to the zipfile. Note these attributes are not
+ valid and will raise a ``ValueError`` if the zipfile
+ has no filename.
+
+ >>> root.name
+ 'abcde.zip'
+ >>> str(root.filename).replace(os.sep, posixpath.sep)
+ 'mem/abcde.zip'
+ >>> str(root.parent)
+ 'mem'
+ """
+
+ __repr = "{self.__class__.__name__}({self.root.filename!r}, {self.at!r})"
+
+ def __init__(self, root, at=""):
+ """
+ Construct a Path from a ZipFile or filename.
+
+ Note: When the source is an existing ZipFile object,
+ its type (__class__) will be mutated to a
+ specialized type. If the caller wishes to retain the
+ original type, the caller should either create a
+ separate ZipFile object or pass a filename.
+ """
+ self.root = FastLookup.make(root)
+ self.at = at
+
+ def open(self, mode='r', *args, pwd=None, **kwargs):
+ """
+ Open this entry as text or binary following the semantics
+ of ``pathlib.Path.open()`` by passing arguments through
+ to io.TextIOWrapper().
+ """
+ if self.is_dir():
+ raise IsADirectoryError(self)
+ zip_mode = mode[0]
+ if not self.exists() and zip_mode == 'r':
+ raise FileNotFoundError(self)
+ stream = self.root.open(self.at, zip_mode, pwd=pwd)
+ if 'b' in mode:
+ if args or kwargs:
+ raise ValueError("encoding args invalid for binary operation")
+ return stream
+ return io.TextIOWrapper(stream, *args, **kwargs)
+
+ @property
+ def name(self):
+ return pathlib.Path(self.at).name or self.filename.name
+
+ @property
+ def suffix(self):
+ return pathlib.Path(self.at).suffix or self.filename.suffix
+
+ @property
+ def suffixes(self):
+ return pathlib.Path(self.at).suffixes or self.filename.suffixes
+
+ @property
+ def stem(self):
+ return pathlib.Path(self.at).stem or self.filename.stem
+
+ @property
+ def filename(self):
+ return pathlib.Path(self.root.filename).joinpath(self.at)
+
+ def read_text(self, *args, **kwargs):
+ with self.open('r', *args, **kwargs) as strm:
+ return strm.read()
+
+ def read_bytes(self):
+ with self.open('rb') as strm:
+ return strm.read()
+
+ def _is_child(self, path):
+ return posixpath.dirname(path.at.rstrip("/")) == self.at.rstrip("/")
+
+ def _next(self, at):
+ return self.__class__(self.root, at)
+
+ def is_dir(self):
+ return not self.at or self.at.endswith("/")
+
+ def is_file(self):
+ return self.exists() and not self.is_dir()
+
+ def exists(self):
+ return self.at in self.root._name_set()
+
+ def iterdir(self):
+ if not self.is_dir():
+ raise ValueError("Can't listdir a file")
+ subs = map(self._next, self.root.namelist())
+ return filter(self._is_child, subs)
+
+ def __str__(self):
+ return posixpath.join(self.root.filename, self.at)
+
+ def __repr__(self):
+ return self.__repr.format(self=self)
+
+ def joinpath(self, *other):
+ next = posixpath.join(self.at, *other)
+ return self._next(self.root.resolve_dir(next))
+
+ __truediv__ = joinpath
+
+ @property
+ def parent(self):
+ if not self.at:
+ return self.filename.parent
+ parent_at = posixpath.dirname(self.at.rstrip('/'))
+ if parent_at:
+ parent_at += '/'
+ return self._next(parent_at)
diff --git a/src/pip/_vendor/zipp.pyi b/src/pip/_vendor/zipp.pyi
new file mode 100644
index 00000000000..509eb279202
--- /dev/null
+++ b/src/pip/_vendor/zipp.pyi
@@ -0,0 +1 @@
+from zipp import *
\ No newline at end of file
diff --git a/tests/conftest.py b/tests/conftest.py
index 44aa56026b6..a72a93b33e5 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -10,6 +10,7 @@
from pathlib import Path
from typing import (
TYPE_CHECKING,
+ AnyStr,
Callable,
Dict,
Iterable,
@@ -514,7 +515,10 @@ def with_wheel(virtualenv: VirtualEnvironment, wheel_install: Path) -> None:
class ScriptFactory(Protocol):
def __call__(
- self, tmpdir: Path, virtualenv: Optional[VirtualEnvironment] = None
+ self,
+ tmpdir: Path,
+ virtualenv: Optional[VirtualEnvironment] = None,
+ environ: Optional[Dict[AnyStr, AnyStr]] = None,
) -> PipTestEnvironment:
...
@@ -528,7 +532,12 @@ def script_factory(
def factory(
tmpdir: Path,
virtualenv: Optional[VirtualEnvironment] = None,
+ environ: Optional[Dict[AnyStr, AnyStr]] = None,
) -> PipTestEnvironment:
+ kwargs = {}
+ if environ:
+ kwargs["environ"] = environ
+ print(kwargs)
if virtualenv is None:
virtualenv = virtualenv_factory(tmpdir.joinpath("venv"))
return PipTestEnvironment(
@@ -548,6 +557,7 @@ def factory(
pip_expect_warning=deprecated_python,
# Tell the Test Environment if we want to run pip via a zipapp
zipapp=zipapp,
+ **kwargs,
)
return factory
diff --git a/tests/functional/test_install_config.py b/tests/functional/test_install_config.py
index 66043fa1f08..5f41a41b8a0 100644
--- a/tests/functional/test_install_config.py
+++ b/tests/functional/test_install_config.py
@@ -2,10 +2,12 @@
import ssl
import tempfile
import textwrap
+from pathlib import Path
+from typing import Callable
import pytest
-from tests.conftest import CertFactory, MockServer
+from tests.conftest import CertFactory, MockServer, ScriptFactory
from tests.lib import PipTestEnvironment, TestData
from tests.lib.server import (
authorization_response,
@@ -354,14 +356,30 @@ def test_do_not_prompt_for_authentication(
@pytest.mark.parametrize("auth_needed", (True, False))
def test_prompt_for_keyring_if_needed(
- script: PipTestEnvironment,
data: TestData,
cert_factory: CertFactory,
auth_needed: bool,
+ tmpdir: Path,
+ script_factory: ScriptFactory,
+ virtualenv_factory: Callable[[Path], VirtualEnvironment],
) -> None:
- """Test behaviour while installing from a index url
+ """Test behaviour while installing from an index url
requiring authentication and keyring is possible.
"""
+ workspace = tmpdir.joinpath("workspace")
+ keyring_virtualenv = virtualenv_factory(workspace.joinpath("keyring"))
+ keyring_script = script_factory(workspace.joinpath("keyring"), keyring_virtualenv)
+
+ environ = os.environ.copy()
+ environ["PATH"] = str(keyring_script.bin_path) + os.pathsep + environ["PATH"]
+ virtualenv = virtualenv_factory(workspace.joinpath("venv"))
+ script = script_factory(workspace.joinpath("venv"), virtualenv, environ=environ)
+
+ keyring_script.pip(
+ "install",
+ "keyring_subprocess_landmark",
+ )
+
cert_path = cert_factory()
ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
ctx.load_cert_chain(cert_path, cert_path)
@@ -387,16 +405,29 @@ def test_prompt_for_keyring_if_needed(
"""\
import os
import sys
- from collections import namedtuple
+ import keyring
+ from keyring.backend import KeyringBackend
+ from keyring.credentials import SimpleCredential
+
+ class TestBackend(KeyringBackend):
+ priority = 1
+
+ def get_credential(self, url, username):
+ sys.stderr.write("get_credential was called" + os.linesep)
+ return SimpleCredential(username="USERNAME", password="PASSWORD")
- Cred = namedtuple("Cred", ["username", "password"])
+ def get_password(self, url, username):
+ pass
- def get_credential(url, username):
- sys.stderr.write("get_credential was called" + os.linesep)
- return Cred("USERNAME", "PASSWORD")
+ def set_password(self, url, username):
+ pass
"""
)
- keyring_path = script.site_packages_path / "keyring.py"
+ keyring_path = keyring_script.site_packages_path / "keyring_test.py"
+ keyring_path.write_text(keyring_content)
+
+ keyring_content = "import keyring_test;" + os.linesep
+ keyring_path = keyring_path.with_suffix(".pth")
keyring_path.write_text(keyring_content)
with server_running(server):