Skip to content

Commit 30855ff

Browse files
authored
Merge pull request #6542 from cjerdonek/issue-5082-missing-metadata-error
Improve error message if METADATA or PKG-INFO metadata is None
2 parents 6178f96 + cd5bd2c commit 30855ff

File tree

4 files changed

+166
-36
lines changed

4 files changed

+166
-36
lines changed

news/5082.feature

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Improve the error message when ``METADATA`` or ``PKG-INFO`` is None when
2+
accessing metadata.

src/pip/_internal/exceptions.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
if MYPY_CHECK_RUNNING:
1111
from typing import Optional
12+
from pip._vendor.pkg_resources import Distribution
1213
from pip._internal.req.req_install import InstallRequirement
1314

1415

@@ -28,6 +29,36 @@ class UninstallationError(PipError):
2829
"""General exception during uninstallation"""
2930

3031

32+
class NoneMetadataError(PipError):
33+
"""
34+
Raised when accessing "METADATA" or "PKG-INFO" metadata for a
35+
pip._vendor.pkg_resources.Distribution object and
36+
`dist.has_metadata('METADATA')` returns True but
37+
`dist.get_metadata('METADATA')` returns None (and similarly for
38+
"PKG-INFO").
39+
"""
40+
41+
def __init__(self, dist, metadata_name):
42+
# type: (Distribution, str) -> None
43+
"""
44+
:param dist: A Distribution object.
45+
:param metadata_name: The name of the metadata being accessed
46+
(can be "METADATA" or "PKG-INFO").
47+
"""
48+
self.dist = dist
49+
self.metadata_name = metadata_name
50+
51+
def __str__(self):
52+
# type: () -> str
53+
# Use `dist` in the error message because its stringification
54+
# includes more information, like the version and location.
55+
return (
56+
'None {} metadata found for distribution: {}'.format(
57+
self.metadata_name, self.dist,
58+
)
59+
)
60+
61+
3162
class DistributionNotFound(InstallationError):
3263
"""Raised when a distribution cannot be found to satisfy a requirement"""
3364

src/pip/_internal/utils/packaging.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from pip._vendor import pkg_resources
77
from pip._vendor.packaging import specifiers, version
88

9+
from pip._internal.exceptions import NoneMetadataError
910
from pip._internal.utils.misc import display_path
1011
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
1112

@@ -43,16 +44,27 @@ def check_requires_python(requires_python, version_info):
4344

4445
def get_metadata(dist):
4546
# type: (Distribution) -> Message
47+
"""
48+
:raises NoneMetadataError: if the distribution reports `has_metadata()`
49+
True but `get_metadata()` returns None.
50+
"""
51+
metadata_name = 'METADATA'
4652
if (isinstance(dist, pkg_resources.DistInfoDistribution) and
47-
dist.has_metadata('METADATA')):
48-
metadata = dist.get_metadata('METADATA')
53+
dist.has_metadata(metadata_name)):
54+
metadata = dist.get_metadata(metadata_name)
4955
elif dist.has_metadata('PKG-INFO'):
50-
metadata = dist.get_metadata('PKG-INFO')
56+
metadata_name = 'PKG-INFO'
57+
metadata = dist.get_metadata(metadata_name)
5158
else:
5259
logger.warning("No metadata found in %s", display_path(dist.location))
5360
metadata = ''
5461

62+
if metadata is None:
63+
raise NoneMetadataError(dist, metadata_name)
64+
5565
feed_parser = FeedParser()
66+
# The following line errors out if with a "NoneType" TypeError if
67+
# passed metadata=None.
5668
feed_parser.feed(metadata)
5769
return feed_parser.close()
5870

tests/unit/test_legacy_resolve.py

Lines changed: 118 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,77 @@
11
import logging
22

33
import pytest
4-
from mock import patch
4+
from pip._vendor import pkg_resources
55

6-
from pip._internal.exceptions import UnsupportedPythonVersion
6+
from pip._internal.exceptions import (
7+
NoneMetadataError, UnsupportedPythonVersion,
8+
)
79
from pip._internal.legacy_resolve import _check_dist_requires_python
10+
from pip._internal.utils.packaging import get_requires_python
811

912

10-
class FakeDist(object):
13+
# We need to inherit from DistInfoDistribution for the `isinstance()`
14+
# check inside `packaging.get_metadata()` to work.
15+
class FakeDist(pkg_resources.DistInfoDistribution):
1116

12-
def __init__(self, project_name):
13-
self.project_name = project_name
17+
def __init__(self, metadata, metadata_name=None):
18+
"""
19+
:param metadata: The value that dist.get_metadata() should return
20+
for the `metadata_name` metadata.
21+
:param metadata_name: The name of the metadata to store
22+
(can be "METADATA" or "PKG-INFO"). Defaults to "METADATA".
23+
"""
24+
if metadata_name is None:
25+
metadata_name = 'METADATA'
1426

27+
self.project_name = 'my-project'
28+
self.metadata_name = metadata_name
29+
self.metadata = metadata
1530

16-
@pytest.fixture
17-
def dist():
18-
return FakeDist('my-project')
31+
def __str__(self):
32+
return '<distribution {!r}>'.format(self.project_name)
33+
34+
def has_metadata(self, name):
35+
return (name == self.metadata_name)
36+
37+
def get_metadata(self, name):
38+
assert name == self.metadata_name
39+
return self.metadata
40+
41+
42+
def make_fake_dist(requires_python=None, metadata_name=None):
43+
metadata = 'Name: test\n'
44+
if requires_python is not None:
45+
metadata += 'Requires-Python:{}'.format(requires_python)
46+
47+
return FakeDist(metadata, metadata_name=metadata_name)
1948

