Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade vendored packaging lib #12300

Merged
merged 43 commits into from
May 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
47a8480
Upgrade vendored packaging lib
sbidoul Sep 29, 2023
335f01c
Fix test?
sbidoul Sep 29, 2023
87bda4b
Rename invalid sdists in our test data
sbidoul Sep 29, 2023
3b01221
Fix test following stricter version specifiers parsing
sbidoul Sep 29, 2023
62215ca
Don't use legacy version numbers in tests
sbidoul Sep 29, 2023
9dcd269
Remove now redundant error detection
sbidoul Sep 30, 2023
4d70566
Remove various extras normalization workarounds
sbidoul Sep 29, 2023
5463bba
Fix obvious typo in test wheel generator
sbidoul Oct 1, 2023
92cb9c9
Strip Requires-Dist metadata parsed from METADATA files
sbidoul Oct 1, 2023
04ea0da
Remove vendored pyparsing
sbidoul Oct 1, 2023
372c616
Explicitly keep track of canonical extra names with `pkg_resources`
pradyunsg Oct 6, 2023
e38c2b9
Compare `Requirement` objects rather than requirement strings
pradyunsg Oct 6, 2023
908e913
Do not normalise `iter_provided_extras` from pkg_resources
pradyunsg Oct 6, 2023
5cc540b
Use extras directly from metadata when using `pkg_resources`
pradyunsg Oct 6, 2023
8d22e80
Implement PEP 685 on distribution objects directly
pradyunsg Oct 6, 2023
cec49ea
Add failing test for index with legacy versions
sbidoul Mar 30, 2024
b63e279
Ignore legacy versions in the package finder
sbidoul Mar 30, 2024
587854a
Add failing test for package with invalid specifier in dependencies
sbidoul Mar 30, 2024
bf8b887
Discard candidates with invalid dependencies
sbidoul Apr 6, 2024
c44c6a4
Add failing test for uninstallation of dist with legacy version
sbidoul Apr 7, 2024
73f6744
Allow uninstallation of dist with legacy version
sbidoul Apr 7, 2024
0563132
Refactor test
sbidoul Apr 7, 2024
27807fb
Add failing test for pip list in presence of legacy version
sbidoul Apr 7, 2024
0529c04
Fix pip list in presence of installed legacy versions
sbidoul Apr 7, 2024
93e52cf
Add failing test for pip freeze in presence of legacy version
sbidoul Apr 7, 2024
a28d13a
Fix pip freeze in presence of legacy version
sbidoul Apr 7, 2024
be652aa
Add failing test for pip show of legacy version
sbidoul Apr 7, 2024
8f97eb5
Fix pip show of legacy version
sbidoul Apr 7, 2024
08ae751
Upgrade packaging to 24.0
sbidoul Apr 7, 2024
196d536
Add news
sbidoul Apr 7, 2024
83e77bf
Add test sdist that requires invalid version
sbidoul Apr 13, 2024
b458d3e
Ignore all candidates of a version when one has invalid metadata
sbidoul Apr 13, 2024
350c980
Simplify
sbidoul Apr 13, 2024
f4b821c
Don't create a list when an iterator is sufficient
sbidoul Apr 13, 2024
c50290e
Rename version_str to raw_version for consistency with raw_name
sbidoul Apr 14, 2024
00edcf4
Add failing test for pip show of dist with legacy specifier
sbidoul Apr 21, 2024
f24fa66
Add iterator of raw dependencie (Requires-Dist) to BaseDistribution
sbidoul Apr 21, 2024
d85efce
Fix pip show of dist with legacy dependency specifiers
sbidoul Apr 21, 2024
4052f6a
Clarify warning messages
sbidoul Apr 22, 2024
edc98cd
Clarify test packages README
sbidoul Apr 28, 2024
4bb5085
Add a failing test for upgrading a distribution with an invalid version
sbidoul Apr 28, 2024
c51fb4f
Add a failing test for upgrading a distribution with invalid metadata
sbidoul May 2, 2024
84ad55a
Fix test_show_require_invalid_version with pkg_resources backend
sbidoul May 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions news/12063.removal.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Remove support for legacy versions and dependency specifiers. Packages with non
standard-compliant versions or dependency specifiers are now ignored by the resolver.
Already installed packages with non standard-compliant versions or dependency specifiers
must be uninstalled before upgrading them.
1 change: 1 addition & 0 deletions news/packaging.vendor.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Upgrade packaging to 24.0
2 changes: 1 addition & 1 deletion news/pyparsing.vendor.rst
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Upgrade pyparsing to 3.1.2
Remove pyparsing
2 changes: 0 additions & 2 deletions src/pip/_internal/commands/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from pip._internal.operations.check import (
check_package_set,
create_package_set_from_installed,
warn_legacy_versions_and_specifiers,
)
from pip._internal.utils.misc import write_output

