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):