Skip to content
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

Implement Metadata 2.3 #676

Merged
merged 5 commits into from
Apr 30, 2024
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.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion flit/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ def build_post_data(action, metadata:Metadata):
"version": metadata.version,

# additional meta-data
"metadata_version": '2.1',
"metadata_version": '2.3',
"summary": metadata.summary,
"home_page": metadata.home_page,
"author": metadata.author,
Expand Down
37 changes: 31 additions & 6 deletions flit_core/flit_core/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,8 +347,9 @@ class Metadata(object):
obsoletes_dist = ()
requires_external = ()
provides_extra = ()
takluyver marked this conversation as resolved.
Show resolved Hide resolved
dynamic = ()

metadata_version = "2.1"
metadata_version = "2.3"

def __init__(self, data):
data = data.copy()
Expand All @@ -359,9 +360,31 @@ def __init__(self, data):
assert hasattr(self, k), "data does not have attribute '{}'".format(k)
setattr(self, k, v)

def _normalise_name(self, n):
def _normalise_field_name(self, n):
return n.lower().replace('-', '_')

def _normalise_core_metadata_name(self, name):
# Normalized Names (PEP 503)
return re.sub(r"[-_.]+", "-", name).lower()

def _extract_extras(self, req):
match = re.search(r'\[([^]]*)\]', req)
if match:
list_str = match.group(1)
return [item.strip() for item in list_str.split(',')]
else:
return None

def _normalise_requires_dist(self, req):
extras = self._extract_extras(req)
if extras:
normalised_extras = [self._normalise_core_metadata_name(extra) for extra in extras]
normalised_extras_str = ', '.join(normalised_extras)
normalised_req = re.sub(r'\[([^]]*)\]', f"[{normalised_extras_str}]", req)
return normalised_req
else:
return req

def write_metadata_file(self, fp):
"""Write out metadata in the email headers format"""
fields = [
Expand All @@ -383,11 +406,11 @@ def write_metadata_file(self, fp):
]

for field in fields:
value = getattr(self, self._normalise_name(field))
value = getattr(self, self._normalise_field_name(field))
fp.write(u"{}: {}\n".format(field, value))

for field in optional_fields:
value = getattr(self, self._normalise_name(field))
value = getattr(self, self._normalise_field_name(field))
if value is not None:
# TODO: verify which fields can be multiline
# The spec has multiline examples for Author, Maintainer &
Expand All @@ -400,13 +423,15 @@ def write_metadata_file(self, fp):
fp.write(u'Classifier: {}\n'.format(clsfr))

for req in self.requires_dist:
fp.write(u'Requires-Dist: {}\n'.format(req))
normalised_req = self._normalise_requires_dist(req)
fp.write(u'Requires-Dist: {}\n'.format(normalised_req))

for url in self.project_urls:
fp.write(u'Project-URL: {}\n'.format(url))

for extra in self.provides_extra:
fp.write(u'Provides-Extra: {}\n'.format(extra))
normalised_extra = self._normalise_core_metadata_name(extra)
fp.write(u'Provides-Extra: {}\n'.format(normalised_extra))

if self.description is not None:
fp.write(u'\n' + self.description + u'\n')
Expand Down
49 changes: 49 additions & 0 deletions flit_core/flit_core/tests/test_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,52 @@ def test_metadata_multiline(tmp_path):
assert msg['Version'] == d['version']
assert [l.lstrip() for l in msg['Author'].splitlines()] == d['author'].splitlines()
assert not msg.defects

@pytest.mark.parametrize(
("requires_dist", "expected_result"),
[
('foo [extra_1, extra.2, extra-3, extra__4, extra..5, extra--6]', 'foo [extra-1, extra-2, extra-3, extra-4, extra-5, extra-6]'),
('foo', 'foo'),
('foo[bar]', 'foo[bar]'),
# https://packaging.python.org/en/latest/specifications/core-metadata/#requires-dist-multiple-use
('pkginfo', 'pkginfo'),
('zope.interface (>3.5.0)', 'zope.interface (>3.5.0)'),
("pywin32 >1.0; sys_platform == 'win32'", "pywin32 >1.0; sys_platform == 'win32'"),
],
)
def test_metadata_2_3_requires_dist(requires_dist, expected_result):
d = {
'name': 'foo',
'version': '1.0',
'requires_dist': [requires_dist],
}
md = Metadata(d)
sio = StringIO()
md.write_metadata_file(sio)
sio.seek(0)

msg = email.parser.Parser(policy=email.policy.compat32).parse(sio)
assert msg['Requires-Dist'] == expected_result
assert not msg.defects

@pytest.mark.parametrize(
("provides_extra", "expected_result"),
[
('foo', 'foo'),
('foo__bar..baz', 'foo-bar-baz'),
],
)
def test_metadata_2_3_provides_extra(provides_extra, expected_result):
d = {
'name': 'foo',
'version': '1.0',
'provides_extra': [provides_extra],
}
md = Metadata(d)
sio = StringIO()
md.write_metadata_file(sio)
sio.seek(0)

msg = email.parser.Parser(policy=email.policy.compat32).parse(sio)
assert msg['Provides-Extra'] == expected_result
assert not msg.defects