Skip to content

Commit b3065f0

Browse files
authored
Merge pull request #519 from python/bugfix/493-metadata-missing
Return None for missing metadata
2 parents b9c4be4 + e4351c2 commit b3065f0

File tree

5 files changed

+47
-11
lines changed

5 files changed

+47
-11
lines changed

importlib_metadata/__init__.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from importlib import import_module
2828
from importlib.abc import MetaPathFinder
2929
from itertools import starmap
30-
from typing import Any, cast
30+
from typing import Any
3131

3232
from . import _meta
3333
from ._collections import FreezableDefaultDict, Pair
@@ -38,6 +38,7 @@
3838
from ._functools import method_cache, pass_none
3939
from ._itertools import always_iterable, bucket, unique_everseen
4040
from ._meta import PackageMetadata, SimplePath
41+
from ._typing import md_none
4142
from .compat import py39, py311
4243

4344
__all__ = [
@@ -511,7 +512,7 @@ def _discover_resolvers():
511512
return filter(None, declared)
512513

513514
@property
514-
def metadata(self) -> _meta.PackageMetadata:
515+
def metadata(self) -> _meta.PackageMetadata | None:
515516
"""Return the parsed metadata for this Distribution.
516517
517518
The returned object will have keys that name the various bits of
@@ -521,24 +522,29 @@ def metadata(self) -> _meta.PackageMetadata:
521522
Custom providers may provide the METADATA file or override this
522523
property.
523524
"""
524-
# deferred for performance (python/cpython#109829)
525-
from . import _adapters
526525

527-
opt_text = (
526+
text = (
528527
self.read_text('METADATA')
529528
or self.read_text('PKG-INFO')
530529
# This last clause is here to support old egg-info files. Its
531530
# effect is to just end up using the PathDistribution's self._path
532531
# (which points to the egg-info file) attribute unchanged.
533532
or self.read_text('')
534533
)
535-
text = cast(str, opt_text)
534+
return self._assemble_message(text)
535+
536+
@staticmethod
537+
@pass_none
538+
def _assemble_message(text: str) -> _meta.PackageMetadata:
539+
# deferred for performance (python/cpython#109829)
540+
from . import _adapters
541+
536542
return _adapters.Message(email.message_from_string(text))
537543

538544
@property
539545
def name(self) -> str:
540546
"""Return the 'Name' metadata for the distribution package."""
541-
return self.metadata['Name']
547+
return md_none(self.metadata)['Name']
542548

543549
@property
544550
def _normalized_name(self):
@@ -548,7 +554,7 @@ def _normalized_name(self):
548554
@property
549555
def version(self) -> str:
550556
"""Return the 'Version' metadata for the distribution package."""
551-
return self.metadata['Version']
557+
return md_none(self.metadata)['Version']
552558

553559
@property
554560
def entry_points(self) -> EntryPoints:
@@ -1045,7 +1051,7 @@ def distributions(**kwargs) -> Iterable[Distribution]:
10451051
return Distribution.discover(**kwargs)
10461052

10471053

1048-
def metadata(distribution_name: str) -> _meta.PackageMetadata:
1054+
def metadata(distribution_name: str) -> _meta.PackageMetadata | None:
10491055
"""Get the metadata for the named package.
10501056
10511057
:param distribution_name: The name of the distribution package to query.
@@ -1120,7 +1126,7 @@ def packages_distributions() -> Mapping[str, list[str]]:
11201126
pkg_to_dist = collections.defaultdict(list)
11211127
for dist in distributions():
11221128
for pkg in _top_level_declared(dist) or _top_level_inferred(dist):
1123-
pkg_to_dist[pkg].append(dist.metadata['Name'])
1129+
pkg_to_dist[pkg].append(md_none(dist.metadata)['Name'])
11241130
return dict(pkg_to_dist)
11251131

11261132

importlib_metadata/_typing.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import functools
2+
import typing
3+
4+
from ._meta import PackageMetadata
5+
6+
md_none = functools.partial(typing.cast, PackageMetadata)
7+
"""
8+
Suppress type errors for optional metadata.
9+
10+
Although Distribution.metadata can return None when metadata is corrupt
11+
and thus None, allow callers to assume it's not None and crash if
12+
that's the case.
13+
14+
# python/importlib_metadata#493
15+
"""

importlib_metadata/compat/py39.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
else:
1313
Distribution = EntryPoint = Any
1414

15+
from .._typing import md_none
16+
1517

1618
def normalized_name(dist: Distribution) -> str | None:
1719
"""
@@ -22,7 +24,9 @@ def normalized_name(dist: Distribution) -> str | None:
2224
except AttributeError:
2325
from .. import Prepared # -> delay to prevent circular imports.
2426

25-
return Prepared.normalize(getattr(dist, "name", None) or dist.metadata['Name'])
27+
return Prepared.normalize(
28+
getattr(dist, "name", None) or md_none(dist.metadata)['Name']
29+
)
2630

2731

2832
def ep_matches(ep: EntryPoint, **params) -> bool:

newsfragments/493.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
``.metadata()`` (and ``Distribution.metadata``) can now return ``None`` if the metadata directory exists but not metadata file is present.

tests/test_main.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,16 @@ def test_valid_dists_preferred(self):
155155
dist = Distribution.from_name('foo')
156156
assert dist.version == "1.0"
157157

158+
def test_missing_metadata(self):
159+
"""
160+
Dists with a missing metadata file should return None.
161+
162+
Ref python/importlib_metadata#493.
163+
"""
164+
fixtures.build_files(self.make_pkg('foo-4.3', files={}), self.site_dir)
165+
assert Distribution.from_name('foo').metadata is None
166+
assert metadata('foo') is None
167+
158168

159169
class NonASCIITests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
160170
@staticmethod

0 commit comments

Comments
 (0)