Expand All @@ -22,7 +21,6 @@ class CheckCommand(Command):

def run(self, options: Values, args: List[str]) -> int:
package_set, parsing_probs = create_package_set_from_installed()
warn_legacy_versions_and_specifiers(package_set)
missing, conflicting = check_package_set(package_set)

for project_name in missing:
Expand Down
1 change: 0 additions & 1 deletion src/pip/_internal/commands/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,6 @@ def run(self, options: Values, args: List[str]) -> int:
downloaded.append(req.name)

preparer.prepare_linked_requirements_more(requirement_set.requirements.values())
requirement_set.warn_legacy_versions_and_specifiers()

if downloaded:
write_output("Successfully downloaded %s", " ".join(downloaded))
Expand Down
6 changes: 3 additions & 3 deletions src/pip/_internal/commands/index.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import logging
from optparse import Values
from typing import Any, Iterable, List, Optional, Union
from typing import Any, Iterable, List, Optional

from pip._vendor.packaging.version import LegacyVersion, Version
from pip._vendor.packaging.version import Version

from pip._internal.cli import cmdoptions
from pip._internal.cli.req_command import IndexGroupCommand
Expand Down Expand Up @@ -115,7 +115,7 @@ def get_available_package_versions(self, options: Values, args: List[Any]) -> No
ignore_requires_python=options.ignore_requires_python,
)

