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
'PackageMetadata',
'PackageNotFoundError',
'PackagePath',
+ 'MetadataNotFound',
'SimplePath',
'distribution',
'distributions',
return name
+class MetadataNotFound(FileNotFoundError):
+ """No metadata file is present in the distribution."""
+
+
class Sectioned:
"""
A simple entry point config parser for performance
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
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
Custom providers may provide the METADATA file or override this
property.
+
+ :raises MetadataNotFound: If no metadata file is present.
"""
text = (
# (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):
@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:
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
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)
--- /dev/null
+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_)
from importlib.metadata import (
Distribution,
EntryPoint,
+ MetadataNotFound,
PackageNotFoundError,
_unique,
distributions,
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):
--- /dev/null
+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.