2049

21-
@patch('pip._internal.legacy_resolve.get_requires_python')
2250
class TestCheckDistRequiresPython(object):
2351

2452
"""
2553
Test _check_dist_requires_python().
2654
"""
2755

28-
def test_compatible(self, mock_get_requires, caplog, dist):
56+
def test_compatible(self, caplog):
57+
"""
58+
Test a Python version compatible with the dist's Requires-Python.
59+
"""
2960
caplog.set_level(logging.DEBUG)
30-
mock_get_requires.return_value = '== 3.6.5'
31-
_check_dist_requires_python(
32-
dist,
33-
version_info=(3, 6, 5),
34-
ignore_requires_python=False,
35-
)
36-
assert not len(caplog.records)
61+
dist = make_fake_dist('== 3.6.5')
3762

38-
def test_invalid_specifier(self, mock_get_requires, caplog, dist):
39-
caplog.set_level(logging.DEBUG)
40-
mock_get_requires.return_value = 'invalid'
4163
_check_dist_requires_python(
4264
dist,
4365
version_info=(3, 6, 5),
4466
ignore_requires_python=False,
4567
)
46-
assert len(caplog.records) == 1
47-
record = caplog.records[0]
48-
assert record.levelname == 'WARNING'
49-
assert record.message == (
50-
"Package 'my-project' has an invalid Requires-Python: "
51-
"Invalid specifier: 'invalid'"
52-
)
68+
assert not len(caplog.records)
5369

54-
def test_incompatible(self, mock_get_requires, dist):
55-
mock_get_requires.return_value = '== 3.6.4'
70+
def test_incompatible(self):
71+
"""
72+
Test a Python version incompatible with the dist's Requires-Python.
73+
"""
74+
dist = make_fake_dist('== 3.6.4')
5675
with pytest.raises(UnsupportedPythonVersion) as exc:
5776
_check_dist_requires_python(
5877
dist,
@@ -64,11 +83,13 @@ def test_incompatible(self, mock_get_requires, dist):
6483
"3.6.5 not in '== 3.6.4'"
6584
)
6685

67-
def test_incompatible_with_ignore_requires(
68-
self, mock_get_requires, caplog, dist,
69-
):
86+
def test_incompatible_with_ignore_requires(self, caplog):
87+
"""
88+
Test a Python version incompatible with the dist's Requires-Python
89+
while passing ignore_requires_python=True.
90+
"""
7091
caplog.set_level(logging.DEBUG)
71-
mock_get_requires.return_value = '== 3.6.4'
92+
dist = make_fake_dist('== 3.6.4')
7293
_check_dist_requires_python(
7394
dist,
7495
version_info=(3, 6, 5),
@@ -81,3 +102,67 @@ def test_incompatible_with_ignore_requires(
81102
"Ignoring failed Requires-Python check for package 'my-project': "
82103
"3.6.5 not in '== 3.6.4'"
83104
)
105+
106+
def test_none_requires_python(self, caplog):
107+
"""
108+
Test a dist with Requires-Python None.
109+
"""
110+
caplog.set_level(logging.DEBUG)
111+
dist = make_fake_dist()
112+
# Make sure our test setup is correct.
113+
assert get_requires_python(dist) is None
114+
assert len(caplog.records) == 0
115+
116+
# Then there is no exception and no log message.
117+
_check_dist_requires_python(
118+
dist,
119+
version_info=(3, 6, 5),
120+
ignore_requires_python=False,
121+
)
122+
assert len(caplog.records) == 0
123+
124+
def test_invalid_requires_python(self, caplog):
125+
"""
126+
Test a dist with an invalid Requires-Python.
127+
"""
128+
caplog.set_level(logging.DEBUG)
129+
dist = make_fake_dist('invalid')
130+
_check_dist_requires_python(
131+
dist,
132+
version_info=(3, 6, 5),
133+
ignore_requires_python=False,
134+
)
135+
assert len(caplog.records) == 1
136+
record = caplog.records[0]
137+
assert record.levelname == 'WARNING'
138+
assert record.message == (
139+
"Package 'my-project' has an invalid Requires-Python: "
140+
"Invalid specifier: 'invalid'"
141+
)
142+
143+
@pytest.mark.parametrize('metadata_name', [
144+
'METADATA',
145+
'PKG-INFO',
146+
])
147+
def test_empty_metadata_error(self, caplog, metadata_name):
148+
"""
149+
Test dist.has_metadata() returning True and dist.get_metadata()
150+
returning None.
151+
"""
152+
dist = make_fake_dist(metadata_name=metadata_name)
153+
dist.metadata = None
154+
155+
# Make sure our test setup is correct.
156+
assert dist.has_metadata(metadata_name)
157+
assert dist.get_metadata(metadata_name) is None
158+
159+
with pytest.raises(NoneMetadataError) as exc:
160+
_check_dist_requires_python(
161+
dist,
162+
version_info=(3, 6, 5),
163+
ignore_requires_python=False,
164+
)
165+
assert str(exc.value) == (
166+
"None {} metadata found for distribution: "
167+
"<distribution 'my-project'>".format(metadata_name)
168+
)

0 commit comments

Comments
 (0)