mirror of
https://github.com/python/cpython.git
synced 2026-04-13 23:31:02 +00:00
gh-143387: Raise an exception instead of returning None when metadata file is missing. (#146234)
This commit is contained in:
parent
1114d7f7f8
commit
f5d47fceb0
4 changed files with 159 additions and 12 deletions
|
|
@ -31,6 +31,7 @@
|
|||
|
||||
from . import _meta
|
||||
from ._collections import FreezableDefaultDict, Pair
|
||||
from ._context import ExceptionTrap
|
||||
from ._functools import method_cache, noop, pass_none, passthrough
|
||||
from ._itertools import always_iterable, bucket, unique_everseen
|
||||
from ._meta import PackageMetadata, SimplePath
|
||||
|
|
@ -42,6 +43,7 @@
|
|||
'PackageMetadata',
|
||||
'PackageNotFoundError',
|
||||
'PackagePath',
|
||||
'MetadataNotFound',
|
||||
'SimplePath',
|
||||
'distribution',
|
||||
'distributions',
|
||||
|
|
@ -66,6 +68,10 @@ def name(self) -> str: # type: ignore[override] # make readonly
|
|||
return name
|
||||
|
||||
|
||||
class MetadataNotFound(FileNotFoundError):
|
||||
"""No metadata file is present in the distribution."""
|
||||
|
||||
|
||||
class Sectioned:
|
||||
"""
|
||||
A simple entry point config parser for performance
|
||||
|
|
@ -487,7 +493,12 @@ def _prefer_valid(dists: Iterable[Distribution]) -> Iterable[Distribution]:
|
|||
|
||||
Ref python/importlib_resources#489.
|
||||
"""
|
||||
buckets = bucket(dists, lambda dist: bool(dist.metadata))
|
||||
|
||||
has_metadata = ExceptionTrap(MetadataNotFound).passes(
|
||||
operator.attrgetter('metadata')
|
||||
)
|
||||
|
||||
buckets = bucket(dists, has_metadata)
|
||||
return itertools.chain(buckets[True], buckets[False])
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -508,7 +519,7 @@ def _discover_resolvers():
|
|||
return filter(None, declared)
|
||||
|
||||
@property
|
||||
def metadata(self) -> _meta.PackageMetadata | None:
|
||||
def metadata(self) -> _meta.PackageMetadata:
|
||||
"""Return the parsed metadata for this Distribution.
|
||||
|
||||
The returned object will have keys that name the various bits of
|
||||
|
|
@ -517,6 +528,8 @@ def metadata(self) -> _meta.PackageMetadata | None:
|
|||
|
||||
Custom providers may provide the METADATA file or override this
|
||||
property.
|
||||
|
||||
:raises MetadataNotFound: If no metadata file is present.
|
||||
"""
|
||||
|
||||
text = (
|
||||
|
|
@ -527,20 +540,25 @@ def metadata(self) -> _meta.PackageMetadata | None:
|
|||
# (which points to the egg-info file) attribute unchanged.
|
||||
or self.read_text('')
|
||||
)
|
||||
return self._assemble_message(text)
|
||||
return self._assemble_message(self._ensure_metadata_present(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))
|
||||
|
||||
def _ensure_metadata_present(self, text: str | None) -> str:
|
||||
if text is not None:
|
||||
return text
|
||||
|
||||
raise MetadataNotFound('No package metadata was found.')
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the 'Name' metadata for the distribution package."""
|
||||
return md_none(self.metadata)['Name']
|
||||
return self.metadata['Name']
|
||||
|
||||
@property
|
||||
def _normalized_name(self):
|
||||
|
|
@ -550,7 +568,7 @@ def _normalized_name(self):
|
|||
@property
|
||||
def version(self) -> str:
|
||||
"""Return the 'Version' metadata for the distribution package."""
|
||||
return md_none(self.metadata)['Version']
|
||||
return self.metadata['Version']
|
||||
|
||||
@property
|
||||
def entry_points(self) -> EntryPoints:
|
||||
|
|
@ -1063,11 +1081,12 @@ def distributions(**kwargs) -> Iterable[Distribution]:
|
|||
return Distribution.discover(**kwargs)
|
||||
|
||||
|
||||
def metadata(distribution_name: str) -> _meta.PackageMetadata | None:
|
||||
def metadata(distribution_name: str) -> _meta.PackageMetadata:
|
||||
"""Get the metadata for the named package.
|
||||
|
||||
:param distribution_name: The name of the distribution package to query.
|
||||
:return: A PackageMetadata containing the parsed metadata.
|
||||
:raises MetadataNotFound: If no metadata file is present in the distribution.
|
||||
"""
|
||||
return Distribution.from_name(distribution_name).metadata
|
||||
|
||||
|
|
@ -1138,7 +1157,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(md_none(dist.metadata)['Name'])
|
||||
pkg_to_dist[pkg].append(dist.metadata['Name'])
|
||||
return dict(pkg_to_dist)
|
||||
|
||||
|
||||
|
|
|
|||
118
Lib/importlib/metadata/_context.py
Normal file
118
Lib/importlib/metadata/_context.py
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import operator
|
||||
|
||||
|
||||
# from jaraco.context 6.1
|
||||
class ExceptionTrap:
|
||||
"""
|
||||
A context manager that will catch certain exceptions and provide an
|
||||
indication they occurred.
|
||||
|
||||
>>> with ExceptionTrap() as trap:
|
||||
... raise Exception()
|
||||
>>> bool(trap)
|
||||
True
|
||||
|
||||
>>> with ExceptionTrap() as trap:
|
||||
... pass
|
||||
>>> bool(trap)
|
||||
False
|
||||
|
||||
>>> with ExceptionTrap(ValueError) as trap:
|
||||
... raise ValueError("1 + 1 is not 3")
|
||||
>>> bool(trap)
|
||||
True
|
||||
>>> trap.value
|
||||
ValueError('1 + 1 is not 3')
|
||||
>>> trap.tb
|
||||
<traceback object at ...>
|
||||
|
||||
>>> with ExceptionTrap(ValueError) as trap:
|
||||
... raise Exception()
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
Exception
|
||||
|
||||
>>> bool(trap)
|
||||
False
|
||||
"""
|
||||
|
||||
exc_info = None, None, None
|
||||
|
||||
def __init__(self, exceptions=(Exception,)):
|
||||
self.exceptions = exceptions
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return self.exc_info[0]
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return self.exc_info[1]
|
||||
|
||||
@property
|
||||
def tb(self):
|
||||
return self.exc_info[2]
|
||||
|
||||
def __exit__(self, *exc_info):
|
||||
type = exc_info[0]
|
||||
matches = type and issubclass(type, self.exceptions)
|
||||
if matches:
|
||||
self.exc_info = exc_info
|
||||
return matches
|
||||
|
||||
def __bool__(self):
|
||||
return bool(self.type)
|
||||
|
||||
def raises(self, func, *, _test=bool):
|
||||
"""
|
||||
Wrap func and replace the result with the truth
|
||||
value of the trap (True if an exception occurred).
|
||||
|
||||
First, give the decorator an alias to support Python 3.8
|
||||
Syntax.
|
||||
|
||||
>>> raises = ExceptionTrap(ValueError).raises
|
||||
|
||||
Now decorate a function that always fails.
|
||||
|
||||
>>> @raises
|
||||
... def fail():
|
||||
... raise ValueError('failed')
|
||||
>>> fail()
|
||||
True
|
||||
"""
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
with ExceptionTrap(self.exceptions) as trap:
|
||||
func(*args, **kwargs)
|
||||
return _test(trap)
|
||||
|
||||
return wrapper
|
||||
|
||||
def passes(self, func):
|
||||
"""
|
||||
Wrap func and replace the result with the truth
|
||||
value of the trap (True if no exception).
|
||||
|
||||
First, give the decorator an alias to support Python 3.8
|
||||
Syntax.
|
||||
|
||||
>>> passes = ExceptionTrap(ValueError).passes
|
||||
|
||||
Now decorate a function that always fails.
|
||||
|
||||
>>> @passes
|
||||
... def fail():
|
||||
... raise ValueError('failed')
|
||||
|
||||
>>> fail()
|
||||
False
|
||||
"""
|
||||
return self.raises(func, _test=operator.not_)
|
||||
|
|
@ -12,6 +12,7 @@
|
|||
from importlib.metadata import (
|
||||
Distribution,
|
||||
EntryPoint,
|
||||
MetadataNotFound,
|
||||
PackageNotFoundError,
|
||||
_unique,
|
||||
distributions,
|
||||
|
|
@ -159,13 +160,15 @@ def test_valid_dists_preferred(self):
|
|||
|
||||
def test_missing_metadata(self):
|
||||
"""
|
||||
Dists with a missing metadata file should return None.
|
||||
Dists with a missing metadata file should raise ``MetadataNotFound``.
|
||||
|
||||
Ref python/importlib_metadata#493.
|
||||
Ref python/importlib_metadata#493 and python/cpython#143387.
|
||||
"""
|
||||
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
|
||||
with self.assertRaises(MetadataNotFound):
|
||||
Distribution.from_name('foo').metadata
|
||||
with self.assertRaises(MetadataNotFound):
|
||||
metadata('foo')
|
||||
|
||||
|
||||
class NonASCIITests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
In importlib.metadata, when a distribution file is corrupt and there is no
|
||||
metadata file, calls to ``Distribution.metadata()`` (including implicit
|
||||
calls from other properties like ``.name`` and ``.requires``) will now raise
|
||||
a ``MetadataNotFound`` Exception. This allows callers to distinguish between
|
||||
missing metadata and a degenerate (empty) metadata. Previously, if the file
|
||||
was missing, an empty ``PackageMetadata`` would be returned and would be
|
||||
indistinguishable from the presence of an empty file.
|
||||
Loading…
Add table
Add a link
Reference in a new issue