From f8bca5b8e1b28b5ee2355f52328e2407d4e7d417 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 25 Jun 2024 19:07:23 -0700 Subject: [PATCH 1/4] Fix incompatibility with importlib-metadata 8 Fixes #2974 --- news/2974.bugfix.md | 1 + src/pdm/models/candidates.py | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 news/2974.bugfix.md diff --git a/news/2974.bugfix.md b/news/2974.bugfix.md new file mode 100644 index 0000000000..e73b7035d2 --- /dev/null +++ b/news/2974.bugfix.md @@ -0,0 +1 @@ +Fix crash when pdm is used with `importlib-metadata` version 8.0. \ No newline at end of file diff --git a/src/pdm/models/candidates.py b/src/pdm/models/candidates.py index c012da3db1..d2da1b61ff 100644 --- a/src/pdm/models/candidates.py +++ b/src/pdm/models/candidates.py @@ -615,7 +615,12 @@ def metadata(self) -> im.Distribution: if not self.candidate.version: self.candidate.version = result.version if not self.candidate.requires_python: - self.candidate.requires_python = cast(str, result.metadata["Requires-Python"] or "") + # Starting in importlib_metadata 8.0, KeyError is thrown if the key is missing + try: + requires_python = result.metadata["Requires-Python"] or "" + except KeyError: + requires_python = "" + self.candidate.requires_python = requires_python self._metadata = result return self._metadata From 0ce0b0481261f4de535fabf6438f238ec54392c3 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 25 Jun 2024 19:14:46 -0700 Subject: [PATCH 2/4] Fix another occurrence --- src/pdm/cli/commands/list.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/pdm/cli/commands/list.py b/src/pdm/cli/commands/list.py index f83ec49b3a..4aadd75709 100644 --- a/src/pdm/cli/commands/list.py +++ b/src/pdm/cli/commands/list.py @@ -309,6 +309,19 @@ def parse_comma_separated_string( return items +def _get_metadata_key(dist: im.Distribution, key: str) -> str | None: + """Get a key from metadata, or return None if it is not found or 'UNKNOWN'.""" + # Starting with importlib-metadata 8.0, KeyError is thrown for missing keys. + try: + value = dist.metadata[key] + except KeyError: + return None + else: + if not value or value == "UNKNOWN": + return None + return value + + class Listable: """Wrapper makes sorting and exporting information about a Distribution easier. It also retrieves license information from dist-info metadata. @@ -322,20 +335,17 @@ class Listable: def __init__(self, dist: im.Distribution, groups: set[str]): self.dist = dist - self.name: str | None = dist.metadata["Name"] + self.name = _get_metadata_key(dist, "Name") self.groups = "|".join(groups) - self.version: str | None = dist.metadata["Version"] - self.version = None if self.version == "UNKNOWN" else self.version + self.version = _get_metadata_key(dist, "Version") - self.homepage: str | None = dist.metadata["Home-Page"] - self.homepage = None if self.homepage == "UNKNOWN" else self.homepage + self.homepage = _get_metadata_key(dist, "Home-Page") # If the License metadata field is empty or UNKNOWN then try to # find the license in the Trove classifiers. There may be more than one # so generate a pipe separated list (to avoid complexity with CSV export). - self.licenses: str | None = dist.metadata["License"] - self.licenses = None if self.licenses == "UNKNOWN" else self.licenses + self.licenses = _get_metadata_key(dist, "License") # Sometimes package metadata contains the full license text. # e.g. license = { file="LICENSE" } in pyproject.toml From 24a91a3165848e76d2b0977990607ac4e0734765 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 25 Jun 2024 19:34:29 -0700 Subject: [PATCH 3/4] Don't need the helper function --- src/pdm/cli/commands/list.py | 24 +++++++----------------- src/pdm/cli/commands/self_cmd.py | 9 ++++----- src/pdm/installers/manager.py | 4 ++-- src/pdm/installers/uninstallers.py | 5 +++-- src/pdm/models/candidates.py | 9 ++------- src/pdm/models/project_info.py | 4 ++-- src/pdm/models/repositories.py | 2 +- src/pdm/models/requirements.py | 2 +- src/pdm/models/specifiers.py | 2 +- src/pdm/models/working_set.py | 4 ++-- src/pdm/utils.py | 2 +- 11 files changed, 26 insertions(+), 41 deletions(-) diff --git a/src/pdm/cli/commands/list.py b/src/pdm/cli/commands/list.py index 4aadd75709..501cd8b23b 100644 --- a/src/pdm/cli/commands/list.py +++ b/src/pdm/cli/commands/list.py @@ -309,19 +309,6 @@ def parse_comma_separated_string( return items -def _get_metadata_key(dist: im.Distribution, key: str) -> str | None: - """Get a key from metadata, or return None if it is not found or 'UNKNOWN'.""" - # Starting with importlib-metadata 8.0, KeyError is thrown for missing keys. - try: - value = dist.metadata[key] - except KeyError: - return None - else: - if not value or value == "UNKNOWN": - return None - return value - - class Listable: """Wrapper makes sorting and exporting information about a Distribution easier. It also retrieves license information from dist-info metadata. @@ -335,17 +322,20 @@ class Listable: def __init__(self, dist: im.Distribution, groups: set[str]): self.dist = dist - self.name = _get_metadata_key(dist, "Name") + self.name = dist.metadata.get("Name") self.groups = "|".join(groups) - self.version = _get_metadata_key(dist, "Version") + self.version = dist.metadata.get("Version") + self.version = None if self.version == "UNKNOWN" else self.version - self.homepage = _get_metadata_key(dist, "Home-Page") + self.homepage = dist.metadata.get("Home-Page") + self.homepage = None if self.homepage == "UNKNOWN" else self.homepage # If the License metadata field is empty or UNKNOWN then try to # find the license in the Trove classifiers. There may be more than one # so generate a pipe separated list (to avoid complexity with CSV export). - self.licenses = _get_metadata_key(dist, "License") + self.licenses = dist.metadata.get("License") + self.licenses = None if self.licenses == "UNKNOWN" else self.licenses # Sometimes package metadata contains the full license text. # e.g. license = { file="LICENSE" } in pyproject.toml diff --git a/src/pdm/cli/commands/self_cmd.py b/src/pdm/cli/commands/self_cmd.py index c6ed6e071b..f636d93729 100644 --- a/src/pdm/cli/commands/self_cmd.py +++ b/src/pdm/cli/commands/self_cmd.py @@ -26,7 +26,7 @@ def list_distributions(plugin_only: bool = False) -> list[Distribution]: for dist in working_set.values(): if not plugin_only or any(ep.group in ("pdm", "pdm.plugin") for ep in dist.entry_points): result.append(dist) - return sorted(result, key=lambda d: d.metadata["Name"] or "UNKNOWN") + return sorted(result, key=lambda d: d.metadata.get("Name", "UNKNOWN")) def run_pip(project: Project, args: list[str]) -> subprocess.CompletedProcess[str]: @@ -96,12 +96,11 @@ def handle(self, project: Project, options: argparse.Namespace) -> None: echo("Installed packages:", err=True) rows = [] for dist in distributions: - metadata = dist.metadata rows.append( ( - f"[success]{metadata['Name']}[/]", - f"[warning]{metadata['Version']}[/]", - metadata["Summary"] or "", + f"[success]{dist.metadata.get("Name")}[/]", + f"[warning]{dist.metadata.get("Version")}[/]", + dist.metadata.get("Summary", ""), ), ) project.core.ui.display_columns(rows) diff --git a/src/pdm/installers/manager.py b/src/pdm/installers/manager.py index 2ad07e54d4..4a2061c458 100644 --- a/src/pdm/installers/manager.py +++ b/src/pdm/installers/manager.py @@ -45,7 +45,7 @@ def get_paths_to_remove(self, dist: Distribution) -> BaseRemovePaths: def uninstall(self, dist: Distribution) -> None: """Perform the uninstallation for a given distribution""" remove_path = self.get_paths_to_remove(dist) - dist_name = dist.metadata["Name"] + dist_name = dist.metadata.get("Name") termui.logger.info("Removing distribution %s", dist_name) try: remove_path.remove() @@ -58,7 +58,7 @@ def uninstall(self, dist: Distribution) -> None: def overwrite(self, dist: Distribution, candidate: Candidate) -> None: """An in-place update to overwrite the distribution with a new candidate""" paths_to_remove = self.get_paths_to_remove(dist) - termui.logger.info("Overwriting distribution %s", dist.metadata["Name"]) + termui.logger.info("Overwriting distribution %s", dist.metadata.get("Name")) installed = self.install(candidate) installed_paths = self.get_paths_to_remove(installed) # Remove the paths that are in the new distribution diff --git a/src/pdm/installers/uninstallers.py b/src/pdm/installers/uninstallers.py index 13377ac98b..3cb9fa0211 100644 --- a/src/pdm/installers/uninstallers.py +++ b/src/pdm/installers/uninstallers.py @@ -159,10 +159,11 @@ def from_dist(cls: type[_T], dist: Distribution, environment: BaseEnvironment) - dist_location = os.path.dirname(meta_location) if is_egg_link(dist): # pragma: no cover egg_link_path = cast("Path | None", getattr(dist, "link_file", None)) + dist_name = dist.metadata.get("Name") if not egg_link_path: termui.logger.warn( "No egg link is found for editable distribution %s, do nothing.", - dist.metadata["Name"], + dist_name, ) else: with egg_link_path.open("rb") as f: @@ -170,7 +171,7 @@ def from_dist(cls: type[_T], dist: Distribution, environment: BaseEnvironment) - if link_pointer != dist_location: raise UninstallError( f"The link pointer in {egg_link_path} doesn't match " - f"the location of {dist.metadata['Name']}(at {dist_location}" + f"the location of {dist_name} (at {dist_location}" ) instance.add_path(str(egg_link_path)) instance.add_pth(link_pointer) diff --git a/src/pdm/models/candidates.py b/src/pdm/models/candidates.py index d2da1b61ff..578adb00ae 100644 --- a/src/pdm/models/candidates.py +++ b/src/pdm/models/candidates.py @@ -611,16 +611,11 @@ def metadata(self) -> im.Distribution: if self._metadata is None: result = self.prepare_metadata() if not self.candidate.name: - self.req.name = self.candidate.name = cast(str, result.metadata["Name"]) + self.req.name = self.candidate.name = cast(str, result.metadata.get("Name")) if not self.candidate.version: self.candidate.version = result.version if not self.candidate.requires_python: - # Starting in importlib_metadata 8.0, KeyError is thrown if the key is missing - try: - requires_python = result.metadata["Requires-Python"] or "" - except KeyError: - requires_python = "" - self.candidate.requires_python = requires_python + self.candidate.requires_python = result.metadata.get("Requires-Python", "") self._metadata = result return self._metadata diff --git a/src/pdm/models/project_info.py b/src/pdm/models/project_info.py index 8c4bc82487..cea936c576 100644 --- a/src/pdm/models/project_info.py +++ b/src/pdm/models/project_info.py @@ -42,8 +42,8 @@ def from_distribution(cls, data: Distribution) -> ProjectInfo: project_urls = {} return cls( - name=metadata["Name"], - version=metadata["Version"], + name=metadata.get("Name", ""), + version=metadata.get("Version", ""), summary=metadata.get("Summary", ""), author=metadata.get("Author", ""), email=metadata.get("Author-email", ""), diff --git a/src/pdm/models/repositories.py b/src/pdm/models/repositories.py index ebab162eae..483d1c5497 100644 --- a/src/pdm/models/repositories.py +++ b/src/pdm/models/repositories.py @@ -269,7 +269,7 @@ def _get_dependencies_from_metadata(self, candidate: Candidate) -> CandidateInfo prepared = candidate.prepare(self.environment) deps = prepared.get_dependencies_from_metadata() requires_python = candidate.requires_python - summary = prepared.metadata.metadata["Summary"] + summary = prepared.metadata.metadata.get("Summary", "") return deps, requires_python, summary def _get_dependency_from_local_package(self, candidate: Candidate) -> CandidateInfo: diff --git a/src/pdm/models/requirements.py b/src/pdm/models/requirements.py index 37c644f803..dcf51d366b 100644 --- a/src/pdm/models/requirements.py +++ b/src/pdm/models/requirements.py @@ -152,7 +152,7 @@ def from_dist(cls, dist: Distribution) -> Requirement: if direct_url_json is not None: direct_url = json.loads(direct_url_json) data = { - "name": dist.metadata["Name"], + "name": dist.metadata.get("Name"), "url": direct_url.get("url"), "editable": direct_url.get("dir_info", {}).get("editable"), "subdirectory": direct_url.get("subdirectory"), diff --git a/src/pdm/models/specifiers.py b/src/pdm/models/specifiers.py index c4af376ba2..5fc9d38929 100644 --- a/src/pdm/models/specifiers.py +++ b/src/pdm/models/specifiers.py @@ -32,7 +32,7 @@ def _read_max_versions() -> dict[Version, int]: @lru_cache -def get_specifier(version_str: str) -> SpecifierSet: +def get_specifier(version_str: str | None) -> SpecifierSet: if not version_str or version_str == "*": return SpecifierSet() return SpecifierSet(fix_legacy_specifier(version_str)) diff --git a/src/pdm/models/working_set.py b/src/pdm/models/working_set.py index 4d535613f8..78aea3531c 100644 --- a/src/pdm/models/working_set.py +++ b/src/pdm/models/working_set.py @@ -69,12 +69,12 @@ def __init__(self, paths: list[str] | None = None, shared_paths: list[str] | Non self._dist_map = { normalize_name(dist.metadata["Name"]): dist for dist in distributions(path=list(dict.fromkeys(paths))) - if dist.metadata["Name"] + if dist.metadata.get("Name") } self._shared_map = { normalize_name(dist.metadata["Name"]): dist for dist in distributions(path=list(dict.fromkeys(shared_paths))) - if dist.metadata["Name"] + if dist.metadata.get("Name") } self._iter_map = ChainMap(self._dist_map, self._shared_map) diff --git a/src/pdm/utils.py b/src/pdm/utils.py index 212e167771..4813884d60 100644 --- a/src/pdm/utils.py +++ b/src/pdm/utils.py @@ -441,7 +441,7 @@ def is_pip_compatible_with_python(python_version: Version | str) -> bool: from pdm.models.specifiers import get_specifier pip = importlib_metadata.distribution("pip") - requires_python = get_specifier(pip.metadata["Requires-Python"]) + requires_python = get_specifier(pip.metadata.get("Requires-Python")) return requires_python.contains(python_version, True) From 639e00a6cc3fd014ab03fe77711a05bb317861c8 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 25 Jun 2024 19:47:29 -0700 Subject: [PATCH 4/4] Fix pre-3.12 syntax --- src/pdm/cli/commands/self_cmd.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pdm/cli/commands/self_cmd.py b/src/pdm/cli/commands/self_cmd.py index f636d93729..fe249b3bb8 100644 --- a/src/pdm/cli/commands/self_cmd.py +++ b/src/pdm/cli/commands/self_cmd.py @@ -98,8 +98,8 @@ def handle(self, project: Project, options: argparse.Namespace) -> None: for dist in distributions: rows.append( ( - f"[success]{dist.metadata.get("Name")}[/]", - f"[warning]{dist.metadata.get("Version")}[/]", + f"[success]{dist.metadata.get('Name')}[/]", + f"[warning]{dist.metadata.get('Version')}[/]", dist.metadata.get("Summary", ""), ), )