versions: Iterable[Union[LegacyVersion, Version]] = (
versions: Iterable[Version] = (
candidate.version for candidate in finder.find_all_candidates(query)
)

Expand Down
3 changes: 0 additions & 3 deletions src/pip/_internal/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,9 +387,6 @@ def run(self, options: Values, args: List[str]) -> int:
json.dump(report.to_dict(), f, indent=2, ensure_ascii=False)

if options.dry_run:
# In non dry-run mode, the legacy versions and specifiers check
# will be done as part of conflict detection.
requirement_set.warn_legacy_versions_and_specifiers()
would_install_items = sorted(
(r.metadata["name"], r.metadata["version"])
for r in requirement_set.requirements_to_install
Expand Down
6 changes: 3 additions & 3 deletions src/pip/_internal/commands/list.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import TYPE_CHECKING, Generator, List, Optional, Sequence, Tuple, cast

from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.packaging.version import Version

from pip._internal.cli import cmdoptions
from pip._internal.cli.req_command import IndexGroupCommand
Expand All @@ -18,7 +19,6 @@
from pip._internal.utils.misc import tabulate, write_output

if TYPE_CHECKING:
from pip._internal.metadata.base import DistributionVersion

class _DistWithLatestInfo(BaseDistribution):
"""Give the distribution object a couple of extra fields.
Expand All @@ -27,7 +27,7 @@ class _DistWithLatestInfo(BaseDistribution):
makes the rest of the code much cleaner.
"""

latest_version: DistributionVersion
latest_version: Version
latest_filetype: str

_ProcessedDists = Sequence[_DistWithLatestInfo]
Expand Down Expand Up @@ -333,7 +333,7 @@ def format_for_columns(
for proj in pkgs:
# if we're working on the 'outdated' list, separate out the
# latest_version and type
row = [proj.raw_name, str(proj.version)]
row = [proj.raw_name, proj.raw_version]

if running_outdated:
row.append(str(proj.latest_version))
Expand Down
22 changes: 15 additions & 7 deletions src/pip/_internal/commands/show.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from optparse import Values
from typing import Generator, Iterable, Iterator, List, NamedTuple, Optional

from pip._vendor.packaging.requirements import InvalidRequirement
from pip._vendor.packaging.utils import canonicalize_name

from pip._internal.cli.base_command import Command
Expand Down Expand Up @@ -100,12 +101,19 @@ def _get_requiring_packages(current_dist: BaseDistribution) -> Iterator[str]:
except KeyError:
continue

requires = sorted(
# Avoid duplicates in requirements (e.g. due to environment markers).
{req.name for req in dist.iter_dependencies()},
key=str.lower,
)
required_by = sorted(_get_requiring_packages(dist), key=str.lower)
try:
requires = sorted(
# Avoid duplicates in requirements (e.g. due to environment markers).
{req.name for req in dist.iter_dependencies()},
key=str.lower,
)
except InvalidRequirement:
requires = sorted(dist.iter_raw_dependencies(), key=str.lower)

try:
required_by = sorted(_get_requiring_packages(dist), key=str.lower)
except InvalidRequirement:
required_by = ["#N/A"]

try:
entry_points_text = dist.read_text("entry_points.txt")
Expand Down Expand Up @@ -139,7 +147,7 @@ def _get_requiring_packages(current_dist: BaseDistribution) -> Iterator[str]:

yield _PackageInfo(
name=dist.raw_name,
version=str(dist.version),
version=dist.raw_version,
location=dist.location or "",
editable_project_location=dist.editable_project_location,
requires=requires,
Expand Down
1 change: 0 additions & 1 deletion src/pip/_internal/commands/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,6 @@ def run(self, options: Values, args: List[str]) -> int:
reqs_to_build.append(req)

preparer.prepare_linked_requirements_more(requirement_set.requirements.values())
requirement_set.warn_legacy_versions_and_specifiers()

# build wheels
build_successes, build_failures = build(
Expand Down
11 changes: 11 additions & 0 deletions src/pip/_internal/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,17 @@ def __str__(self) -> str:
)


class MetadataInvalid(InstallationError):
"""Metadata is invalid."""

def __init__(self, ireq: "InstallRequirement", error: str) -> None:
self.ireq = ireq
self.error = error

def __str__(self) -> str:
return f"Requested {self.ireq} has invalid metadata: {self.error}"


class InstallationSubprocessError(DiagnosticPipError, InstallationError):
"""A subprocess call failed."""

Expand Down
15 changes: 9 additions & 6 deletions src/pip/_internal/index/package_finder.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from pip._vendor.packaging import specifiers
from pip._vendor.packaging.tags import Tag
from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.packaging.version import _BaseVersion
from pip._vendor.packaging.version import InvalidVersion, _BaseVersion
from pip._vendor.packaging.version import parse as parse_version

from pip._internal.exceptions import (
Expand Down Expand Up @@ -752,11 +752,14 @@ def get_install_candidate(
self._log_skipped_link(link, result, detail)
return None

return InstallationCandidate(
name=link_evaluator.project_name,
link=link,
version=detail,
)
try:
return InstallationCandidate(
name=link_evaluator.project_name,
link=link,
version=detail,
)
except InvalidVersion:
return None

def evaluate_links(
self, link_evaluator: LinkEvaluator, links: Iterable[Link]
Expand Down
35 changes: 16 additions & 19 deletions src/pip/_internal/metadata/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from pip._vendor.packaging.requirements import Requirement
from pip._vendor.packaging.specifiers import InvalidSpecifier, SpecifierSet
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
from pip._vendor.packaging.version import LegacyVersion, Version
from pip._vendor.packaging.version import Version

from pip._internal.exceptions import NoneMetadataError
from pip._internal.locations import site_packages, user_site
Expand All @@ -41,8 +41,6 @@

from ._json import msg_to_json

DistributionVersion = Union[LegacyVersion, Version]

InfoPath = Union[str, pathlib.PurePath]

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -140,10 +138,10 @@ def from_wheel(cls, wheel: "Wheel", name: str) -> "BaseDistribution":
raise NotImplementedError()

def __repr__(self) -> str:
return f"{self.raw_name} {self.version} ({self.location})"
return f"{self.raw_name} {self.raw_version} ({self.location})"

def __str__(self) -> str:
return f"{self.raw_name} {self.version}"
return f"{self.raw_name} {self.raw_version}"

@property
def location(self) -> Optional[str]:
Expand Down Expand Up @@ -274,7 +272,11 @@ def canonical_name(self) -> NormalizedName:
raise NotImplementedError()

@property
def version(self) -> DistributionVersion:
def version(self) -> Version:
raise NotImplementedError()

@property
def raw_version(self) -> str:
raise NotImplementedError()

@property
Expand Down Expand Up @@ -443,24 +445,19 @@ def iter_dependencies(self, extras: Collection[str] = ()) -> Iterable[Requiremen
"""
raise NotImplementedError()

def iter_provided_extras(self) -> Iterable[str]:
def iter_raw_dependencies(self) -> Iterable[str]:
"""Raw Requires-Dist metadata."""
return self.metadata.get_all("Requires-Dist", [])

def iter_provided_extras(self) -> Iterable[NormalizedName]:
"""Extras provided by this distribution.

For modern .dist-info distributions, this is the collection of
"Provides-Extra:" entries in distribution metadata.

The return value of this function is not particularly useful other than
display purposes due to backward compatibility issues and the extra
names being poorly normalized prior to PEP 685. If you want to perform
logic operations on extras, use :func:`is_extra_provided` instead.
"""
raise NotImplementedError()

def is_extra_provided(self, extra: str) -> bool:
"""Check whether an extra is provided by this distribution.

This is needed mostly for compatibility issues with pkg_resources not
following the extra normalization rules defined in PEP 685.
The return value of this function is expected to be normalised names,
per PEP 685, with the returned value being handled appropriately by
`iter_dependencies`.
"""
raise NotImplementedError()

Expand Down
25 changes: 14 additions & 11 deletions src/pip/_internal/metadata/importlib/_dists.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@

from pip._vendor.packaging.requirements import Requirement
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
from pip._vendor.packaging.version import Version
from pip._vendor.packaging.version import parse as parse_version

from pip._internal.exceptions import InvalidWheel, UnsupportedWheel
from pip._internal.metadata.base import (
BaseDistribution,
BaseEntryPoint,
DistributionVersion,
InfoPath,
Wheel,
)
Expand Down Expand Up @@ -171,9 +171,13 @@ def canonical_name(self) -> NormalizedName:
return canonicalize_name(name)

@property
def version(self) -> DistributionVersion:
def version(self) -> Version:
return parse_version(self._dist.version)

@property
def raw_version(self) -> str:
return self._dist.version

def is_file(self, path: InfoPath) -> bool:
return self._dist.read_text(str(path)) is not None

Expand Down Expand Up @@ -204,19 +208,18 @@ def _metadata_impl(self) -> email.message.Message:
# until upstream can improve the protocol. (python/cpython#94952)
return cast(email.message.Message, self._dist.metadata)

def iter_provided_extras(self) -> Iterable[str]:
return self.metadata.get_all("Provides-Extra", [])

def is_extra_provided(self, extra: str) -> bool:
return any(
canonicalize_name(provided_extra) == canonicalize_name(extra)
for provided_extra in self.metadata.get_all("Provides-Extra", [])
)
def iter_provided_extras(self) -> Iterable[NormalizedName]:
return [
canonicalize_name(extra)
for extra in self.metadata.get_all("Provides-Extra", [])
]

def iter_dependencies(self, extras: Collection[str] = ()) -> Iterable[Requirement]:
contexts: Sequence[Dict[str, str]] = [{"extra": e} for e in extras]
for req_string in self.metadata.get_all("Requires-Dist", []):
req = Requirement(req_string)
# strip() because email.message.Message.get_all() may return a leading \n
# in case a long header was wrapped.
req = Requirement(req_string.strip())
if not req.marker:
yield req
elif not extras and req.marker.evaluate({"extra": ""}):
Expand Down