From 39a607d25def76ef760334a494554847da8c8f0f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 3 Jan 2025 10:23:13 -0500 Subject: [PATCH 01/20] Bump badge for 2025. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index efabeee4..4d3cabee 100644 --- a/README.rst +++ b/README.rst @@ -14,5 +14,5 @@ .. .. image:: https://readthedocs.org/projects/PROJECT_RTD/badge/?version=latest .. :target: https://PROJECT_RTD.readthedocs.io/en/latest/?badge=latest -.. image:: https://img.shields.io/badge/skeleton-2024-informational +.. image:: https://img.shields.io/badge/skeleton-2025-informational :target: https://blog.jaraco.com/skeleton From aee344d781920bba42ddbee4b4b44af29d7bab6e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 12 Feb 2025 10:44:24 -0500 Subject: [PATCH 02/20] Removing dependabot config. Closes jaraco/skeleton#156 --- .github/dependabot.yml | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 89ff3396..00000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,8 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "pip" - directory: "/" - schedule: - interval: "daily" - allow: - - dependency-type: "all" From 75ce9aba3ed9f4002fa01db0287dfdb1600fb635 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 23 Feb 2025 18:57:40 -0500 Subject: [PATCH 03/20] Add support for building lxml on pre-release Pythons. Closes jaraco/skeleton#161 --- .github/workflows/main.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9c01fc4d..5841cc37 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -56,6 +56,13 @@ jobs: continue-on-error: ${{ matrix.python == '3.14' }} steps: - uses: actions/checkout@v4 + - name: Install build dependencies + # Install dependencies for building packages on pre-release Pythons + # jaraco/skeleton#161 + if: matrix.python == '3.14' && matrix.platform == 'ubuntu-latest' + run: | + sudo apt update + sudo apt install -y libxml2-dev libxslt-dev - name: Setup Python uses: actions/setup-python@v4 with: From 1c9467fdec1cc1456772cd71c7e740f048ce86fc Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 24 Feb 2025 22:00:11 +0000 Subject: [PATCH 04/20] Fix new mandatory configuration field for RTD (jaraco/skeleton#159) This field is now required and prevents the build from running if absent. Details in https://about.readthedocs.com/blog/2024/12/deprecate-config-files-without-sphinx-or-mkdocs-config/ --- .readthedocs.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index dc8516ac..72437063 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -5,6 +5,9 @@ python: extra_requirements: - doc +sphinx: + configuration: docs/conf.py + # required boilerplate readthedocs/readthedocs.org#10401 build: os: ubuntu-lts-latest From 1a2f93053d789f041d88c97c5da4eea9e949bdfe Mon Sep 17 00:00:00 2001 From: Avasam Date: Tue, 25 Feb 2025 13:21:13 -0500 Subject: [PATCH 05/20] Select Ruff rules for modern type annotations (jaraco/skeleton#160) * Select Ruff rules for modern type annotations Ensure modern type annotation syntax and best practices Not including those covered by type-checkers or exclusive to Python 3.11+ Not including rules currently in preview either. These are the same set of rules I have in pywin32 as of https://github.com/mhammond/pywin32/pull/2458 setuptools has all the same rules enabled (except it also includes the `UP` group directly) * Add PYI011 ignore and #local section * Update ruff.toml Co-authored-by: Jason R. Coombs * Add # upstream --------- Co-authored-by: Jason R. Coombs --- ruff.toml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/ruff.toml b/ruff.toml index 9379d6e1..1d65c7c2 100644 --- a/ruff.toml +++ b/ruff.toml @@ -3,11 +3,32 @@ extend = "pyproject.toml" [lint] extend-select = [ + # upstream + "C901", "PERF401", "W", + + # Ensure modern type annotation syntax and best practices + # Not including those covered by type-checkers or exclusive to Python 3.11+ + "FA", # flake8-future-annotations + "F404", # late-future-import + "PYI", # flake8-pyi + "UP006", # non-pep585-annotation + "UP007", # non-pep604-annotation + "UP010", # unnecessary-future-import + "UP035", # deprecated-import + "UP037", # quoted-annotation + "UP043", # unnecessary-default-type-args + + # local ] ignore = [ + # upstream + + # Typeshed rejects complex or non-literal defaults for maintenance and testing reasons, + # irrelevant to this project. + "PYI011", # typed-argument-default-in-stub # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules "W191", "E111", @@ -23,6 +44,8 @@ ignore = [ "COM819", "ISC001", "ISC002", + + # local ] [format] From aa891069099398fe2eb294ac4b781460d8c0a39b Mon Sep 17 00:00:00 2001 From: Avasam Date: Wed, 26 Feb 2025 17:56:42 -0500 Subject: [PATCH 06/20] Consistent import sorting (isort) (jaraco/skeleton#157) --- ruff.toml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ruff.toml b/ruff.toml index 1d65c7c2..b52a6d7c 100644 --- a/ruff.toml +++ b/ruff.toml @@ -5,9 +5,10 @@ extend = "pyproject.toml" extend-select = [ # upstream - "C901", - "PERF401", - "W", + "C901", # complex-structure + "I", # isort + "PERF401", # manual-list-comprehension + "W", # pycodestyle Warning # Ensure modern type annotation syntax and best practices # Not including those covered by type-checkers or exclusive to Python 3.11+ From d9fc620fd5d00b439397dc15f1acfdd6f583b770 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 09:56:23 -0400 Subject: [PATCH 07/20] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins=20?= =?UTF-8?q?(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- importlib_metadata/__init__.py | 36 ++++++++++++++++++---------------- importlib_metadata/_meta.py | 20 ++++++++----------- tests/_path.py | 3 ++- 3 files changed, 29 insertions(+), 30 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 46a14e64..87c9eb51 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -22,11 +22,13 @@ import sys import textwrap import types +from collections.abc import Iterable, Mapping from contextlib import suppress from importlib import import_module from importlib.abc import MetaPathFinder from itertools import starmap -from typing import Any, Iterable, List, Mapping, Match, Optional, Set, cast +from re import Match +from typing import Any, List, Optional, Set, cast from . import _meta from ._collections import FreezableDefaultDict, Pair @@ -175,7 +177,7 @@ class EntryPoint: value: str group: str - dist: Optional[Distribution] = None + dist: Distribution | None = None def __init__(self, name: str, value: str, group: str) -> None: vars(self).update(name=name, value=value, group=group) @@ -203,7 +205,7 @@ def attr(self) -> str: return match.group('attr') @property - def extras(self) -> List[str]: + def extras(self) -> list[str]: match = self.pattern.match(self.value) assert match is not None return re.findall(r'\w+', match.group('extras') or '') @@ -305,14 +307,14 @@ def select(self, **params) -> EntryPoints: return EntryPoints(ep for ep in self if py39.ep_matches(ep, **params)) @property - def names(self) -> Set[str]: + def names(self) -> set[str]: """ Return the set of all names of all entry points. """ return {ep.name for ep in self} @property - def groups(self) -> Set[str]: + def groups(self) -> set[str]: """ Return the set of all groups of all entry points. """ @@ -333,7 +335,7 @@ def _from_text(text): class PackagePath(pathlib.PurePosixPath): """A reference to a path in a package""" - hash: Optional[FileHash] + hash: FileHash | None size: int dist: Distribution @@ -368,7 +370,7 @@ class Distribution(metaclass=abc.ABCMeta): """ @abc.abstractmethod - def read_text(self, filename) -> Optional[str]: + def read_text(self, filename) -> str | None: """Attempt to load metadata file given by the name. Python distribution metadata is organized by blobs of text @@ -428,7 +430,7 @@ def from_name(cls, name: str) -> Distribution: @classmethod def discover( - cls, *, context: Optional[DistributionFinder.Context] = None, **kwargs + cls, *, context: DistributionFinder.Context | None = None, **kwargs ) -> Iterable[Distribution]: """Return an iterable of Distribution objects for all packages. @@ -524,7 +526,7 @@ def entry_points(self) -> EntryPoints: return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self) @property - def files(self) -> Optional[List[PackagePath]]: + def files(self) -> list[PackagePath] | None: """Files in this distribution. :return: List of PackagePath for this distribution or None @@ -616,7 +618,7 @@ def _read_files_egginfo_sources(self): return text and map('"{}"'.format, text.splitlines()) @property - def requires(self) -> Optional[List[str]]: + def requires(self) -> list[str] | None: """Generated requirements specified for this Distribution""" reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs() return reqs and list(reqs) @@ -722,7 +724,7 @@ def __init__(self, **kwargs): vars(self).update(kwargs) @property - def path(self) -> List[str]: + def path(self) -> list[str]: """ The sequence of directory path that a distribution finder should search. @@ -874,7 +876,7 @@ class Prepared: normalized = None legacy_normalized = None - def __init__(self, name: Optional[str]): + def __init__(self, name: str | None): self.name = name if name is None: return @@ -944,7 +946,7 @@ def __init__(self, path: SimplePath) -> None: """ self._path = path - def read_text(self, filename: str | os.PathLike[str]) -> Optional[str]: + def read_text(self, filename: str | os.PathLike[str]) -> str | None: with suppress( FileNotFoundError, IsADirectoryError, @@ -1051,7 +1053,7 @@ def entry_points(**params) -> EntryPoints: return EntryPoints(eps).select(**params) -def files(distribution_name: str) -> Optional[List[PackagePath]]: +def files(distribution_name: str) -> list[PackagePath] | None: """Return a list of files for the named package. :param distribution_name: The name of the distribution package to query. @@ -1060,7 +1062,7 @@ def files(distribution_name: str) -> Optional[List[PackagePath]]: return distribution(distribution_name).files -def requires(distribution_name: str) -> Optional[List[str]]: +def requires(distribution_name: str) -> list[str] | None: """ Return a list of requirements for the named package. @@ -1070,7 +1072,7 @@ def requires(distribution_name: str) -> Optional[List[str]]: return distribution(distribution_name).requires -def packages_distributions() -> Mapping[str, List[str]]: +def packages_distributions() -> Mapping[str, list[str]]: """ Return a mapping of top-level packages to their distributions. @@ -1091,7 +1093,7 @@ def _top_level_declared(dist): return (dist.read_text('top_level.txt') or '').split() -def _topmost(name: PackagePath) -> Optional[str]: +def _topmost(name: PackagePath) -> str | None: """ Return the top-most parent as long as there is a parent. """ diff --git a/importlib_metadata/_meta.py b/importlib_metadata/_meta.py index 0942bbd9..0c20eff3 100644 --- a/importlib_metadata/_meta.py +++ b/importlib_metadata/_meta.py @@ -1,15 +1,11 @@ from __future__ import annotations import os +from collections.abc import Iterator from typing import ( Any, - Dict, - Iterator, - List, - Optional, Protocol, TypeVar, - Union, overload, ) @@ -28,25 +24,25 @@ def __iter__(self) -> Iterator[str]: ... # pragma: no cover @overload def get( self, name: str, failobj: None = None - ) -> Optional[str]: ... # pragma: no cover + ) -> str | None: ... # pragma: no cover @overload - def get(self, name: str, failobj: _T) -> Union[str, _T]: ... # pragma: no cover + def get(self, name: str, failobj: _T) -> str | _T: ... # pragma: no cover # overload per python/importlib_metadata#435 @overload def get_all( self, name: str, failobj: None = None - ) -> Optional[List[Any]]: ... # pragma: no cover + ) -> list[Any] | None: ... # pragma: no cover @overload - def get_all(self, name: str, failobj: _T) -> Union[List[Any], _T]: + def get_all(self, name: str, failobj: _T) -> list[Any] | _T: """ Return all values associated with a possibly multi-valued key. """ @property - def json(self) -> Dict[str, Union[str, List[str]]]: + def json(self) -> dict[str, str | list[str]]: """ A JSON-compatible form of the metadata. """ @@ -58,11 +54,11 @@ class SimplePath(Protocol): """ def joinpath( - self, other: Union[str, os.PathLike[str]] + self, other: str | os.PathLike[str] ) -> SimplePath: ... # pragma: no cover def __truediv__( - self, other: Union[str, os.PathLike[str]] + self, other: str | os.PathLike[str] ) -> SimplePath: ... # pragma: no cover @property diff --git a/tests/_path.py b/tests/_path.py index c66cf5f8..e63d889f 100644 --- a/tests/_path.py +++ b/tests/_path.py @@ -4,7 +4,8 @@ import functools import pathlib -from typing import TYPE_CHECKING, Mapping, Protocol, Union, runtime_checkable +from collections.abc import Mapping +from typing import TYPE_CHECKING, Protocol, Union, runtime_checkable if TYPE_CHECKING: from typing_extensions import Self From 75670d283f379bbe7072cf5ec8fe1f6c7703f9ea Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 09:58:01 -0400 Subject: [PATCH 08/20] Remove unused imports. --- importlib_metadata/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 87c9eb51..275c7106 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -28,7 +28,7 @@ from importlib.abc import MetaPathFinder from itertools import starmap from re import Match -from typing import Any, List, Optional, Set, cast +from typing import Any, cast from . import _meta from ._collections import FreezableDefaultDict, Pair From 2bfbaf3bed463fc85646d5d57c04d257876844b5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 10:01:40 -0400 Subject: [PATCH 09/20] Prefer typing.NamedTuple --- importlib_metadata/_collections.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/importlib_metadata/_collections.py b/importlib_metadata/_collections.py index cf0954e1..fc5045d3 100644 --- a/importlib_metadata/_collections.py +++ b/importlib_metadata/_collections.py @@ -1,4 +1,5 @@ import collections +import typing # from jaraco.collections 3.3 @@ -24,7 +25,10 @@ def freeze(self): self._frozen = lambda key: self.default_factory() -class Pair(collections.namedtuple('Pair', 'name value')): +class Pair(typing.NamedTuple): + name: str + value: str + @classmethod def parse(cls, text): return cls(*map(str.strip, text.split("=", 1))) From c10bdf30dafb55ec471a289e751089255e7f281d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 10:02:50 -0400 Subject: [PATCH 10/20] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins=20?= =?UTF-8?q?(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- importlib_metadata/compat/py39.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/importlib_metadata/compat/py39.py b/importlib_metadata/compat/py39.py index 1f15bd97..2592436d 100644 --- a/importlib_metadata/compat/py39.py +++ b/importlib_metadata/compat/py39.py @@ -2,7 +2,9 @@ Compatibility layer with Python 3.8/3.9 """ -from typing import TYPE_CHECKING, Any, Optional +from __future__ import annotations + +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: # pragma: no cover # Prevent circular imports on runtime. @@ -11,7 +13,7 @@ Distribution = EntryPoint = Any -def normalized_name(dist: Distribution) -> Optional[str]: +def normalized_name(dist: Distribution) -> str | None: """ Honor name normalization for distributions that don't provide ``_normalized_name``. """ From 55c6070ad7f337a423962698d3e02c62a8e1b10e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 09:38:56 -0400 Subject: [PATCH 11/20] Refactored parsing and handling of EntryPoint.value. --- importlib_metadata/__init__.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 275c7106..849ce068 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -27,7 +27,6 @@ from importlib import import_module from importlib.abc import MetaPathFinder from itertools import starmap -from re import Match from typing import Any, cast from . import _meta @@ -135,6 +134,12 @@ def valid(line: str): return line and not line.startswith('#') +class _EntryPointMatch(types.SimpleNamespace): + module: str + attr: str + extras: str + + class EntryPoint: """An entry point as defined by Python packaging conventions. @@ -187,28 +192,27 @@ def load(self) -> Any: is indicated by the value, return that module. Otherwise, return the named object. """ - match = cast(Match, self.pattern.match(self.value)) - module = import_module(match.group('module')) - attrs = filter(None, (match.group('attr') or '').split('.')) + module = import_module(self.module) + attrs = filter(None, (self.attr or '').split('.')) return functools.reduce(getattr, attrs, module) @property def module(self) -> str: - match = self.pattern.match(self.value) - assert match is not None - return match.group('module') + return self._match.module @property def attr(self) -> str: - match = self.pattern.match(self.value) - assert match is not None - return match.group('attr') + return self._match.attr @property def extras(self) -> list[str]: + return re.findall(r'\w+', self._match.extras or '') + + @property + def _match(self) -> _EntryPointMatch: match = self.pattern.match(self.value) assert match is not None - return re.findall(r'\w+', match.group('extras') or '') + return _EntryPointMatch(**match.groupdict()) def _for(self, dist): vars(self).update(dist=dist) From eae6a754d004e8ea72d5d07b7dc3733a6be71f1b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 09:41:26 -0400 Subject: [PATCH 12/20] Raise a ValueError if no match. Closes #488 --- importlib_metadata/__init__.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 849ce068..d527e403 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -155,6 +155,22 @@ class EntryPoint: 'attr' >>> ep.extras ['extra1', 'extra2'] + + If the value package or module are not valid identifiers, a + ValueError is raised on access. + + >>> EntryPoint(name=None, group=None, value='invalid-name').module + Traceback (most recent call last): + ... + ValueError: ('Invalid object reference...invalid-name... + >>> EntryPoint(name=None, group=None, value='invalid-name').attr + Traceback (most recent call last): + ... + ValueError: ('Invalid object reference...invalid-name... + >>> EntryPoint(name=None, group=None, value='invalid-name').extras + Traceback (most recent call last): + ... + ValueError: ('Invalid object reference...invalid-name... """ pattern = re.compile( @@ -211,7 +227,13 @@ def extras(self) -> list[str]: @property def _match(self) -> _EntryPointMatch: match = self.pattern.match(self.value) - assert match is not None + if not match: + raise ValueError( + 'Invalid object reference. ' + 'See https://packaging.python.org' + '/en/latest/specifications/entry-points/#data-model', + self.value, + ) return _EntryPointMatch(**match.groupdict()) def _for(self, dist): From f179e28888b2c6caf12baaf5449ff1cd82513dfe Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 09:45:56 -0400 Subject: [PATCH 13/20] Also raise ValueError on construction if the value is invalid. --- importlib_metadata/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index d527e403..ff3c2a44 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -171,6 +171,14 @@ class EntryPoint: Traceback (most recent call last): ... ValueError: ('Invalid object reference...invalid-name... + + The same thing happens on construction. + + >>> EntryPoint(name=None, group=None, value='invalid-name') + Traceback (most recent call last): + ... + ValueError: ('Invalid object reference...invalid-name... + """ pattern = re.compile( @@ -202,6 +210,7 @@ class EntryPoint: def __init__(self, name: str, value: str, group: str) -> None: vars(self).update(name=name, value=value, group=group) + self.module def load(self) -> Any: """Load the entry point from its definition. If only a module From 9f8af013635833cf3ac348413c9ac63b37caa3dd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 09:50:19 -0400 Subject: [PATCH 14/20] Prefer a cached property, as the property is likely to be retrieved at least 3 times (on construction and for module:attr access). --- importlib_metadata/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index ff3c2a44..157b2c6f 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -233,7 +233,7 @@ def attr(self) -> str: def extras(self) -> list[str]: return re.findall(r'\w+', self._match.extras or '') - @property + @functools.cached_property def _match(self) -> _EntryPointMatch: match = self.pattern.match(self.value) if not match: From 57f31d77e18fef11dfadfd44775f253971c36920 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 27 Jun 2024 12:53:04 -0400 Subject: [PATCH 15/20] Allow metadata to return None when there is no metadata present. --- importlib_metadata/__init__.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 157b2c6f..4717f3d7 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -511,7 +511,7 @@ def _discover_resolvers(): return filter(None, declared) @property - def metadata(self) -> _meta.PackageMetadata: + def metadata(self) -> _meta.PackageMetadata | None: """Return the parsed metadata for this Distribution. The returned object will have keys that name the various bits of @@ -521,10 +521,8 @@ def metadata(self) -> _meta.PackageMetadata: Custom providers may provide the METADATA file or override this property. """ - # deferred for performance (python/cpython#109829) - from . import _adapters - opt_text = ( + text = ( self.read_text('METADATA') or self.read_text('PKG-INFO') # This last clause is here to support old egg-info files. Its @@ -532,7 +530,14 @@ def metadata(self) -> _meta.PackageMetadata: # (which points to the egg-info file) attribute unchanged. or self.read_text('') ) - text = cast(str, opt_text) + return self._assemble_message(text) + + @staticmethod + @pass_none + def _assemble_message(text: str) -> _meta.PackageMetadata: + # deferred for performance (python/cpython#109829) + from . import _adapters + return _adapters.Message(email.message_from_string(text)) @property From 22bb567692d8e7bd216f864a9d8dee1272ee8674 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 10:32:46 -0400 Subject: [PATCH 16/20] Fix type errors where metadata could be None. --- importlib_metadata/__init__.py | 8 ++++---- importlib_metadata/compat/py39.py | 8 ++++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 4717f3d7..ded27e13 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -543,7 +543,7 @@ def _assemble_message(text: str) -> _meta.PackageMetadata: @property def name(self) -> str: """Return the 'Name' metadata for the distribution package.""" - return self.metadata['Name'] + return cast(PackageMetadata, self.metadata)['Name'] @property def _normalized_name(self): @@ -553,7 +553,7 @@ def _normalized_name(self): @property def version(self) -> str: """Return the 'Version' metadata for the distribution package.""" - return self.metadata['Version'] + return cast(PackageMetadata, self.metadata)['Version'] @property def entry_points(self) -> EntryPoints: @@ -1050,7 +1050,7 @@ def distributions(**kwargs) -> Iterable[Distribution]: return Distribution.discover(**kwargs) -def metadata(distribution_name: str) -> _meta.PackageMetadata: +def metadata(distribution_name: str) -> _meta.PackageMetadata | None: """Get the metadata for the named package. :param distribution_name: The name of the distribution package to query. @@ -1125,7 +1125,7 @@ def packages_distributions() -> Mapping[str, list[str]]: 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']) + pkg_to_dist[pkg].append(cast(PackageMetadata, dist.metadata)['Name']) return dict(pkg_to_dist) diff --git a/importlib_metadata/compat/py39.py b/importlib_metadata/compat/py39.py index 2592436d..1fbcbf7b 100644 --- a/importlib_metadata/compat/py39.py +++ b/importlib_metadata/compat/py39.py @@ -4,7 +4,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast if TYPE_CHECKING: # pragma: no cover # Prevent circular imports on runtime. @@ -12,6 +12,8 @@ else: Distribution = EntryPoint = Any +from .._meta import PackageMetadata + def normalized_name(dist: Distribution) -> str | None: """ @@ -22,7 +24,9 @@ def normalized_name(dist: Distribution) -> str | None: except AttributeError: from .. import Prepared # -> delay to prevent circular imports. - return Prepared.normalize(getattr(dist, "name", None) or dist.metadata['Name']) + return Prepared.normalize( + getattr(dist, "name", None) or cast(PackageMetadata, dist.metadata)['Name'] + ) def ep_matches(ep: EntryPoint, **params) -> bool: From 0830c39b8a23e48024365120c0e97a6f7c36c5ec Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 10:34:40 -0400 Subject: [PATCH 17/20] Add news fragment. --- newsfragments/493.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/493.feature.rst diff --git a/newsfragments/493.feature.rst b/newsfragments/493.feature.rst new file mode 100644 index 00000000..e75e0e3e --- /dev/null +++ b/newsfragments/493.feature.rst @@ -0,0 +1 @@ +``.metadata()`` (and ``Distribution.metadata``) can now return ``None`` if the metadata directory exists but not metadata file is present. From 5a657051f7386de6f0560c200d78e941be2c8058 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 10:47:08 -0400 Subject: [PATCH 18/20] Refactor the casting into a wrapper for brevity and to document its purpose. --- importlib_metadata/__init__.py | 9 +++++---- importlib_metadata/_typing.py | 15 +++++++++++++++ importlib_metadata/compat/py39.py | 6 +++--- 3 files changed, 23 insertions(+), 7 deletions(-) create mode 100644 importlib_metadata/_typing.py diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index ded27e13..cdfc1f62 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -27,7 +27,7 @@ from importlib import import_module from importlib.abc import MetaPathFinder from itertools import starmap -from typing import Any, cast +from typing import Any from . import _meta from ._collections import FreezableDefaultDict, Pair @@ -38,6 +38,7 @@ from ._functools import method_cache, pass_none from ._itertools import always_iterable, bucket, unique_everseen from ._meta import PackageMetadata, SimplePath +from ._typing import md_none from .compat import py39, py311 __all__ = [ @@ -543,7 +544,7 @@ def _assemble_message(text: str) -> _meta.PackageMetadata: @property def name(self) -> str: """Return the 'Name' metadata for the distribution package.""" - return cast(PackageMetadata, self.metadata)['Name'] + return md_none(self.metadata)['Name'] @property def _normalized_name(self): @@ -553,7 +554,7 @@ def _normalized_name(self): @property def version(self) -> str: """Return the 'Version' metadata for the distribution package.""" - return cast(PackageMetadata, self.metadata)['Version'] + return md_none(self.metadata)['Version'] @property def entry_points(self) -> EntryPoints: @@ -1125,7 +1126,7 @@ def packages_distributions() -> Mapping[str, list[str]]: 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(cast(PackageMetadata, dist.metadata)['Name']) + pkg_to_dist[pkg].append(md_none(dist.metadata)['Name']) return dict(pkg_to_dist) diff --git a/importlib_metadata/_typing.py b/importlib_metadata/_typing.py new file mode 100644 index 00000000..32b1d2b9 --- /dev/null +++ b/importlib_metadata/_typing.py @@ -0,0 +1,15 @@ +import functools +import typing + +from ._meta import PackageMetadata + +md_none = functools.partial(typing.cast, PackageMetadata) +""" +Suppress type errors for optional metadata. + +Although Distribution.metadata can return None when metadata is corrupt +and thus None, allow callers to assume it's not None and crash if +that's the case. + +# python/importlib_metadata#493 +""" diff --git a/importlib_metadata/compat/py39.py b/importlib_metadata/compat/py39.py index 1fbcbf7b..3eb9c01e 100644 --- a/importlib_metadata/compat/py39.py +++ b/importlib_metadata/compat/py39.py @@ -4,7 +4,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: # pragma: no cover # Prevent circular imports on runtime. @@ -12,7 +12,7 @@ else: Distribution = EntryPoint = Any -from .._meta import PackageMetadata +from .._typing import md_none def normalized_name(dist: Distribution) -> str | None: @@ -25,7 +25,7 @@ def normalized_name(dist: Distribution) -> str | None: from .. import Prepared # -> delay to prevent circular imports. return Prepared.normalize( - getattr(dist, "name", None) or cast(PackageMetadata, dist.metadata)['Name'] + getattr(dist, "name", None) or md_none(dist.metadata)['Name'] ) From e4351c226765f53a40316fa6aab50488aee8a90f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 10:51:40 -0400 Subject: [PATCH 19/20] Add a new test capturing the new expectation. --- tests/test_main.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_main.py b/tests/test_main.py index 7c9851fc..5ed08c89 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -155,6 +155,16 @@ def test_valid_dists_preferred(self): dist = Distribution.from_name('foo') assert dist.version == "1.0" + def test_missing_metadata(self): + """ + Dists with a missing metadata file should return None. + + Ref python/importlib_metadata#493. + """ + fixtures.build_files(self.make_pkg('foo-4.3', files={}), self.site_dir) + assert Distribution.from_name('foo').metadata is None + assert metadata('foo') is None + class NonASCIITests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase): @staticmethod From 708dff4f1ab89bdd126e3e8c56098d04282c5809 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 11:22:50 -0400 Subject: [PATCH 20/20] Finalize --- NEWS.rst | 15 +++++++++++++++ newsfragments/493.feature.rst | 1 - newsfragments/518.bugfix.rst | 1 - 3 files changed, 15 insertions(+), 2 deletions(-) delete mode 100644 newsfragments/493.feature.rst delete mode 100644 newsfragments/518.bugfix.rst diff --git a/NEWS.rst b/NEWS.rst index e5a4b397..4d0c4bdc 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,18 @@ +v8.7.0 +====== + +Features +-------- + +- ``.metadata()`` (and ``Distribution.metadata``) can now return ``None`` if the metadata directory exists but not metadata file is present. (#493) + + +Bugfixes +-------- + +- Raise consistent ValueError for invalid EntryPoint.value (#518) + + v8.6.1 ====== diff --git a/newsfragments/493.feature.rst b/newsfragments/493.feature.rst deleted file mode 100644 index e75e0e3e..00000000 --- a/newsfragments/493.feature.rst +++ /dev/null @@ -1 +0,0 @@ -``.metadata()`` (and ``Distribution.metadata``) can now return ``None`` if the metadata directory exists but not metadata file is present. diff --git a/newsfragments/518.bugfix.rst b/newsfragments/518.bugfix.rst deleted file mode 100644 index 416071f7..00000000 --- a/newsfragments/518.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Raise consistent ValueError for invalid EntryPoint.value pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy