mirror of https://github.com/pypa/pip
Merge branch 'main' into release/23.0.1
This commit is contained in:
commit
2c68bb14dd
2
NEWS.rst
2
NEWS.rst
|
@ -41,7 +41,7 @@ Features
|
|||
modify the externally managed environment. (`#11381 <https://github.com/pypa/pip/issues/11381>`_)
|
||||
- Enable the use of ``keyring`` found on ``PATH``. This allows ``keyring``
|
||||
installed using ``pipx`` to be used by ``pip``. (`#11589 <https://github.com/pypa/pip/issues/11589>`_)
|
||||
- The inspect and installation report formats are now declared stabled, and their version
|
||||
- The inspect and installation report formats are now declared stable, and their version
|
||||
has been bumped from ``0`` to ``1``. (`#11757 <https://github.com/pypa/pip/issues/11757>`_)
|
||||
|
||||
Bug Fixes
|
||||
|
|
|
@ -19,13 +19,14 @@ and how they are related to pip's various command line options.
|
|||
|
||||
## Configuration Files
|
||||
|
||||
Configuration files can change the default values for command line option.
|
||||
They are written using a standard INI style configuration files.
|
||||
Configuration files can change the default values for command line options.
|
||||
They are written using standard INI style configuration files.
|
||||
|
||||
pip has 3 "levels" of configuration files:
|
||||
pip has 4 "levels" of configuration files:
|
||||
|
||||
- `global`: system-wide configuration file, shared across users.
|
||||
- `user`: per-user configuration file.
|
||||
- `global`: system-wide configuration file, shared across all users.
|
||||
- `user`: per-user configuration file, shared across all environments.
|
||||
- `base` : per-base environment configuration file, shared across all virtualenvs with the same base. (available since pip 23.0)
|
||||
- `site`: per-environment configuration file; i.e. per-virtualenv.
|
||||
|
||||
### Location
|
||||
|
@ -47,6 +48,9 @@ User
|
|||
|
||||
The legacy "per-user" configuration file is also loaded, if it exists: {file}`$HOME/.pip/pip.conf`.
|
||||
|
||||
Base
|
||||
: {file}`\{sys.base_prefix\}/pip.conf`
|
||||
|
||||
Site
|
||||
: {file}`$VIRTUAL_ENV/pip.conf`
|
||||
```
|
||||
|
@ -63,6 +67,9 @@ User
|
|||
|
||||
The legacy "per-user" configuration file is also loaded, if it exists: {file}`$HOME/.pip/pip.conf`.
|
||||
|
||||
Base
|
||||
: {file}`\{sys.base_prefix\}/pip.conf`
|
||||
|
||||
Site
|
||||
: {file}`$VIRTUAL_ENV/pip.conf`
|
||||
```
|
||||
|
@ -81,6 +88,9 @@ User
|
|||
|
||||
The legacy "per-user" configuration file is also loaded, if it exists: {file}`%HOME%\\pip\\pip.ini`
|
||||
|
||||
Base
|
||||
: {file}`\{sys.base_prefix\}\\pip.ini`
|
||||
|
||||
Site
|
||||
: {file}`%VIRTUAL_ENV%\\pip.ini`
|
||||
```
|
||||
|
@ -102,6 +112,7 @@ order:
|
|||
- `PIP_CONFIG_FILE`, if given.
|
||||
- Global
|
||||
- User
|
||||
- Base
|
||||
- Site
|
||||
|
||||
Each file read overrides any values read from previous files, so if the
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Fix grammar by changing "A new release of pip available:" to "A new release of pip is available:" in the notice used for indicating that.
|
|
@ -0,0 +1 @@
|
|||
Ignore PIP_REQUIRE_VIRTUALENV for ``pip index``
|
|
@ -0,0 +1,3 @@
|
|||
Improve handling of isolated build environments on platforms that
|
||||
customize the Python's installation schemes, such as Debian and
|
||||
Homebrew.
|
|
@ -0,0 +1 @@
|
|||
Do not crash in presence of misformatted hash field in ``direct_url.json``.
|
|
@ -0,0 +1 @@
|
|||
Correct the way to decide if keyring is available.
|
|
@ -0,0 +1,2 @@
|
|||
Cross-reference the ``--python`` flag from the ``--prefix`` flag,
|
||||
and mention limitations of ``--prefix`` regarding script installation.
|
|
@ -0,0 +1,2 @@
|
|||
Implement ``--break-system-packages`` to permit installing packages into
|
||||
``EXTERNALLY-MANAGED`` Python installations.
|
|
@ -0,0 +1 @@
|
|||
In the case of virtual environments, configuration files are now also included from the base installation.
|
|
@ -0,0 +1 @@
|
|||
Upgrade resolvelib to 0.9.0
|
13
noxfile.py
13
noxfile.py
|
@ -1,6 +1,7 @@
|
|||
"""Automation using nox.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import os
|
||||
import shutil
|
||||
|
@ -183,7 +184,13 @@ def lint(session: nox.Session) -> None:
|
|||
def vendoring(session: nox.Session) -> None:
|
||||
session.install("vendoring~=1.2.0")
|
||||
|
||||
if "--upgrade" not in session.posargs:
|
||||
parser = argparse.ArgumentParser(prog="nox -s vendoring")
|
||||
parser.add_argument("--upgrade-all", action="store_true")
|
||||
parser.add_argument("--upgrade", action="append", default=[])
|
||||
parser.add_argument("--skip", action="append", default=[])
|
||||
args = parser.parse_args(session.posargs)
|
||||
|
||||
if not (args.upgrade or args.upgrade_all):
|
||||
session.run("vendoring", "sync", "-v")
|
||||
return
|
||||
|
||||
|
@ -199,7 +206,9 @@ def vendoring(session: nox.Session) -> None:
|
|||
|
||||
vendor_txt = Path("src/pip/_vendor/vendor.txt")
|
||||
for name, old_version in pinned_requirements(vendor_txt):
|
||||
if name == "setuptools":
|
||||
if name in args.skip:
|
||||
continue
|
||||
if args.upgrade and name not in args.upgrade:
|
||||
continue
|
||||
|
||||
# update requirements.txt
|
||||
|
|
|
@ -156,7 +156,12 @@ class InstallCommand(RequirementCommand):
|
|||
default=None,
|
||||
help=(
|
||||
"Installation prefix where lib, bin and other top-level "
|
||||
"folders are placed"
|
||||
"folders are placed. Note that the resulting installation may "
|
||||
"contain scripts and other resources which reference the "
|
||||
"Python interpreter of pip, and not that of ``--prefix``. "
|
||||
"See also the ``--python`` option if the intention is to "
|
||||
"install packages into another (possibly pip-free) "
|
||||
"environment."
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
@ -36,12 +36,20 @@ ENV_NAMES_IGNORED = "version", "help"
|
|||
kinds = enum(
|
||||
USER="user", # User Specific
|
||||
GLOBAL="global", # System Wide
|
||||
SITE="site", # [Virtual] Environment Specific
|
||||
BASE="base", # Base environment specific (e.g. for all venvs with the same base)
|
||||
SITE="site", # Environment Specific (e.g. per venv)
|
||||
ENV="env", # from PIP_CONFIG_FILE
|
||||
ENV_VAR="env-var", # from Environment Variables
|
||||
)
|
||||
OVERRIDE_ORDER = kinds.GLOBAL, kinds.USER, kinds.SITE, kinds.ENV, kinds.ENV_VAR
|
||||
VALID_LOAD_ONLY = kinds.USER, kinds.GLOBAL, kinds.SITE
|
||||
OVERRIDE_ORDER = (
|
||||
kinds.GLOBAL,
|
||||
kinds.USER,
|
||||
kinds.BASE,
|
||||
kinds.SITE,
|
||||
kinds.ENV,
|
||||
kinds.ENV_VAR,
|
||||
)
|
||||
VALID_LOAD_ONLY = kinds.USER, kinds.GLOBAL, kinds.BASE, kinds.SITE
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
@ -70,6 +78,7 @@ def get_configuration_files() -> Dict[Kind, List[str]]:
|
|||
os.path.join(path, CONFIG_BASENAME) for path in appdirs.site_config_dirs("pip")
|
||||
]
|
||||
|
||||
base_config_file = os.path.join(sys.base_prefix, CONFIG_BASENAME)
|
||||
site_config_file = os.path.join(sys.prefix, CONFIG_BASENAME)
|
||||
legacy_config_file = os.path.join(
|
||||
os.path.expanduser("~"),
|
||||
|
@ -78,6 +87,7 @@ def get_configuration_files() -> Dict[Kind, List[str]]:
|
|||
)
|
||||
new_config_file = os.path.join(appdirs.user_config_dir("pip"), CONFIG_BASENAME)
|
||||
return {
|
||||
kinds.BASE: [base_config_file],
|
||||
kinds.GLOBAL: global_config_files,
|
||||
kinds.SITE: [site_config_file],
|
||||
kinds.USER: [legacy_config_file, new_config_file],
|
||||
|
@ -344,6 +354,8 @@ class Configuration:
|
|||
# The legacy config file is overridden by the new config file
|
||||
yield kinds.USER, config_files[kinds.USER]
|
||||
|
||||
yield kinds.BASE, config_files[kinds.BASE]
|
||||
|
||||
# finally virtualenv configuration first trumping others
|
||||
yield kinds.SITE, config_files[kinds.SITE]
|
||||
|
||||
|
|
|
@ -39,6 +39,8 @@ class Credentials(NamedTuple):
|
|||
class KeyRingBaseProvider(ABC):
|
||||
"""Keyring base provider interface"""
|
||||
|
||||
has_keyring: bool
|
||||
|
||||
@abstractmethod
|
||||
def get_auth_info(self, url: str, username: Optional[str]) -> Optional[AuthInfo]:
|
||||
...
|
||||
|
@ -51,6 +53,8 @@ class KeyRingBaseProvider(ABC):
|
|||
class KeyRingNullProvider(KeyRingBaseProvider):
|
||||
"""Keyring null provider"""
|
||||
|
||||
has_keyring = False
|
||||
|
||||
def get_auth_info(self, url: str, username: Optional[str]) -> Optional[AuthInfo]:
|
||||
return None
|
||||
|
||||
|
@ -61,6 +65,8 @@ class KeyRingNullProvider(KeyRingBaseProvider):
|
|||
class KeyRingPythonProvider(KeyRingBaseProvider):
|
||||
"""Keyring interface which uses locally imported `keyring`"""
|
||||
|
||||
has_keyring = True
|
||||
|
||||
def __init__(self) -> None:
|
||||
import keyring
|
||||
|
||||
|
@ -97,6 +103,8 @@ class KeyRingCliProvider(KeyRingBaseProvider):
|
|||
PATH.
|
||||
"""
|
||||
|
||||
has_keyring = True
|
||||
|
||||
def __init__(self, cmd: str) -> None:
|
||||
self.keyring = cmd
|
||||
|
||||
|
@ -359,7 +367,7 @@ class MultiDomainBasicAuth(AuthBase):
|
|||
|
||||
# Factored out to allow for easy patching in tests
|
||||
def _should_save_password_to_keyring(self) -> bool:
|
||||
if get_keyring_provider() is None:
|
||||
if not get_keyring_provider().has_keyring:
|
||||
return False
|
||||
return ask("Save credentials to keyring [y/N]: ", ["y", "n"]) == "y"
|
||||
|
||||
|
@ -432,9 +440,7 @@ class MultiDomainBasicAuth(AuthBase):
|
|||
def save_credentials(self, resp: Response, **kwargs: Any) -> None:
|
||||
"""Response callback to save credentials on success."""
|
||||
keyring = get_keyring_provider()
|
||||
assert not isinstance(
|
||||
keyring, KeyRingNullProvider
|
||||
), "should never reach here without keyring"
|
||||
assert keyring.has_keyring, "should never reach here without keyring"
|
||||
|
||||
creds = self._credentials_to_save
|
||||
self._credentials_to_save = None
|
||||
|
|
|
@ -104,7 +104,7 @@ class PipProvider(_ProviderBase):
|
|||
def identify(self, requirement_or_candidate: Union[Requirement, Candidate]) -> str:
|
||||
return requirement_or_candidate.name
|
||||
|
||||
def get_preference( # type: ignore
|
||||
def get_preference(
|
||||
self,
|
||||
identifier: str,
|
||||
resolutions: Mapping[str, Candidate],
|
||||
|
@ -124,14 +124,29 @@ class PipProvider(_ProviderBase):
|
|||
* If equal, prefer if any requirement is "pinned", i.e. contains
|
||||
operator ``===`` or ``==``.
|
||||
* If equal, calculate an approximate "depth" and resolve requirements
|
||||
closer to the user-specified requirements first.
|
||||
closer to the user-specified requirements first. If the depth cannot
|
||||
by determined (eg: due to no matching parents), it is considered
|
||||
infinite.
|
||||
* Order user-specified requirements by the order they are specified.
|
||||
* If equal, prefers "non-free" requirements, i.e. contains at least one
|
||||
operator, such as ``>=`` or ``<``.
|
||||
* If equal, order alphabetically for consistency (helps debuggability).
|
||||
"""
|
||||
lookups = (r.get_candidate_lookup() for r, _ in information[identifier])
|
||||
candidate, ireqs = zip(*lookups)
|
||||
try:
|
||||
next(iter(information[identifier]))
|
||||
except StopIteration:
|
||||
# There is no information for this identifier, so there's no known
|
||||
# candidates.
|
||||
has_information = False
|
||||
else:
|
||||
has_information = True
|
||||
|
||||
if has_information:
|
||||
lookups = (r.get_candidate_lookup() for r, _ in information[identifier])
|
||||
candidate, ireqs = zip(*lookups)
|
||||
else:
|
||||
candidate, ireqs = None, ()
|
||||
|
||||
operators = [
|
||||
specifier.operator
|
||||
for specifier_set in (ireq.specifier for ireq in ireqs if ireq)
|
||||
|
@ -146,11 +161,14 @@ class PipProvider(_ProviderBase):
|
|||
requested_order: Union[int, float] = self._user_requested[identifier]
|
||||
except KeyError:
|
||||
requested_order = math.inf
|
||||
parent_depths = (
|
||||
self._known_depths[parent.name] if parent is not None else 0.0
|
||||
for _, parent in information[identifier]
|
||||
)
|
||||
inferred_depth = min(d for d in parent_depths) + 1.0
|
||||
if has_information:
|
||||
parent_depths = (
|
||||
self._known_depths[parent.name] if parent is not None else 0.0
|
||||
for _, parent in information[identifier]
|
||||
)
|
||||
inferred_depth = min(d for d in parent_depths) + 1.0
|
||||
else:
|
||||
inferred_depth = math.inf
|
||||
else:
|
||||
inferred_depth = 1.0
|
||||
self._known_depths[identifier] = inferred_depth
|
||||
|
|
|
@ -11,9 +11,9 @@ logger = getLogger(__name__)
|
|||
|
||||
class PipReporter(BaseReporter):
|
||||
def __init__(self) -> None:
|
||||
self.backtracks_by_package: DefaultDict[str, int] = defaultdict(int)
|
||||
self.reject_count_by_package: DefaultDict[str, int] = defaultdict(int)
|
||||
|
||||
self._messages_at_backtrack = {
|
||||
self._messages_at_reject_count = {
|
||||
1: (
|
||||
"pip is looking at multiple versions of {package_name} to "
|
||||
"determine which version is compatible with other "
|
||||
|
@ -32,14 +32,14 @@ class PipReporter(BaseReporter):
|
|||
),
|
||||
}
|
||||
|
||||
def backtracking(self, candidate: Candidate) -> None:
|
||||
self.backtracks_by_package[candidate.name] += 1
|
||||
def rejecting_candidate(self, criterion: Any, candidate: Candidate) -> None:
|
||||
self.reject_count_by_package[candidate.name] += 1
|
||||
|
||||
count = self.backtracks_by_package[candidate.name]
|
||||
if count not in self._messages_at_backtrack:
|
||||
count = self.reject_count_by_package[candidate.name]
|
||||
if count not in self._messages_at_reject_count:
|
||||
return
|
||||
|
||||
message = self._messages_at_backtrack[count]
|
||||
message = self._messages_at_reject_count[count]
|
||||
logger.info("INFO: %s", message.format(package_name=candidate.name))
|
||||
|
||||
|
||||
|
@ -61,8 +61,8 @@ class PipDebuggingReporter(BaseReporter):
|
|||
def adding_requirement(self, requirement: Requirement, parent: Candidate) -> None:
|
||||
logger.info("Reporter.adding_requirement(%r, %r)", requirement, parent)
|
||||
|
||||
def backtracking(self, candidate: Candidate) -> None:
|
||||
logger.info("Reporter.backtracking(%r)", candidate)
|
||||
def rejecting_candidate(self, criterion: Any, candidate: Candidate) -> None:
|
||||
logger.info("Reporter.rejecting_candidate(%r, %r)", criterion, candidate)
|
||||
|
||||
def pinning(self, candidate: Candidate) -> None:
|
||||
logger.info("Reporter.pinning(%r)", candidate)
|
||||
|
|
|
@ -11,7 +11,7 @@ __all__ = [
|
|||
"ResolutionTooDeep",
|
||||
]
|
||||
|
||||
__version__ = "0.8.1"
|
||||
__version__ = "0.9.0"
|
||||
|
||||
|
||||
from .providers import AbstractProvider, AbstractResolver
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
from collections.abc import Mapping, Sequence
|
|
@ -1,5 +1,5 @@
|
|||
class AbstractProvider(object):
|
||||
"""Delegate class to provide requirement interface for the resolver."""
|
||||
"""Delegate class to provide the required interface for the resolver."""
|
||||
|
||||
def identify(self, requirement_or_candidate):
|
||||
"""Given a requirement, return an identifier for it.
|
||||
|
@ -24,9 +24,9 @@ class AbstractProvider(object):
|
|||
this group of arguments is.
|
||||
|
||||
:param identifier: An identifier as returned by ``identify()``. This
|
||||
identifies the dependency matches of which should be returned.
|
||||
identifies the dependency matches which should be returned.
|
||||
:param resolutions: Mapping of candidates currently pinned by the
|
||||
resolver. Each key is an identifier, and the value a candidate.
|
||||
resolver. Each key is an identifier, and the value is a candidate.
|
||||
The candidate may conflict with requirements from ``information``.
|
||||
:param candidates: Mapping of each dependency's possible candidates.
|
||||
Each value is an iterator of candidates.
|
||||
|
@ -39,10 +39,10 @@ class AbstractProvider(object):
|
|||
|
||||
* ``requirement`` specifies a requirement contributing to the current
|
||||
list of candidates.
|
||||
* ``parent`` specifies the candidate that provides (dependend on) the
|
||||
* ``parent`` specifies the candidate that provides (depended on) the
|
||||
requirement, or ``None`` to indicate a root requirement.
|
||||
|
||||
The preference could depend on a various of issues, including (not
|
||||
The preference could depend on various issues, including (not
|
||||
necessarily in this order):
|
||||
|
||||
* Is this package pinned in the current resolution result?
|
||||
|
@ -61,7 +61,7 @@ class AbstractProvider(object):
|
|||
raise NotImplementedError
|
||||
|
||||
def find_matches(self, identifier, requirements, incompatibilities):
|
||||
"""Find all possible candidates that satisfy given constraints.
|
||||
"""Find all possible candidates that satisfy the given constraints.
|
||||
|
||||
:param identifier: An identifier as returned by ``identify()``. This
|
||||
identifies the dependency matches of which should be returned.
|
||||
|
@ -92,7 +92,7 @@ class AbstractProvider(object):
|
|||
def is_satisfied_by(self, requirement, candidate):
|
||||
"""Whether the given requirement can be satisfied by a candidate.
|
||||
|
||||
The candidate is guarenteed to have been generated from the
|
||||
The candidate is guaranteed to have been generated from the
|
||||
requirement.
|
||||
|
||||
A boolean should be returned to indicate whether ``candidate`` is a
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
from typing import (
|
||||
Any,
|
||||
Collection,
|
||||
Generic,
|
||||
Iterable,
|
||||
Iterator,
|
||||
Mapping,
|
||||
Optional,
|
||||
Protocol,
|
||||
Sequence,
|
||||
Union,
|
||||
)
|
||||
|
||||
|
@ -25,6 +24,7 @@ class AbstractProvider(Generic[RT, CT, KT]):
|
|||
resolutions: Mapping[KT, CT],
|
||||
candidates: Mapping[KT, Iterator[CT]],
|
||||
information: Mapping[KT, Iterator[RequirementInformation[RT, CT]]],
|
||||
backtrack_causes: Sequence[RequirementInformation[RT, CT]],
|
||||
) -> Preference: ...
|
||||
def find_matches(
|
||||
self,
|
||||
|
|
|
@ -36,7 +36,7 @@ class BaseReporter(object):
|
|||
:param causes: The information on the collision that caused the backtracking.
|
||||
"""
|
||||
|
||||
def backtracking(self, candidate):
|
||||
def rejecting_candidate(self, criterion, candidate):
|
||||
"""Called when rejecting a candidate during backtracking."""
|
||||
|
||||
def pinning(self, candidate):
|
||||
|
|
|
@ -6,6 +6,6 @@ class BaseReporter:
|
|||
def ending_round(self, index: int, state: Any) -> Any: ...
|
||||
def ending(self, state: Any) -> Any: ...
|
||||
def adding_requirement(self, requirement: Any, parent: Any) -> Any: ...
|
||||
def backtracking(self, candidate: Any) -> Any: ...
|
||||
def rejecting_candidate(self, criterion: Any, candidate: Any) -> Any: ...
|
||||
def resolving_conflicts(self, causes: Any) -> Any: ...
|
||||
def pinning(self, candidate: Any) -> Any: ...
|
||||
|
|
|
@ -173,6 +173,31 @@ class Resolution(object):
|
|||
raise RequirementsConflicted(criterion)
|
||||
criteria[identifier] = criterion
|
||||
|
||||
def _remove_information_from_criteria(self, criteria, parents):
|
||||
"""Remove information from parents of criteria.
|
||||
|
||||
Concretely, removes all values from each criterion's ``information``
|
||||
field that have one of ``parents`` as provider of the requirement.
|
||||
|
||||
:param criteria: The criteria to update.
|
||||
:param parents: Identifiers for which to remove information from all criteria.
|
||||
"""
|
||||
if not parents:
|
||||
return
|
||||
for key, criterion in criteria.items():
|
||||
criteria[key] = Criterion(
|
||||
criterion.candidates,
|
||||
[
|
||||
information
|
||||
for information in criterion.information
|
||||
if (
|
||||
information[1] is None
|
||||
or self._p.identify(information[1]) not in parents
|
||||
)
|
||||
],
|
||||
criterion.incompatibilities,
|
||||
)
|
||||
|
||||
def _get_preference(self, name):
|
||||
return self._p.get_preference(
|
||||
identifier=name,
|
||||
|
@ -212,6 +237,7 @@ class Resolution(object):
|
|||
try:
|
||||
criteria = self._get_updated_criteria(candidate)
|
||||
except RequirementsConflicted as e:
|
||||
self._r.rejecting_candidate(e.criterion, candidate)
|
||||
causes.append(e.criterion)
|
||||
continue
|
||||
|
||||
|
@ -281,8 +307,6 @@ class Resolution(object):
|
|||
# Also mark the newly known incompatibility.
|
||||
incompatibilities_from_broken.append((name, [candidate]))
|
||||
|
||||
self._r.backtracking(candidate=candidate)
|
||||
|
||||
# Create a new state from the last known-to-work one, and apply
|
||||
# the previously gathered incompatibility information.
|
||||
def _patch_criteria():
|
||||
|
@ -368,6 +392,11 @@ class Resolution(object):
|
|||
self._r.ending(state=self.state)
|
||||
return self.state
|
||||
|
||||
# keep track of satisfied names to calculate diff after pinning
|
||||
satisfied_names = set(self.state.criteria.keys()) - set(
|
||||
unsatisfied_names
|
||||
)
|
||||
|
||||
# Choose the most preferred unpinned criterion to try.
|
||||
name = min(unsatisfied_names, key=self._get_preference)
|
||||
failure_causes = self._attempt_to_pin_criterion(name)
|
||||
|
@ -384,6 +413,17 @@ class Resolution(object):
|
|||
if not success:
|
||||
raise ResolutionImpossible(self.state.backtrack_causes)
|
||||
else:
|
||||
# discard as information sources any invalidated names
|
||||
# (unsatisfied names that were previously satisfied)
|
||||
newly_unsatisfied_names = {
|
||||
key
|
||||
for key, criterion in self.state.criteria.items()
|
||||
if key in satisfied_names
|
||||
and not self._is_current_pin_satisfying(key, criterion)
|
||||
}
|
||||
self._remove_information_from_criteria(
|
||||
self.state.criteria, newly_unsatisfied_names
|
||||
)
|
||||
# Pinning was successful. Push a new state to do another pin.
|
||||
self._push_new_state()
|
||||
|
||||
|
|
|
@ -55,6 +55,18 @@ class ResolutionImpossible(ResolutionError, Generic[RT, CT]):
|
|||
class ResolutionTooDeep(ResolutionError):
|
||||
round_count: int
|
||||
|
||||
# This should be a NamedTuple, but Python 3.6 has a bug that prevents it.
|
||||
# https://stackoverflow.com/a/50531189/1376863
|
||||
class State(tuple, Generic[RT, CT, KT]):
|
||||
mapping: Mapping[KT, CT]
|
||||
criteria: Mapping[KT, Criterion[RT, CT, KT]]
|
||||
backtrack_causes: Collection[RequirementInformation[RT, CT]]
|
||||
|
||||
class Resolution(Generic[RT, CT, KT]):
|
||||
def resolve(
|
||||
self, requirements: Iterable[RT], max_rounds: int
|
||||
) -> State[RT, CT, KT]: ...
|
||||
|
||||
class Result(Generic[RT, CT, KT]):
|
||||
mapping: Mapping[KT, CT]
|
||||
graph: DirectedGraph[Optional[KT]]
|
||||
|
|
|
@ -117,13 +117,14 @@ class _FactoryIterableView(object):
|
|||
|
||||
def __init__(self, factory):
|
||||
self._factory = factory
|
||||
self._iterable = None
|
||||
|
||||
def __repr__(self):
|
||||
return "{}({})".format(type(self).__name__, list(self._factory()))
|
||||
return "{}({})".format(type(self).__name__, list(self))
|
||||
|
||||
def __bool__(self):
|
||||
try:
|
||||
next(self._factory())
|
||||
next(iter(self))
|
||||
except StopIteration:
|
||||
return False
|
||||
return True
|
||||
|
@ -131,7 +132,11 @@ class _FactoryIterableView(object):
|
|||
__nonzero__ = __bool__ # XXX: Python 2.
|
||||
|
||||
def __iter__(self):
|
||||
return self._factory()
|
||||
iterable = (
|
||||
self._factory() if self._iterable is None else self._iterable
|
||||
)
|
||||
self._iterable, current = itertools.tee(iterable)
|
||||
return current
|
||||
|
||||
|
||||
class _SequenceIterableView(object):
|
||||
|
|
|
@ -16,7 +16,7 @@ RT = TypeVar("RT") # Requirement.
|
|||
CT = TypeVar("CT") # Candidate.
|
||||
_T = TypeVar("_T")
|
||||
|
||||
Matches = Union[Iterable[CT], Callable[[], Iterator[CT]]]
|
||||
Matches = Union[Iterable[CT], Callable[[], Iterable[CT]]]
|
||||
|
||||
class IteratorMapping(Mapping[KT, _T], metaclass=ABCMeta):
|
||||
pass
|
||||
|
|
|
@ -15,7 +15,7 @@ requests==2.28.2
|
|||
rich==12.6.0
|
||||
pygments==2.13.0
|
||||
typing_extensions==4.4.0
|
||||
resolvelib==0.8.1
|
||||
resolvelib==0.9.0
|
||||
setuptools==44.0.0
|
||||
six==1.16.0
|
||||
tenacity==8.1.0
|
||||
|
|
|
@ -24,12 +24,18 @@ class TestConfigurationLoading(ConfigurationMixin):
|
|||
self.configuration.load()
|
||||
assert self.configuration.get_value("test.hello") == "2"
|
||||
|
||||
def test_site_loading(self) -> None:
|
||||
self.patch_configuration(kinds.SITE, {"test.hello": "3"})
|
||||
def test_base_loading(self) -> None:
|
||||
self.patch_configuration(kinds.BASE, {"test.hello": "3"})
|
||||
|
||||
self.configuration.load()
|
||||
assert self.configuration.get_value("test.hello") == "3"
|
||||
|
||||
def test_site_loading(self) -> None:
|
||||
self.patch_configuration(kinds.SITE, {"test.hello": "4"})
|
||||
|
||||
self.configuration.load()
|
||||
assert self.configuration.get_value("test.hello") == "4"
|
||||
|
||||
def test_environment_config_loading(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
contents = """
|
||||
[test]
|
||||
|
@ -107,6 +113,15 @@ class TestConfigurationLoading(ConfigurationMixin):
|
|||
with pytest.raises(ConfigurationError, match=pat):
|
||||
self.configuration.get_value("global.index-url")
|
||||
|
||||
def test_overrides_normalization(self) -> None:
|
||||
# Check that normalized names are used in precedence calculations.
|
||||
# Reminder: USER has higher precedence than GLOBAL.
|
||||
self.patch_configuration(kinds.USER, {"test.hello-world": "1"})
|
||||
self.patch_configuration(kinds.GLOBAL, {"test.hello_world": "0"})
|
||||
self.configuration.load()
|
||||
|
||||
assert self.configuration.get_value("test.hello_world") == "1"
|
||||
|
||||
|
||||
class TestConfigurationPrecedence(ConfigurationMixin):
|
||||
# Tests for methods to that determine the order of precedence of
|
||||
|
@ -133,6 +148,13 @@ class TestConfigurationPrecedence(ConfigurationMixin):
|
|||
|
||||
assert self.configuration.get_value("test.hello") == "0"
|
||||
|
||||
def test_site_overides_base(self) -> None:
|
||||
self.patch_configuration(kinds.BASE, {"test.hello": "2"})
|
||||
self.patch_configuration(kinds.SITE, {"test.hello": "1"})
|
||||
self.configuration.load()
|
||||
|
||||
assert self.configuration.get_value("test.hello") == "1"
|
||||
|
||||
def test_site_overides_user(self) -> None:
|
||||
self.patch_configuration(kinds.USER, {"test.hello": "2"})
|
||||
self.patch_configuration(kinds.SITE, {"test.hello": "1"})
|
||||
|
@ -147,6 +169,13 @@ class TestConfigurationPrecedence(ConfigurationMixin):
|
|||
|
||||
assert self.configuration.get_value("test.hello") == "1"
|
||||
|
||||
def test_base_overides_user(self) -> None:
|
||||
self.patch_configuration(kinds.USER, {"test.hello": "2"})
|
||||
self.patch_configuration(kinds.BASE, {"test.hello": "1"})
|
||||
self.configuration.load()
|
||||
|
||||
assert self.configuration.get_value("test.hello") == "1"
|
||||
|
||||
def test_user_overides_global(self) -> None:
|
||||
self.patch_configuration(kinds.GLOBAL, {"test.hello": "3"})
|
||||
self.patch_configuration(kinds.USER, {"test.hello": "2"})
|
||||
|
|
|
@ -588,7 +588,7 @@ class TestOptionsConfigFiles:
|
|||
for _, val in cp.iter_config_files():
|
||||
files.extend(val)
|
||||
|
||||
assert len(files) == 4
|
||||
assert len(files) == 5
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"args, expect",
|
||||
|
|
Loading…
Reference in New Issue