Skip to content

Commit 9b0dfdf

Browse files
authored
Raise an exception when no metadata file is found (#532)
2 parents a5c2154 + 0f2229c commit 9b0dfdf

File tree

5 files changed

+153
-28
lines changed

5 files changed

+153
-28
lines changed

importlib_metadata/__init__.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,18 @@
3535
NullFinder,
3636
install,
3737
)
38+
from ._context import ExceptionTrap
3839
from ._functools import method_cache, noop, pass_none, passthrough
3940
from ._itertools import always_iterable, bucket, unique_everseen
4041
from ._meta import PackageMetadata, SimplePath
41-
from ._typing import md_none
4242
from .compat import py311
4343

4444
__all__ = [
4545
'Distribution',
4646
'DistributionFinder',
4747
'PackageMetadata',
4848
'PackageNotFoundError',
49+
'MetadataNotFound',
4950
'SimplePath',
5051
'distribution',
5152
'distributions',
@@ -70,6 +71,10 @@ def name(self) -> str: # type: ignore[override] # make readonly
7071
return name
7172

7273

74+
class MetadataNotFound(FileNotFoundError):
75+
"""No metadata file is present in the distribution."""
76+
77+
7378
class Sectioned:
7479
"""
7580
A simple entry point config parser for performance
@@ -491,7 +496,12 @@ def _prefer_valid(dists: Iterable[Distribution]) -> Iterable[Distribution]:
491496
492497
Ref python/importlib_resources#489.
493498
"""
494-
buckets = bucket(dists, lambda dist: bool(dist.metadata))
499+
500+
has_metadata = ExceptionTrap(MetadataNotFound).passes(
501+
operator.attrgetter('metadata')
502+
)
503+
504+
buckets = bucket(dists, has_metadata)
495505
return itertools.chain(buckets[True], buckets[False])
496506

497507
@staticmethod
@@ -512,7 +522,7 @@ def _discover_resolvers():
512522
return filter(None, declared)
513523

514524
@property
515-
def metadata(self) -> _meta.PackageMetadata | None:
525+
def metadata(self) -> _meta.PackageMetadata:
516526
"""Return the parsed metadata for this Distribution.
517527
518528
The returned object will have keys that name the various bits of
@@ -521,6 +531,8 @@ def metadata(self) -> _meta.PackageMetadata | None:
521531
522532
Custom providers may provide the METADATA file or override this
523533
property.
534+
535+
:raises MetadataNotFound: If no metadata file is present.
524536
"""
525537

526538
text = (
@@ -531,20 +543,25 @@ def metadata(self) -> _meta.PackageMetadata | None:
531543
# (which points to the egg-info file) attribute unchanged.
532544
or self.read_text('')
533545
)
534-
return self._assemble_message(text)
546+
return self._assemble_message(self._ensure_metadata_present(text))
535547

536548
@staticmethod
537-
@pass_none
538549
def _assemble_message(text: str) -> _meta.PackageMetadata:
539550
# deferred for performance (python/cpython#109829)
540551
from . import _adapters
541552

542553
return _adapters.Message(email.message_from_string(text))
543554

555+
def _ensure_metadata_present(self, text: str | None) -> str:
556+
if text is not None:
557+
return text
558+
559+
raise MetadataNotFound('No package metadata was found.')
560+
544561
@property
545562
def name(self) -> str:
546563
"""Return the 'Name' metadata for the distribution package."""
547-
return md_none(self.metadata)['Name']
564+
return self.metadata['Name']
548565

549566
@property
550567
def _normalized_name(self):
@@ -554,7 +571,7 @@ def _normalized_name(self):
554571
@property
555572
def version(self) -> str:
556573
"""Return the 'Version' metadata for the distribution package."""
557-
return md_none(self.metadata)['Version']
574+
return self.metadata['Version']
558575

559576
@property
560577
def entry_points(self) -> EntryPoints:
@@ -1067,11 +1084,12 @@ def distributions(**kwargs) -> Iterable[Distribution]:
10671084
return Distribution.discover(**kwargs)
10681085

10691086

1070-
def metadata(distribution_name: str) -> _meta.PackageMetadata | None:
1087+
def metadata(distribution_name: str) -> _meta.PackageMetadata:
10711088
"""Get the metadata for the named package.
10721089
10731090
:param distribution_name: The name of the distribution package to query.
10741091
:return: A PackageMetadata containing the parsed metadata.
1092+
:raises MetadataNotFound: If no metadata file is present in the distribution.
10751093
"""
10761094
return Distribution.from_name(distribution_name).metadata
10771095

@@ -1142,7 +1160,7 @@ def packages_distributions() -> Mapping[str, list[str]]:
11421160
pkg_to_dist = collections.defaultdict(list)
11431161
for dist in distributions():
11441162
for pkg in _top_level_declared(dist) or _top_level_inferred(dist):
1145-
pkg_to_dist[pkg].append(md_none(dist.metadata)['Name'])
1163+
pkg_to_dist[pkg].append(dist.metadata['Name'])
11461164
return dict(pkg_to_dist)
11471165

11481166

importlib_metadata/_context.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
from __future__ import annotations
2+
3+
import functools
4+
import operator
5+
6+
7+
# from jaraco.context 6.1
8+
class ExceptionTrap:
9+
"""
10+
A context manager that will catch certain exceptions and provide an
11+
indication they occurred.
12+
13+
>>> with ExceptionTrap() as trap:
14+
... raise Exception()
15+
>>> bool(trap)
16+
True
17+
18+
>>> with ExceptionTrap() as trap:
19+
... pass
20+
>>> bool(trap)
21+
False
22+
23+
>>> with ExceptionTrap(ValueError) as trap:
24+
... raise ValueError("1 + 1 is not 3")
25+
>>> bool(trap)
26+
True
27+
>>> trap.value
28+
ValueError('1 + 1 is not 3')
29+
>>> trap.tb
30+
<traceback object at ...>
31+
32+
>>> with ExceptionTrap(ValueError) as trap:
33+
... raise Exception()
34+
Traceback (most recent call last):
35+
...
36+
Exception
37+
38+
>>> bool(trap)
39+
False
40+
"""
41+
42+
exc_info = None, None, None
43+
44+
def __init__(self, exceptions=(Exception,)):
45+
self.exceptions = exceptions
46+
47+
def __enter__(self):
48+
return self
49+
50+
@property
51+
def type(self):
52+
return self.exc_info[0]
53+
54+
@property
55+
def value(self):
56+
return self.exc_info[1]
57+
58+
@property
59+
def tb(self):
60+
return self.exc_info[2]
61+
62+
def __exit__(self, *exc_info):
63+
type = exc_info[0]
64+
matches = type and issubclass(type, self.exceptions)
65+
if matches:
66+
self.exc_info = exc_info
67+
return matches
68+
69+
def __bool__(self):
70+
return bool(self.type)
71+
72+
def raises(self, func, *, _test=bool):
73+
"""
74+
Wrap func and replace the result with the truth
75+
value of the trap (True if an exception occurred).
76+
77+
First, give the decorator an alias to support Python 3.8
78+
Syntax.
79+
80+
>>> raises = ExceptionTrap(ValueError).raises
81+
82+
Now decorate a function that always fails.
83+
84+
>>> @raises
85+
... def fail():
86+
... raise ValueError('failed')
87+
>>> fail()
88+
True
89+
"""
90+
91+
@functools.wraps(func)
92+
def wrapper(*args, **kwargs):
93+
with ExceptionTrap(self.exceptions) as trap:
94+
func(*args, **kwargs)
95+
return _test(trap)
96+
97+
return wrapper
98+
99+
def passes(self, func):
100+
"""
101+
Wrap func and replace the result with the truth
102+
value of the trap (True if no exception).
103+
104+
First, give the decorator an alias to support Python 3.8
105+
Syntax.
106+
107+
>>> passes = ExceptionTrap(ValueError).passes
108+
109+
Now decorate a function that always fails.
110+
111+
>>> @passes
112+
... def fail():
113+
... raise ValueError('failed')
114+
115+
>>> fail()
116+
False
117+
"""
118+
return self.raises(func, _test=operator.not_)

importlib_metadata/_typing.py

Lines changed: 0 additions & 15 deletions
This file was deleted.

newsfragments/532.removal.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added ``MetadataNotFound`` (subclass of ``FileNotFoundError``) and updated ``Distribution.metadata``/``metadata()`` to raise it when the metadata files are missing instead of returning ``None`` (python/cpython#143387).

tests/test_main.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from importlib_metadata import (
1111
Distribution,
1212
EntryPoint,
13+
MetadataNotFound,
1314
PackageNotFoundError,
1415
_unique,
1516
distributions,
@@ -157,13 +158,15 @@ def test_valid_dists_preferred(self):
157158

158159
def test_missing_metadata(self):
159160
"""
160-
Dists with a missing metadata file should return None.
161+
Dists with a missing metadata file should raise ``MetadataNotFound``.
161162
162-
Ref python/importlib_metadata#493.
163+
Ref python/importlib_metadata#493 and python/cpython#143387.
163164
"""
164165
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
166+
with self.assertRaises(MetadataNotFound):
167+
Distribution.from_name('foo').metadata
168+
with self.assertRaises(MetadataNotFound):
169+
metadata('foo')
167170

168171

169172
class NonASCIITests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):

0 commit comments

Comments
 (0)