8000 Return None for missing metadata by jaraco · Pull Request #519 · python/importlib_metadata · GitHub
[go: up one dir, main page]

Skip to content

Return None for missing metadata #519

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Lo 8000 ading
Diff view
Diff view
26 changes: 16 additions & 10 deletions importlib_metadata/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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__ = [
Expand Down Expand Up @@ -511,7 +512,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
Expand All @@ -521,24 +522,29 @@ 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
# effect is to just end up using the PathDistribution's self._path
# (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
def name(self) -> str:
"""Return the 'Name' metadata for the distribution package."""
return self.metadata['Name']
return md_none(self.metadata)['Name']

@property
def _normalized_name(self):
Expand All @@ -548,7 +554,7 @@ def _normalized_name(self):
@property
def version(self) -> str:
"""Return the 'Version' metadata for the distribution package."""
return self.metadata['Version']
return md_none(self.metadata)['Version']

@property
def entry_points(self) -> EntryPoints:
Expand Down Expand Up @@ -1045,7 +1051,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.
Expand Down Expand Up @@ -1120,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(dist.metadata['Name'])
pkg_to_dist[pkg].append(md_none(dist.metadata)['Name'])
return dict(pkg_to_dist)


Expand Down
15 changes: 15 additions & 0 deletions importlib_metadata/_typing.py
Original file line number Diff line number Diff line change
@@ -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
"""
6 changes: 5 additions & 1 deletion importlib_metadata/compat/py39.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
else:
Distribution = EntryPoint = Any

from .._typing import md_none


def normalized_name(dist: Distribution) -> str | None:
"""
Expand All @@ -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 md_none(dist.metadata)['Name']
)


def ep_matches(ep: EntryPoint, **params) -> bool:
Expand Down
1 change: 1 addition & 0 deletions newsfragments/493.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
``.metadata()`` (and ``Distribution.metadata``) can now return ``None`` if the metadata directory exists but not metadata file is present.
10 changes: 10 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
0